"""
.. 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 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)