diff --git a/README.md b/README.md index 8a9149cf1..1c5c56e22 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ There is also a sandbox API you can use for testing purposes, [available here][s * [Markdown] - Markdown support for the self describing API. * [PyYAML] - YAML content type support. +* [msgpack-python] - MessagePack content type support. +* [python-dateutil] - Date parsing for MessagePack. * [django-filter] - Filtering support. # Installation @@ -284,4 +286,5 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [markdown]: http://pypi.python.org/pypi/Markdown/ [pyyaml]: http://pypi.python.org/pypi/PyYAML [django-filter]: https://github.com/alex/django-filter - +[msgpack-python]: https://github.com/msgpack/msgpack-python +[python-dateutil]: http://labix.org/python-dateutil diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 185b616cb..3fed786d7 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -61,6 +61,12 @@ Parses `YAML` request content. **.media_type**: `application/yaml` +## MessagePackParser + +Parses `MessagePack` request content. + +**.media_type**: `application/msgpack` + ## XMLParser Parses REST framework's default style of `XML` request content. diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 374ff0ab2..c4d160961 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -94,6 +94,14 @@ Renders the request data into `YAML`. **.format**: `'.yaml'` +## MessagePackRenderer + +Renders the request data into `MessagePack`. + +**.media_type**: `application/msgpack` + +**.format**: `'.msgpack'` + ## XMLRenderer Renders REST framework's default style of `XML` response content. diff --git a/docs/index.md b/docs/index.md index 080eca6f2..483482c3c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,8 @@ The following packages are optional: * [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API. * [PyYAML][yaml] (3.10+) - YAML content-type support. * [django-filter][django-filter] (0.5.4+) - Filtering support. +* [python-dateutil] - Date parsing for MessagePack. +* [django-filter] - Filtering support. ## Installation @@ -167,6 +169,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [markdown]: http://pypi.python.org/pypi/Markdown/ [yaml]: http://pypi.python.org/pypi/PyYAML [django-filter]: https://github.com/alex/django-filter +[msgpack-python]: https://github.com/msgpack/msgpack-python +[python-dateutil]: http://labix.org/python-dateutil [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [image]: img/quickstart.png [sandbox]: http://restframework.herokuapp.com/ diff --git a/optionals.txt b/optionals.txt index 1d2358c6e..338b300fe 100644 --- a/optionals.txt +++ b/optionals.txt @@ -1,3 +1,5 @@ markdown>=2.1.0 PyYAML>=3.10 django-filter>=0.5.4 +msgpack-python>=0.2.4 +python-dateutil==2.1 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5508f6c05..89e438424 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -391,6 +391,19 @@ except ImportError: yaml = None +# MessagePack is optional +try: + import msgpack +except ImportError: + msgpack = None + +# dateutil is optional +try: + from dateutil import parser as dateutil_parser +except ImportError: + dateutil_parser = None + + # xml.etree.parse only throws ParseError for python >= 2.7 try: from xml.etree import ParseError as ETParseError diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 149d64311..05b144e26 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -8,7 +8,7 @@ on the request, such as form content or json encoded data. from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError -from rest_framework.compat import yaml, ETParseError +from rest_framework.compat import yaml, msgpack, dateutil_parser, ETParseError from rest_framework.exceptions import ParseError from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError @@ -80,6 +80,40 @@ class YAMLParser(BaseParser): raise ParseError('YAML parse error - %s' % unicode(exc)) +class MessagePackParser(BaseParser): + """ + Parses MessagePack-serialized data. + """ + + media_type = 'application/msgpack' + + def parse(self, stream, media_type=None, parser_context=None): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will be an object which is the parsed content of the response. + `files` will always be `None`. + """ + try: + return msgpack.unpackb(stream, + use_list=True, + object_hook=self._decode_object) + except Exception, exc: + raise ParseError('MessagePack parse error - %s' % unicode(exc)) + + def _decode_object(self, obj): + if dateutil_parser: + if '__datetime__' in obj: + return dateutil_parser.parse(obj['as_str']) + elif b'__date__' in obj: + return dateutil_parser.parse(obj['as_str']).date() + elif b'__time__' in obj: + return dateutil_parser.parse(obj['as_str']).time() + if b'__decimal__' in obj: + return decimal.Decimal(obj['as_str']) + return obj + + class FormParser(BaseParser): """ Parser for form data. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a34abaa0..ea5341adc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -12,7 +12,7 @@ import json from django import forms from django.http.multipartparser import parse_header from django.template import RequestContext, loader, Template -from rest_framework.compat import yaml +from rest_framework.compat import yaml, msgpack from rest_framework.exceptions import ConfigurationError from rest_framework.settings import api_settings from rest_framework.request import clone_request @@ -139,6 +139,23 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) +class MessagePackRenderer(BaseRenderer): + """ + Renderer which serializes to MessagePack. + """ + + media_type = 'application/msgpack' + format = 'msgpack' + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Renders *obj* into serialized MessagePack. + """ + if data is None: + return '' + return msgpack.packb(data, default=encoders.msgpack_encoder) + + class TemplateHTMLRenderer(BaseRenderer): """ An HTML renderer for use with templates. diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index c1b4e624b..0b8317e6b 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -6,12 +6,12 @@ from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status, permissions -from rest_framework.compat import yaml, patterns, url, include +from rest_framework.compat import yaml, msgpack, patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ - XMLRenderer, JSONPRenderer, BrowsableAPIRenderer -from rest_framework.parsers import YAMLParser, XMLParser + XMLRenderer, JSONPRenderer, MessagePackRenderer, BrowsableAPIRenderer +from rest_framework.parsers import YAMLParser, XMLParser, MessagePackParser from rest_framework.settings import api_settings from StringIO import StringIO @@ -323,6 +323,38 @@ if yaml: self.assertEquals(obj, data) +if msgpack: + _msgpack_repr = '\x81\xa3foo\x92\xa3bar\xa3baz' + + class MessagePackRendererTests(TestCase): + """ + Tests specific to the MessagePack Renderer + """ + + def test_render(self): + """ + Test basic MessagePack rendering. + """ + obj = {'foo': ['bar', 'baz']} + renderer = MessagePackRenderer() + content = renderer.render(obj, 'application/msgpack') + self.assertEquals(content, _msgpack_repr) + + def test_render_and_parse(self): + """ + Test rendering and then parsing returns the original object. + IE obj -> render -> parse -> obj. + """ + obj = {'foo': ['bar', 'baz']} + + renderer = MessagePackRenderer() + parser = MessagePackParser() + + content = renderer.render(obj, 'application/msgpack') + data = parser.parse(content) + self.assertEquals(obj, data) + + class XMLRendererTestCase(TestCase): """ Tests specific to the XML Renderer diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index c70b24dda..7015a009f 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -6,7 +6,7 @@ import decimal import types import json from django.utils.datastructures import SortedDict -from rest_framework.compat import timezone +from rest_framework.compat import timezone, msgpack from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata @@ -89,3 +89,19 @@ else: yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(types.GeneratorType, yaml.representer.SafeRepresenter.represent_list) + + +if msgpack: + def msgpack_encoder(obj): + if isinstance(obj, datetime.datetime): + return {'__datetime__': True, 'as_str': obj.isoformat()} + elif isinstance(obj, datetime.date): + return {'__date__': True, 'as_str': obj.isoformat()} + elif isinstance(obj, datetime.time): + return {'__time__': True, 'as_str': obj.isoformat()} + elif isinstance(obj, decimal.Decimal): + return {'__decimal__': True, 'as_str': str(obj)} + else: + return obj +else: + msgpack_encoder = None