From 1fa750a0d7cccec67a73f121a52b45e0b04530e3 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 18:20:30 +0100 Subject: [PATCH 1/5] Support MessagePack --- optionals.txt | 1 + rest_framework/compat.py | 7 ++++++ rest_framework/parsers.py | 38 ++++++++++++++++++++++++++++++- rest_framework/renderers.py | 20 +++++++++++++++- rest_framework/tests/renderers.py | 38 ++++++++++++++++++++++++++++--- rest_framework/utils/encoders.py | 31 ++++++++++++++++++++++++- 6 files changed, 129 insertions(+), 6 deletions(-) diff --git a/optionals.txt b/optionals.txt index 1d2358c6e..a4f4db8de 100644 --- a/optionals.txt +++ b/optionals.txt @@ -1,3 +1,4 @@ markdown>=2.1.0 PyYAML>=3.10 django-filter>=0.5.4 +msgpack-python==0.3.0dev1 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5508f6c05..df042c2ac 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -391,6 +391,13 @@ except ImportError: yaml = None +# MessagePack is optional +try: + import msgpack +except ImportError: + msgpack = 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..64262b3b6 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, ETParseError from rest_framework.exceptions import ParseError from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError @@ -80,6 +80,42 @@ 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.Unpacker( + stream, + use_list=True, + object_hook=self._decode_object).unpack() + except Exception, exc: + raise ParseError('MessagePack parse error - %s' % unicode(exc)) + + def _decode_object(self, o): + # TODO(juanriaza): decode objects + if b'__datetime__' in o: + return o['as_str'] + elif b'__date__' in o: + return o['as_str'] + elif b'__time__' in o: + return o['as_str'] + elif b'__decimal__' in o: + return o['as_str'] + else: + return o + + class FormParser(BaseParser): """ Parser for form data. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a34abaa0..b21037ec5 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,24 @@ 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' + encoder = encoders.msgpack_encoder + + 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=self.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..185bebfdb 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(StringIO(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..2e309ba4d 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,32 @@ else: yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(types.GeneratorType, yaml.representer.SafeRepresenter.represent_list) + + +if msgpack: + def msgpack_encoder(o): + # For Date Time string spec, see ECMA 262 + # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 + if isinstance(o, datetime.datetime): + r = o.isoformat() + if o.microsecond: + r = r[:23] + r[26:] + if r.endswith('+00:00'): + r = r[:-6] + 'Z' + return {'__datetime__': True, 'as_str': r} + elif isinstance(o, datetime.date): + r = o.isoformat() + return {'__date__': True, 'as_str': r} + elif isinstance(o, datetime.time): + if timezone and timezone.is_aware(o): + raise ValueError("MessagePack can't represent timezone-aware times.") + r = o.isoformat() + if o.microsecond: + r = r[:12] + return {'__time__': True, 'as_str': r} + elif isinstance(o, decimal.Decimal): + return {'__decimal__': True, 'as_str': str(o)} + else: + return o +else: + msgpack_encoder = None From c360433de7b1f4196cfe18b96b6247d66659f7c1 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 19:00:10 +0100 Subject: [PATCH 2/5] one-shot unpack --- rest_framework/parsers.py | 5 ++--- rest_framework/tests/renderers.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 64262b3b6..b16df4f11 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -95,10 +95,9 @@ class MessagePackParser(BaseParser): `files` will always be `None`. """ try: - return msgpack.Unpacker( - stream, + return msgpack.unpackb(stream, use_list=True, - object_hook=self._decode_object).unpack() + object_hook=self._decode_object) except Exception, exc: raise ParseError('MessagePack parse error - %s' % unicode(exc)) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 185bebfdb..0b8317e6b 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -351,7 +351,7 @@ if msgpack: parser = MessagePackParser() content = renderer.render(obj, 'application/msgpack') - data = parser.parse(StringIO(content)) + data = parser.parse(content) self.assertEquals(obj, data) From 0234b6161bf83006e9dd278b3299548239f145d6 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 23:28:15 +0100 Subject: [PATCH 3/5] include dateutil and some cleaning --- optionals.txt | 3 ++- rest_framework/compat.py | 6 ++++++ rest_framework/parsers.py | 23 +++++++++++----------- rest_framework/renderers.py | 3 +-- rest_framework/utils/encoders.py | 33 ++++++++++---------------------- 5 files changed, 30 insertions(+), 38 deletions(-) diff --git a/optionals.txt b/optionals.txt index a4f4db8de..338b300fe 100644 --- a/optionals.txt +++ b/optionals.txt @@ -1,4 +1,5 @@ markdown>=2.1.0 PyYAML>=3.10 django-filter>=0.5.4 -msgpack-python==0.3.0dev1 +msgpack-python>=0.2.4 +python-dateutil==2.1 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index df042c2ac..89e438424 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -397,6 +397,12 @@ try: 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: diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index b16df4f11..801a9229b 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, msgpack, 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 @@ -101,18 +101,17 @@ class MessagePackParser(BaseParser): except Exception, exc: raise ParseError('MessagePack parse error - %s' % unicode(exc)) - def _decode_object(self, o): - # TODO(juanriaza): decode objects - if b'__datetime__' in o: - return o['as_str'] - elif b'__date__' in o: - return o['as_str'] - elif b'__time__' in o: - return o['as_str'] - elif b'__decimal__' in o: - return o['as_str'] + def _decode_object(self, obj): + 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() + elif b'__decimal__' in obj: + return decimal.Decimal(obj['as_str']) else: - return o + return obj class FormParser(BaseParser): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b21037ec5..ea5341adc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -146,7 +146,6 @@ class MessagePackRenderer(BaseRenderer): media_type = 'application/msgpack' format = 'msgpack' - encoder = encoders.msgpack_encoder def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -154,7 +153,7 @@ class MessagePackRenderer(BaseRenderer): """ if data is None: return '' - return msgpack.packb(data, default=self.encoder) + return msgpack.packb(data, default=encoders.msgpack_encoder) class TemplateHTMLRenderer(BaseRenderer): diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 2e309ba4d..7015a009f 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -92,29 +92,16 @@ else: if msgpack: - def msgpack_encoder(o): - # For Date Time string spec, see ECMA 262 - # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 - if isinstance(o, datetime.datetime): - r = o.isoformat() - if o.microsecond: - r = r[:23] + r[26:] - if r.endswith('+00:00'): - r = r[:-6] + 'Z' - return {'__datetime__': True, 'as_str': r} - elif isinstance(o, datetime.date): - r = o.isoformat() - return {'__date__': True, 'as_str': r} - elif isinstance(o, datetime.time): - if timezone and timezone.is_aware(o): - raise ValueError("MessagePack can't represent timezone-aware times.") - r = o.isoformat() - if o.microsecond: - r = r[:12] - return {'__time__': True, 'as_str': r} - elif isinstance(o, decimal.Decimal): - return {'__decimal__': True, 'as_str': str(o)} + 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 o + return obj else: msgpack_encoder = None From f223cf70e9d0190df401ceab4020bb8574c69164 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 23:43:53 +0100 Subject: [PATCH 4/5] docs --- README.md | 5 ++++- docs/api-guide/parsers.md | 6 ++++++ docs/api-guide/renderers.md | 8 ++++++++ docs/index.md | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) 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/ From 6ecf7599a942fa97ee39be528f0e9e38c7e1a7b6 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 23:46:41 +0100 Subject: [PATCH 5/5] only parse date objects if dateutil is available --- rest_framework/parsers.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 801a9229b..05b144e26 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -102,16 +102,16 @@ class MessagePackParser(BaseParser): raise ParseError('MessagePack parse error - %s' % unicode(exc)) def _decode_object(self, obj): - 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() - elif b'__decimal__' in obj: - return decimal.Decimal(obj['as_str']) - else: - return 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):