pyactiveresource-2.2.2/0002755007777700200000000000000014007062320014700 5ustar appuser00000000000000pyactiveresource-2.2.2/setup.cfg0000644007777700200000000000007314007062320016517 0ustar appuser00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyactiveresource-2.2.2/pyactiveresource.egg-info/0002755007777700200000000000000014007062320021766 5ustar appuser00000000000000pyactiveresource-2.2.2/pyactiveresource.egg-info/top_level.txt0000644007777700200000000000005214007062320024513 0ustar appuser00000000000000pyactiveresource pyactiveresource/testing pyactiveresource-2.2.2/pyactiveresource.egg-info/dependency_links.txt0000644007777700200000000000000114007062320026032 0ustar appuser00000000000000 pyactiveresource-2.2.2/pyactiveresource.egg-info/requires.txt0000644007777700200000000000000414007062320024356 0ustar appuser00000000000000six pyactiveresource-2.2.2/pyactiveresource.egg-info/PKG-INFO0000644007777700200000000000170714007062320023066 0ustar appuser00000000000000Metadata-Version: 1.1 Name: pyactiveresource Version: 2.2.2 Summary: ActiveResource for Python Home-page: https://github.com/Shopify/pyactiveresource/ Author: Shopify Author-email: developers@shopify.com License: MIT License Description: UNKNOWN Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules pyactiveresource-2.2.2/pyactiveresource.egg-info/SOURCES.txt0000644007777700200000000000105414007062320023650 0ustar appuser00000000000000LICENSE MANIFEST.in setup.py pyactiveresource/__init__.py pyactiveresource/activeresource.py pyactiveresource/collection.py pyactiveresource/connection.py pyactiveresource/element_containers.py pyactiveresource/fake_connection.py pyactiveresource/formats.py pyactiveresource/util.py pyactiveresource.egg-info/PKG-INFO pyactiveresource.egg-info/SOURCES.txt pyactiveresource.egg-info/dependency_links.txt pyactiveresource.egg-info/requires.txt pyactiveresource.egg-info/top_level.txt pyactiveresource/testing/__init__.py pyactiveresource/testing/http_fake.pypyactiveresource-2.2.2/MANIFEST.in0000644007777700200000000000002014007062317016432 0ustar appuser00000000000000include LICENSE pyactiveresource-2.2.2/pyactiveresource/0002755007777700200000000000000014007062320020274 5ustar appuser00000000000000pyactiveresource-2.2.2/pyactiveresource/element_containers.py0000644007777700200000000000113014007062317024523 0ustar appuser00000000000000# Copyright 2010 Google Inc. All Rights Reserved. __author__ = 'danv@google.com (Daniel Van Derveer)' class ElementList(list): """A list object with an element_type attribute.""" def __init__(self, element_type, *args): """Constructor for ElementList.""" self.element_type = element_type super(ElementList, self).__init__(*args) class ElementDict(dict): """A dictionary object with an element_type attribute.""" def __init__(self, element_type, *args): """Constructor for ElementDict.""" self.element_type = element_type super(ElementDict, self).__init__(*args) pyactiveresource-2.2.2/pyactiveresource/collection.py0000644007777700200000000000202314007062317023002 0ustar appuser00000000000000import copy class Collection(list): """ Defines a collection of objects. The collection holds extra metadata about the objects which are used in things like pagination. """ def __init__(self, *args, **kwargs): self._metadata = kwargs.pop("metadata", {}) super(Collection, self).__init__(*args, **kwargs) @property def metadata(self): return self._metadata @metadata.setter def metadata(self, value): self._metadata = value def copy(self): """Override list.copy so that it returns a Collection.""" copied_list = list(self) return Collection(copied_list, metadata=copy.deepcopy(self._metadata)) def __eq__(self, other): """Test equality of metadata as well as the items.""" same_list = super(Collection, self).__eq__(other) if isinstance(other, Collection): return same_list and self.metadata == other.metadata if isinstance(other, list): return same_list return False pyactiveresource-2.2.2/pyactiveresource/fake_connection.py0000644007777700200000000000704514007062317024005 0ustar appuser00000000000000# Copyright 2008 Google Inc. All Rights Reserved. """A fake HTTP connection for testing""" __author__ = 'Mark Roach (mrroach@google.com)' from six.moves import urllib from pyactiveresource import connection from pyactiveresource import formats class Error(Exception): """The base exception class for this module.""" class FakeConnection(object): """A fake HTTP connection for testing. Inspired by ActiveResource's HttpMock class. This class is designed to take a list of inputs and their corresponding outputs. Inputs will be matched on the method, path, query and data arguments Example: >>> connection = FakeConnection() >>> body = '' >>> connection.respond_to('get', '/foos/1.json', None, None, body) >>> class Foo(resource.Resource): ... _site = 'http://localhost/' ... >>> Foo._connection_obj = connection >>> Foo.find(1) foo(1) """ def __init__(self, format=formats.JSONFormat): """Constructor for FakeConnection object.""" self.format = format self._request_map = {} self._debug_only = False def _split_path(self, path): """Return the path and the query string as a dictionary.""" path_only, query_string = urllib.parse.splitquery(path) if query_string: query_dict = dict([i.split('=') for i in query_string.split('&')]) else: query_dict = {} return path_only, query_dict def debug_only(self, debug=True): self._debug_only = debug def respond_to(self, method, path, headers, data, body, response_headers=None): """Set the response for a given request. Args: method: The http method (e.g. 'get', 'put' etc.). path: The path being requested (e.g. '/collection/id.json') headers: Dictionary of headers passed along with the request. data: The data being passed in as the request body. body: The string that should be returned for a matching request. response_headers: The headers returned for a matching request Returns: None """ path_only, query = self._split_path(path) if response_headers is None: response_headers = {} self._request_map.setdefault(method, []).append( ((path_only, query, headers, data), (body, response_headers))) def _lookup_response(self, method, path, headers, data): path_only, query = self._split_path(path) for key, value in self._request_map.get(method, {}): if key == (path_only, query, headers, data): response_body, response_headers = value return connection.Response(200, response_body, response_headers) raise Error('Invalid or unknown request: %s %s\n%s' % (path, headers, data)) def get(self, path, headers=None): """Perform an HTTP get request.""" return self.format.decode( self._lookup_response('get', path, headers, None).body) def post(self, path, headers=None, data=None): """Perform an HTTP post request.""" return self._lookup_response('post', path, headers, data) def put(self, path, headers=None, data=None): """Perform an HTTP post request.""" return self._lookup_response('put', path, headers, data) def delete(self, path, headers=None): """Perform an HTTP delete request.""" return self._lookup_response('delete', path, headers, None) pyactiveresource-2.2.2/pyactiveresource/connection.py0000644007777700200000000003332314007062317023015 0ustar appuser00000000000000# Copyright 2008 Google, Inc. All Rights Reserved. """A connection object to interface with REST services.""" import base64 import logging import socket import sys import six from six.moves import urllib from pyactiveresource import formats class Error(Exception): """A general error derived from Exception.""" def __init__(self, msg=None, url=None, code=None): Exception.__init__(self, msg) self.url = url self.code = code class ServerError(Error): """An error caused by an ActiveResource server.""" # HTTP error code 5xx (500..599) def __init__(self, response=None): if response is not None: Error.__init__(self, response.msg, response.url, response.code) else: Error.__init__(self) class ConnectionError(Error): """An error caused by network connection.""" def __init__(self, response=None, message=None): if not response: self.response = Response(None, '') url = None else: self.response = Response.from_httpresponse(response) url = response.url if not message: message = str(self.response) Error.__init__(self, message, url, self.response.code) class Redirection(ConnectionError): """HTTP 3xx redirection.""" pass class ClientError(ConnectionError): """An error caused by an ActiveResource client.""" # HTTP error 4xx (401..499) pass class ResourceConflict(ClientError): """An error raised when there is a resource conflict.""" # 409 Conflict pass class ResourceInvalid(ClientError): """An error raised when a resource is invalid.""" # 422 Resource Invalid pass class ResourceNotFound(ClientError): """An error raised when a resource is not found.""" # 404 Resource Not Found def __init__(self, response=None, message=None): if response is not None and message is None: message = '%s: %s' % (response.msg, response.url) ClientError.__init__(self, response=response, message=message) class BadRequest(ClientError): """An error raised when client sends a bad request.""" # 400 Bad Request pass class UnauthorizedAccess(ClientError): """An error raised when an access is unauthorized.""" # 401 Unauthorized pass class ForbiddenAccess(ClientError): """An error raised when access is not allowed.""" # 403 Forbidden pass class MethodNotAllowed(ClientError): """An error raised when a method is not allowed.""" # 405 Method Not Allowed pass class Request(urllib.request.Request): """A request object which allows additional methods.""" def __init__(self, *args, **kwargs): self._method = None urllib.request.Request.__init__(self, *args, **kwargs) def get_method(self): """Return the HTTP method.""" if not self._method: return urllib.request.Request.get_method(self) return self._method def set_method(self, method): """Set the HTTP method.""" self._method = method def _urllib_has_timeout(): """Determines if our version of urllib.request.urlopen has a timeout argument.""" # NOTE: This is a terrible hack, but there's no other indication that this # argument was added to the function. version = sys.version_info return version[0] >= 2 and version[1] >= 6 class Response(object): """Represents a response from the http server.""" def __init__(self, code, body, headers=None, msg='', response=None): """Initialize a new Response object. code, body, headers, msg are retrievable as instance attributes. Individual headers can be retrieved using dictionary syntax (i.e. response['header'] => value. Args: code: The HTTP response code returned by the server. body: The body of the response. headers: A dictionary of HTTP headers. msg: The HTTP message (e.g. 200 OK => 'OK'). response: The original httplib.HTTPResponse (if any). """ self.code = code self.msg = msg self.body = body if headers is None: headers = {} self.headers = headers self.response = response def __eq__(self, other): if isinstance(other, Response): return ((self.code, self.body, self.headers) == (other.code, other.body, other.headers)) return False def __repr__(self): return 'Response(code=%s, body="%s", headers=%s, msg="%s")' % ( self.code, self.body, self.headers, self.msg) def __getitem__(self, key): return self.headers[key] def get(self, key, value=None): return self.headers.get(key, value) @classmethod def from_httpresponse(cls, response): """Create a Response object based on an httplib.HTTPResponse object. Args: response: An httplib.HTTPResponse object. Returns: A Response object. """ return cls(response.code, response.read(), dict(response.headers), response.msg, response) class Connection(object): """A connection object to interface with REST services.""" def __init__(self, site, user=None, password=None, timeout=None, format=formats.JSONFormat): """Initialize a new Connection object. Args: site: The base url for connections (e.g. 'http://foo') user: username for basic authentication. password: password for basic authentication. timeout: socket timeout. format: format object for en/decoding resource data. """ if site is None: raise ValueError("Connection site argument requires site") self.site, self.user, self.password = self._parse_site(site) self.user = user or self.user or '' self.password = password or self.password or '' if self.user or self.password: self.auth = base64.b64encode(('%s:%s' % (self.user, self.password)).encode('utf-8')).decode('utf-8') else: self.auth = None self.timeout = timeout self.log = logging.getLogger('pyactiveresource.connection') self.format = format def _parse_site(self, site): """Retrieve the auth information and base url for a site. Args: site: The URL to parse. Returns: A tuple containing (site, username, password). """ parts = urllib.parse.urlparse(site) host = parts.hostname if parts.port: host += ":" + str(parts.port) new_site = urllib.parse.urlunparse((parts.scheme, host, '', '', '', '')) return (new_site, parts.username, parts.password) def _request(self, url): """Return a new request object. Args: url: The url to connect to. Returns: A Request object. """ return Request(url) def _open(self, method, path, headers=None, data=None): """Perform an HTTP request. Args: method: The HTTP method (GET, PUT, POST, DELETE). path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. data: The data to send as the body of the request. Returns: A Response object. """ url = urllib.parse.urljoin(self.site, path) self.log.info('%s %s', method, url) request = self._request(url) request.set_method(method) if headers: for key, value in six.iteritems(headers): request.add_header(key, value) if self.auth: # Insert basic authentication header request.add_header('Authorization', 'Basic ' + self.auth) if request.headers: header_string = '\n'.join([':'.join((k, v)) for k, v in six.iteritems(request.headers)]) self.log.debug('request-headers:%s', header_string) if data: request.add_header('Content-Type', self.format.mime_type) request.data = data self.log.debug('request-body:%s', request.data) elif method in ['POST', 'PUT']: # Some web servers need a content length on all POST/PUT operations request.add_header('Content-Type', self.format.mime_type) request.add_header('Content-Length', '0') if self.timeout and not _urllib_has_timeout(): # Hack around lack of timeout option in python < 2.6 old_timeout = socket.getdefaulttimeout() socket.setdefaulttimeout(self.timeout) try: http_response = None try: http_response = self._handle_error(self._urlopen(request)) except urllib.error.HTTPError as err: http_response = self._handle_error(err) except urllib.error.URLError as err: raise Error(err, url) response = Response.from_httpresponse(http_response) self.log.debug('Response(code=%d, headers=%s, msg="%s")', response.code, response.headers, response.msg) finally: if http_response: http_response.close() if self.timeout and not _urllib_has_timeout(): socket.setdefaulttimeout(old_timeout) self.log.info('--> %d %s %db', response.code, response.msg, len(response.body)) return response def _urlopen(self, request): """Wrap calls to urllib so they can be overriden. Args: request: A Request object. Returns: An httplib.HTTPResponse object. Raises: urllib.error.HTTPError on server errors. urllib.error.URLError on IO errors. """ if _urllib_has_timeout(): return urllib.request.urlopen(request, timeout=self.timeout) else: return urllib.request.urlopen(request) def get(self, path, headers=None): """Perform an HTTP get request. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. Returns: A Response object. """ return self._open('GET', path, headers=headers) def get_formatted(self, path, headers=None): """Perform an HTTP get request and return the formatted response. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. Returns: The resource as a dict. """ return self.format.decode(self.get(path, headers).body) def delete(self, path, headers=None): """Perform an HTTP delete request. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. Returns: A Response object. """ return self._open('DELETE', path, headers=headers) def put(self, path, headers=None, data=None): """Perform an HTTP put request. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. data: The data to send as the body of the request. Returns: A Response object. """ return self._open('PUT', path, headers=headers, data=data) def post(self, path, headers=None, data=None): """Perform an HTTP post request. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. data: The data to send as the body of the request. Returns: A Response object. """ return self._open('POST', path, headers=headers, data=data) def head(self, path, headers=None): """Perform an HTTP put request. Args: path: The HTTP path to retrieve. headers: A dictionary of HTTP headers to add. Returns: A Response object. """ return self._open('HEAD', path, headers=headers) def _handle_error(self, err): """Handle an HTTP error. Args: err: A urllib.error.HTTPError object. Returns: An HTTP response object if the error is recoverable. Raises: Redirection: if HTTP error code 301,302 returned. BadRequest: if HTTP error code 400 returned. UnauthorizedAccess: if HTTP error code 401 returned. ForbiddenAccess: if HTTP error code 403 returned. ResourceNotFound: if HTTP error code 404 is returned. MethodNotAllowed: if HTTP error code 405 is returned. ResourceConflict: if HTTP error code 409 is returned. ResourceInvalid: if HTTP error code 422 is returned. ClientError: if HTTP error code falls in 401 - 499. ServerError: if HTTP error code falls in 500 - 599. ConnectionError: if unknown HTTP error code returned. """ if err.code in (301, 302): raise Redirection(err) elif 200 <= err.code < 400: return err elif err.code == 400: raise BadRequest(err) elif err.code == 401: raise UnauthorizedAccess(err) elif err.code == 403: raise ForbiddenAccess(err) elif err.code == 404: raise ResourceNotFound(err) elif err.code == 405: raise MethodNotAllowed(err) elif err.code == 409: raise ResourceConflict(err) elif err.code == 422: raise ResourceInvalid(err) elif 401 <= err.code < 500: raise ClientError(err) elif 500 <= err.code < 600: raise ServerError(err) else: raise ConnectionError(err) pyactiveresource-2.2.2/pyactiveresource/util.py0000644007777700200000000003352114007062317021633 0ustar appuser00000000000000# Copyright 2008 Google Inc. All Rights Reserved. """Utilities for pyActiveResource.""" __author__ = 'Mark Roach (mrroach@google.com)' import base64 import calendar import decimal import re import time import datetime import six from six.moves import urllib from pyactiveresource import element_containers try: import yaml except ImportError: yaml = None try: import simplejson as json except ImportError: try: import json except ImportError: json = None try: from dateutil.parser import parse as date_parse except ImportError: try: from xml.utils import iso8601 def date_parse(time_string): """Return a datetime object for the given ISO8601 string. Args: time_string: An ISO8601 timestamp. Returns: A datetime.datetime object. """ return datetime.datetime.utcfromtimestamp( iso8601.parse(time_string)) except ImportError: date_parse = None try: from xml.etree import cElementTree as ET except ImportError: try: import cElementTree as ET except ImportError: from xml.etree import ElementTree as ET XML_HEADER = b'\n' # Patterns blatently stolen from Rails' Inflector PLURALIZE_PATTERNS = [ (r'(quiz)$', r'\1zes'), (r'^(ox)$', r'\1en'), (r'([m|l])ouse$', r'\1ice'), (r'(matr|vert|ind)(?:ix|ex)$', r'\1ices'), (r'(x|ch|ss|sh)$', r'\1es'), (r'([^aeiouy]|qu)y$', r'\1ies'), (r'(hive)$', r'1s'), (r'(?:([^f])fe|([lr])f)$', r'\1\2ves'), (r'sis$', r'ses'), (r'([ti])um$', r'\1a'), (r'(buffal|tomat)o$', r'\1oes'), (r'(bu)s$', r'\1ses'), (r'(alias|status)$', r'\1es'), (r'(octop|vir)us$', r'\1i'), (r'(ax|test)is$', r'\1es'), (r's$', 's'), (r'$', 's') ] SINGULARIZE_PATTERNS = [ (r'(quiz)zes$', r'\1'), (r'(matr)ices$', r'\1ix'), (r'(vert|ind)ices$', r'\1ex'), (r'^(ox)en', r'\1'), (r'(alias|status)es$', r'\1'), (r'(octop|vir)i$', r'\1us'), (r'(cris|ax|test)es$', r'\1is'), (r'(shoe)s$', r'\1'), (r'(o)es$', r'\1'), (r'(bus)es$', r'\1'), (r'([m|l])ice$', r'\1ouse'), (r'(x|ch|ss|sh)es$', r'\1'), (r'(m)ovies$', r'\1ovie'), (r'(s)eries$', r'\1eries'), (r'([^aeiouy]|qu)ies$', r'\1y'), (r'([lr])ves$', r'\1f'), (r'(tive)s$', r'\1'), (r'(hive)s$', r'\1'), (r'([^f])ves$', r'\1fe'), (r'(^analy)ses$', r'\1sis'), (r'((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', r'\1\2sis'), (r'([ti])a$', r'\1um'), (r'(n)ews$', r'\1ews'), (r's$', r'') ] IRREGULAR = [ ('person', 'people'), ('man', 'men'), ('child', 'children'), ('sex', 'sexes'), ('move', 'moves'), #('cow', 'kine') WTF? ] UNCOUNTABLES = ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'] # An array of type-specific serializer methods which will be passed the value # and should return the element type and modified value. SERIALIZERS = [ {'type': bool, 'method': lambda value: ('boolean', six.text_type(value).lower())}, {'type': six.integer_types, 'method': lambda value: ('integer', six.text_type(value))}] if six.PY2: SERIALIZERS.append({ 'type': str, 'method': lambda value: (None, unicode(value, 'utf-8'))}) else: SERIALIZERS.append({ 'type': bytes, 'method': lambda value: ('base64Binary', base64.b64encode(value).decode('ascii'))}) DEFAULT_SERIALIZER = { 'type': object, 'method': lambda value: (None, six.text_type(value))} class Error(Exception): """Base exception class for this module.""" class FileObject(object): """Represent a 'file' xml entity.""" def __init__(self, data, name='untitled', content_type='application/octet-stream'): self.data = data self.name = name self.content_type = content_type def pluralize(singular): """Convert singular word to its plural form. Args: singular: A word in its singular form. Returns: The word in its plural form. """ if singular in UNCOUNTABLES: return singular for i in IRREGULAR: if i[0] == singular: return i[1] for i in PLURALIZE_PATTERNS: if re.search(i[0], singular): return re.sub(i[0], i[1], singular) def singularize(plural): """Convert plural word to its singular form. Args: plural: A word in its plural form. Returns: The word in its singular form. """ if plural in UNCOUNTABLES: return plural for i in IRREGULAR: if i[1] == plural: return i[0] for i in SINGULARIZE_PATTERNS: if re.search(i[0], plural): return re.sub(i[0], i[1], plural) return plural def camelize(word): """Convert a word from lower_with_underscores to CamelCase. Args: word: The string to convert. Returns: The modified string. """ return ''.join(w[0].upper() + w[1:] for w in re.sub('[^A-Z^a-z^0-9^:]+', ' ', word).strip().split(' ')) def underscore(word): """Convert a word from CamelCase to lower_with_underscores. Args: word: The string to convert. Returns: The modified string. """ return re.sub(r'\B((?<=[a-z])[A-Z]|[A-Z](?=[a-z]))', r'_\1', word).lower() def to_query(query_params): """Convert a dictionary to url query parameters. Args: query_params: A dictionary of arguments. Returns: A string of query parameters. """ def annotate_params(params): annotated = {} for key, value in six.iteritems(params): if isinstance(value, list): key = '%s[]' % key elif isinstance(value, dict): dict_options = {} for dk, dv in six.iteritems(value): dict_options['%s[%s]' % (key, dk)] = dv annotated.update(annotate_params(dict_options)) continue elif isinstance(value, six.text_type): value = value.encode('utf-8') annotated[key] = value return annotated annotated = annotate_params(query_params) return urllib.parse.urlencode(annotated, True) def xml_pretty_format(element, level=0): """Add PrettyPrint formatting to an ElementTree element. Args: element: An ElementTree element which is modified in-place. Returns: None """ indent = '\n%s' % (' ' * level) if len(element): if not element.text or not element.text.strip(): element.text = indent + ' ' for i, child in enumerate(element): xml_pretty_format(child, level + 1) if not child.tail or not child.tail.strip(): if i + 1 < len(element): child.tail = indent + " " else: child.tail = indent else: if level and (not element.tail or not element.tail.strip()): element.tail = indent def serialize(value, element): """Write a serialized value to an xml element. Args: value: The value to serialize. element: An xml element to write to. Returns: None """ if value is None: element.set('nil', 'true') return for serializer in SERIALIZERS + [DEFAULT_SERIALIZER]: if isinstance(value, serializer['type']): element_type, element.text = serializer['method'](value) if element_type: element.set('type', element_type) break def to_json(obj, root='object'): """Convert a dictionary, list or Collection to an JSON string. Args: obj: The object to serialize. Returns: A json string. """ if root: obj = { root: obj } return json.dumps(obj) def json_to_dict(jsonstr): """Parse the json into a dictionary of attributes. Args: jsonstr: A JSON formatted string. Returns: The deserialized object. """ return json.loads(jsonstr) def _to_xml_element(obj, root, dasherize): root = dasherize and root.replace('_', '-') or root root_element = ET.Element(root) if isinstance(obj, list): root_element.set('type', 'array') for value in obj: root_element.append(_to_xml_element(value, singularize(root), dasherize)) elif isinstance(obj, dict): for key, value in six.iteritems(obj): root_element.append(_to_xml_element(value, key, dasherize)) else: serialize(obj, root_element) return root_element def to_xml(obj, root='object', pretty=False, header=True, dasherize=True): """Convert a dictionary or list to an XML string. Args: obj: The dictionary/list object to convert. root: The name of the root xml element. pretty: Whether to pretty-format the xml (default False). header: Whether to include an xml header (default True). dasherize: Whether to convert underscores to dashes in attribute names (default True). Returns: An xml string. """ root_element = _to_xml_element(obj, root, dasherize) if pretty: xml_pretty_format(root_element) xml_data = ET.tostring(root_element) if header: return XML_HEADER + xml_data return xml_data def xml_to_dict(xmlobj, saveroot=True): """Parse the xml into a dictionary of attributes. Args: xmlobj: An ElementTree element or an xml string. saveroot: Keep the xml element names (ugly format) Returns: An ElementDict object or ElementList for multiple objects """ if isinstance(xmlobj, (six.text_type, six.binary_type)): # Allow for blank (usually HEAD) result on success if xmlobj.isspace(): return {} try: element = ET.fromstring(xmlobj) except Exception as err: raise Error('Unable to parse xml data: %s' % err) else: element = xmlobj element_type = element.get('type', '').lower() if element_type == 'array': element_list_type = element.tag.replace('-', '_') return_list = element_containers.ElementList(element_list_type) for child in list(element): return_list.append(xml_to_dict(child, saveroot=False)) if saveroot: return element_containers.ElementDict(element_list_type, {element_list_type: return_list}) else: return return_list elif element.get('nil') == 'true': return None elif element_type in ('integer', 'datetime', 'date', 'decimal', 'double', 'float') and not element.text: return None elif element_type == 'integer': return int(element.text) elif element_type == 'datetime': if date_parse: return date_parse(element.text) else: try: timestamp = calendar.timegm( time.strptime(element.text, '%Y-%m-%dT%H:%M:%S+0000')) return datetime.datetime.utcfromtimestamp(timestamp) except ValueError as err: raise Error('Unable to parse timestamp. Install dateutil' ' (http://labix.org/python-dateutil) or' ' pyxml (http://pyxml.sf.net/topics/)' ' for ISO8601 support.') elif element_type == 'date': time_tuple = time.strptime(element.text, '%Y-%m-%d') return datetime.date(*time_tuple[:3]) elif element_type == 'decimal': return decimal.Decimal(element.text) elif element_type in ('float', 'double'): return float(element.text) elif element_type == 'boolean': if not element.text: return False return element.text.strip() in ('true', '1') elif element_type == 'yaml': if not yaml: raise ImportError('PyYaml is not installed: http://pyyaml.org/') return yaml.safe_load(element.text) elif element_type == 'base64binary': return base64.b64decode(element.text.encode('ascii')) elif element_type == 'file': content_type = element.get('content_type', 'application/octet-stream') filename = element.get('name', 'untitled') return FileObject(element.text, filename, content_type) elif element_type in ('symbol', 'string'): if not element.text: return '' return element.text elif list(element): # This is an element with children. The children might be simple # values, or nested hashes. if element_type: attributes = element_containers.ElementDict( underscore(element.get('type', '')), element.items()) else: attributes = element_containers.ElementDict(singularize( element.tag.replace('-', '_')), element.items()) for child in list(element): attribute = xml_to_dict(child, saveroot=False) child_tag = child.tag.replace('-', '_') # Handle multiple elements with the same tag name if child_tag in attributes: if isinstance(attributes[child_tag], list): attributes[child_tag].append(attribute) else: attributes[child_tag] = [attributes[child_tag], attribute] else: attributes[child_tag] = attribute if saveroot: return {element.tag.replace('-', '_'): attributes} else: return attributes elif element.items(): return element_containers.ElementDict(element.tag.replace('-', '_'), element.items()) else: return element.text pyactiveresource-2.2.2/pyactiveresource/__init__.py0000644007777700200000000000000114007062317022400 0ustar appuser00000000000000 pyactiveresource-2.2.2/pyactiveresource/formats.py0000644007777700200000000000343514007062317022332 0ustar appuser00000000000000# Copyright 2008 Google Inc. All Rights Reserved. """Resource format handlers.""" __author__ = 'Mark Roach (mrroach@google.com)' import logging from pyactiveresource import util def remove_root(data): if isinstance(data, dict) and len(data) == 1: return next(iter(data.values())) return data class Error(Exception): """Base exception type for this module.""" class Base(object): """A base format object for inheritance.""" class XMLFormat(Base): """Read XML formatted ActiveResource objects.""" extension = 'xml' mime_type = 'application/xml' @staticmethod def decode(resource_string): """Convert a resource string to a dictionary.""" log = logging.getLogger('pyactiveresource.format') log.debug('decoding resource: %s', resource_string) try: data = util.xml_to_dict(resource_string, saveroot=False) except util.Error as err: raise Error(err) return remove_root(data) class JSONFormat(Base): """Encode and Decode JSON formatted ActiveResource objects.""" extension = 'json' mime_type = 'application/json' @staticmethod def decode(resource_string): """Convert a resource string to a dictionary.""" log = logging.getLogger('pyactiveresource.format') log.debug('decoding resource: %s', resource_string) try: data = util.json_to_dict(resource_string.decode('utf-8')) except ValueError as err: raise Error(err) return remove_root(data) @staticmethod def encode(data): """Convert a dictionary to a resource string.""" log = logging.getLogger('pyactiveresource.format') log.debug('encoding resource: %r', data) return util.to_json(data).encode('utf-8') pyactiveresource-2.2.2/pyactiveresource/testing/0002755007777700200000000000000014007062320021751 5ustar appuser00000000000000pyactiveresource-2.2.2/pyactiveresource/testing/__init__.py0000644007777700200000000000000014007062317024054 0ustar appuser00000000000000pyactiveresource-2.2.2/pyactiveresource/testing/http_fake.py0000644007777700200000000001203014007062317024270 0ustar appuser00000000000000# Copyright 2008 Google Inc. All Rights Reserved. """Fake urllib HTTP connection objects.""" __author__ = 'Mark Roach (mrroach@google.com)' from pprint import pformat import six from six import BytesIO from six.moves import urllib class Error(Exception): """Base exception type for this module.""" def initialize(): """Install TestHandler as the only active handler for http requests.""" opener = urllib.request.build_opener(TestHandler) urllib.request.install_opener(opener) def create_response_key(method, url, request_headers): """Create the response key for a request. Args: method: The http method (e.g. 'get', 'put', etc.) url: The path being requested including site. request_headers: Dictionary of headers passed along with the request. Returns: The key as a string. """ parsed = urllib.parse.urlsplit(url) qs = urllib.parse.parse_qs(parsed.query) query = urllib.parse.urlencode([(k, qs[k]) for k in sorted(qs.keys())]) return str(( method, urllib.parse.urlunsplit(( parsed.scheme, parsed.netloc, parsed.path, query, parsed.fragment)), dictionary_to_canonical_str(request_headers))) def dictionary_to_canonical_str(dictionary): """Create canonical string from a dictionary. Args: dictionary: The dictionary to convert. Returns: A string of the dictionary in canonical form. """ return str([(k.capitalize(), dictionary[k]) for k in sorted( dictionary.keys())]) class TestHandler(urllib.request.HTTPHandler, urllib.request.HTTPSHandler): """A urllib handler object which returns a predefined response.""" _response = None _response_map = {} request = None site = '' def __init__(self, debuglevel=0, **kwargs): self._debuglevel = debuglevel self._context = kwargs.get('context') self._check_hostname = kwargs.get('check_hostname') @classmethod def set_response(cls, response): """Set a static response to be returned for all requests. Args: response: A FakeResponse object to be returned. """ cls._response_map = {} cls._response = response @classmethod def respond_to(cls, method, path, request_headers, body, code=200, response_headers=None): """Build a response object to be used for a specific request. Args: method: The http method (e.g. 'get', 'put' etc.) path: The path being requested (e.g. '/collection/id.json') request_headers: Dictionary of headers passed along with the request body: The string that should be returned for a matching request code: The http response code to return response_headers: Dictionary of headers to return Returns: None """ key = create_response_key(method, urllib.parse.urljoin(cls.site, path), request_headers) value = (code, body, response_headers) cls._response_map[str(key)] = value def do_open(self, http_class, request, **http_conn_args): """Return the response object for the given request. Overrides the HTTPHandler method of the same name to return a FakeResponse instead of creating any network connections. Args: http_class: The http protocol being used. request: A urllib.request.Request object. Returns: A FakeResponse object. """ self.__class__.request = request # Store the most recent request object if self._response_map: key = create_response_key( request.get_method(), request.get_full_url(), request.headers) if str(key) in self._response_map: (code, body, response_headers) = self._response_map[str(key)] return FakeResponse(code, body, response_headers) else: raise Error('Unknown request %s %s' '\nrequest:%s\nresponse_map:%s' % ( request.get_method(), request.get_full_url(), str(key), pformat(list(self._response_map.keys())))) elif isinstance(self._response, Exception): raise(self._response) else: return self._response class FakeResponse(object): """A fake HTTPResponse object for testing.""" def __init__(self, code, body, headers=None): self.code = code self.msg = str(code) if headers is None: headers = {} self.headers = headers self.info = lambda: self.headers if isinstance(body, six.text_type): body = body.encode('utf-8') self.body_file = BytesIO(body) def read(self): """Read the entire response body.""" return self.body_file.read() def readline(self): """Read a single line from the response body.""" return self.body_file.readline() def close(self): """Close the connection.""" pass pyactiveresource-2.2.2/pyactiveresource/activeresource.py0000644007777700200000000011506614007062317023706 0ustar appuser00000000000000# Authors: Jared Kuolt , Mark Roach """Connect to and interact with a REST server and its objects.""" import re import sys from string import Template import six from six.moves import urllib, range from pyactiveresource import connection from pyactiveresource import element_containers from pyactiveresource import formats from pyactiveresource import util from pyactiveresource.collection import Collection VALID_NAME = re.compile(r'[a-z_]\w*') # Valid python attribute names class Error(Exception): """A general error derived from Exception.""" pass class Errors(object): """Represents error lists returned by the server.""" def __init__(self, base): """Constructor for Errors object. Args: base: The parent resource object. """ self.base = base self.errors = {} @property def size(self): return len(self.errors) def __len__(self): return len(self.errors) def add(self, attribute, error): """Add an error to a resource object's attribute. Args: attribute: The attribute to add the error to. error: The error string to add. Returns: None """ self.errors.setdefault(attribute, []).append(error) def add_to_base(self, error): """Add an error to the base resource object rather than an attribute. Args: error: the error string to add. Returns: None """ self.add('base', error) def clear(self): """Clear any errors that have been set. Args: None Returns: None """ self.errors = {} def from_array(self, messages): attribute_keys = self.base.attributes.keys() for message in messages: attr_name = message.split()[0] key = util.underscore(attr_name) if key in attribute_keys: self.add(key, message[len(attr_name)+1:]) else: self.add_to_base(message) def from_hash(self, messages): attribute_keys = self.base.attributes.keys() for key, errors in six.iteritems(messages): for message in errors: if key in attribute_keys: self.add(key, message) else: self.add_to_base(message) def from_xml(self, xml_string): """Grab errors from an XML response. Args: xml_string: An xml errors object (e.g. '') Returns: None """ try: messages = util.xml_to_dict(xml_string)['errors']['error'] if not isinstance(messages, list): messages = [messages] except util.Error: messages = [] self.from_array(messages) def from_json(self, json_string): """Grab errors from a JSON response. Args: json_string: An json errors object (e.g. "{ 'errors': {} }") Returns: None """ try: decoded = util.json_to_dict(json_string.decode('utf-8')) except ValueError: decoded = {} if not decoded: decoded = {} if isinstance(decoded, dict) and ('errors' in decoded or len(decoded) == 0): errors = decoded.get('errors', {}) if isinstance(errors, list): # Deprecated in ActiveResource self.from_array(errors) else: self.from_hash(errors) else: # Deprecated in ActiveResource self.from_hash(decoded) def on(self, attribute): """Return the errors for the given attribute. Args: attribute: The attribute to retrieve errors for. Returns: An error string, or a list of error message strings or None if none exist for the given attribute. """ errors = self.errors.get(attribute, []) if len(errors) == 1: return errors[0] return errors def full_messages(self): """Returns all the full error messages in an array. Args: None Returns: An array of error strings. """ messages = [] for key, errors in six.iteritems(self.errors): for error in errors: if key == 'base': messages.append(error) else: messages.append(' '.join((key, error))) return messages class ClassAndInstanceMethod(object): """A descriptor to allow class/instance methods with the same name.""" def __init__(self, class_method, instance_method): self.class_method = class_method self.instance_method = instance_method def __get__(self, instance, owner): if instance: return getattr(instance, self.instance_method) return getattr(owner, self.class_method) class ResourceMeta(type): """A metaclass for ActiveResource objects. Provides a separate namespace for configuration objects (user,password, site, etc)""" def __new__(mcs, name, bases, new_attrs): """Create a new class. Args: mcs: The metaclass. name: The name of the class. bases: List of base classes from which mcs inherits. new_attrs: The class attribute dictionary. """ 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) # if _site is defined, use the site property to ensure that user # and password are properly initialized. if '_site' in new_attrs: klass.site = new_attrs['_site'] return klass @property def connection(cls): """A connection object which handles all HTTP requests.""" super_class = cls.__mro__[1] if super_class == object or '_connection' in cls.__dict__: if cls._connection is None: cls._connection = connection.Connection( cls.site, cls.user, cls.password, cls.timeout, cls.format) return cls._connection else: return super_class.connection def get_user(cls): return cls._user def set_user(cls, value): cls._connection = None cls._user = value user = property(get_user, set_user, None, 'A username for HTTP Basic Auth.') def get_password(cls): return cls._password def set_password(cls, value): cls._connection = None cls._password = value password = property(get_password, set_password, None, 'A password for HTTP Basic Auth.') def get_site(cls): return cls._site def set_site(cls, value): if value is not None: parts = urllib.parse.urlparse(value) if parts.username: cls._user = urllib.parse.unquote(parts.username) if parts.password: cls._password = urllib.parse.unquote(parts.password) cls._connection = None cls._site = value site = property(get_site, set_site, None, 'The base REST site to connect to.') def get_headers(cls): return cls._headers def set_headers(cls, value): cls._headers = value headers = property(get_headers, set_headers, None, 'HTTP headers.') def get_timeout(cls): return cls._timeout def set_timeout(cls, value): cls._connection = None cls._timeout = value timeout = property(get_timeout, set_timeout, None, 'Socket timeout for HTTP operations') def get_format(cls): return cls._format def set_format(cls, value): cls._connection = None cls._format = value format = property(get_format, set_format, None, 'A format object for encoding/decoding requests') 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_prefix_source(cls): """Return the prefix source, by default derived from site.""" if hasattr(cls, '_prefix_source'): return cls._prefix_source else: return urllib.parse.urlsplit(cls.site)[2] def set_prefix_source(cls, value): """Set the prefix source, which will be rendered into the prefix.""" cls._prefix_source = value prefix_source = property(get_prefix_source, set_prefix_source, None, 'prefix for lookups for this type of object.') def prefix(cls, options=None): """Return the rendered prefix for this object.""" return cls._prefix(options) 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') class ActiveResource(six.with_metaclass(ResourceMeta, object)): """Represents an activeresource object.""" _connection = None _format = formats.JSONFormat _headers = None _password = None _site = None _timeout = None _user = None _primary_key = "id" def __init__(self, attributes=None, prefix_options=None): """Initialize a new ActiveResource object. Args: attributes: A dictionary of attributes which represent this object. prefix_options: A dict of prefixes to add to the request for nested URLs. """ if attributes is None: attributes = {} self.klass = self.__class__ self.attributes = {} if prefix_options: self._prefix_options = prefix_options else: self._prefix_options = {} self._update(attributes) self.errors = Errors(self) self._initialized = True # Public class methods which act as factory functions @classmethod def find(cls, id_=None, from_=None, **kwargs): """Core method for finding resources. Args: id_: A specific resource to retrieve. from_: The path that resources will be fetched from. kwargs: any keyword arguments for query. Returns: An ActiveResource object. Raises: connection.Error: On any communications errors. Error: On any other errors. """ if id_: return cls._find_single(id_, **kwargs) return cls._find_every(from_=from_, **kwargs) @classmethod def find_first(cls, from_=None, **kwargs): """Core method for finding resources. Args: from_: The path that resources will be fetched from. kwargs: any keyword arguments for query. Returns: The first resource from the list of returned resources or None if none are found. Raises: connection.Error: On any communications errors. Error: On any other errors. """ resources = cls._find_every(from_=from_, **kwargs) if resources: return resources[0] @classmethod def find_one(cls, from_, **kwargs): """Get a single resource from a specific URL. Args: from_: The path that resources will be fetched from. kwargs: Any keyword arguments for query. Returns: An ActiveResource object. Raises: connection.Error: On any communications errors. Error: On any other errors. """ return cls._find_one(from_, kwargs) @classmethod def exists(cls, id_, **kwargs): """Check whether a resource exists. Args: id_: The id or other key which specifies a unique object. kwargs: Any keyword arguments for query. Returns: True if the resource is found, False otherwise. """ prefix_options, query_options = cls._split_options(kwargs) path = cls._element_path(id_, prefix_options, query_options) try: _ = cls.connection.head(path, cls.headers) return True except connection.Error: return False @classmethod def create(cls, attributes): """Create and save a resource with the given attributes. Args: attributes: A dictionary of attributes which represent this object. Returns: The new resource (which may or may not have been saved successfully). """ resource = cls(attributes) resource.save() return resource # Non-public class methods to support the above @classmethod def _split_options(cls, options): """Split prefix options and query options. Args: options: A dictionary of prefix and/or query options. Returns: A tuple containing (prefix_options, query_options) """ #TODO(mrroach): figure out prefix_options prefix_options = {} query_options = {} for key, value in six.iteritems(options): if key in cls._prefix_parameters(): prefix_options[key] = value else: query_options[key] = value return [prefix_options, query_options] @classmethod def _find_single(cls, id_, **kwargs): """Get a single object from the default URL. Args: id_: The id or other key which specifies a unique object. kwargs: Any keyword arguments for the query. Returns: An ActiveResource object. Raises: ConnectionError: On any error condition. """ prefix_options, query_options = cls._split_options(kwargs) path = cls._element_path(id_, prefix_options, query_options) return cls._build_object(cls.connection.get_formatted(path, cls.headers), prefix_options) @classmethod def _find_one(cls, from_, query_options): """Find a single resource from a one-off URL. Args: from_: The path from which to retrieve the resource. query_options: Any keyword arguments for the query. Returns: An ActiveResource object. Raises: connection.ConnectionError: On any error condition. """ #TODO(mrroach): allow from_ to be a string-generating function path = from_ + cls._query_string(query_options) return cls._build_object(cls.connection.get_formatted(path, cls.headers)) @classmethod def _find_every(cls, from_=None, **kwargs): """Get all resources. Args: from_: (optional) The path from which to retrieve the resource. kwargs: Any keyword arguments for the query. Returns: A list of resources. """ prefix_options, query_options = cls._split_options(kwargs) if from_: query_options.update(prefix_options) path = from_ + cls._query_string(query_options) prefix_options = None else: path = cls._collection_path(prefix_options, query_options) response = cls.connection.get(path, cls.headers) objs = cls.format.decode(response.body) return cls._build_collection(objs, prefix_options, response.headers) @classmethod def _build_object(cls, attributes, prefix_options=None): """Create an object or objects from the given resource. Args: attributes: A dictionary representing a resource. prefix_options: A dict of prefixes to add to the request for nested URLs. Returns: An ActiveResource object. """ return cls(attributes, prefix_options) @classmethod def _build_collection(cls, elements, prefix_options=None, headers={}): """Create a Collection of objects from the given resources. Args: elements: A list of dictionaries representing resources. prefix_options: A dict of prefixes to add to the request for nested URLs. headers: The response headers that came with the resources. Returns: A Collection of ActiveResource objects. """ if isinstance(elements, dict): # FIXME(emdemir): this is not an ActiveResource object but is # preserved for backwards compatibility. What should this be # instead? elements = [elements] else: elements = ( cls._build_object(el, prefix_options) for el in elements ) # TODO(emdemir): Figure out whether passing all headers is needed. # I am currently assuming that the Link header is not standard # ActiveResource stuff so I am passing all headers up the chain to # python_shopify_api which will handle pagination. return Collection(elements, metadata={ "headers": headers }) @classmethod def _query_string(cls, query_options): """Return a query string for the given options. Args: query_options: A dictionary of query keys/values. Returns: A string containing the encoded query. """ if query_options: return '?' + util.to_query(query_options) else: return '' @classmethod def _element_path(cls, id_, prefix_options=None, query_options=None): """Get the element path for the given id. Examples: Comment.element_path(1, {'post_id': 5}) -> /posts/5/act Args: id_: The id of the object to retrieve. prefix_options: A dict of prefixes to add to the request for nested URLs. query_options: A dict of items to add to the query string for the request. Returns: The path (relative to site) to the element formatted with the query. """ return '%(prefix)s/%(plural)s/%(id)s.%(format)s%(query)s' % { 'prefix': cls._prefix(prefix_options), 'plural': cls._plural, 'id': id_, 'format': cls.format.extension, 'query': cls._query_string(query_options)} @classmethod def _collection_path(cls, prefix_options=None, query_options=None): """Get the collection path for this object type. Examples: Comment.collection_path() -> /comments.xml Comment.collection_path(query_options={'active': 1}) -> /comments.xml?active=1 Comment.collection_path({'posts': 5}) -> /posts/5/comments.xml Args: prefix_options: A dict of prefixes to add to the request for nested URLs query_options: A dict of items to add to the query string for the request. Returns: The path (relative to site) to this type of collection. """ return '%(prefix)s/%(plural)s.%(format)s%(query)s' % { 'prefix': cls._prefix(prefix_options), 'plural': cls._plural, 'format': cls.format.extension, 'query': cls._query_string(query_options)} @classmethod def _custom_method_collection_url(cls, method_name, options): """Get the collection path for this resource type. Args: method_name: The HTTP method being used. options: A dictionary of query/prefix options. Returns: The path (relative to site) to this type of collection. """ prefix_options, query_options = cls._split_options(options) path = ( '%(prefix)s/%(plural)s/%(method_name)s.%(format)s%(query)s' % {'prefix': cls._prefix(prefix_options), 'plural': cls._plural, 'method_name': method_name, 'format': cls.format.extension, 'query': cls._query_string(query_options)}) return path @classmethod def _class_get(cls, method_name, **kwargs): """Get a nested resource or resources. Args: method_name: the nested resource to retrieve. kwargs: Any keyword arguments for the query. Returns: A dictionary representing the returned data. """ url = cls._custom_method_collection_url(method_name, kwargs) return cls.connection.get_formatted(url, cls.headers) @classmethod def _class_post(cls, method_name, body=b'', **kwargs): """Get a nested resource or resources. Args: method_name: the nested resource to retrieve. body: The data to send as the body of the request. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = cls._custom_method_collection_url(method_name, kwargs) return cls.connection.post(url, cls.headers, body) @classmethod def _class_put(cls, method_name, body=b'', **kwargs): """Update a nested resource or resources. Args: method_name: the nested resource to update. body: The data to send as the body of the request. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = cls._custom_method_collection_url(method_name, kwargs) return cls.connection.put(url, cls.headers, body) @classmethod def _class_delete(cls, method_name, **kwargs): """Delete a nested resource or resources. Args: method_name: the nested resource to delete. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = cls._custom_method_collection_url(method_name, kwargs) return cls.connection.delete(url, cls.headers) @classmethod def _class_head(cls, method_name, **kwargs): """Predicate a nested resource or resources exists. Args: method_name: the nested resource to predicate exists. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = cls._custom_method_collection_url(method_name, kwargs) return cls.connection.head(url, cls.headers) @classmethod def _prefix_parameters(cls): """Return a list of the parameters used in the site prefix. e.g. /objects/$object_id would yield ['object_id'] /objects/${object_id}/people/$person_id/ would yield ['object_id', 'person_id'] Args: None Returns: A set of named parameters. """ path = cls.prefix_source template = Template(path) keys = set() for match in template.pattern.finditer(path): for match_type in 'braced', 'named': if match.groupdict()[match_type]: keys.add(match.groupdict()[match_type]) return keys @classmethod def _prefix(cls, options=None): """Return the prefix for this object type. Args: options: A dictionary containing additional prefixes to prepend. Returns: A string containing the path to this element. """ if options is None: options = {} path = re.sub('/$', '', cls.prefix_source) template = Template(path) keys = cls._prefix_parameters() options = dict([(k, options.get(k, '')) for k in keys]) prefix = template.safe_substitute(options) return re.sub('^/+', '', prefix) # Public instance methods def to_dict(self): """Convert the object to a dictionary.""" values = {} for key, value in six.iteritems(self.attributes): if isinstance(value, list): new_value = [] for item in value: if isinstance(item, ActiveResource): new_value.append(item.to_dict()) else: new_value.append(item) values[key] = new_value elif isinstance(value, ActiveResource): values[key] = value.to_dict() else: values[key] = value return values def encode(self, **options): return getattr(self, "to_" + self.klass.format.extension)(**options) def to_xml(self, root=None, header=True, pretty=False, dasherize=True): """Convert the object to an xml string. Args: root: The name of the root element for xml output. header: Whether to include the xml header. pretty: Whether to "pretty-print" format the output. dasherize: Whether to dasherize the xml attribute names. Returns: An xml string. """ if not root: root = self._singular return util.to_xml(self.to_dict(), root=root, header=header, pretty=pretty, dasherize=dasherize) def to_json(self, root=True): """Convert the object to a json string.""" if root == True: root = self._singular return util.to_json(self.to_dict(), root=root).encode('utf-8') def reload(self): """Connect to the server and update this resource's attributes. Args: None Returns: None """ attributes = self.klass.connection.get_formatted( self._element_path(self.id, self._prefix_options), self.klass.headers) self._update(attributes) def save(self): """Save the object to the server. Args: None Returns: True on success, False on ResourceInvalid errors (sets the errors attribute if an object is returned by the server). Raises: connection.Error: On any communications problems. """ try: self.errors.clear() if self.id: response = self.klass.connection.put( self._element_path(self.id, self._prefix_options), self.klass.headers, data=self.encode()) else: response = self.klass.connection.post( self._collection_path(self._prefix_options), self.klass.headers, data=self.encode()) new_id = self._id_from_response(response) if new_id: self.id = new_id except connection.ResourceInvalid as err: if self.klass.format == formats.XMLFormat: self.errors.from_xml(err.response.body) elif self.klass.format == formats.JSONFormat: self.errors.from_json(err.response.body) return False try: attributes = self.klass.format.decode(response.body) except formats.Error: return True if attributes: self._update(attributes) return True def is_valid(self): """Returns True if no errors have been set. Args: None Returns: True if no errors have been set, False otherwise. """ return not len(self.errors) def _id_from_response(self, response): """Pull the ID out of a response from a create POST. Args: response: A Response object. Returns: An id string. """ match = re.search(r'\/([^\/]*?)(\.\w+)?$', response.get('Location', response.get('location', ''))) if match: try: return int(match.group(1)) except ValueError: return match.group(1) def destroy(self): """Deletes the resource from the remote service. Args: None Returns: None """ self.klass.connection.delete( self._element_path(self.id, self._prefix_options), self.klass.headers) 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 __getattr__(self, name): """Retrieve the requested attribute if it exists. Args: name: The attribute name. Returns: The attribute's value. Raises: AttributeError: if no such attribute exists. """ if 'attributes' in self.__dict__: if name in self.attributes: return self.attributes[name] raise AttributeError(name) def __setattr__(self, name, value): """Set the named attributes. Args: name: The attribute name. value: The attribute's value. Returns: None """ 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: 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 hash(tuple(sorted(six.iteritems(self.attributes)))) def _update(self, attributes): """Update the object with the given attributes. Args: attributes: A dictionary of attributes. Returns: None """ if not isinstance(attributes, dict): return for key, value in six.iteritems(attributes): 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: klass = self._find_class_for_collection(key) attr.append(klass(child)) else: attr.append(child) else: attr = value # Store the actual value in the attributes dictionary self.attributes[key] = attr @classmethod def _find_class_for_collection(cls, collection_name): """Look in the parent modules for classes matching the element name. One or both of element/class name must be specified. Args: collection_name: The name of the collection type. Returns: A Resource class. """ return cls._find_class_for(util.singularize(collection_name)) @classmethod def _find_class_for(cls, element_name=None, class_name=None, create_missing=True): """Look in the parent modules for classes matching the element name. One or both of element/class name must be specified. Args: element_name: The name of the element type. class_name: The class name of the element type. create_missing: Whether classes should be auto-created if no existing match is found. Returns: A Resource class. """ if not element_name and not class_name: raise Error('One of element_name,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 # If we made it this far, no such class was found if create_missing: return type(str(class_name), (cls,), {'__module__': cls.__module__}) # methods corresponding to Ruby's custom_methods def _custom_method_element_url(self, method_name, options): """Get the element path for this type of object. Args: method_name: The HTTP method being used. options: A dictionary of query/prefix options. Returns: The path (relative to site) to the element formatted with the query. """ prefix_options, query_options = self._split_options(options) prefix_options.update(self._prefix_options) path = ( '%(prefix)s/%(plural)s/%(id)s/%(method_name)s.%(format)s%(query)s' % {'prefix': self.klass.prefix(prefix_options), 'plural': self._plural, 'id': self.id, 'method_name': method_name, 'format': self.klass.format.extension, 'query': self._query_string(query_options)}) return path def _custom_method_new_element_url(self, method_name, options): """Get the element path for creating new objects of this type. Args: method_name: The HTTP method being used. options: A dictionary of query/prefix options. Returns: The path (relative to site) to the element formatted with the query. """ prefix_options, query_options = self._split_options(options) prefix_options.update(self._prefix_options) path = ( '%(prefix)s/%(plural)s/new/%(method_name)s.%(format)s%(query)s' % {'prefix': self.klass.prefix(prefix_options), 'plural': self._plural, 'method_name': method_name, 'format': self.klass.format.extension, 'query': self._query_string(query_options)}) return path def _instance_get(self, method_name, **kwargs): """Get a nested resource or resources. Args: method_name: the nested resource to retrieve. kwargs: Any keyword arguments for the query. Returns: A dictionary representing the returned data. """ url = self._custom_method_element_url(method_name, kwargs) return self.klass.connection.get_formatted(url, self.klass.headers) def _instance_post(self, method_name, body=b'', **kwargs): """Create a new resource/nested resource. Args: method_name: the nested resource to post to. body: The data to send as the body of the request. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ if self.id: url = self._custom_method_element_url(method_name, kwargs) else: if not body: body = self.encode() url = self._custom_method_new_element_url(method_name, kwargs) return self.klass.connection.post(url, self.klass.headers, body) def _instance_put(self, method_name, body=b'', **kwargs): """Update a nested resource. Args: method_name: the nested resource to update. body: The data to send as the body of the request. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = self._custom_method_element_url(method_name, kwargs) return self.klass.connection.put(url, self.klass.headers, body) def _instance_delete(self, method_name, **kwargs): """Delete a nested resource or resources. Args: method_name: the nested resource to delete. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = self._custom_method_element_url(method_name, kwargs) return self.klass.connection.delete(url, self.klass.headers) def _instance_head(self, method_name, **kwargs): """Predicate a nested resource or resources exists. Args: method_name: the nested resource to predicate exists. kwargs: Any keyword arguments for the query. Returns: A connection.Response object. """ url = self._custom_method_element_url(method_name, kwargs) return self.klass.connection.head(url, self.klass.headers) # Create property which returns class/instance method based on context get = ClassAndInstanceMethod('_class_get', '_instance_get') post = ClassAndInstanceMethod('_class_post', '_instance_post') put = ClassAndInstanceMethod('_class_put', '_instance_put') delete = ClassAndInstanceMethod('_class_delete', '_instance_delete') head = ClassAndInstanceMethod('_class_head', '_instance_head') pyactiveresource-2.2.2/setup.py0000644007777700200000000000303714007062317016421 0ustar appuser00000000000000from setuptools import setup import sys version = '2.2.2' if sys.version_info >= (3,): python_dateutils_version = 'python-dateutil>=2.0' else: python_dateutils_version = 'python-dateutil<2.0' setup(name='pyactiveresource', version=version, description='ActiveResource for Python', author='Shopify', author_email='developers@shopify.com', url='https://github.com/Shopify/pyactiveresource/', packages=['pyactiveresource', 'pyactiveresource/testing'], license='MIT License', test_suite='test', install_requires=[ 'six', ], tests_require=[ python_dateutils_version, 'PyYAML', ], platforms=['any'], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules'] ) pyactiveresource-2.2.2/PKG-INFO0000644007777700200000000000170714007062320016000 0ustar appuser00000000000000Metadata-Version: 1.1 Name: pyactiveresource Version: 2.2.2 Summary: ActiveResource for Python Home-page: https://github.com/Shopify/pyactiveresource/ Author: Shopify Author-email: developers@shopify.com License: MIT License Description: UNKNOWN Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules pyactiveresource-2.2.2/LICENSE0000644007777700200000000000224114007062317015710 0ustar appuser00000000000000This is the MIT license: http://www.opensource.org/licenses/mit-license.php Copyright (c) 2014 "Shopify inc." Copyright (C) 2008 Jared Kuolt and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.