Merge pull request #7 from sprockets/repackage

Repackage into a package
This commit is contained in:
amberheilman 2016-01-12 15:07:21 -05:00
commit d215718d4e
15 changed files with 223 additions and 68 deletions

View file

@ -2,6 +2,7 @@ language: python
python:
- 2.7
- 3.4
- 3.5
- pypy
install:
- pip install codecov

View file

@ -1,4 +1,4 @@
Copyright (c) 2015 AWeber Communications
Copyright (c) 2015-2016 AWeber Communications
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View file

@ -21,7 +21,7 @@ functions as parameters:
import json
from sprockets.mixins import mediatype
from sprockets.mixins.mediatype import content
from tornado import web
def make_application():
@ -29,9 +29,9 @@ functions as parameters:
# insert your handlers here
])
mediatype.add_text_content_type(application,
'application/json', 'utf-8',
json.dumps, json.loads)
content.add_text_content_type(application,
'application/json', 'utf-8',
json.dumps, json.loads)
return application
@ -40,10 +40,10 @@ instance that the mix-in uses to manipulate the request and response bodies.
.. code-block:: python
from sprockets.mixins import mediatype
from sprockets.mixins.mediatype import content
from tornado import web
class SomeHandler(mediatype.ContentMixin, web.RequestHandler):
class SomeHandler(content.ContentMixin, web.RequestHandler):
def get(self):
self.send_response({'data': 'value'})
self.finish()

View file

@ -1,6 +1,6 @@
API Documentation
=================
.. currentmodule:: sprockets.mixins.mediatype
.. currentmodule:: sprockets.mixins.mediatype.content
Content Type Handling
---------------------

View file

@ -9,7 +9,7 @@ extensions = ['sphinx.ext.autodoc',
source_suffix = '.rst'
master_doc = 'index'
project = 'sprockets.mixins.mediatype'
copyright = '2015, AWeber Communications'
copyright = '2015-2016, AWeber Communications'
release = __version__
version = '.'.join(release.split('.')[0:1])

View file

@ -1,4 +0,0 @@
Examples
========
.. literalinclude:: ../examples.py

View file

@ -1,13 +1,18 @@
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
contain the Accept header
`1.0.3`_ (10 Sep 2015)
---------------------
----------------------
- Update installation files
`1.0.2`_ (9 Sep 2015)
@ -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

View file

@ -2,12 +2,12 @@ import json
import logging
import signal
from sprockets.mixins import mediatype
from sprockets.mixins.mediatype import content
from tornado import ioloop, web
import msgpack
class SimpleHandler(mediatype.ContentMixin, web.RequestHandler):
class SimpleHandler(content.ContentMixin, web.RequestHandler):
def post(self):
body = self.get_request_body()
@ -18,12 +18,12 @@ class SimpleHandler(mediatype.ContentMixin, web.RequestHandler):
def make_application(**settings):
application = web.Application([web.url(r'/', SimpleHandler)], **settings)
mediatype.set_default_content_type(application, 'application/json',
encoding='utf-8')
mediatype.add_binary_content_type(application, 'application/msgpack',
msgpack.packb, msgpack.unpackb)
mediatype.add_text_content_type(application, 'application/json', 'utf-8',
json.dumps, json.loads)
content.set_default_content_type(application, 'application/json',
encoding='utf-8')
content.add_binary_content_type(application, 'application/msgpack',
msgpack.packb, msgpack.unpackb)
content.add_text_content_type(application, 'application/json', 'utf-8',
json.dumps, json.loads)
return application

View file

@ -2,7 +2,6 @@
universal = 1
[nosetests]
with-coverage = 1
cover-branches = 1
cover-erase = 1
cover-package = sprockets.mixins

View file

@ -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',

View file

@ -0,0 +1,45 @@
"""
sprockets.mixins.media_type
"""
import functools
import warnings
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
def _mark_deprecated(func):
msg = '{0}.{1} is deprecated, use {0}.content.{1} instead'.format(
'sprockets.mixins.mediatype', func.__name__)
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(msg, category=DeprecationWarning)
return func(*args, **kwargs)
return wrapper
add_binary_content_type = _mark_deprecated(add_binary_content_type)
add_text_content_type = _mark_deprecated(add_text_content_type)
set_default_content_type = _mark_deprecated(set_default_content_type)
ContentMixin = _mark_deprecated(ContentMixin)
ContentSettings = _mark_deprecated(ContentSettings)
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__')

View file

@ -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))

View file

@ -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))

View file

@ -35,7 +35,7 @@ class SendResponseTests(testing.AsyncHTTPTestCase):
'application/msgpack')
def test_that_default_content_type_is_set_on_response(self):
response = self.fetch('/', method='POST', body=msgpack.packb('{}'),
response = self.fetch('/', method='POST', body=msgpack.packb({}),
headers={'Content-Type': 'application/msgpack'})
self.assertEqual(response.code, 200)
self.assertEqual(response.headers['Content-Type'],

View file

@ -1,8 +1,9 @@
[tox]
envlist = py27,py34
envlist = py27,py34,py35,pypy
indexserver =
default = https://pypi.python.org/simple
toxworkdir = build/tox
skip_unknown_interpreters = true
[testenv]
deps =