Source code for ripozo.viewsets.resource_base

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import re

import six

from six.moves.urllib import parse
from ripozo.decorators import classproperty
from ripozo.viewsets.constructor import ResourceMetaClass
from ripozo.utilities import convert_to_underscore, join_url_parts

import inspect
import logging

logger = logging.getLogger(__name__)


url_part_finder = re.compile(r'<([^>]+)>')


[docs]@six.add_metaclass(ResourceMetaClass) class ResourceBase(object): """ The core of ripozo. :param bool __abstract__: abstract classes are not registered by the ResourceMetaClass :param dict _relationships: The relationships that will be constructed by instances :param list _pks: The pks for this resource. These, along with the ``namespace`` and ``resource_name`` are combined to generate the url :param type _manager: The BaseManager subclass that is responsible for persistence within the applictation. I.E. the AlchemyManager from ripozo-sqlalchemy :param unicode _namespace: The namespace of this resource. This is prepended to the resource_name and pks to create the url :param unicode _resource_name: The name of the resource. :param list _preprocessors: A list of functions that will be run before any apimethod is called. :param list _postprocessors: A list of functions that will be run after any apimethod from this class is called. :param dict _links: Works similarly to relationships. The primary difference between this and links is that links will assume the resource is the same as this class if a relation is not specified. Additionally, links are supposed to be meta information effectively. They are not necessarily about this specific resource. For example, next and previous links for a list resource or created links when creating a new resource on a list resource. """ __abstract__ = True _relationships = None _pks = None _manager = None _namespace = '/' _resource_name = None _preprocessors = None _postprocessors = None _links = None def __init__(self, properties=None, errors=None, meta=None, status_code=200, query_args=None, include_relationships=True): """ Initializes a response :param dict properties: :param int status_code: :param list errors: :param dict meta: :param bool include_relationships: If not True, then this resource will not include relationships or links. This is primarily used to increase performance with linked resources. """ self.properties = properties or {} self.status_code = status_code self.errors = errors or [] self.meta = meta or {} self.query_args = query_args or {} self._url = None if include_relationships: self.related_resources = self._generate_links(self.relationships, self.properties) meta_links = self.meta.get('links', {}).copy() self.linked_resources = self._generate_links(self.links, meta_links) else: self.related_resources = [] self.linked_resources = [] @staticmethod def _generate_links(relationship_list, links_properties): """ Generates a list of linked resources from the links_dict. :param list links_dict: :param dict links_properties: :return: A list of ResourceBase objects :rtype: list """ # TODO need much better testing for this links = [] relationship_list = relationship_list or [] for relationship in relationship_list: res = relationship.construct_resource(links_properties) if res is None: continue links.append((res, relationship.name, relationship.embedded)) return links @property def has_error(self): return len(self.errors) > 0 or self.status_code >= 400 @property def has_all_pks(self): """ :return: Indicates whether an instance of this class has all of the items in the pks list as a key in the ``self.properties`` attribute. :rtype: bool """ # TODO test for pk in self.pks: if pk not in self.properties: return False return True @property def url(self): """ Lazily constructs the url for this specific resource using the specific pks as specified in the pks tuple. :return: The url for this resource :rtype: unicode """ if not self._url: url = create_url(self.base_url, **self.item_pks) query_string = '&'.join('{0}={1}'.format(x, y) for x, y in six.iteritems(self.query_args)) if query_string: url = '{0}?{1}'.format(url, query_string) self._url = url return self._url @property def item_pks(self): """ Gets a dictionary of an individual resource's primary keys. The key of the dictionary is the name of the primary key and the value is the actual value of the primary key specified :rtype: dict """ pks = self.pks or [] pk_dict = {} for pk in pks: pk_dict[pk] = self.properties.get(pk, None) return pk_dict @classproperty def base_url(cls): """ Gets the base_url for the resource This is prepended to all routes indicated by an apimethod decorator. :return: The base_url for the resource(s) :rtype: unicode """ pks = cls.pks or [] parts = list(map(lambda pk: '<{0}>'.format(pk), pks)) base_url = join_url_parts(cls.base_url_sans_pks, *parts).lstrip('/') return '/{0}'.format(base_url) @classproperty def base_url_sans_pks(cls): """ A class property that eturns the base url without the pks. This is just the /{namespace}/{resource_name} For example if the _namespace = '/api' and the _resource_name = 'resource' this would return '/api/resource' regardless if there are pks or not. :return: The base url without the pks :rtype: unicode """ base_url = join_url_parts(cls.namespace, cls.resource_name).lstrip('/') return '/{0}'.format(base_url)
[docs] @classmethod def endpoint_dictionary(cls): """ A dictionary of the endpoints with the method as the key and the route options as the value :return: dictionary of endpoints :rtype: dict """ # TODO update this documentation return _generate_endpoint_dict(cls)
@classproperty def links(cls): """ This should be overridden in __abstract__ subclasses that require default links such as the Create rest mixin. :return: A tuple of the links for this class :rtype: tuple """ return cls._links or () @classproperty def manager(cls): if cls._manager is None: return None return cls._manager() @classproperty def namespace(cls): return cls._namespace or '' @classproperty def pks(cls): return cls._pks or [] @classproperty def postprocessors(cls): return cls._postprocessors or [] @classproperty def preprocessors(cls): return cls._preprocessors or [] @classproperty def relationships(cls): """ This should be overridden in __abstract__ classes that require a default relationship. For example, you have a series of classes that should have an ``user`` relationship. In that case you would simply append the User relationship to the _relationships attribute. :return: A tuple of the relationships for this class :rtype: tuple """ return cls._relationships or () @classproperty def resource_name(cls): """ The resource name for this Resource class returns resource_name if not None Otherwise it returns the model_name :return: The name of the resource for this class :rtype: unicode """ if cls._resource_name: return cls._resource_name return convert_to_underscore(cls.__name__)
def _generate_endpoint_dict(cls): # TODO test and doc string endpoint_dictionary = {} for name, method in _get_apimethods(cls): logger.debug('Found the apimethod {0} on the class {1}'.format(name, cls.__name__)) all_routes = [] for route, endpoint, options in method.routes: base_url = cls.base_url_sans_pks if options.get('no_pks', False) else cls.base_url route = join_url_parts(base_url, route) all_routes.append(dict(route=route, endpoint_func=method, **options)) logger.info('Registering routes: {0} as key {1}'.format(all_routes, name)) endpoint_dictionary[name] = all_routes return endpoint_dictionary def _get_apimethods(cls): """ A generator that yields tuples of the name and method of all ``@apimethod`` decorated methods on the class. :param type cls: The instance of a ResourceMetaClass that you wish to retrieve the apimethod decorated methods from. :type cls: :return: A generator for tuples of the name, method combo :rtype: type.GeneratorType """ for name, obj in inspect.getmembers(cls, predicate=_apimethod_predicate): yield name, getattr(cls, name) def _apimethod_predicate(obj): return getattr(obj, 'rest_route', False) or getattr(obj, '__rest_route__', False)
[docs]def create_url(base_url, **kwargs): """ Generates a fully qualified url. It iterates over the keyword arguments and for each key it replaces any instances of "<key>" with "value" The keys of the dictionary must be strings. :param unicode base_url: The url template that will be used to generate an acutal url :param dict kwargs: The dictionary of template variables and their associated values :return: A complete url. :rtype: unicode """ for key, value in six.iteritems(kwargs): to_replace = '<{0}>'.format(key) base_url = re.sub(to_replace, six.text_type(value), base_url) return base_url