Source code for bzz.model

#!/usr/bin/python
# -*- coding: utf-8 -*-

# This file is part of bzz.
# https://github.com/heynemann/bzz

# Licensed under the MIT license:
# http://www.opensource.org/licenses/MIT-license
# Copyright (c) 2014 Bernardo Heynemann heynemann@gmail.com

import tornado.web
import tornado.gen as gen
from six.moves.urllib.parse import unquote

import bzz.core as core
import bzz.signals as signals
import bzz.utils as utils


AVAILABLE_PROVIDERS = {
    'mongoengine': 'bzz.providers.mongoengine_provider.MongoEngineProvider',
    'sqlalchemy': 'bzz.providers.sqlalchemy_provider.SQLAlchemyProvider',
}


[docs]class ModelHive(object): @classmethod
[docs] def routes_for(cls, provider, model, prefix='', resource_name=None): ''' Returns the list of routes for the specified model. * [POST] Create new instances; * [PUT] Update existing instances; * [DELETE] Delete existing instances; * [GET] Retrieve existing instances with the id for the instance; * [GET] List existing instances (and filter them); :param provider: The ORM provider to be used for this model :type provider: Full-name provider or built-in provider :param model: The model to be mapped :type model: Class Type :param prefix: Optional argument to include a prefix route (i.e.: '/api'); :type prefix: string :param resource_name: an optional argument that can be specified to change the route name. If no resource_name specified the route name is the __class__.__name__ for the specified model with underscores instead of camel case. :type resource_name: string :returns: route list (can be flattened with bzz.flatten) If you specify a prefix of '/api/' as well as resource_name of 'people' your route would be similar to: http://myserver/api/people/ (do a post to this url to create a new person) Usage: .. testcode:: model_hive_example_1 import tornado.web from mongoengine import * import bzz server = None # just create your own documents class User(Document): __collection__ = "MongoEngineHandlerUser" name = StringField() def create_user(): # let's create a new user by posting it's data http_client.fetch( 'http://localhost:8890/user/', method='POST', body='name=Bernardo%20Heynemann', callback=handle_user_created ) def handle_user_created(response): # just making sure we got the actual user try: assert response.code == 200, response.code finally: io_loop.stop() # bzz includes a helper to return the routes for your models # returns a list of routes that match '/user/<user-id>/' and allows for: routes = bzz.ModelHive.routes_for('mongoengine', User) User.objects.delete() application = tornado.web.Application(routes) server = HTTPServer(application, io_loop=io_loop) server.listen(8895) io_loop.add_timeout(1, create_user) io_loop.start() ''' provider_name = AVAILABLE_PROVIDERS.get(provider, provider) provider_class = utils.get_class(provider_name) name = resource_name if name is None: name = utils.convert(model.__name__) details_regex = r'/(%s(?:/[^/]+)?)((?:/[^/]+)*)/?' details_regex = utils.add_prefix(prefix, details_regex) tree = provider_class.get_tree(model) options = dict(model=model, name=name, prefix=prefix, tree=tree) routes = core.RouteList() routes.append( (details_regex % name, provider_class, options) ) return routes
class ModelProvider(tornado.web.RequestHandler): @classmethod def get_tree(cls, model, node=None): if node is None: node = core.Node(cls.get_model_name(model), is_root=True) node.add_to_cache(model, node) node.target_name = cls.get_model_collection(model) node.is_multiple = False if node.target_name is None: node.target_name = node.slug node.model_type = model cls.parse_children(model, node.children, node) return node @classmethod def parse_children(cls, model, collection, root_node): for field_name, field in cls.get_model_fields(model).items(): model = cls.get_model(field) child_node = core.Node(field_name) collection[field_name] = child_node child_node.is_multiple = cls.is_list_field(field) child_node.target_name = cls.get_field_target_name(field) child_node.allows_create_on_associate = \ cls.allows_create_on_associate(field) child_node.is_lazy_loaded = \ cls.is_lazy_loaded(field) child_node.model_type = model if child_node.model_type is not None: cached_node = root_node.find_by_class(model) if cached_node is None: root_node.add_to_cache(model, child_node) cls.parse_children(child_node.model_type, child_node.children, root_node) else: child_node.children = cached_node.children def get_node(self, path): if '.' not in path: return self.tree.get(path, None) node = self.tree for item in path.split('.'): node = node.get(item, None) if node is None: return None return node def initialize(self, model, name, prefix, tree): self.model = model self.name = name self.prefix = prefix self.tree = tree def write_json(self, obj): self.set_header("Content-Type", "application/json") self.write(utils.dumps(obj)) def parse_arguments(self, args): args = [arg.lstrip('/') for arg in args if arg] if len(args) == 1: return args parts = args[1].split('/') items = [] current_item = [] for part in parts: current_item.append(part) if len(current_item) == 2: items.append('/'.join(current_item)) current_item = [] if current_item: items.append(current_item[0]) return [args[0]] + items @classmethod def get_path_from_args(cls, args): path = ".".join([arg.split('/')[0] for arg in args[1:]]) return path @gen.coroutine def get(self, *args, **kwargs): args = self.parse_arguments(args) path = self.get_path_from_args(args) node = self.tree.find_by_path(path) if (node.is_root or node.is_multiple) and '/' not in args[-1]: yield signals.pre_get_list.send(node.model_type, arguments=args, handler=self) yield self.handle_get_list(args) else: yield signals.pre_get_instance.send(node.model_type, arguments=args, handler=self) yield self.handle_get_one(args) @gen.coroutine def handle_get_one(self, args): success, obj, parent = yield self.get_instance_from_args(args) if not success: return if obj is None: self.send_error(status_code=404) return yield signals.post_get_instance.send(obj.__class__, instance=obj, handler=self) self.write_json(self.dump_instance(obj)) self.finish() @gen.coroutine def handle_get_list(self, args): if '/' not in args[0] and len(args) > 1: # /team/user -> missing user pk self.send_error(status_code=400) return if len(args) == 1: request_data = self.get_request_data() try: page = int(request_data.pop('page', 1)) except ValueError: page = 1 try: per_page = int(request_data.pop('per_page', 20)) except ValueError: per_page = 20 items = yield self.get_list(page=page, per_page=per_page, filters=request_data) model_type = self.model else: success, items, parent = yield self.get_instance_from_args(args) if not success: self.send_error(status_code=400) return model_type = self.get_property_model(parent, args[-1]) yield signals.post_get_list.send(model_type, items=items, handler=self) self.write_json(self.dump_list(items)) self.finish() @gen.coroutine def get_instance_from_args(self, args): model, pk = args[0].split('/') obj = yield self.get_instance(pk) if len(args) == 1: raise gen.Return((True, obj, None)) obj, parent = yield self.get_instance_property(obj, args[1:]) if obj is None: self.send_error(status_code=404) raise gen.Return((False, None)) raise gen.Return((True, obj, parent)) @gen.coroutine def get_instance_property(self, obj, path): parts = [part.lstrip('/').split('/') for part in path if part] parent = obj for part in parts: path = part[0] pk = None if len(part) > 1: pk = part[1] obj = getattr(parent, path) if pk is not None: if isinstance(obj, (list, tuple)): for item in obj: instance_id = yield self.get_instance_id(item) if instance_id == pk: obj = item if part != parts[-1]: parent = obj raise gen.Return([obj, parent]) @gen.coroutine def post(self, *args, **kwargs): args = self.parse_arguments(args) model_type = yield self.get_model_from_path(args) yield signals.pre_create_instance.send( model_type, arguments=args, handler=self, ) instance = None if len(args) == 1: if '/' in args[0]: self.send_error(400) return instance, error = yield self.handle_create_one(args) else: is_reference = yield self.is_reference(args) if is_reference: instance, error = yield self.handle_find_and_associate(args) else: instance, error = yield self.handle_create_and_associate(args) if error is not None: status_code, error = error self.set_status(status_code) self.write(str(error)) return yield signals.post_create_instance.send( instance.__class__, instance=instance, handler=self ) pk = yield self.get_instance_id(instance) self.set_header('X-Created-Id', pk) self.set_header('location', '/%s%s/%s/' % ( self.prefix.lstrip('/'), self.name, pk )) self.write_json(self.dump_instance(instance)) @gen.coroutine def handle_create_one(self, args): instance, error = yield self.save_new_instance(self.model, self.get_request_data()) raise gen.Return((instance, error)) @gen.coroutine def handle_create_and_associate(self, args): path, pk = args[0].split('/') root = yield self.get_instance(pk) model_type = self.get_model_type(root, args[1:]) instance, error = yield self.save_new_instance(model_type, self.get_request_data()) if error is not None: raise gen.Return((None, error)) _, error = yield self.associate_instance(root, args[-1], instance) if error is not None: raise gen.Return((None, error)) raise gen.Return((instance, error)) @gen.coroutine def handle_find_and_associate(self, args): path, pk = args[0].split('/') root = yield self.get_instance(pk) _, parent = yield self.get_instance_property(root, args[1:]) request_data = self.get_request_data() model_type = self.get_property_model(parent, args[-1]) key = "%s[]" % args[-1] value = request_data[key] instance = yield self.get_instance(value, model=model_type) _, error = yield self.associate_instance(root, args[-1], instance) if error is not None: raise gen.Return((None, error)) raise gen.Return((instance, None)) def get_model_type(self, obj, args): for index, arg in enumerate(args[:-1]): property_name, pk = arg.split('/') obj = getattr(obj, property_name) return self.get_property_model(obj, args[-1]) @gen.coroutine def put(self, *args, **kwargs): args = self.parse_arguments(args) model_type = yield self.get_model_from_path(args) yield signals.pre_update_instance.send( model_type, arguments=args, handler=self ) instance = None if len(args) == 1 and '/' not in args[0]: self.send_error(400) return is_multiple = yield self.is_multiple(args) is_reference = yield self.is_reference(args) if is_multiple and is_reference: self.send_error(400) return instance, updated, model = yield self.handle_update(args) yield signals.post_update_instance.send(model, instance=instance, updated_fields=updated, handler=self) self.write('OK') @gen.coroutine def handle_update(self, args): path, pk = args[0].split('/') root = yield self.get_instance(pk) model_type = root.__class__ instance = parent = None if not self.validate_update_request_data(root, model_type): self.send_error(400, reason="Invalid multiple field") raise tornado.web.Finish() if len(args) > 1: instance, parent = yield self.get_instance_property(root, args[1:]) model_type = instance.__class__ property_name, pk = args[-1].split('/') error, instance, updated = yield self.update_instance(pk, self.get_request_data(), model_type, instance, parent) if error is not None: status_code, error = error self.set_status(status_code) self.write(str(error)) return raise gen.Return([instance, updated, model_type]) def validate_update_request_data(self, root, model_type): data = self.get_request_data() for key, value in data.items(): if key.endswith('[]'): return False return True @gen.coroutine def delete(self, *args, **kwargs): args = self.parse_arguments(args) if len(args) == 1 and '/' not in args[0]: self.send_error(400) return model_type = yield self.get_model_from_path(args) yield signals.pre_delete_instance.send(model_type, arguments=args, handler=self) path, pk = args[0].split('/') root = yield self.get_instance(pk) instance = None if len(args) > 1: instance, parent = yield self.get_instance_property(root, args[1:]) property_name, pk = args[-1], None if '/' in property_name: property_name, pk = property_name.split('/') instance, error = yield self.handle_delete_association(parent, instance, property_name) else: instance = yield self.handle_delete_instance(pk) error = None if error is not None: status_code, error = error self.set_status(status_code) self.write(str(error)) return if instance: yield signals.post_delete_instance.send(model_type, instance=instance, handler=self) self.write('OK') else: self.write('FAIL') @gen.coroutine def handle_delete_instance(self, pk): instance = yield self.delete_instance(pk) raise gen.Return(instance) @gen.coroutine def handle_delete_association(self, parent, instance, property_name): fields = self.get_model_fields(parent.__class__) field = fields.get(property_name) if self.is_list_field(field): property_list = getattr(parent, property_name, []) try: property_list.remove(instance) except ValueError: self.send_error(400) return else: setattr(parent, property_name, None) _, error = yield self.save_instance(parent) raise gen.Return((instance, error)) @gen.coroutine def list(self): items = yield self.get_list() dump = [] for item in items: dump.append(self.dump_object(item)) self.write_json(dump) def get_request_data(self): data = {} if self.request.body: items = self.request.body.decode('utf-8').split('&') for item in items: if '=' in item: key, value = item.split('=') else: key, value = 'item', item if key in data: if not isinstance(data[key], (tuple, list)): old = data[key] data[key] = [] data[key].append(old) data[key].append(unquote(value)) else: if '[]' in key: data[key] = [unquote(value)] else: data[key] = unquote(value) else: for arg in list(self.request.arguments.keys()): data[arg] = self.get_argument(arg) if data[arg] == '': # Tornado 3.0+ compatibility... Hard to test... data[arg] = None return data def dump_object(self, instance): return utils.dumps(instance)