diff --git a/docs/history.rst b/docs/history.rst index 2a90a48..e71a345 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,11 @@ Version History =============== +`Next Release`_ +--------------- +- Repackage from a module into a package. Distributing raw modules inside + of a namespace package is unreliable and questionably correct. + `1.0.4`_ (14 Sep 2015) ---------------------- - Support using the default_content_type in the settings if request does not @@ -22,6 +27,7 @@ Version History --------------------- - Initial Release +.. _Next Release: https://github.com/sprockets/sprockets.http/compare/1.0.4...HEAD .. _1.0.4: https://github.com/sprockets/sprockets.http/compare/1.0.3...1.0.4 .. _1.0.3: https://github.com/sprockets/sprockets.http/compare/1.0.2...1.0.3 .. _1.0.2: https://github.com/sprockets/sprockets.http/compare/1.0.1...1.0.2 diff --git a/setup.py b/setup.py index a7f374d..a4a5d2d 100755 --- a/setup.py +++ b/setup.py @@ -4,6 +4,8 @@ import os import setuptools +from sprockets.mixins import mediatype + def read_requirements(file_name): requirements = [] @@ -28,7 +30,7 @@ tests_require = read_requirements('testing.txt') setuptools.setup( name='sprockets.mixins.mediatype', - version='1.0.4', + version=mediatype.__version__, description='A mixin for reporting handling content-type/accept headers', long_description='\n' + open('README.rst').read(), url='https://github.com/sprockets/sprockets.mixins.media_type', @@ -36,7 +38,7 @@ setuptools.setup( author_email='api@aweber.com', license='BSD', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', diff --git a/sprockets/mixins/mediatype/__init__.py b/sprockets/mixins/mediatype/__init__.py new file mode 100644 index 0000000..8e70e82 --- /dev/null +++ b/sprockets/mixins/mediatype/__init__.py @@ -0,0 +1,24 @@ +""" +sprockets.mixins.media_type + +""" +try: + from .content import (ContentMixin, ContentSettings, + add_binary_content_type, add_text_content_type, + set_default_content_type) + +except ImportError as error: # pragma no cover + def _error_closure(*args, **kwargs): + raise error + + ContentMixin = _error_closure + ContentSettings = _error_closure + add_binary_content_type = _error_closure + add_text_content_type = _error_closure + set_default_content_type = _error_closure + +version_info = (1, 0, 4) +__version__ = '.'.join(str(v) for v in version_info) +__all__ = ('ContentMixin', 'ContentSettings', 'add_binary_content_type', + 'add_text_content_type', 'set_default_content_type', + 'version_info', '__version__') diff --git a/sprockets/mixins/mediatype.py b/sprockets/mixins/mediatype/content.py similarity index 84% rename from sprockets/mixins/mediatype.py rename to sprockets/mixins/mediatype/content.py index 0a562d1..c0fbea5 100644 --- a/sprockets/mixins/mediatype.py +++ b/sprockets/mixins/mediatype/content.py @@ -1,16 +1,33 @@ """ -sprockets.mixins.media_type -=========================== +Content handling for Tornado. + +- :func:`.set_default_content_type` sets the content type that is + used when an ``Accept`` or ``Content-Type`` header is omitted. +- :func:`.add_binary_content_type` register transcoders for a binary + content type +- :func:`.add_text_content_type` register transcoders for a textual + content type +- :class:`.ContentSettings` an instance of this is attached to + :class:`tornado.web.Application` to hold the content mapping + information for the application +- :class:`.ContentMixin` attaches a :class:`.ContentSettings` + instance to the application and implements request decoding & + response encoding methods + +This module is the primary interface for this library. It exposes +functions for registering new content handlers and a mix-in that +adds content handling methods to :class:`~tornado.web.RequestHandler` +instances. """ import logging from ietfparse import algorithms, errors, headers -from tornado import escape, web +from tornado import web + +from . import handlers -version_info = (1, 0, 4) -__version__ = '.'.join(str(v) for v in version_info) logger = logging.getLogger(__name__) @@ -110,7 +127,8 @@ def add_binary_content_type(application, content_type, pack, unpack): """ settings = ContentSettings.from_application(application) - settings[content_type] = _BinaryContentHandler(content_type, pack, unpack) + settings[content_type] = handlers.BinaryContentHandler( + content_type, pack, unpack) def add_text_content_type(application, content_type, default_encoding, @@ -128,8 +146,8 @@ def add_text_content_type(application, content_type, default_encoding, """ settings = ContentSettings.from_application(application) - settings[content_type] = _TextContentHandler(content_type, dumps, loads, - default_encoding) + settings[content_type] = handlers.TextContentHandler( + content_type, dumps, loads, default_encoding) def set_default_content_type(application, content_type, encoding=None): @@ -232,35 +250,3 @@ class ContentMixin(object): if set_content_type: self.set_header('Content-Type', content_type) self.write(data_bytes) - - -class _BinaryContentHandler(object): - - def __init__(self, content_type, pack, unpack): - self._pack = pack - self._unpack = unpack - self.content_type = content_type - - def to_bytes(self, data_dict, encoding=None): - return self.content_type, self._pack(data_dict) - - def from_bytes(self, data, encoding=None): - return self._unpack(data) - - -class _TextContentHandler(object): - - def __init__(self, content_type, dumps, loads, default_encoding): - self._dumps = dumps - self._loads = loads - self.content_type = content_type - self.default_encoding = default_encoding - - def to_bytes(self, data_dict, encoding=None): - selected = encoding or self.default_encoding - content_type = '{0}; charset="{1}"'.format(self.content_type, selected) - dumped = self._dumps(escape.recursive_unicode(data_dict)) - return content_type, dumped.encode(selected) - - def from_bytes(self, data, encoding=None): - return self._loads(data.decode(encoding or self.default_encoding)) diff --git a/sprockets/mixins/mediatype/handlers.py b/sprockets/mixins/mediatype/handlers.py new file mode 100644 index 0000000..a4708ef --- /dev/null +++ b/sprockets/mixins/mediatype/handlers.py @@ -0,0 +1,119 @@ +""" +Basic content handlers. + +- :class:`BinaryContentHandler` basic transcoder for binary types that + simply calls functions for encoding and decoding +- :class:`TextContentHandler` transcoder that translates binary bodies + to text before calling functions that encode & decode text + +""" +from tornado import escape + + +class BinaryContentHandler(object): + """ + Pack and unpack binary types. + + :param str content_type: registered content type + :param pack: function that transforms an object instance + into :class:`bytes` + :param unpack: function that transforms :class:`bytes` + into an object instance + + This transcoder is a thin veneer around a pair of packing + and unpacking functions. + + """ + + def __init__(self, content_type, pack, unpack): + self._pack = pack + self._unpack = unpack + self.content_type = content_type + + def to_bytes(self, inst_data, encoding=None): + """ + Transform an object into :class:`bytes`. + + :param object inst_data: object to encode + :param str encoding: ignored + :returns: :class:`tuple` of the selected content + type and the :class:`bytes` representation of + `inst_data` + + """ + return self.content_type, self._pack(inst_data) + + def from_bytes(self, data_bytes, encoding=None): + """ + Get an object from :class:`bytes` + + :param bytes data_bytes: stream of bytes to decode + :param str encoding: ignored + :param dict content_parameters: optional :class:`dict` of + content type parameters from the :mailheader:`Content-Type` + header + :returns: decoded :class:`object` instance + + """ + return self._unpack(data_bytes) + + +class TextContentHandler(object): + """ + Transcodes between textual and object representations. + + :param str content_type: registered content type + :param dumps: function that transforms an object instance + into a :class:`str` + :param loads: function that transforms a :class:`str` + into an object instance + :param str default_encoding: encoding to apply when + transcoding from the underlying body :class:`byte` + instance + + This transcoder wraps functions that transcode between :class:`str` + and :class:`object` instances. In particular, it handles the + additional step of transcoding into the :class:`byte` instances + that tornado expects. + + """ + + def __init__(self, content_type, dumps, loads, default_encoding): + self._dumps = dumps + self._loads = loads + self.content_type = content_type + self.default_encoding = default_encoding + + def to_bytes(self, inst_data, encoding=None): + """ + Transform an object into :class:`bytes`. + + :param object inst_data: object to encode + :param str encoding: character set used to encode the bytes + returned from the ``dumps`` function. This defaults to + :attr:`default_encoding` + :returns: :class:`tuple` of the selected content + type and the :class:`bytes` representation of + `inst_data` + + """ + selected = encoding or self.default_encoding + content_type = '{0}; charset="{1}"'.format(self.content_type, selected) + dumped = self._dumps(escape.recursive_unicode(inst_data)) + return content_type, dumped.encode(selected) + + def from_bytes(self, data, encoding=None): + """ + Get an object from :class:`bytes` + + :param bytes data: stream of bytes to decode + :param str encoding: character set used to decode the incoming + bytes before calling the ``loads`` function. This defaults + to :attr:`default_encoding` + :param dict content_parameters: optional :class:`dict` of + content type parameters from the :mailheader:`Content-Type` + header + :returns: decoded :class:`object` instance + + """ + return self._loads(data.decode(encoding or self.default_encoding))