Support MessagePack

This commit is contained in:
Juan Riaza 2013-01-05 18:20:30 +01:00
parent a061e3d9e2
commit 1fa750a0d7
6 changed files with 129 additions and 6 deletions

View File

@ -1,3 +1,4 @@
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
django-filter>=0.5.4 django-filter>=0.5.4
msgpack-python==0.3.0dev1

View File

@ -391,6 +391,13 @@ except ImportError:
yaml = None yaml = None
# MessagePack is optional
try:
import msgpack
except ImportError:
msgpack = None
# xml.etree.parse only throws ParseError for python >= 2.7 # xml.etree.parse only throws ParseError for python >= 2.7
try: try:
from xml.etree import ParseError as ETParseError from xml.etree import ParseError as ETParseError

View File

@ -8,7 +8,7 @@ on the request, such as form content or json encoded data.
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError 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 rest_framework.exceptions import ParseError
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
@ -80,6 +80,42 @@ class YAMLParser(BaseParser):
raise ParseError('YAML parse error - %s' % unicode(exc)) 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): class FormParser(BaseParser):
""" """
Parser for form data. Parser for form data.

View File

@ -12,7 +12,7 @@ import json
from django import forms from django import forms
from django.http.multipartparser import parse_header from django.http.multipartparser import parse_header
from django.template import RequestContext, loader, Template 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.exceptions import ConfigurationError
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.request import clone_request from rest_framework.request import clone_request
@ -139,6 +139,24 @@ class YAMLRenderer(BaseRenderer):
return yaml.dump(data, stream=None, Dumper=self.encoder) 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): class TemplateHTMLRenderer(BaseRenderer):
""" """
An HTML renderer for use with templates. An HTML renderer for use with templates.

View File

@ -6,12 +6,12 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from rest_framework import status, permissions 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.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer XMLRenderer, JSONPRenderer, MessagePackRenderer, BrowsableAPIRenderer
from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.parsers import YAMLParser, XMLParser, MessagePackParser
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from StringIO import StringIO from StringIO import StringIO
@ -323,6 +323,38 @@ if yaml:
self.assertEquals(obj, data) 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): class XMLRendererTestCase(TestCase):
""" """
Tests specific to the XML Renderer Tests specific to the XML Renderer

View File

@ -6,7 +6,7 @@ import decimal
import types import types
import json import json
from django.utils.datastructures import SortedDict 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 from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
@ -89,3 +89,32 @@ else:
yaml.representer.SafeRepresenter.represent_dict) yaml.representer.SafeRepresenter.represent_dict)
SafeDumper.add_representer(types.GeneratorType, SafeDumper.add_representer(types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list) 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