Source code for vivialconnect.resources.resource

"""
.. module:: resource
   :synopsis: Resource module.
"""
# Inspired by https://github.com/Shopify/pyactiveresource

import sys

from collections import MutableSequence, MutableMapping
from itertools import chain

from vivialconnect.common.requestor import Requestor
from vivialconnect.common.error import ResourceError
from vivialconnect.common.util import Util
import six


class BaseField(object):
    pass


class ResourceAttributeStorage(MutableMapping):
    def __init__(self, parent, attributes=None, backing_type=dict):
        if not isinstance(parent, Resource):
            raise ValueError("Parent should be a Resource instance")
        self.parent_resource = parent
        self._items = backing_type()
        self._fields = parent.declared_fields
        if isinstance(attributes, dict):
            self.update(attributes)

    def __getitem__(self, key):
        if key in self._fields:
            return getattr(self.parent_resource, key)
        return self._items[key]

    def __iter__(self):
        return chain(self._items, self._fields)

    def __len__(self):
        return len(self._items) + len(self._fields)

    def __delitem__(self, key):
        if key in self._fields:
            delattr(self.parent_resource, key)
        del self._items[key]

    def __setitem__(self, key, value):
        if key in self._fields:
            setattr(self.parent_resource, key, value)
        else:
            self._items[key] = value


[docs]class ResourceMeta(type): """A metaclass for :class:`Resource` objects. This class provides a way for configuration of API properties (user, password, account_id, etc). """ def __new__(mcs, name, bases, new_attrs): if "_singular" not in new_attrs or not new_attrs["_singular"]: new_attrs["_singular"] = Util.underscore(name) if "_plural" not in new_attrs or not new_attrs["_plural"]: new_attrs["_plural"] = Util.pluralize(new_attrs["_singular"]) klass = type.__new__(mcs, name, bases, new_attrs) klass._fields = {} for attr, val in new_attrs.items(): if isinstance(val, BaseField): klass._fields[attr] = val return klass def get_plural(cls): return cls._plural def set_plural(cls, value): cls._plural = value plural = property( get_plural, set_plural, None, "The plural name of this object type" ) def get_singular(cls): return cls._singular def set_singular(cls, value): cls._singular = value singular = property( get_singular, set_singular, None, "The singular name of this object type" ) def get_primary_key(cls): return cls._primary_key def set_primary_key(cls, value): cls._primary_key = value primary_key = property( get_primary_key, set_primary_key, None, "Name of attribute that uniquely identies the resource", ) @property def request(cls): super_class = cls.__mro__[1] if super_class == object or "_request" in cls.__dict__: if cls._request is None: cls._request = Requestor( api_key=cls.api_key, api_secret=cls.api_secret, api_base_url=cls.api_base_url, api_account_id=cls.api_account_id, verify_request=cls._verify_request, request_timeout=cls._request_timeout, ) return cls._request else: return super_class.request def get_api_key(cls): return cls._api_key def set_api_key(cls, value): cls._request = None cls._api_key = value api_key = property( get_api_key, set_api_key, None, "An API key used for HMAC authentication" ) def get_api_secret(cls): return cls._api_secret def set_api_secret(cls, value): cls._request = None cls._api_secret = value api_secret = property( get_api_secret, set_api_secret, None, "An API secret used for HMAC authentication", ) def get_api_account_id(cls): return cls._api_account_id def set_api_account_id(cls, value): cls._request = None cls._api_account_id = value api_account_id = property( get_api_account_id, set_api_account_id, None, "An API account id" ) def get_api_base_url(cls): return cls._api_base_url def set_api_base_url(cls, value): cls._request = None cls._api_base_url = value api_base_url = property( get_api_base_url, set_api_base_url, None, "An API base_url used for HMAC authentication", ) def get_verify_request(cls): return cls._verify_request def set_verify_request(cls, value): cls._request = None cls._verify_request = value api_verify_request = property( get_verify_request, set_verify_request, None, "Verify SSL certs" ) def get_request_timeout(cls): return cls._request_timeout def set_request_timeout(cls, value): cls._request_timeout = value request_timeout = property( get_request_timeout, set_request_timeout, None, "Request Timeout" )
[docs]class Resource(six.with_metaclass(ResourceMeta, object)): """This class represents a base :class:`Resource` object. """ _request = None _api_key = None _api_secret = None _api_account_id = None _api_base_url = None _verify_request = True _primary_key = "id" _request_timeout = 30 API_ACCOUNT_PREFIX = "/accounts/%(account_id)s" def __init__(self, attributes=None, prefix_options=None): if attributes is None: attributes = {} if prefix_options: self._prefix_options = prefix_options else: self._prefix_options = [] self.klass = self.__class__ self.attributes = ResourceAttributeStorage(self, attributes) self._initialized = True # Public class methods which act as factory functions @classmethod
[docs] def find(cls, id_=None, path=None, **kwargs): """Find resources. :param id_: A specific resource to retrieve. :type id_: ``int``. :param path: The path that resources will be fetched from. :type path: ``str``. :param \**kwargs: Any keyword arguments used for forming a query. :returns: :class:`Resource` -- a Resource object. :raises: :class:`RequestorError`: On any communications errors. :class:`ResourceError`: On any other errors. """ if id_: return cls._find_single(id_, path=path, **kwargs) return cls._find_every(path=path, **kwargs)
@classmethod
[docs] def find_first(cls, path=None, **kwargs): """Find first available resource from the list. :param path: The path that resources will be fetched from. :type path: ``str``. :param \**kwargs: Any keyword arguments used for forming a query. Valid query keywords include: search, order, limit, page :returns: The first found resource from the list of returned resources, otherwise ``None``. :raises: :class:`RequestorError`: On any communications errors. :class:`ResourceError`: On any other errors. """ resources = cls._find_every(path=path, **kwargs) if resources: return resources[0]
@classmethod
[docs] def create(cls, attributes): """Creates and saves a resource with the given attributes. :param attributes: A dictionary of attributes which represent this object. :type attributes: ``dict``. :returns: :class:`Resource` -- a newly created :class:`Resource` object. """ resource = cls(attributes) resource.save() return resource
[docs] def save(self): """Saves :class:`Resource` object to the server. :returns: ``True`` on success, or throws an error. :raises: :class:`RequestorError`: On any communications errors. :class:`ResourceError`: On any other errors. """ attributes = self._wrap_attributes(root=self._singular) if self.id: response = self.klass.request.put( self._element_path(self.id, path=None, options=self._prefix_options), attributes, ) else: response = self.klass.request.post( self._collection_path(path=None, options=self._prefix_options), params=attributes, ) self._update(Util.remove_root(response)) return True
[docs] def reload(self): """Reloads :class:`Resource` object from the server. :raises: :class:`RequestorError`: On any communications errors. :class:`ResourceError`: On any other errors. """ attributes = self.klass.request.get( self._element_path(self.id, path=None, options=self._prefix_options) ) self._update(attributes)
[docs] def destroy(self): """Deletes :class:`Resource` object from the server. :raises: :class:`RequestorError`: On any communications errors. :class:`ResourceError`: On any other errors. """ self.klass.request.delete( self._element_path(self.id, path=None, options=self._prefix_options) )
[docs] def is_new(self): """Returns True if resource is new and have not been saved. :returns: ``True`` if resource is new, ``False`` otherwise. """ return not self.id
@classmethod def get(cls, id_=None, path=None, custom_path="", **kwargs): url = cls._custom_path( id_=id_, path=path, custom_path=custom_path, options=None ) + cls._query_string(kwargs) return cls.request.get(url) @classmethod def post(cls, id_=None, path=None, custom_path="", params=None, **kwargs): url = cls._custom_path( id_=id_, path=path, custom_path=custom_path, options=None ) + cls._query_string(kwargs) return cls.request.post(url, params=params) @classmethod def _find_single(cls, id_, path=None, **kwargs): url = cls._element_path(id_, path=path, options=None) + cls._query_string( kwargs ) response = cls.request.get(url) return cls._build_object(response) @classmethod def _find_every(cls, path=None, **kwargs): url = cls._collection_path(path=path, options=None) + cls._query_string(kwargs) response = cls.request.get(url) return cls._build_list(response) @classmethod def _build_object(cls, attributes): return cls(Util.remove_root(attributes)) @classmethod def _build_list(cls, attributes): resources = [] elements = Util.remove_root(attributes) if isinstance(elements, dict): elements = [elements] for element in elements: resources.append(cls(Util.remove_root(element))) return resources def _update(self, attributes): if not isinstance(attributes, dict): return for key, value in six.iteritems(attributes): desc = self.declared_fields.get(key) if isinstance(value, dict): klass = self._find_class_for(key) attr = klass(value) elif isinstance(value, list): klass = None attr = [] for child in value: if isinstance(child, dict): if klass is None: # Figure out the class based on the descriptor, if any if desc: klass = desc.member_type else: # Guess the subclass klass = self._find_class_for_collection(key) if issubclass(klass, SubordinateResource): attr.append(klass(child, parent_resource=self)) else: attr.append(klass(child)) else: attr.append(child) if klass and issubclass(klass, SubordinateResource): setattr(self, key, attr) else: attr = value # Store the actual value in the attributes dictionary self.attributes[key] = attr @classmethod def _find_class_for_collection(cls, collection_name): return cls._find_class_for(Util.singularize(collection_name)) @classmethod def _find_class_for(cls, element_name=None, class_name=None, create_missing=True): if not element_name and not class_name: raise ResourceError("element_name or class_name must be specified") elif not element_name: element_name = Util.underscore(class_name) elif not class_name: class_name = Util.camelize(element_name) module_path = cls.__module__.split(".") for depth in range(len(module_path), 0, -1): try: __import__(".".join(module_path[:depth])) module = sys.modules[".".join(module_path[:depth])] except ImportError: continue try: klass = getattr(module, class_name) return klass except AttributeError: try: __import__(".".join([module.__name__, element_name])) submodule = sys.modules[".".join([module.__name__, element_name])] except ImportError: continue try: klass = getattr(submodule, class_name) return klass except AttributeError: continue # Woow, we made it this far, and no class was found if create_missing: return type(str(class_name), (cls,), {"__module__": cls.__module__}) @property def declared_fields(self): return self._fields def _to_dict(self): attributes = {} for key, value in six.iteritems(self.attributes): if isinstance(value, MutableSequence): new_value = [] for item in value: if isinstance(item, Resource): new_value.append(item._to_dict()) else: new_value.append(item) attributes[key] = new_value elif isinstance(value, Resource): attributes[key] = value._to_dict() else: attributes[key] = value return attributes def _wrap_attributes(self, root="object"): if root: return {root: self._to_dict()} return self._to_dict() def get_id(self): return self.attributes.get(self.klass.primary_key) def set_id(self, value): self.attributes[self.klass.primary_key] = value id = property(get_id, set_id, None, "Value stored in the primary key") def _attr_valid(self, name): if "attributes" in self.__dict__: if name in self.attributes: return True return False def __getattr__(self, name): if "attributes" in self.__dict__: if name in self.attributes: return self.attributes[name] raise AttributeError(name) def __setattr__(self, name, value): if "_initialized" in self.__dict__: if name in self.__dict__ or getattr(self.__class__, name, None): # Update a normal attribute object.__setattr__(self, name, value) else: # Add/update an attribute self.attributes[name] = value else: object.__setattr__(self, name, value) def __repr__(self): return "%s(%s)" % (self._singular, self.id) if six.PY2: # pylint: disable=undefined-variable def __cmp__(self, other): if isinstance(other, self.__class__): return cmp(self.id, other.id) else: return cmp(self.id, other) else: def __eq__(self, other): return ( other.__class__ == self.__class__ and self.id == other.id and self._prefix_options == other._prefix_options ) def __hash__(self): return id(self) @classmethod def _prefix(cls, path=None, options=None): prefix = "" if path: prefix = path if options and isinstance(options, list) and len(options) > 0: prefix = prefix + ("/%s/%s" % tuple(options)) return prefix @classmethod def _element_path(cls, id_, path=None, options=None, ext=".json"): return (cls.API_ACCOUNT_PREFIX + "%(prefix)s/%(plural)s/%(id)s%(ext)s") % { "account_id": cls._api_account_id, "prefix": cls._prefix(path, options), "plural": cls._plural, "id": id_, "ext": ext, } @classmethod def _collection_path(cls, path=None, options=None, ext=".json"): return (cls.API_ACCOUNT_PREFIX + "%(prefix)s/%(plural)s%(ext)s") % { "account_id": cls._api_account_id, "prefix": cls._prefix(path, options), "plural": cls._plural, "ext": ext, } @classmethod def _custom_path( cls, id_=None, path=None, custom_path="", options=None, ext=".json" ): if id_: return ( cls.API_ACCOUNT_PREFIX + "%(prefix)s/%(plural)s/%(id)s%(custom)s%(ext)s" ) % { "account_id": cls._api_account_id, "prefix": cls._prefix(path, options), "plural": cls._plural, "id": id_, "custom": custom_path, "ext": ext, } else: return ( cls.API_ACCOUNT_PREFIX + "%(prefix)s/%(plural)s%(custom)s%(ext)s" ) % { "account_id": cls._api_account_id, "prefix": cls._prefix(path, options), "plural": cls._plural, "custom": custom_path, "ext": ext, } @classmethod def _query_string(cls, query_options=None): if query_options: return "?" + Util.to_query(query_options) else: return ""
class SubordinateResource(Resource): def __init__(self, attributes=None, prefix_options=None, parent_resource=None): self.parent_resource = parent_resource super(SubordinateResource, self).__init__(attributes, prefix_options) @classmethod def find(cls, *args, **kwargs): raise NotImplementedError("Cannot find subordinate resources.") def __repr__(self): try: identity = "({})".format(self.identity) except AttributeError: identity = "" return "{}.{}{}".format(self.parent_resource, self._singular, identity)