[Python-modules-commits] [coreapi] 01/02: Import Upstream version 2.3.3
Pierre-Elliott Bécue
peb-guest at moszumanska.debian.org
Tue Jan 9 21:34:58 UTC 2018
This is an automated email from the git hooks/post-receive script.
peb-guest pushed a commit to branch master
in repository coreapi.
commit c861a249e5532637c270d48cf35e80a70675b0c2
Author: Pierre-Elliott Bécue <becue at crans.org>
Date: Tue Jan 9 22:32:41 2018 +0100
Import Upstream version 2.3.3
---
MANIFEST.in | 3 +
PKG-INFO | 24 +++
coreapi.egg-info/PKG-INFO | 24 +++
coreapi.egg-info/SOURCES.txt | 27 +++
coreapi.egg-info/dependency_links.txt | 1 +
coreapi.egg-info/entry_points.txt | 9 +
coreapi.egg-info/requires.txt | 4 +
coreapi.egg-info/top_level.txt | 3 +
coreapi/__init__.py | 12 ++
coreapi/auth.py | 69 ++++++
coreapi/client.py | 178 ++++++++++++++++
coreapi/codecs/__init__.py | 14 ++
coreapi/codecs/base.py | 44 ++++
coreapi/codecs/corejson.py | 346 ++++++++++++++++++++++++++++++
coreapi/codecs/display.py | 124 +++++++++++
coreapi/codecs/download.py | 149 +++++++++++++
coreapi/codecs/jsondata.py | 22 ++
coreapi/codecs/python.py | 82 +++++++
coreapi/codecs/text.py | 10 +
coreapi/compat.py | 65 ++++++
coreapi/document.py | 310 +++++++++++++++++++++++++++
coreapi/exceptions.py | 62 ++++++
coreapi/transports/__init__.py | 8 +
coreapi/transports/base.py | 9 +
coreapi/transports/http.py | 388 ++++++++++++++++++++++++++++++++++
coreapi/utils.py | 336 +++++++++++++++++++++++++++++
setup.cfg | 8 +
setup.py | 98 +++++++++
28 files changed, 2429 insertions(+)
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1d493ff
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+global-exclude __pycache__
+global-exclude *.pyc
+global-exclude *.pyo
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..b8903f9
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,24 @@
+Metadata-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 at 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
diff --git a/coreapi.egg-info/PKG-INFO b/coreapi.egg-info/PKG-INFO
new file mode 100644
index 0000000..b8903f9
--- /dev/null
+++ b/coreapi.egg-info/PKG-INFO
@@ -0,0 +1,24 @@
+Metadata-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 at 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
diff --git a/coreapi.egg-info/SOURCES.txt b/coreapi.egg-info/SOURCES.txt
new file mode 100644
index 0000000..54337bc
--- /dev/null
+++ b/coreapi.egg-info/SOURCES.txt
@@ -0,0 +1,27 @@
+MANIFEST.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.py
\ No newline at end of file
diff --git a/coreapi.egg-info/dependency_links.txt b/coreapi.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/coreapi.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/coreapi.egg-info/entry_points.txt b/coreapi.egg-info/entry_points.txt
new file mode 100644
index 0000000..d6f8600
--- /dev/null
+++ b/coreapi.egg-info/entry_points.txt
@@ -0,0 +1,9 @@
+[coreapi.codecs]
+corejson = coreapi.codecs:CoreJSONCodec
+download = coreapi.codecs:DownloadCodec
+json = coreapi.codecs:JSONCodec
+text = coreapi.codecs:TextCodec
+
+[coreapi.transports]
+http = coreapi.transports:HTTPTransport
+
diff --git a/coreapi.egg-info/requires.txt b/coreapi.egg-info/requires.txt
new file mode 100644
index 0000000..4c1a87e
--- /dev/null
+++ b/coreapi.egg-info/requires.txt
@@ -0,0 +1,4 @@
+coreschema
+requests
+itypes
+uritemplate
diff --git a/coreapi.egg-info/top_level.txt b/coreapi.egg-info/top_level.txt
new file mode 100644
index 0000000..9ae44ba
--- /dev/null
+++ b/coreapi.egg-info/top_level.txt
@@ -0,0 +1,3 @@
+coreapi
+coreapi/codecs
+coreapi/transports
diff --git a/coreapi/__init__.py b/coreapi/__init__.py
new file mode 100644
index 0000000..92ac890
--- /dev/null
+++ b/coreapi/__init__.py
@@ -0,0 +1,12 @@
+# 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',
+]
diff --git a/coreapi/auth.py b/coreapi/auth.py
new file mode 100644
index 0000000..e110547
--- /dev/null
+++ b/coreapi/auth.py
@@ -0,0 +1,69 @@
+from 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="<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
diff --git a/coreapi/client.py b/coreapi/client.py
new file mode 100644
index 0000000..d02a59c
--- /dev/null
+++ b/coreapi/client.py
@@ -0,0 +1,178 @@
+from 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)
diff --git a/coreapi/codecs/__init__.py b/coreapi/codecs/__init__.py
new file mode 100644
index 0000000..4fa6a1a
--- /dev/null
+++ b/coreapi/codecs/__init__.py
@@ -0,0 +1,14 @@
+# 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'
+]
diff --git a/coreapi/codecs/base.py b/coreapi/codecs/base.py
new file mode 100644
index 0000000..6f20044
--- /dev/null
+++ b/coreapi/codecs/base.py
@@ -0,0 +1,44 @@
+import 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]
diff --git a/coreapi/codecs/corejson.py b/coreapi/codecs/corejson.py
new file mode 100644
index 0000000..f025533
--- /dev/null
+++ b/coreapi/codecs/corejson.py
@@ -0,0 +1,346 @@
+from __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))
diff --git a/coreapi/codecs/display.py b/coreapi/codecs/display.py
new file mode 100644
index 0000000..250e0cc
--- /dev/null
+++ b/coreapi/codecs/display.py
@@ -0,0 +1,124 @@
+# 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('<Error: %s>' % node.title.strip() if node.title else '<Error>')
+
+ 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
+ ])
+
+
... 1634 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/coreapi.git
More information about the Python-modules-commits
mailing list