from functools import wraps import json import os import uuid import logbook import network from robots.state import GameState log = logbook.Logger(__name__) def n_required_args(argspec): return len(argspec.args) - len(argspec.defaults or ()) def bad_request(msg): return {'status': 400, 'message': msg} def not_found(msg): return {'status': 404, 'message': msg} def server_error(msg): return {'status': 500, 'message': msg} def success(**kwargs): rv = {'status': 200} rv.update(kwargs) return rv def json_handler(handle): SERVER_ERROR = json.dumps(server_error('Unexpected server error.')).encode('utf-8') @wraps(handle) def handler(data): try: request = json.loads(data.decode('utf-8')) except ValueError: log.exception('Bad JSON data: {!r}', request) try: response = handle(request) data = json.dumps(response).encode('utf-8') except Exception: log.exception('Exception in handler.') data = SERVER_ERROR return data return handler class Server(object): SERVER_NAME = os.getenv('NAME') SERVER_KIND = 'robots' def __init__(self): # Maps bot names to a function which will return the bot callable. # (or, you know, a class) self.bots = {} # Maps instance ids to a bot callable. self.instances = {} def add_bot(self, fn, name=None): """ >>> s = Server() >>> s.add_bot(lambda: 5, name='Alice') >>> list(s.bots) ['Alice'] """ if name is None: name = fn.__name__ self.bots[name] = fn def add_simple_bot(self, fn, name=None): if name is None: name = fn.__name__ return self.add_bot(lambda: fn, name) def list_bots(self): bots = { name: {} for name, fn in self.bots.items() } return success(bots=bots) def create(self, name, options): try: bot_class = self.bots[name] except KeyError: message = 'Could not find bot %s' % (name,) log.warning(message) return not_found(message) try: instance = bot_class(**options) except Exception: message = 'Could not create bot %s' % (name,) log.exception(message) return server_error(message) instance_id = str(uuid.uuid4()) self.instances[instance_id] = instance log.debug('Created new bot instance %s' % instance_id) return success(instance_id=instance_id) def next_move(self, instance_id, whoami, state): try: instance = self.instances[instance_id] except KeyError: return not_found('No instance %s on server.' % instance_id) state = GameState.from_json(state) try: result = instance(whoami, state) except Exception: message = 'Exception running instance %s.' % instance_id log.exception(message) return server_error(message) return success(result=result) def destroy(self, instance_id): self.instances.pop(instance_id, None) return success() def handle(self, request): try: action = request['action'] if action == 'list': return self.list_bots() elif action == 'create': name = request['name'] options = request['options'] return self.create(name, options) elif action == 'process': instance_id = request['instance_id'] whoami = request['whoami'] state = request['state'] return self.next_move(instance_id, whoami, state) elif action == 'destroy': instance_id = request['instance_id'] return self.destroy(instance_id) else: return bad_request('Unrecognised action %s' % (action,)) except KeyError as e: return bad_request('No %s specified' % e.args) def run(self): server = network.Server( self.SERVER_NAME, self.SERVER_KIND, handler=json_handler(self.handle), ) server.run()