mirror of
https://github.com/sprockets/sprockets.mixins.mediatype.git
synced 2024-12-29 11:17:10 +00:00
Merge pull request #7 from sprockets/repackage
Repackage into a package
This commit is contained in:
commit
d215718d4e
15 changed files with 223 additions and 68 deletions
|
@ -2,6 +2,7 @@ language: python
|
|||
python:
|
||||
- 2.7
|
||||
- 3.4
|
||||
- 3.5
|
||||
- pypy
|
||||
install:
|
||||
- pip install codecov
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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,
|
||||
|
|
12
README.rst
12
README.rst
|
@ -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()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
API Documentation
|
||||
=================
|
||||
.. currentmodule:: sprockets.mixins.mediatype
|
||||
.. currentmodule:: sprockets.mixins.mediatype.content
|
||||
|
||||
Content Type Handling
|
||||
---------------------
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
Examples
|
||||
========
|
||||
|
||||
.. literalinclude:: ../examples.py
|
|
@ -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
|
||||
|
|
16
examples.py
16
examples.py
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
universal = 1
|
||||
|
||||
[nosetests]
|
||||
with-coverage = 1
|
||||
cover-branches = 1
|
||||
cover-erase = 1
|
||||
cover-package = sprockets.mixins
|
||||
|
|
6
setup.py
6
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',
|
||||
|
|
45
sprockets/mixins/mediatype/__init__.py
Normal file
45
sprockets/mixins/mediatype/__init__.py
Normal 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__')
|
|
@ -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))
|
119
sprockets/mixins/mediatype/handlers.py
Normal file
119
sprockets/mixins/mediatype/handlers.py
Normal 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))
|
2
tests.py
2
tests.py
|
@ -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'],
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -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 =
|
||||
|
|
Loading…
Reference in a new issue