[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