Source code for ripozo.resources.relationships.relationship

"""
Contains the relationship class and additional
shortcut classes.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from ripozo.exceptions import RestException
from ripozo.resources.constructor import ResourceMetaClass
from ripozo.utilities import get_or_pop

import logging
import six

_logger = logging.getLogger(__name__)


[docs]class Relationship(object): """ Defines a relationship on a resource. This allows you to create related resources and construct them appropriately. As usual, the actual response is created by an adapter which must determine whether to return the whole resource, a link, etc... """ _resource_meta_class = ResourceMetaClass
[docs] def __init__(self, name, property_map=None, relation=None, embedded=False, required=False, no_pks=False, query_args=None, templated=False, remove_properties=True): """ :param unicode name: :param dict property_map: A map of the parent's property name to the corresponding related fields properties. For example, it may be called "child" on the parent but it corresponds to the id field on the related field. :param unicode relation: The name of the resource class that this relation is a type of. It uses a string so that you do not have to worry about the order of how relations were defined. It looks up the actual type from the ResourceMetaClass. :param bool embedded: Indicates whether the related resource should be embedded in the parent resource when returned. Otherwise, a more basic representation will be used (e.g. a link or id) :param bool required: An indicator for whether the relation must be constructed. :param bool no_pks: A flag that indicates that the resources created do not need pks (for example a next link in RetrieveList mixin) :param list[unicode]|tuple[unicode] query_args: A list of strings that should be passed to the query_args parameter for resource construction. :param bool templated: If templated is True, then the resource does not need to have all pks. However, embedded is negated if templated = True to prevent infinite loops. :param bool remove_properties: If True, then the properties in the child relationship will be removed. Otherwise, the properties will simply be copied to the relationship """ self.query_args = query_args or tuple() self.property_map = property_map or {} self._relation = relation self.embedded = embedded self.required = required self.name = name self.no_pks = no_pks self.templated = templated self.remove_properties = remove_properties
@property def relation(self): """ The ResourceBase subclass that describes the related object If no _relation property is available on the instance it returns None. Raises a key error when the relation keyword argument passed on construction is not available in the self._resource_meta_class.registered_names_map dictionary (By default the self._resource_meta_class is the ResourceMetaClass). :return: The ResourceBase subclass that describes the related resource :rtype: type :raises: KeyError """ return self._resource_meta_class.registered_names_map[self._relation]
[docs] def construct_resource(self, properties): """ Takes the properties from the parent and and maps them to the named properties for the parent resource to its relationships :param dict properties: :return: An instance of a self.relation class that corresponds to this related resource :rtype: rest.viewsets.resource_base.ResourceBase """ _logger.debug('Constructing resource %s of type %s', self.name, self.relation) related_properties = self._map_pks(properties) resource = None if related_properties or self.templated: include_relationships = self.embedded and not self.templated resource = self.relation(properties=related_properties, query_args=self.query_args, include_relationships=include_relationships, no_pks=self.no_pks) if self.required and (not resource or not resource.has_all_pks): raise RestException('The relationship {0} could not construct a valid {1}' ' with all of its pks. Properties' ' {2}'.format(self.name, self.relation, related_properties)) elif not resource or self._should_return_none(resource): return None return resource
def _should_return_none(self, resource): """ Helper method for construct_resource. :param ResourceBase resource: The resource to evaluate. :return: A boolean indicating whether returning None is valid. :rtype: bool """ return not (resource.has_all_pks or resource.no_pks or self.templated)
[docs] def remove_child_resource_properties(self, properties): """ Removes the properties that are supposed to be on the child resource and not on the parent resource. It copies the properties argument before it removes the copied values. It does not have side effects in other words. :param dict properties: The properties that are in the related resource map that should not be in the parent resource. :return: a dictionary of the updated properties :rtype: :py:class:`dict` """ properties = properties.copy() for key in six.iterkeys(self.property_map): properties.pop(key, None) properties.pop(self.name, None) return properties
def _map_pks(self, parent_properties): """ Takes a dictionary of the values of the parent resources properties. It then maps those properties to the named properties of the related resource and creates a dictionary of the related resources property values. Raises a KeyError if the parent does not contain keys that matches every key in the self.property_map :param dict parent_properties: A dictionary of the parent resource's properties. The key is the name of the property and the value is the parent resources value for that property :return: A dictionary of the related resources properties. The key is the name of the related resource's property and the value is the value of that resource's property. :rtype: :py:class:`dict` :raises: KeyError """ properties = {} for parent_prop, prop in six.iteritems(self.property_map): val = get_or_pop(parent_properties, parent_prop, pop=self.remove_properties) if val is not None: properties[prop] = val name_values = get_or_pop(parent_properties, self.name, pop=self.remove_properties) if name_values: properties.update(name_values) return properties
[docs]class FilteredRelationship(Relationship): """ A relationship class that helps to easily create relationships that point to a filtered relationship. For example, suppose we had the following resources .. code-block:: python from ripozo import restmixins class Parent(ResourceBase): resource_name = 'parent' pks = 'id', class Child(restmixins.RetrieveList): resource_name = 'child' pks = 'id', Assuming that a parent can have many children and that a child has a property called `parent_id`, we want a link to get all of the children, but we don't want to embed the links to all of the individual children. We simply want a link with `'/child?parent_id=<id>'`. This can be done by doing: .. code-block:: python class Parent(ResourceBase): _relationships = Relationship('children', relation='Child', property_map=dict(id='parent_id'), query_args=['parent_id'], no_pks = True, remove_properties=False) However, that is a lot of set up. With this class you would simply do: .. code-block:: python class Parent(ResourceBase): _relationships = FilteredRelationship('children', relation='Child', property_map=dict(id='parent_id')) """
[docs] def __init__(self, *args, **kwargs): """Sets the query_args to the values of the property_map remove_properties=False, and no_pks=True then calls super""" kwargs['query_args'] = kwargs.get('property_map', {}).values() kwargs['remove_properties'] = False kwargs['no_pks'] = True super(FilteredRelationship, self).__init__(*args, **kwargs)