class FlaskRestServer(object): ''' Small server which can be run on a different thread. Wires REST calls to DAOs. ''' def __init__(self, dbUri='sqlite:///:memory:', port=5000, verbose=False, serializer=jsonSerializerWithUri): self.flaskApp = Flask(__name__, static_url_path='', static_folder=abspath('./static')) print 'Using following sqlite database URI:', dbUri self.flaskApp.config['SQLALCHEMY_DATABASE_URI'] = dbUri self.flaskApp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False if verbose: self.flaskApp.config['SQLALCHEMY_ECHO'] = True @self.flaskApp.route('/') def index(): return self.flaskApp.send_static_file('index.html') self.port = port self.serializer = serializer self.namespaces = {} def wire(self, cls, icls=None, namespace=None): ''' Wire HTTP calls to a given class to a DAO. ''' if icls is None: icls = cls # assign default namespace if need be if namespace is None: iclsName = icls.__name__ namespace = iclsName[0] + iclsName[1:] + 's' namespaceSingle = namespace[:-1] self.namespaces[cls] = { 'cls': cls, 'icls': icls, 'namespace': namespace, 'namespaceSingle': namespaceSingle, 'innerNamespaces': {} } def _ok(content={'result': True}): ''' Build positive response with result:True by default ''' ret = Response(self.serializer._to(content), status=200, content_type=self.serializer.contentType) return ret def _abort(exceptionText): ''' Create response with an error. Serialize exception to be sent to caller. ''' self.currentError = exceptionText abort(400) def _deserializeRequestData(): ''' Takes request data (be it POST or PUT) and deserializes it using given method. ''' if flask_request.get_data() is '': return {} # if there is POST/PUT data, it should be serialized using expected method if str(flask_request.headers['Content-Type'] ) != self.serializer.contentType: _abort('Request Content-Type does not match expected: %s!=%s' % (flask_request.headers['Content-Type'], self.serializer.contentType)) try: # deserialize data ret = self.serializer._from(flask_request.get_data()) except: _abort( 'Could not deserialize data %s using deserialization method %s' % (flask_request.get_data(), self.serializer._from.__name__)) return ret def handleGetAll(): ''' Fetch a variable dynamically when GET call comes in. Simply return serialized version ''' if flask_request.args: filterArgs = { name: str(value) for name, value in flask_request.args.iteritems() if name != '_' } items = cls.query.filter_by(**filterArgs).all() else: items = cls.query.all() self.serializer.currentUri = url_for(namespace, _external=True) self.serializer.currentType = icls ret = _ok({namespace: items}) self.serializer.currentUri = None self.serializer.currentType = None return ret def handleGet(uid): ''' Handle findById. ''' self.serializer.currentUri = url_for(namespace + '_findById', uid=uid, _external=True) self.serializer.currentType = icls ret = _ok({namespaceSingle: cls.query.get(uid)}) self.serializer.currentUri = None self.serializer.currentType = None return ret def handleGetInner(uid, propName): ''' Handle finding of attributes mapped with one-to-many. ''' innerNamespace = self.namespaces[cls]['innerNamespaces'][propName] item = cls.query.get(uid) innerQuery = getattr(item, propName) if flask_request.args: filterArgs = { name: str(value) for name, value in flask_request.args.iteritems() } innerItems = innerQuery.filter_by(**filterArgs).all() else: innerItems = innerQuery.all() self.serializer.currentUri = url_for(innerNamespace['namespace'], _external=True) self.serializer.currentType = innerNamespace['icls'] ret = _ok({innerNamespace['namespace']: innerItems}) self.serializer.currentUri = None self.serializer.currentType = None return ret def handlePost(): ''' Handle POST calls. If attempted prop is a list, create new element with parameters in request data. ''' data = _deserializeRequestData() newItem = cls(**data) dbsession.add(newItem) dbsession.commit() dbsession.refresh(newItem) self.serializer.currentUri = url_for(namespace + '_findById', uid=newItem.uid, _external=True) self.serializer.currentType = icls ret = _ok({namespaceSingle: newItem}) self.serializer.currentUri = None self.serializer.currentType = None return ret def handlePut(uid): ''' Handle PUT calls. If attempted prop is a dict or class instance, it gets updated based on request data. ''' data = _deserializeRequestData() dbsession.query(cls).filter_by(uid=uid).update(data) dbsession.commit() item = cls.query.get(uid) self.serializer.currentUri = url_for(namespace + '_findById', uid=item.uid, _external=True) self.serializer.currentType = icls ret = _ok({namespaceSingle: item}) self.serializer.currentUri = None self.serializer.currentType = None return ret def handleDelete(uid): ''' Handle DELETE calls. Deletes item with given ID. ''' dbsession.delete(cls.query.get(uid)) dbsession.commit() return _ok() ''' Add HTTP hooks. ''' self.flaskApp.add_url_rule('/' + namespace, namespace, handleGetAll, methods=['GET']) self.flaskApp.add_url_rule('/' + namespace + '/<int:uid>', namespace + '_findById', handleGet, methods=['GET']) self.flaskApp.add_url_rule('/' + namespace + '/<int:uid>/<string:propName>', namespace + '_findInner', handleGetInner, methods=['GET']) self.flaskApp.add_url_rule('/' + namespace, namespace + '_create', handlePost, methods=['POST']) self.flaskApp.add_url_rule('/' + namespace + '/<int:uid>', namespace + '_update', handlePut, methods=['PUT']) self.flaskApp.add_url_rule('/' + namespace + '/<int:uid>', namespace + '_delete', handleDelete, methods=['DELETE']) print 'Wired: http://localhost:%d/%s' % (self.port, namespace) def wireOneToMany(self, cls, innerCls, propName): ''' Note that there is a one-to-many relationship between cls and innerClass that can be accessed in cls through propName. ''' self.namespaces[cls]['innerNamespaces'][propName] = self.namespaces[ innerCls] def start(self, threaded=True): ''' Launch server. ''' def _handleBadRequest(error): ''' Create response with an error. Serialize exception to be sent to caller. ''' e = ServerException(self.currentError) stderr.write('Server Exception: %s\n' % e) return Response(self.serializer._to(e), status=400, content_type=self.serializer.contentType) # handle 400 error self.flaskApp.register_error_handler(400, _handleBadRequest) print 'Starting server' if isPortListening(port=self.port): raise ServerException('Port %d already is use' % self.port) if threaded: # start http server on different thread so current one can go on changing the variables. start_new_thread(self.flaskApp.run, ('0.0.0.0', self.port)) else: self.flaskApp.run('0.0.0.0', self.port)