pax_global_header00006660000000000000000000000064132252323710014512gustar00rootroot0000000000000052 comment=c861a249e5532637c270d48cf35e80a70675b0c2 coreapi-2.3.3/000077500000000000000000000000001322523237100131415ustar00rootroot00000000000000coreapi-2.3.3/MANIFEST.in000066400000000000000000000001051322523237100146730ustar00rootroot00000000000000global-exclude __pycache__ global-exclude *.pyc global-exclude *.pyo coreapi-2.3.3/PKG-INFO000066400000000000000000000016241322523237100142410ustar00rootroot00000000000000Metadata-Version: 1.1 Name: coreapi Version: 2.3.3 Summary: Python client library for Core API. Home-page: https://github.com/core-api/python-client Author: Tom Christie Author-email: tom@tomchristie.com License: BSD Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP coreapi-2.3.3/coreapi.egg-info/000077500000000000000000000000001322523237100162555ustar00rootroot00000000000000coreapi-2.3.3/coreapi.egg-info/PKG-INFO000066400000000000000000000016241322523237100173550ustar00rootroot00000000000000Metadata-Version: 1.1 Name: coreapi Version: 2.3.3 Summary: Python client library for Core API. Home-page: https://github.com/core-api/python-client Author: Tom Christie Author-email: tom@tomchristie.com License: BSD Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP coreapi-2.3.3/coreapi.egg-info/SOURCES.txt000066400000000000000000000011771322523237100201470ustar00rootroot00000000000000MANIFEST.in setup.cfg setup.py coreapi/__init__.py coreapi/auth.py coreapi/client.py coreapi/compat.py coreapi/document.py coreapi/exceptions.py coreapi/utils.py coreapi.egg-info/PKG-INFO coreapi.egg-info/SOURCES.txt coreapi.egg-info/dependency_links.txt coreapi.egg-info/entry_points.txt coreapi.egg-info/requires.txt coreapi.egg-info/top_level.txt coreapi/codecs/__init__.py coreapi/codecs/base.py coreapi/codecs/corejson.py coreapi/codecs/display.py coreapi/codecs/download.py coreapi/codecs/jsondata.py coreapi/codecs/python.py coreapi/codecs/text.py coreapi/transports/__init__.py coreapi/transports/base.py coreapi/transports/http.pycoreapi-2.3.3/coreapi.egg-info/dependency_links.txt000066400000000000000000000000011322523237100223230ustar00rootroot00000000000000 coreapi-2.3.3/coreapi.egg-info/entry_points.txt000066400000000000000000000003401322523237100215500ustar00rootroot00000000000000[coreapi.codecs] corejson = coreapi.codecs:CoreJSONCodec download = coreapi.codecs:DownloadCodec json = coreapi.codecs:JSONCodec text = coreapi.codecs:TextCodec [coreapi.transports] http = coreapi.transports:HTTPTransport coreapi-2.3.3/coreapi.egg-info/requires.txt000066400000000000000000000000471322523237100206560ustar00rootroot00000000000000coreschema requests itypes uritemplate coreapi-2.3.3/coreapi.egg-info/top_level.txt000066400000000000000000000000521322523237100210040ustar00rootroot00000000000000coreapi coreapi/codecs coreapi/transports coreapi-2.3.3/coreapi/000077500000000000000000000000001322523237100145635ustar00rootroot00000000000000coreapi-2.3.3/coreapi/__init__.py000066400000000000000000000005471322523237100167020ustar00rootroot00000000000000# coding: utf-8 from coreapi import auth, codecs, exceptions, transports, utils from coreapi.client import Client from coreapi.document import Array, Document, Link, Object, Error, Field __version__ = '2.3.3' __all__ = [ 'Array', 'Document', 'Link', 'Object', 'Error', 'Field', 'Client', 'auth', 'codecs', 'exceptions', 'transports', 'utils', ] coreapi-2.3.3/coreapi/auth.py000066400000000000000000000043501322523237100161000ustar00rootroot00000000000000from coreapi.utils import domain_matches from requests.auth import AuthBase, HTTPBasicAuth class BasicAuthentication(HTTPBasicAuth): allow_cookies = False def __init__(self, username, password, domain=None): self.domain = domain super(BasicAuthentication, self).__init__(username, password) def __call__(self, request): if not domain_matches(request, self.domain): return request return super(BasicAuthentication, self).__call__(request) class TokenAuthentication(AuthBase): allow_cookies = False scheme = 'Bearer' def __init__(self, token, scheme=None, domain=None): """ * Use an unauthenticated client, and make a request to obtain a token. * Create an authenticated client using eg. `TokenAuthentication(token="")` """ self.token = token self.domain = domain if scheme is not None: self.scheme = scheme def __call__(self, request): if not domain_matches(request, self.domain): return request request.headers['Authorization'] = '%s %s' % (self.scheme, self.token) return request class SessionAuthentication(AuthBase): """ Enables session based login. * Make an initial request to obtain a CSRF token. * Make a login request. """ allow_cookies = True safe_methods = ('GET', 'HEAD', 'OPTIONS', 'TRACE') def __init__(self, csrf_cookie_name=None, csrf_header_name=None, domain=None): self.csrf_cookie_name = csrf_cookie_name self.csrf_header_name = csrf_header_name self.csrf_token = None self.domain = domain def store_csrf_token(self, response, **kwargs): if self.csrf_cookie_name in response.cookies: self.csrf_token = response.cookies[self.csrf_cookie_name] def __call__(self, request): if not domain_matches(request, self.domain): return request if self.csrf_token and self.csrf_header_name is not None and (request.method not in self.safe_methods): request.headers[self.csrf_header_name] = self.csrf_token if self.csrf_cookie_name is not None: request.register_hook('response', self.store_csrf_token) return request coreapi-2.3.3/coreapi/client.py000066400000000000000000000146541322523237100164250ustar00rootroot00000000000000from coreapi import codecs, exceptions, transports from coreapi.compat import string_types from coreapi.document import Document, Link from coreapi.utils import determine_transport, get_installed_codecs import collections import itypes LinkAncestor = collections.namedtuple('LinkAncestor', ['document', 'keys']) def _lookup_link(document, keys): """ Validates that keys looking up a link are correct. Returns a two-tuple of (link, link_ancestors). """ if not isinstance(keys, (list, tuple)): msg = "'keys' must be a list of strings or ints." raise TypeError(msg) if any([ not isinstance(key, string_types) and not isinstance(key, int) for key in keys ]): raise TypeError("'keys' must be a list of strings or ints.") # Determine the link node being acted on, and its parent document. # 'node' is the link we're calling the action for. # 'document_keys' is the list of keys to the link's parent document. node = document link_ancestors = [LinkAncestor(document=document, keys=[])] for idx, key in enumerate(keys): try: node = node[key] except (KeyError, IndexError, TypeError): index_string = ''.join('[%s]' % repr(key).strip('u') for key in keys) msg = 'Index %s did not reference a link. Key %s was not found.' raise exceptions.LinkLookupError(msg % (index_string, repr(key).strip('u'))) if isinstance(node, Document): ancestor = LinkAncestor(document=node, keys=keys[:idx + 1]) link_ancestors.append(ancestor) # Ensure that we've correctly indexed into a link. if not isinstance(node, Link): index_string = ''.join('[%s]' % repr(key).strip('u') for key in keys) msg = "Can only call 'action' on a Link. Index %s returned type '%s'." raise exceptions.LinkLookupError( msg % (index_string, type(node).__name__) ) return (node, link_ancestors) def _validate_parameters(link, parameters): """ Ensure that parameters passed to the link are correct. Raises a `ParameterError` if any parameters do not validate. """ provided = set(parameters.keys()) required = set([ field.name for field in link.fields if field.required ]) optional = set([ field.name for field in link.fields if not field.required ]) errors = {} # Determine if any required field names not supplied. missing = required - provided for item in missing: errors[item] = 'This parameter is required.' # Determine any parameter names supplied that are not valid. unexpected = provided - (optional | required) for item in unexpected: errors[item] = 'Unknown parameter.' if errors: raise exceptions.ParameterError(errors) def get_default_decoders(): return [ codecs.CoreJSONCodec(), codecs.JSONCodec(), codecs.TextCodec(), codecs.DownloadCodec() ] def get_default_transports(auth=None, session=None): return [ transports.HTTPTransport(auth=auth, session=session) ] class Client(itypes.Object): def __init__(self, decoders=None, transports=None, auth=None, session=None): assert transports is None or auth is None, ( "Cannot specify both 'auth' and 'transports'. " "When specifying transport instances explicitly you should set " "the authentication directly on the transport." ) if decoders is None: decoders = get_default_decoders() if transports is None: transports = get_default_transports(auth=auth) self._decoders = itypes.List(decoders) self._transports = itypes.List(transports) @property def decoders(self): return self._decoders @property def transports(self): return self._transports def get(self, url, format=None, force_codec=False): link = Link(url, action='get') decoders = self.decoders if format: force_codec = True decoders = [decoder for decoder in self.decoders if decoder.format == format] if not decoders: installed_codecs = get_installed_codecs() if format in installed_codecs: decoders = [installed_codecs[format]] else: raise ValueError("No decoder available with format='%s'" % format) # Perform the action, and return a new document. transport = determine_transport(self.transports, link.url) return transport.transition(link, decoders, force_codec=force_codec) def reload(self, document, format=None, force_codec=False): # Fallback for v1.x. To be removed in favour of explict `get` style. return self.get(document.url, format=format, force_codec=force_codec) def action(self, document, keys, params=None, validate=True, overrides=None, action=None, encoding=None, transform=None): if (action is not None) or (encoding is not None) or (transform is not None): # Fallback for v1.x overrides. # Will be removed at some point, most likely in a 2.1 release. if overrides is None: overrides = {} if action is not None: overrides['action'] = action if encoding is not None: overrides['encoding'] = encoding if transform is not None: overrides['transform'] = transform if isinstance(keys, string_types): keys = [keys] if params is None: params = {} # Validate the keys and link parameters. link, link_ancestors = _lookup_link(document, keys) if validate: _validate_parameters(link, params) if overrides: # Handle any explicit overrides. url = overrides.get('url', link.url) action = overrides.get('action', link.action) encoding = overrides.get('encoding', link.encoding) transform = overrides.get('transform', link.transform) fields = overrides.get('fields', link.fields) link = Link(url, action=action, encoding=encoding, transform=transform, fields=fields) # Perform the action, and return a new document. transport = determine_transport(self.transports, link.url) return transport.transition(link, self.decoders, params=params, link_ancestors=link_ancestors) coreapi-2.3.3/coreapi/codecs/000077500000000000000000000000001322523237100160235ustar00rootroot00000000000000coreapi-2.3.3/coreapi/codecs/__init__.py000066400000000000000000000007231322523237100201360ustar00rootroot00000000000000# coding: utf-8 from coreapi.codecs.base import BaseCodec from coreapi.codecs.corejson import CoreJSONCodec from coreapi.codecs.display import DisplayCodec from coreapi.codecs.download import DownloadCodec from coreapi.codecs.jsondata import JSONCodec from coreapi.codecs.python import PythonCodec from coreapi.codecs.text import TextCodec __all__ = [ 'BaseCodec', 'CoreJSONCodec', 'DisplayCodec', 'JSONCodec', 'PythonCodec', 'TextCodec', 'DownloadCodec' ] coreapi-2.3.3/coreapi/codecs/base.py000066400000000000000000000023761322523237100173170ustar00rootroot00000000000000import itypes class BaseCodec(itypes.Object): media_type = None # We don't implement stubs, to ensure that we can check which of these # two operations a codec supports. For example: # `if hasattr(codec, 'decode'): ...` # def decode(self, bytestring, **options): # pass # def encode(self, document, **options): # pass # The following will be removed at some point, most likely in a 2.1 release: def dump(self, *args, **kwargs): # Fallback for v1.x interface return self.encode(*args, **kwargs) def load(self, *args, **kwargs): # Fallback for v1.x interface return self.decode(*args, **kwargs) @property def supports(self): # Fallback for v1.x interface. if '+' not in self.media_type: return ['data'] ret = [] if hasattr(self, 'encode'): ret.append('encoding') if hasattr(self, 'decode'): ret.append('decoding') return ret def get_media_types(self): # Fallback, while transitioning from `application/vnd.coreapi+json` # to simply `application/coreapi+json`. if hasattr(self, 'media_types'): return list(self.media_types) return [self.media_type] coreapi-2.3.3/coreapi/codecs/corejson.py000066400000000000000000000237261322523237100202310ustar00rootroot00000000000000from __future__ import unicode_literals from collections import OrderedDict from coreapi.codecs.base import BaseCodec from coreapi.compat import force_bytes, string_types, urlparse from coreapi.compat import COMPACT_SEPARATORS, VERBOSE_SEPARATORS from coreapi.document import Document, Link, Array, Object, Error, Field from coreapi.exceptions import ParseError import coreschema import json # Schema encoding and decoding. # Just a naive first-pass at this point. SCHEMA_CLASS_TO_TYPE_ID = { coreschema.Object: 'object', coreschema.Array: 'array', coreschema.Number: 'number', coreschema.Integer: 'integer', coreschema.String: 'string', coreschema.Boolean: 'boolean', coreschema.Null: 'null', coreschema.Enum: 'enum', coreschema.Anything: 'anything' } TYPE_ID_TO_SCHEMA_CLASS = { value: key for key, value in SCHEMA_CLASS_TO_TYPE_ID.items() } def encode_schema_to_corejson(schema): if hasattr(schema, 'typename'): type_id = schema.typename else: type_id = SCHEMA_CLASS_TO_TYPE_ID.get(schema.__class__, 'anything') retval = { '_type': type_id, 'title': schema.title, 'description': schema.description } if hasattr(schema, 'enum'): retval['enum'] = schema.enum return retval def decode_schema_from_corejson(data): type_id = _get_string(data, '_type') title = _get_string(data, 'title') description = _get_string(data, 'description') kwargs = {} if type_id == 'enum': kwargs['enum'] = _get_list(data, 'enum') schema_cls = TYPE_ID_TO_SCHEMA_CLASS.get(type_id, coreschema.Anything) return schema_cls(title=title, description=description, **kwargs) # Robust dictionary lookups, that always return an item of the correct # type, using an empty default if an incorrect type exists. # Useful for liberal parsing of inputs. def _get_schema(item, key): schema_data = _get_dict(item, key) if schema_data: return decode_schema_from_corejson(schema_data) return None def _get_string(item, key): value = item.get(key) if isinstance(value, string_types): return value return '' def _get_dict(item, key): value = item.get(key) if isinstance(value, dict): return value return {} def _get_list(item, key): value = item.get(key) if isinstance(value, list): return value return [] def _get_bool(item, key): value = item.get(key) if isinstance(value, bool): return value return False def _graceful_relative_url(base_url, url): """ Return a graceful link for a URL relative to a base URL. * If they are the same, return an empty string. * If the have the same scheme and hostname, return the path & query params. * Otherwise return the full URL. """ if url == base_url: return '' base_prefix = '%s://%s' % urlparse.urlparse(base_url or '')[0:2] url_prefix = '%s://%s' % urlparse.urlparse(url or '')[0:2] if base_prefix == url_prefix and url_prefix != '://': return url[len(url_prefix):] return url def _escape_key(string): """ The '_type' and '_meta' keys are reserved. Prefix with an additional '_' if they occur. """ if string.startswith('_') and string.lstrip('_') in ('type', 'meta'): return '_' + string return string def _unescape_key(string): """ Unescape '__type' and '__meta' keys if they occur. """ if string.startswith('__') and string.lstrip('_') in ('type', 'meta'): return string[1:] return string def _get_content(item, base_url=None): """ Return a dictionary of content, for documents, objects and errors. """ return { _unescape_key(key): _primitive_to_document(value, base_url) for key, value in item.items() if key not in ('_type', '_meta') } def _document_to_primitive(node, base_url=None): """ Take a Core API document and return Python primitives ready to be rendered into the JSON style encoding. """ if isinstance(node, Document): ret = OrderedDict() ret['_type'] = 'document' meta = OrderedDict() url = _graceful_relative_url(base_url, node.url) if url: meta['url'] = url if node.title: meta['title'] = node.title if node.description: meta['description'] = node.description if meta: ret['_meta'] = meta # Fill in key-value content. ret.update([ (_escape_key(key), _document_to_primitive(value, base_url=url)) for key, value in node.items() ]) return ret elif isinstance(node, Error): ret = OrderedDict() ret['_type'] = 'error' if node.title: ret['_meta'] = {'title': node.title} # Fill in key-value content. ret.update([ (_escape_key(key), _document_to_primitive(value, base_url=base_url)) for key, value in node.items() ]) return ret elif isinstance(node, Link): ret = OrderedDict() ret['_type'] = 'link' url = _graceful_relative_url(base_url, node.url) if url: ret['url'] = url if node.action: ret['action'] = node.action if node.encoding: ret['encoding'] = node.encoding if node.transform: ret['transform'] = node.transform if node.title: ret['title'] = node.title if node.description: ret['description'] = node.description if node.fields: ret['fields'] = [ _document_to_primitive(field) for field in node.fields ] return ret elif isinstance(node, Field): ret = OrderedDict({'name': node.name}) if node.required: ret['required'] = node.required if node.location: ret['location'] = node.location if node.schema: ret['schema'] = encode_schema_to_corejson(node.schema) return ret elif isinstance(node, Object): return OrderedDict([ (_escape_key(key), _document_to_primitive(value, base_url=base_url)) for key, value in node.items() ]) elif isinstance(node, Array): return [_document_to_primitive(value) for value in node] return node def _primitive_to_document(data, base_url=None): """ Take Python primitives as returned from parsing JSON content, and return a Core API document. """ if isinstance(data, dict) and data.get('_type') == 'document': # Document meta = _get_dict(data, '_meta') url = _get_string(meta, 'url') url = urlparse.urljoin(base_url, url) title = _get_string(meta, 'title') description = _get_string(meta, 'description') content = _get_content(data, base_url=url) return Document( url=url, title=title, description=description, media_type='application/coreapi+json', content=content ) if isinstance(data, dict) and data.get('_type') == 'error': # Error meta = _get_dict(data, '_meta') title = _get_string(meta, 'title') content = _get_content(data, base_url=base_url) return Error(title=title, content=content) elif isinstance(data, dict) and data.get('_type') == 'link': # Link url = _get_string(data, 'url') url = urlparse.urljoin(base_url, url) action = _get_string(data, 'action') encoding = _get_string(data, 'encoding') transform = _get_string(data, 'transform') title = _get_string(data, 'title') description = _get_string(data, 'description') fields = _get_list(data, 'fields') fields = [ Field( name=_get_string(item, 'name'), required=_get_bool(item, 'required'), location=_get_string(item, 'location'), schema=_get_schema(item, 'schema') ) for item in fields if isinstance(item, dict) ] return Link( url=url, action=action, encoding=encoding, transform=transform, title=title, description=description, fields=fields ) elif isinstance(data, dict): # Map content = _get_content(data, base_url=base_url) return Object(content) elif isinstance(data, list): # Array content = [_primitive_to_document(item, base_url) for item in data] return Array(content) # String, Integer, Number, Boolean, null. return data class CoreJSONCodec(BaseCodec): media_type = 'application/coreapi+json' format = 'corejson' # The following is due to be deprecated... media_types = ['application/coreapi+json', 'application/vnd.coreapi+json'] def decode(self, bytestring, **options): """ Takes a bytestring and returns a document. """ base_url = options.get('base_url') try: data = json.loads(bytestring.decode('utf-8')) except ValueError as exc: raise ParseError('Malformed JSON. %s' % exc) doc = _primitive_to_document(data, base_url) if isinstance(doc, Object): doc = Document(content=dict(doc)) elif not (isinstance(doc, Document) or isinstance(doc, Error)): raise ParseError('Top level node should be a document or error.') return doc def encode(self, document, **options): """ Takes a document and returns a bytestring. """ indent = options.get('indent') if indent: kwargs = { 'ensure_ascii': False, 'indent': 4, 'separators': VERBOSE_SEPARATORS } else: kwargs = { 'ensure_ascii': False, 'indent': None, 'separators': COMPACT_SEPARATORS } data = _document_to_primitive(document) return force_bytes(json.dumps(data, **kwargs)) coreapi-2.3.3/coreapi/codecs/display.py000066400000000000000000000104411322523237100200420ustar00rootroot00000000000000# Note that `DisplayCodec` is deliberately omitted from the documentation, # as it is considered an implementation detail. # It may move into a utility function in the future. from __future__ import unicode_literals from coreapi.codecs.base import BaseCodec from coreapi.compat import console_style, string_types from coreapi.document import Document, Link, Array, Object, Error import json def _colorize_document(text): return console_style(text, fg='green') # pragma: nocover def _colorize_error(text): return console_style(text, fg='red') # pragma: nocover def _colorize_keys(text): return console_style(text, fg='cyan') # pragma: nocover def _to_plaintext(node, indent=0, base_url=None, colorize=False, extra_offset=None): colorize_document = _colorize_document if colorize else lambda x: x colorize_error = _colorize_error if colorize else lambda x: x colorize_keys = _colorize_keys if colorize else lambda x: x if isinstance(node, Document): head_indent = ' ' * indent body_indent = ' ' * (indent + 1) body = '\n'.join([ body_indent + colorize_keys(str(key) + ': ') + _to_plaintext(value, indent + 1, base_url=base_url, colorize=colorize, extra_offset=len(str(key))) for key, value in node.data.items() ] + [ body_indent + colorize_keys(str(key) + '(') + _fields_to_plaintext(value, colorize=colorize) + colorize_keys(')') for key, value in node.links.items() ]) head = colorize_document('<%s %s>' % ( node.title.strip() or 'Document', json.dumps(node.url) )) return head if (not body) else head + '\n' + body elif isinstance(node, Object): head_indent = ' ' * indent body_indent = ' ' * (indent + 1) body = '\n'.join([ body_indent + colorize_keys(str(key)) + ': ' + _to_plaintext(value, indent + 1, base_url=base_url, colorize=colorize, extra_offset=len(str(key))) for key, value in node.data.items() ] + [ body_indent + colorize_keys(str(key) + '(') + _fields_to_plaintext(value, colorize=colorize) + colorize_keys(')') for key, value in node.links.items() ]) return '{}' if (not body) else '{\n' + body + '\n' + head_indent + '}' if isinstance(node, Error): head_indent = ' ' * indent body_indent = ' ' * (indent + 1) body = '\n'.join([ body_indent + colorize_keys(str(key) + ': ') + _to_plaintext(value, indent + 1, base_url=base_url, colorize=colorize, extra_offset=len(str(key))) for key, value in node.items() ]) head = colorize_error('' % node.title.strip() if node.title else '') return head if (not body) else head + '\n' + body elif isinstance(node, Array): head_indent = ' ' * indent body_indent = ' ' * (indent + 1) body = ',\n'.join([ body_indent + _to_plaintext(value, indent + 1, base_url=base_url, colorize=colorize) for value in node ]) return '[]' if (not body) else '[\n' + body + '\n' + head_indent + ']' elif isinstance(node, Link): return ( colorize_keys('link(') + _fields_to_plaintext(node, colorize=colorize) + colorize_keys(')') ) if isinstance(node, string_types) and (extra_offset is not None) and ('\n' in node): # Display newlines in strings gracefully. text = json.dumps(node) spacing = (' ' * indent) + (' ' * extra_offset) + ' ' return text.replace('\\n', '\n' + spacing) return json.dumps(node) def _fields_to_plaintext(link, colorize=False): colorize_keys = _colorize_keys if colorize else lambda x: x return colorize_keys(', ').join([ field.name for field in link.fields if field.required ] + [ '[%s]' % field.name for field in link.fields if not field.required ]) class DisplayCodec(BaseCodec): """ A plaintext representation of a Document, intended for readability. """ media_type = 'text/plain' def encode(self, document, **options): colorize = options.get('colorize', False) return _to_plaintext(document, colorize=colorize) coreapi-2.3.3/coreapi/codecs/download.py000066400000000000000000000107641322523237100202140ustar00rootroot00000000000000# coding: utf-8 from coreapi.codecs.base import BaseCodec from coreapi.compat import urlparse from coreapi.utils import DownloadedFile, guess_extension import cgi import os import posixpath import tempfile def _unique_output_path(path): """ Given a path like '/a/b/c.txt' Return the first available filename that doesn't already exist, using an incrementing suffix if needed. For example: '/a/b/c.txt' or '/a/b/c (1).txt' or '/a/b/c (2).txt'... """ basename, ext = os.path.splitext(path) idx = 0 while os.path.exists(path): idx += 1 path = "%s (%d)%s" % (basename, idx, ext) return path def _safe_filename(filename): """ Sanitize output filenames, to remove any potentially unsafe characters. """ filename = os.path.basename(filename) keepcharacters = (' ', '.', '_', '-') filename = ''.join( char for char in filename if char.isalnum() or char in keepcharacters ).strip().strip('.') return filename def _get_filename_from_content_disposition(content_disposition): """ Determine an output filename based on the `Content-Disposition` header. """ params = value, params = cgi.parse_header(content_disposition) if 'filename*' in params: try: charset, lang, filename = params['filename*'].split('\'', 2) filename = urlparse.unquote(filename) filename = filename.encode('iso-8859-1').decode(charset) return _safe_filename(filename) except (ValueError, LookupError): pass if 'filename' in params: filename = params['filename'] return _safe_filename(filename) return None def _get_filename_from_url(url, content_type=None): """ Determine an output filename based on the download URL. """ parsed = urlparse.urlparse(url) final_path_component = posixpath.basename(parsed.path.rstrip('/')) filename = _safe_filename(final_path_component) suffix = guess_extension(content_type or '') if filename: if '.' not in filename: return filename + suffix return filename elif suffix: return 'download' + suffix return None def _get_filename(base_url=None, content_type=None, content_disposition=None): """ Determine an output filename to use for the download. """ filename = None if content_disposition: filename = _get_filename_from_content_disposition(content_disposition) if base_url and not filename: filename = _get_filename_from_url(base_url, content_type) if not filename: return None # Ensure empty filenames return as `None` for consistency. return filename class DownloadCodec(BaseCodec): """ A codec to handle raw file downloads, such as images and other media. """ media_type = '*/*' format = 'download' def __init__(self, download_dir=None): """ `download_dir` - The path to use for file downloads. """ self._delete_on_close = download_dir is None self._download_dir = download_dir @property def download_dir(self): return self._download_dir def decode(self, bytestring, **options): base_url = options.get('base_url') content_type = options.get('content_type') content_disposition = options.get('content_disposition') # Write the download to a temporary .download file. fd, temp_path = tempfile.mkstemp(suffix='.download') file_handle = os.fdopen(fd, 'wb') file_handle.write(bytestring) file_handle.close() # Determine the output filename. output_filename = _get_filename(base_url, content_type, content_disposition) if output_filename is None: output_filename = os.path.basename(temp_path) # Determine the output directory. output_dir = self._download_dir if output_dir is None: output_dir = os.path.dirname(temp_path) # Determine the full output path. output_path = os.path.join(output_dir, output_filename) # Move the temporary download file to the final location. if output_path != temp_path: output_path = _unique_output_path(output_path) os.rename(temp_path, output_path) # Open the file and return the file object. output_file = open(output_path, 'rb') downloaded = DownloadedFile(output_file, output_path, delete=self._delete_on_close) downloaded.basename = output_filename return downloaded coreapi-2.3.3/coreapi/codecs/jsondata.py000066400000000000000000000010701322523237100201760ustar00rootroot00000000000000# coding: utf-8 from coreapi.codecs.base import BaseCodec from coreapi.exceptions import ParseError import collections import json class JSONCodec(BaseCodec): media_type = 'application/json' format = 'json' def decode(self, bytestring, **options): """ Return raw JSON data. """ try: return json.loads( bytestring.decode('utf-8'), object_pairs_hook=collections.OrderedDict ) except ValueError as exc: raise ParseError('Malformed JSON. %s' % exc) coreapi-2.3.3/coreapi/codecs/python.py000066400000000000000000000053211322523237100177170ustar00rootroot00000000000000# Note that `DisplayCodec` is deliberately omitted from the documentation, # as it is considered an implementation detail. # It may move into a utility function in the future. from __future__ import unicode_literals from coreapi.codecs.base import BaseCodec from coreapi.document import Document, Link, Array, Object, Error, Field def _to_repr(node): if isinstance(node, Document): content = ', '.join([ '%s: %s' % (repr(key), _to_repr(value)) for key, value in node.items() ]) return 'Document(url=%s, title=%s, content={%s})' % ( repr(node.url), repr(node.title), content ) elif isinstance(node, Error): content = ', '.join([ '%s: %s' % (repr(key), _to_repr(value)) for key, value in node.items() ]) return 'Error(title=%s, content={%s})' % ( repr(node.title), content ) elif isinstance(node, Object): return '{%s}' % ', '.join([ '%s: %s' % (repr(key), _to_repr(value)) for key, value in node.items() ]) elif isinstance(node, Array): return '[%s]' % ', '.join([ _to_repr(value) for value in node ]) elif isinstance(node, Link): args = "url=%s" % repr(node.url) if node.action: args += ", action=%s" % repr(node.action) if node.encoding: args += ", encoding=%s" % repr(node.encoding) if node.transform: args += ", transform=%s" % repr(node.transform) if node.description: args += ", description=%s" % repr(node.description) if node.fields: fields_repr = ', '.join(_to_repr(item) for item in node.fields) args += ", fields=[%s]" % fields_repr return "Link(%s)" % args elif isinstance(node, Field): args = repr(node.name) if not node.required and not node.location: return args if node.required: args += ', required=True' if node.location: args += ', location=%s' % repr(node.location) if node.schema: args += ', schema=%s' % repr(node.schema) return 'Field(%s)' % args return repr(node) class PythonCodec(BaseCodec): """ A Python representation of a Document, for use with '__repr__'. """ media_type = 'text/python' def encode(self, document, **options): # Object and Array only have the class name wrapper if they # are the outermost element. if isinstance(document, Object): return 'Object(%s)' % _to_repr(document) elif isinstance(document, Array): return 'Array(%s)' % _to_repr(document) return _to_repr(document) coreapi-2.3.3/coreapi/codecs/text.py000066400000000000000000000003361322523237100173630ustar00rootroot00000000000000# coding: utf-8 from coreapi.codecs.base import BaseCodec class TextCodec(BaseCodec): media_type = 'text/*' format = 'text' def decode(self, bytestring, **options): return bytestring.decode('utf-8') coreapi-2.3.3/coreapi/compat.py000066400000000000000000000026111322523237100164200ustar00rootroot00000000000000# coding: utf-8 import base64 __all__ = [ 'urlparse', 'string_types', 'COMPACT_SEPARATORS', 'VERBOSE_SEPARATORS' ] try: # Python 2 import urlparse import cookielib as cookiejar string_types = (basestring,) text_type = unicode COMPACT_SEPARATORS = (b',', b':') VERBOSE_SEPARATORS = (b',', b': ') def b64encode(input_string): # Provide a consistently-as-unicode interface across 2.x and 3.x return base64.b64encode(input_string) except ImportError: # Python 3 import urllib.parse as urlparse from io import IOBase from http import cookiejar string_types = (str,) text_type = str COMPACT_SEPARATORS = (',', ':') VERBOSE_SEPARATORS = (',', ': ') def b64encode(input_string): # Provide a consistently-as-unicode interface across 2.x and 3.x return base64.b64encode(input_string.encode('ascii')).decode('ascii') def force_bytes(string): if isinstance(string, string_types): return string.encode('utf-8') return string def force_text(string): if not isinstance(string, string_types): return string.decode('utf-8') return string try: import click console_style = click.style except ImportError: def console_style(text, **kwargs): return text try: from tempfile import _TemporaryFileWrapper except ImportError: _TemporaryFileWrapper = None coreapi-2.3.3/coreapi/document.py000066400000000000000000000230501322523237100167530ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from collections import OrderedDict, namedtuple from coreapi.compat import string_types import itypes def _to_immutable(value): if isinstance(value, dict): return Object(value) elif isinstance(value, list): return Array(value) return value def _repr(node): from coreapi.codecs.python import PythonCodec return PythonCodec().encode(node) def _str(node): from coreapi.codecs.display import DisplayCodec return DisplayCodec().encode(node) def _key_sorting(item): """ Document and Object sorting. Regular attributes sorted alphabetically. Links are sorted based on their URL and action. """ key, value = item if isinstance(value, Link): action_priority = { 'get': 0, 'post': 1, 'put': 2, 'patch': 3, 'delete': 4 }.get(value.action, 5) return (1, (value.url, action_priority)) return (0, key) # The field class, as used by Link objects: # NOTE: 'type', 'description' and 'example' are now deprecated, # in favor of 'schema'. Field = namedtuple('Field', ['name', 'required', 'location', 'schema', 'description', 'type', 'example']) Field.__new__.__defaults__ = (False, '', None, None, None, None) # The Core API primitives: class Document(itypes.Dict): """ The Core API document type. Expresses the data that the client may access, and the actions that the client may perform. """ def __init__(self, url=None, title=None, description=None, media_type=None, content=None): content = {} if (content is None) else content if url is not None and not isinstance(url, string_types): raise TypeError("'url' must be a string.") if title is not None and not isinstance(title, string_types): raise TypeError("'title' must be a string.") if description is not None and not isinstance(description, string_types): raise TypeError("'description' must be a string.") if media_type is not None and not isinstance(media_type, string_types): raise TypeError("'media_type' must be a string.") if not isinstance(content, dict): raise TypeError("'content' must be a dict.") if any([not isinstance(key, string_types) for key in content.keys()]): raise TypeError('content keys must be strings.') self._url = '' if (url is None) else url self._title = '' if (title is None) else title self._description = '' if (description is None) else description self._media_type = '' if (media_type is None) else media_type self._data = {key: _to_immutable(value) for key, value in content.items()} def clone(self, data): return self.__class__(self.url, self.title, self.description, self.media_type, data) def __iter__(self): items = sorted(self._data.items(), key=_key_sorting) return iter([key for key, value in items]) def __repr__(self): return _repr(self) def __str__(self): return _str(self) def __eq__(self, other): if self.__class__ == other.__class__: return ( self.url == other.url and self.title == other.title and self._data == other._data ) return super(Document, self).__eq__(other) @property def url(self): return self._url @property def title(self): return self._title @property def description(self): return self._description @property def media_type(self): return self._media_type @property def data(self): return OrderedDict([ (key, value) for key, value in self.items() if not isinstance(value, Link) ]) @property def links(self): return OrderedDict([ (key, value) for key, value in self.items() if isinstance(value, Link) ]) class Object(itypes.Dict): """ An immutable mapping of strings to values. """ def __init__(self, *args, **kwargs): data = dict(*args, **kwargs) if any([not isinstance(key, string_types) for key in data.keys()]): raise TypeError('Object keys must be strings.') self._data = {key: _to_immutable(value) for key, value in data.items()} def __iter__(self): items = sorted(self._data.items(), key=_key_sorting) return iter([key for key, value in items]) def __repr__(self): return _repr(self) def __str__(self): return _str(self) @property def data(self): return OrderedDict([ (key, value) for key, value in self.items() if not isinstance(value, Link) ]) @property def links(self): return OrderedDict([ (key, value) for key, value in self.items() if isinstance(value, Link) ]) class Array(itypes.List): """ An immutable list type container. """ def __init__(self, *args): self._data = [_to_immutable(value) for value in list(*args)] def __repr__(self): return _repr(self) def __str__(self): return _str(self) class Link(itypes.Object): """ Links represent the actions that a client may perform. """ def __init__(self, url=None, action=None, encoding=None, transform=None, title=None, description=None, fields=None): if (url is not None) and (not isinstance(url, string_types)): raise TypeError("Argument 'url' must be a string.") if (action is not None) and (not isinstance(action, string_types)): raise TypeError("Argument 'action' must be a string.") if (encoding is not None) and (not isinstance(encoding, string_types)): raise TypeError("Argument 'encoding' must be a string.") if (transform is not None) and (not isinstance(transform, string_types)): raise TypeError("Argument 'transform' must be a string.") if (title is not None) and (not isinstance(title, string_types)): raise TypeError("Argument 'title' must be a string.") if (description is not None) and (not isinstance(description, string_types)): raise TypeError("Argument 'description' must be a string.") if (fields is not None) and (not isinstance(fields, (list, tuple))): raise TypeError("Argument 'fields' must be a list.") if (fields is not None) and any([ not (isinstance(item, string_types) or isinstance(item, Field)) for item in fields ]): raise TypeError("Argument 'fields' must be a list of strings or fields.") self._url = '' if (url is None) else url self._action = '' if (action is None) else action self._encoding = '' if (encoding is None) else encoding self._transform = '' if (transform is None) else transform self._title = '' if (title is None) else title self._description = '' if (description is None) else description self._fields = () if (fields is None) else tuple([ item if isinstance(item, Field) else Field(item, required=False, location='') for item in fields ]) @property def url(self): return self._url @property def action(self): return self._action @property def encoding(self): return self._encoding @property def transform(self): return self._transform @property def title(self): return self._title @property def description(self): return self._description @property def fields(self): return self._fields def __eq__(self, other): return ( isinstance(other, Link) and self.url == other.url and self.action == other.action and self.encoding == other.encoding and self.transform == other.transform and self.description == other.description and sorted(self.fields, key=lambda f: f.name) == sorted(other.fields, key=lambda f: f.name) ) def __repr__(self): return _repr(self) def __str__(self): return _str(self) class Error(itypes.Dict): def __init__(self, title=None, content=None): data = {} if (content is None) else content if title is not None and not isinstance(title, string_types): raise TypeError("'title' must be a string.") if content is not None and not isinstance(content, dict): raise TypeError("'content' must be a dict.") if any([not isinstance(key, string_types) for key in data.keys()]): raise TypeError('content keys must be strings.') self._title = '' if (title is None) else title self._data = {key: _to_immutable(value) for key, value in data.items()} def __iter__(self): items = sorted(self._data.items(), key=_key_sorting) return iter([key for key, value in items]) def __repr__(self): return _repr(self) def __str__(self): return _str(self) def __eq__(self, other): return ( isinstance(other, Error) and self.title == other.title and self._data == other._data ) @property def title(self): return self._title def get_messages(self): messages = [] for value in self.values(): if isinstance(value, Array): messages += [ item for item in value if isinstance(item, string_types) ] return messages coreapi-2.3.3/coreapi/exceptions.py000066400000000000000000000024501322523237100173170ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals class CoreAPIException(Exception): """ A base class for all `coreapi` exceptions. """ pass class ParseError(CoreAPIException): """ Raised when an invalid Core API encoding is encountered. """ pass class NoCodecAvailable(CoreAPIException): """ Raised when there is no available codec that can handle the given media. """ pass class NetworkError(CoreAPIException): """ Raised when the transport layer fails to make a request or get a response. """ pass class LinkLookupError(CoreAPIException): """ Raised when `.action` fails to index a link in the document. """ pass class ParameterError(CoreAPIException): """ Raised when the parameters passed do not match the link fields. * A required field was not included. * An unknown field was included. * A field was passed an invalid type for the link location/encoding. """ pass class ErrorMessage(CoreAPIException): """ Raised when the transition returns an error message. """ def __init__(self, error): self.error = error def __repr__(self): return '%s(%s)' % (self.__class__.__name__, repr(self.error)) def __str__(self): return str(self.error) coreapi-2.3.3/coreapi/transports/000077500000000000000000000000001322523237100170025ustar00rootroot00000000000000coreapi-2.3.3/coreapi/transports/__init__.py000066400000000000000000000002511322523237100211110ustar00rootroot00000000000000# coding: utf-8 from coreapi.transports.base import BaseTransport from coreapi.transports.http import HTTPTransport __all__ = [ 'BaseTransport', 'HTTPTransport' ] coreapi-2.3.3/coreapi/transports/base.py000066400000000000000000000003561322523237100202720ustar00rootroot00000000000000# coding: utf-8 import itypes class BaseTransport(itypes.Object): schemes = None def transition(self, link, decoders, params=None, link_ancestors=None, force_codec=False): raise NotImplementedError() # pragma: nocover coreapi-2.3.3/coreapi/transports/http.py000066400000000000000000000304421322523237100203360ustar00rootroot00000000000000# coding: utf-8 from __future__ import unicode_literals from collections import OrderedDict from coreapi import exceptions, utils from coreapi.compat import cookiejar, urlparse from coreapi.document import Document, Object, Link, Array, Error from coreapi.transports.base import BaseTransport from coreapi.utils import guess_filename, is_file, File import collections import requests import itypes import mimetypes import uritemplate import warnings Params = collections.namedtuple('Params', ['path', 'query', 'data', 'files']) empty_params = Params({}, {}, {}, {}) class ForceMultiPartDict(dict): """ A dictionary that always evaluates as True. Allows us to force requests to use multipart encoding, even when no file parameters are passed. """ def __bool__(self): return True def __nonzero__(self): return True class BlockAll(cookiejar.CookiePolicy): """ A cookie policy that rejects all cookies. Used to override the default `requests` behavior. """ return_ok = set_ok = domain_return_ok = path_return_ok = lambda self, *args, **kwargs: False netscape = True rfc2965 = hide_cookie2 = False class DomainCredentials(requests.auth.AuthBase): """ Custom auth class to support deprecated 'credentials' argument. """ allow_cookies = False credentials = None def __init__(self, credentials=None): self.credentials = credentials def __call__(self, request): if not self.credentials: return request # Include any authorization credentials relevant to this domain. url_components = urlparse.urlparse(request.url) host = url_components.hostname if host in self.credentials: request.headers['Authorization'] = self.credentials[host] return request class CallbackAdapter(requests.adapters.HTTPAdapter): """ Custom requests HTTP adapter, to support deprecated callback arguments. """ def __init__(self, request_callback=None, response_callback=None): self.request_callback = request_callback self.response_callback = response_callback def send(self, request, **kwargs): if self.request_callback is not None: self.request_callback(request) response = super(CallbackAdapter, self).send(request, **kwargs) if self.response_callback is not None: self.response_callback(response) return response def _get_method(action): if not action: return 'GET' return action.upper() def _get_encoding(encoding): if not encoding: return 'application/json' return encoding def _get_params(method, encoding, fields, params=None): """ Separate the params into the various types. """ if params is None: return empty_params field_map = {field.name: field for field in fields} path = {} query = {} data = {} files = {} errors = {} # Ensure graceful behavior in edge-case where both location='body' and # location='form' fields are present. seen_body = False for key, value in params.items(): if key not in field_map or not field_map[key].location: # Default is 'query' for 'GET' and 'DELETE', and 'form' for others. location = 'query' if method in ('GET', 'DELETE') else 'form' else: location = field_map[key].location if location == 'form' and encoding == 'application/octet-stream': # Raw uploads should always use 'body', not 'form'. location = 'body' try: if location == 'path': path[key] = utils.validate_path_param(value) elif location == 'query': query[key] = utils.validate_query_param(value) elif location == 'body': data = utils.validate_body_param(value, encoding=encoding) seen_body = True elif location == 'form': if not seen_body: data[key] = utils.validate_form_param(value, encoding=encoding) except exceptions.ParameterError as exc: errors[key] = "%s" % exc if errors: raise exceptions.ParameterError(errors) # Move any files from 'data' into 'files'. if isinstance(data, dict): for key, value in list(data.items()): if is_file(data[key]): files[key] = data.pop(key) return Params(path, query, data, files) def _get_url(url, path_params): """ Given a templated URL and some parameters that have been provided, expand the URL. """ if path_params: return uritemplate.expand(url, path_params) return url def _get_headers(url, decoders): """ Return a dictionary of HTTP headers to use in the outgoing request. """ accept_media_types = decoders[0].get_media_types() if '*/*' not in accept_media_types: accept_media_types.append('*/*') headers = { 'accept': ', '.join(accept_media_types), 'user-agent': 'coreapi' } return headers def _get_upload_headers(file_obj): """ When a raw file upload is made, determine the Content-Type and Content-Disposition headers to use with the request. """ name = guess_filename(file_obj) content_type = None content_disposition = None # Determine the content type of the upload. if getattr(file_obj, 'content_type', None): content_type = file_obj.content_type elif name: content_type, encoding = mimetypes.guess_type(name) # Determine the content disposition of the upload. if name: content_disposition = 'attachment; filename="%s"' % name return { 'Content-Type': content_type or 'application/octet-stream', 'Content-Disposition': content_disposition or 'attachment' } def _build_http_request(session, url, method, headers=None, encoding=None, params=empty_params): """ Make an HTTP request and return an HTTP response. """ opts = { "headers": headers or {} } if params.query: opts['params'] = params.query if params.data or params.files: if encoding == 'application/json': opts['json'] = params.data elif encoding == 'multipart/form-data': opts['data'] = params.data opts['files'] = ForceMultiPartDict(params.files) elif encoding == 'application/x-www-form-urlencoded': opts['data'] = params.data elif encoding == 'application/octet-stream': if isinstance(params.data, File): opts['data'] = params.data.content else: opts['data'] = params.data upload_headers = _get_upload_headers(params.data) opts['headers'].update(upload_headers) request = requests.Request(method, url, **opts) return session.prepare_request(request) def _coerce_to_error_content(node): """ Errors should not contain nested documents or links. If we get a 4xx or 5xx response with a Document, then coerce the document content into plain data. """ if isinstance(node, (Document, Object)): # Strip Links from Documents, treat Documents as plain dicts. return OrderedDict([ (key, _coerce_to_error_content(value)) for key, value in node.data.items() ]) elif isinstance(node, Array): # Strip Links from Arrays. return [ _coerce_to_error_content(item) for item in node if not isinstance(item, Link) ] return node def _coerce_to_error(obj, default_title): """ Given an arbitrary return result, coerce it into an Error instance. """ if isinstance(obj, Document): return Error( title=obj.title or default_title, content=_coerce_to_error_content(obj) ) elif isinstance(obj, dict): return Error(title=default_title, content=obj) elif isinstance(obj, list): return Error(title=default_title, content={'messages': obj}) elif obj is None: return Error(title=default_title) return Error(title=default_title, content={'message': obj}) def _decode_result(response, decoders, force_codec=False): """ Given an HTTP response, return the decoded Core API document. """ if response.content: # Content returned in response. We should decode it. if force_codec: codec = decoders[0] else: content_type = response.headers.get('content-type') codec = utils.negotiate_decoder(decoders, content_type) options = { 'base_url': response.url } if 'content-type' in response.headers: options['content_type'] = response.headers['content-type'] if 'content-disposition' in response.headers: options['content_disposition'] = response.headers['content-disposition'] result = codec.load(response.content, **options) else: # No content returned in response. result = None # Coerce 4xx and 5xx codes into errors. is_error = response.status_code >= 400 and response.status_code <= 599 if is_error and not isinstance(result, Error): default_title = '%d %s' % (response.status_code, response.reason) result = _coerce_to_error(result, default_title=default_title) return result def _handle_inplace_replacements(document, link, link_ancestors): """ Given a new document, and the link/ancestors it was created, determine if we should: * Make an inline replacement and then return the modified document tree. * Return the new document as-is. """ if not link.transform: if link.action.lower() in ('put', 'patch', 'delete'): transform = 'inplace' else: transform = 'new' else: transform = link.transform if transform == 'inplace': root = link_ancestors[0].document keys_to_link_parent = link_ancestors[-1].keys if document is None: return root.delete_in(keys_to_link_parent) return root.set_in(keys_to_link_parent, document) return document class HTTPTransport(BaseTransport): schemes = ['http', 'https'] def __init__(self, credentials=None, headers=None, auth=None, session=None, request_callback=None, response_callback=None): if headers: headers = {key.lower(): value for key, value in headers.items()} if session is None: session = requests.Session() if auth is not None: session.auth = auth if not getattr(session.auth, 'allow_cookies', False): session.cookies.set_policy(BlockAll()) if credentials is not None: warnings.warn( "The 'credentials' argument is now deprecated in favor of 'auth'.", DeprecationWarning ) auth = DomainCredentials(credentials) if request_callback is not None or response_callback is not None: warnings.warn( "The 'request_callback' and 'response_callback' arguments are now deprecated. " "Use a custom 'session' instance instead.", DeprecationWarning ) session.mount('https://', CallbackAdapter(request_callback, response_callback)) session.mount('http://', CallbackAdapter(request_callback, response_callback)) self._headers = itypes.Dict(headers or {}) self._session = session @property def headers(self): return self._headers def transition(self, link, decoders, params=None, link_ancestors=None, force_codec=False): session = self._session method = _get_method(link.action) encoding = _get_encoding(link.encoding) params = _get_params(method, encoding, link.fields, params) url = _get_url(link.url, params.path) headers = _get_headers(url, decoders) headers.update(self.headers) request = _build_http_request(session, url, method, headers, encoding, params) response = session.send(request) result = _decode_result(response, decoders, force_codec) if isinstance(result, Document) and link_ancestors: result = _handle_inplace_replacements(result, link, link_ancestors) if isinstance(result, Error): raise exceptions.ErrorMessage(result) return result coreapi-2.3.3/coreapi/utils.py000066400000000000000000000255261322523237100163070ustar00rootroot00000000000000from coreapi import exceptions from coreapi.compat import string_types, text_type, urlparse, _TemporaryFileWrapper from collections import namedtuple import os import pkg_resources import tempfile def domain_matches(request, domain): """ Domain string matching against an outgoing request. Patterns starting with '*' indicate a wildcard domain. """ if (domain is None) or (domain == '*'): return True host = urlparse.urlparse(request.url).hostname if domain.startswith('*'): return host.endswith(domain[1:]) return host == domain def get_installed_codecs(): packages = [ (package, package.load()) for package in pkg_resources.iter_entry_points(group='coreapi.codecs') ] return { package.name: cls() for (package, cls) in packages } # File utilities for upload and download support. File = namedtuple('File', 'name content content_type') File.__new__.__defaults__ = (None,) def is_file(obj): if isinstance(obj, File): return True if hasattr(obj, '__iter__') and not isinstance(obj, (string_types, list, tuple, dict)): # A stream object. return True return False def guess_filename(obj): name = getattr(obj, 'name', None) if name and isinstance(name, string_types) and name[0] != '<' and name[-1] != '>': return os.path.basename(name) return None def guess_extension(content_type): """ Python's `mimetypes.guess_extension` is no use because it simply returns the first of an unordered set. We use the same set of media types here, but take a reasonable preference on what extension to map to. """ return { 'application/javascript': '.js', 'application/msword': '.doc', 'application/octet-stream': '.bin', 'application/oda': '.oda', 'application/pdf': '.pdf', 'application/pkcs7-mime': '.p7c', 'application/postscript': '.ps', 'application/vnd.apple.mpegurl': '.m3u', 'application/vnd.ms-excel': '.xls', 'application/vnd.ms-powerpoint': '.ppt', 'application/x-bcpio': '.bcpio', 'application/x-cpio': '.cpio', 'application/x-csh': '.csh', 'application/x-dvi': '.dvi', 'application/x-gtar': '.gtar', 'application/x-hdf': '.hdf', 'application/x-latex': '.latex', 'application/x-mif': '.mif', 'application/x-netcdf': '.nc', 'application/x-pkcs12': '.p12', 'application/x-pn-realaudio': '.ram', 'application/x-python-code': '.pyc', 'application/x-sh': '.sh', 'application/x-shar': '.shar', 'application/x-shockwave-flash': '.swf', 'application/x-sv4cpio': '.sv4cpio', 'application/x-sv4crc': '.sv4crc', 'application/x-tar': '.tar', 'application/x-tcl': '.tcl', 'application/x-tex': '.tex', 'application/x-texinfo': '.texinfo', 'application/x-troff': '.tr', 'application/x-troff-man': '.man', 'application/x-troff-me': '.me', 'application/x-troff-ms': '.ms', 'application/x-ustar': '.ustar', 'application/x-wais-source': '.src', 'application/xml': '.xml', 'application/zip': '.zip', 'audio/basic': '.au', 'audio/mpeg': '.mp3', 'audio/x-aiff': '.aif', 'audio/x-pn-realaudio': '.ra', 'audio/x-wav': '.wav', 'image/gif': '.gif', 'image/ief': '.ief', 'image/jpeg': '.jpe', 'image/png': '.png', 'image/svg+xml': '.svg', 'image/tiff': '.tiff', 'image/vnd.microsoft.icon': '.ico', 'image/x-cmu-raster': '.ras', 'image/x-ms-bmp': '.bmp', 'image/x-portable-anymap': '.pnm', 'image/x-portable-bitmap': '.pbm', 'image/x-portable-graymap': '.pgm', 'image/x-portable-pixmap': '.ppm', 'image/x-rgb': '.rgb', 'image/x-xbitmap': '.xbm', 'image/x-xpixmap': '.xpm', 'image/x-xwindowdump': '.xwd', 'message/rfc822': '.eml', 'text/css': '.css', 'text/csv': '.csv', 'text/html': '.html', 'text/plain': '.txt', 'text/richtext': '.rtx', 'text/tab-separated-values': '.tsv', 'text/x-python': '.py', 'text/x-setext': '.etx', 'text/x-sgml': '.sgml', 'text/x-vcard': '.vcf', 'text/xml': '.xml', 'video/mp4': '.mp4', 'video/mpeg': '.mpeg', 'video/quicktime': '.mov', 'video/webm': '.webm', 'video/x-msvideo': '.avi', 'video/x-sgi-movie': '.movie' }.get(content_type, '') if _TemporaryFileWrapper: # Ideally we subclass this so that we can present a custom representation. class DownloadedFile(_TemporaryFileWrapper): basename = None def __repr__(self): state = "closed" if self.closed else "open" mode = "" if self.closed else " '%s'" % self.file.mode return "" % (self.name, state, mode) def __str__(self): return self.__repr__() else: # On some platforms (eg GAE) the private _TemporaryFileWrapper may not be # available, just use the standard `NamedTemporaryFile` function # in this case. DownloadedFile = tempfile.NamedTemporaryFile # Negotiation utilities. USed to determine which codec or transport class # should be used, given a list of supported instances. def determine_transport(transports, url): """ Given a URL determine the appropriate transport instance. """ url_components = urlparse.urlparse(url) scheme = url_components.scheme.lower() netloc = url_components.netloc if not scheme: raise exceptions.NetworkError("URL missing scheme '%s'." % url) if not netloc: raise exceptions.NetworkError("URL missing hostname '%s'." % url) for transport in transports: if scheme in transport.schemes: return transport raise exceptions.NetworkError("Unsupported URL scheme '%s'." % scheme) def negotiate_decoder(decoders, content_type=None): """ Given the value of a 'Content-Type' header, return the appropriate codec for decoding the request content. """ if content_type is None: return decoders[0] content_type = content_type.split(';')[0].strip().lower() main_type = content_type.split('/')[0] + '/*' wildcard_type = '*/*' for codec in decoders: for media_type in codec.get_media_types(): if media_type in (content_type, main_type, wildcard_type): return codec msg = "Unsupported media in Content-Type header '%s'" % content_type raise exceptions.NoCodecAvailable(msg) def negotiate_encoder(encoders, accept=None): """ Given the value of a 'Accept' header, return the appropriate codec for encoding the response content. """ if accept is None: return encoders[0] acceptable = set([ item.split(';')[0].strip().lower() for item in accept.split(',') ]) for codec in encoders: for media_type in codec.get_media_types(): if media_type in acceptable: return codec for codec in encoders: for media_type in codec.get_media_types(): if codec.media_type.split('/')[0] + '/*' in acceptable: return codec if '*/*' in acceptable: return encoders[0] msg = "Unsupported media in Accept header '%s'" % accept raise exceptions.NoCodecAvailable(msg) # Validation utilities. Used to ensure that we get consistent validation # exceptions when invalid types are passed as a parameter, rather than # an exception occuring when the request is made. def validate_path_param(value): value = _validate_form_field(value, allow_list=False) if not value: msg = 'Parameter %s: May not be empty.' raise exceptions.ParameterError(msg) return value def validate_query_param(value): return _validate_form_field(value) def validate_body_param(value, encoding): if encoding == 'application/json': return _validate_json_data(value) elif encoding == 'multipart/form-data': return _validate_form_object(value, allow_files=True) elif encoding == 'application/x-www-form-urlencoded': return _validate_form_object(value) elif encoding == 'application/octet-stream': if not is_file(value): msg = 'Must be an file upload.' raise exceptions.ParameterError(msg) return value msg = 'Unsupported encoding "%s" for outgoing request.' raise exceptions.NetworkError(msg % encoding) def validate_form_param(value, encoding): if encoding == 'application/json': return _validate_json_data(value) elif encoding == 'multipart/form-data': return _validate_form_field(value, allow_files=True) elif encoding == 'application/x-www-form-urlencoded': return _validate_form_field(value) msg = 'Unsupported encoding "%s" for outgoing request.' raise exceptions.NetworkError(msg % encoding) def _validate_form_object(value, allow_files=False): """ Ensure that `value` can be encoded as form data or as query parameters. """ if not isinstance(value, dict): msg = 'Must be an object.' raise exceptions.ParameterError(msg) return { text_type(item_key): _validate_form_field(item_val, allow_files=allow_files) for item_key, item_val in value.items() } def _validate_form_field(value, allow_files=False, allow_list=True): """ Ensure that `value` can be encoded as a single form data or a query parameter. Basic types that has a simple string representation are supported. A list of basic types is also valid. """ if isinstance(value, string_types): return value elif isinstance(value, bool) or (value is None): return {True: 'true', False: 'false', None: ''}[value] elif isinstance(value, (int, float)): return "%s" % value elif allow_list and isinstance(value, (list, tuple)) and not is_file(value): # Only the top-level element may be a list. return [ _validate_form_field(item, allow_files=False, allow_list=False) for item in value ] elif allow_files and is_file(value): return value msg = 'Must be a primitive type.' raise exceptions.ParameterError(msg) def _validate_json_data(value): """ Ensure that `value` can be encoded into JSON. """ if (value is None) or isinstance(value, (bool, int, float, string_types)): return value elif isinstance(value, (list, tuple)) and not is_file(value): return [_validate_json_data(item) for item in value] elif isinstance(value, dict): return { text_type(item_key): _validate_json_data(item_val) for item_key, item_val in value.items() } msg = 'Must be a JSON primitive.' raise exceptions.ParameterError(msg) coreapi-2.3.3/setup.cfg000066400000000000000000000001221322523237100147550ustar00rootroot00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 coreapi-2.3.3/setup.py000077500000000000000000000055451322523237100146670ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup import re import os import shutil import sys def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_packages(package): """ Return root package and all sub-packages. """ return [dirpath for dirpath, dirnames, filenames in os.walk(package) if os.path.exists(os.path.join(dirpath, '__init__.py'))] def get_package_data(package): """ Return all files under the root package, that are not in a package themselves. """ walk = [(dirpath.replace(package + os.sep, '', 1), filenames) for dirpath, dirnames, filenames in os.walk(package) if not os.path.exists(os.path.join(dirpath, '__init__.py'))] filepaths = [] for base, filenames in walk: filepaths.extend([os.path.join(base, filename) for filename in filenames]) return {package: filepaths} version = get_version('coreapi') if sys.argv[-1] == 'publish': os.system("python setup.py sdist bdist_wheel upload") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") sys.exit() setup( name='coreapi', version=version, url='https://github.com/core-api/python-client', license='BSD', description='Python client library for Core API.', author='Tom Christie', author_email='tom@tomchristie.com', packages=get_packages('coreapi'), package_data=get_package_data('coreapi'), install_requires=[ 'coreschema', 'requests', 'itypes', 'uritemplate' ], entry_points={ 'coreapi.codecs': [ 'corejson=coreapi.codecs:CoreJSONCodec', 'json=coreapi.codecs:JSONCodec', 'text=coreapi.codecs:TextCodec', 'download=coreapi.codecs:DownloadCodec', ], 'coreapi.transports': [ 'http=coreapi.transports:HTTPTransport', ] }, classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', ] )