From 1fa750a0d7cccec67a73f121a52b45e0b04530e3 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Sat, 5 Jan 2013 18:20:30 +0100 Subject: [PATCH] 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