289 lines
8.0 KiB
Python
289 lines
8.0 KiB
Python
import uuid
|
|
|
|
from . import utils
|
|
from .api import Api
|
|
|
|
|
|
class Resource(object):
|
|
"""Base class for all REST services
|
|
"""
|
|
convert_resources = {}
|
|
|
|
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)
|
|
|
|
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,
|
|
{'Attract-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 __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
|
|
|
|
|
|
class Find(Resource):
|
|
|
|
@classmethod
|
|
def find(cls, resource_id, api=None):
|
|
"""Locate resource, usually using ObjectID
|
|
|
|
Usage::
|
|
|
|
>>> Node.find("507f1f77bcf86cd799439011")
|
|
"""
|
|
|
|
api = api or Api.Default()
|
|
|
|
url = utils.join_url(cls.path, str(resource_id))
|
|
return cls(api.get(url))
|
|
|
|
@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
|
|
url = utils.join_url_params(cls.path, params)
|
|
|
|
response = api.get(url)
|
|
res = cls(response)
|
|
if res._items:
|
|
return res._items[0]
|
|
else:
|
|
return None
|
|
|
|
class List(Resource):
|
|
|
|
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'})
|
|
"""
|
|
api = api or Api.Default()
|
|
|
|
if params is None:
|
|
url = cls.path
|
|
else:
|
|
url = utils.join_url_params(cls.path, params)
|
|
|
|
try:
|
|
response = api.get(url)
|
|
return cls.list_class(response)
|
|
except AttributeError:
|
|
# To handle the case when response is JSON Array
|
|
if isinstance(response, list):
|
|
new_resp = [cls.list_class(elem) for elem in response]
|
|
return new_resp
|
|
|
|
|
|
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()
|
|
|
|
new_attributes = api.post(self.path, self.to_dict(), 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')
|
|
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')
|
|
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 {}
|
|
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()
|