import uuid from . import utils from .api import Api from .exceptions import ResourceNotFound class Resource(object): """Base class for all REST services """ convert_resources = {} ensure_query_projections = {} def __init__(self, attributes=None): attributes = attributes or {} self.__dict__['api'] = Api.Default() super(Resource, self).__setattr__('__data__', {}) super(Resource, self).__setattr__('error', None) super(Resource, self).__setattr__('headers', {}) super(Resource, self).__setattr__('header', {}) super(Resource, self).__setattr__('request_id', None) self.merge(attributes) @staticmethod def _ensure_projections(params, extra_projections): """Ensures that if projections are given in the params, they contain the given ones. Only works when `params['projection']` exists and is a dict. @param params: URL parameters @type params: dict @param extra_projections: extra projections to add @type extra_projections: dict """ if not extra_projections: return try: if isinstance(params['projection'], dict): params['projection'].update(extra_projections) except (TypeError, KeyError): # Either params is None or params['projection'] doesn't exist. pass def generate_request_id(self): """Generate unique request id """ if self.request_id is None: self.request_id = str(uuid.uuid4()) return self.request_id def http_headers(self): """Generate HTTP header """ return utils.merge_dict(self.header, self.headers, {'Pillar-Request-Id': self.generate_request_id()}) def __str__(self): return self.__data__.__str__() def __repr__(self): return self.__data__.__str__() def __getattr__(self, name): return self.__data__.get(name) def __contains__(self, item): return item in self.__data__ def __setattr__(self, name, value): try: # Handle attributes(error, header, request_id) super(Resource, self).__getattribute__(name) super(Resource, self).__setattr__(name, value) except AttributeError: self.__data__[name] = self.convert(name, value) def success(self): return self.error is None def merge(self, new_attributes): """Merge new attributes e.g. response from a post to Resource """ for key, val in new_attributes.items(): setattr(self, key, val) def convert(self, name, value): """Convert the attribute values to configured class """ if isinstance(value, dict): cls = self.convert_resources.get(name, Resource) return cls(value) elif isinstance(value, list): new_list = [] for obj in value: new_list.append(self.convert(name, obj)) return new_list else: return value def __getitem__(self, key): return self.__data__[key] def __setitem__(self, key, value): self.__data__[key] = self.convert(key, value) def to_dict(self): def parse_object(value): if isinstance(value, Resource): return value.to_dict() elif isinstance(value, list): new_list = [] for obj in value: new_list.append(parse_object(obj)) return new_list else: return value data = {} for key in self.__data__: data[key] = parse_object(self.__data__[key]) return data def from_dict(self, d): for key, val in d.iteritems(): self[key] = val return self @classmethod def new(cls, dict_or_resource): """None-safe constructor.""" if dict_or_resource is None: return None if isinstance(dict_or_resource, Resource): dict_or_resource = dict_or_resource.to_dict() return cls(dict_or_resource) class Find(Resource): @classmethod def find(cls, resource_id, params=None, api=None): """Locate resource, usually using ObjectID Usage:: >>> Node.find("507f1f77bcf86cd799439011") """ url = utils.join_url(cls.path, str(resource_id)) return cls.find_from_endpoint(url, params=params, api=api) @classmethod def find_from_endpoint(cls, endpoint, params=None, api=None): """Locate resource from a custom endpoint.""" api = api or Api.Default() url = endpoint if params is not None: cls._ensure_projections(params, cls.ensure_query_projections) url = utils.join_url_params(url, params) item = api.get(url) item = utils.convert_datetime(item) return cls(item) @classmethod def find_first(cls, params, api=None): """Get list of resources, allowing some parameters such as: - count - start_time - sort_by - sort_order Usage:: >>> shots = Nodes.all({'count': 2, 'type': 'shot'}) """ api = api or Api.Default() # Force delivery of only 1 result params['max_results'] = 1 cls._ensure_projections(params, cls.ensure_query_projections) url = utils.join_url_params(cls.path, params) response = api.get(url) if not response.get('_items'): return None resource = cls(response['_items'][0]) utils.convert_datetime(resource) return resource @classmethod def find_one(cls, params, api=None): """Get one resource starting from parameters different than the resource id. TODO if more than one match for the query is found, raise exception. """ api = api or Api.Default() # Force delivery of only 1 result params['max_results'] = 1 cls._ensure_projections(params, cls.ensure_query_projections) url = utils.join_url_params(cls.path, params) response = api.get(url) # Keep the response a dictionary, and cast it later into an object. if '_items' in response: return cls(utils.convert_datetime(response['_items'][0])) else: raise ResourceNotFound(response) class List(Resource): # Class of the list result itself. list_class = Resource @classmethod def all(cls, params=None, api=None): """Get list of resources, allowing some parameters such as: - count - start_time - sort_by - sort_order Usage:: >>> shots = Nodes.all({'count': 2, 'type': 'shot'}) """ return cls.all_from_endpoint(cls.path, params=params, api=api) @classmethod def all_from_endpoint(cls, endpoint_url, params=None, api=None): """Retrieves a list of resources from a custom endpoint.""" api = api or Api.Default() if params is None: url = endpoint_url else: cls._ensure_projections(params, cls.ensure_query_projections) url = utils.join_url_params(endpoint_url, params) response = api.get(url) if '_items' in response: items = response['_items'] for idx, item in enumerate(items): items[idx] = cls(utils.convert_datetime(item)) return cls.list_class(response) # To handle the case when response is JSON Array if isinstance(response, list): new_resp = [cls(elem) for elem in response] return new_resp # Fall back to just returning the response. return response class Create(Resource): def create(self, api=None): """Create a resource Usage:: >>> node = Node({}) >>> node.create() """ api = api or self.api headers = self.http_headers() attributes = utils.remove_none_attributes(self.to_dict()) new_attributes = api.post(self.path, attributes, headers) self.error = None self.merge(new_attributes) return self.success() class Update(Resource): """Update a resource """ def update(self, attributes=None, api=None): api = api or self.api attributes = attributes or self.to_dict() etag = attributes['_etag'] attributes.pop('_id') attributes.pop('_etag') attributes.pop('_created') attributes.pop('_updated') attributes.pop('_links', None) attributes.pop('_deleted', None) attributes = utils.remove_none_attributes(attributes) url = utils.join_url(self.path, str(self['_id'])) headers = utils.merge_dict( self.http_headers(), {'If-Match': str(etag)}) new_attributes = api.put(url, attributes, headers) self.error = None self.merge(new_attributes) return self.success() class Replace(Resource): """Partial update or modify resource see http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/ Usage:: >>> node = Node.find("507f1f77bcf86cd799439011") >>> node.replace([{'op': 'replace', 'path': '/name', 'value': 'Renamed Shot 2' }]) """ def replace(self, attributes=None, files=None, api=None): api = api or self.api attributes = attributes or self.to_dict() etag = attributes['_etag'] attributes.pop('_id') attributes.pop('_etag') attributes.pop('_created') attributes.pop('_updated') attributes.pop('_links', None) attributes.pop('_deleted', None) if 'parent' in attributes: attributes.pop('parent') url = utils.join_url(self.path, str(self['_id'])) headers = utils.merge_dict( self.http_headers(), {'If-Match': str(etag)}) new_attributes = api.patch(url, attributes, headers, files) self.error = None self.merge(new_attributes) return self.success() class Delete(Resource): def delete(self, api=None): """Delete a resource Usage:: >>> node = Node.find("507f1f77bcf86cd799439011") >>> node.delete() """ api = api or self.api url = utils.join_url(self.path, str(self['_id'])) etag = self['_etag'] headers = {'If-Match': str(etag)} new_attributes = api.delete(url, headers) self.error = None self.merge(new_attributes) return self.success() class Post(Resource): def post(self, attributes=None, files=None, api=None): """Constructs url with passed in headers and makes post request via post method in api class. """ api = api or self.api attributes = attributes or {} url = utils.join_url(self.path) """if not isinstance(attributes, Resource): attributes = Resource(attributes, api=self.api)""" # files = files or {} attributes = utils.remove_none_attributes(attributes) new_attributes = api.post(url, attributes, {}, files) """if isinstance(cls, Resource): cls.error = None cls.merge(new_attributes) return self.success() else: return cls(new_attributes, api=self.api)""" self.merge(new_attributes) return self.success()