aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/parsers.py
diff options
context:
space:
mode:
authorTom Christie2012-09-20 13:06:27 +0100
committerTom Christie2012-09-20 13:06:27 +0100
commit4b691c402707775c3048a90531024f3bc5be6f91 (patch)
tree3adfc54b0d8b70e4ea78edf7091f7827fa68f47b /rest_framework/parsers.py
parenta1bcfbfe926621820832e32b0427601e1140b4f7 (diff)
downloaddjango-rest-framework-4b691c402707775c3048a90531024f3bc5be6f91.tar.bz2
Change package name: djangorestframework -> rest_framework
Diffstat (limited to 'rest_framework/parsers.py')
-rw-r--r--rest_framework/parsers.py260
1 files changed, 260 insertions, 0 deletions
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
new file mode 100644
index 00000000..b3423131
--- /dev/null
+++ b/rest_framework/parsers.py
@@ -0,0 +1,260 @@
+"""
+Django supports parsing the content of an HTTP request, but only for form POST requests.
+That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well
+to general HTTP requests.
+
+We need a method to be able to:
+
+1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT)
+
+2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded
+ and multipart/form-data. (eg also handle multipart/json)
+"""
+
+from django.http import QueryDict
+from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
+from django.http.multipartparser import MultiPartParserError
+from django.utils import simplejson as json
+from rest_framework.compat import yaml
+from rest_framework.exceptions import ParseError
+from rest_framework.utils.mediatypes import media_type_matches
+from xml.etree import ElementTree as ET
+from rest_framework.compat import ETParseError
+from xml.parsers.expat import ExpatError
+import datetime
+import decimal
+from io import BytesIO
+
+
+__all__ = (
+ 'BaseParser',
+ 'JSONParser',
+ 'PlainTextParser',
+ 'FormParser',
+ 'MultiPartParser',
+ 'YAMLParser',
+ 'XMLParser'
+)
+
+
+class DataAndFiles(object):
+ def __init__(self, data, files):
+ self.data = data
+ self.files = files
+
+
+class BaseParser(object):
+ """
+ All parsers should extend :class:`BaseParser`, specifying a :attr:`media_type` attribute,
+ and overriding the :meth:`parse` method.
+ """
+
+ media_type = None
+
+ def can_handle_request(self, content_type):
+ """
+ Returns :const:`True` if this parser is able to deal with the given *content_type*.
+
+ The default implementation for this function is to check the *content_type*
+ argument against the :attr:`media_type` attribute set on the class to see if
+ they match.
+
+ This may be overridden to provide for other behavior, but typically you'll
+ instead want to just set the :attr:`media_type` attribute on the class.
+ """
+ return media_type_matches(self.media_type, content_type)
+
+ def parse(self, string_or_stream, **opts):
+ """
+ The main entry point to parsers. This is a light wrapper around
+ `parse_stream`, that instead handles both string and stream objects.
+ """
+ if isinstance(string_or_stream, basestring):
+ stream = BytesIO(string_or_stream)
+ else:
+ stream = string_or_stream
+ return self.parse_stream(stream, **opts)
+
+ def parse_stream(self, stream, **opts):
+ """
+ Given a *stream* to read from, return the deserialized output.
+ Should return parsed data, or a DataAndFiles object consisting of the
+ parsed data and files.
+ """
+ raise NotImplementedError(".parse_stream() must be overridden.")
+
+
+class JSONParser(BaseParser):
+ """
+ Parses JSON-serialized data.
+ """
+
+ media_type = 'application/json'
+
+ def parse_stream(self, stream, **opts):
+ """
+ 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 json.load(stream)
+ except ValueError, exc:
+ raise ParseError('JSON parse error - %s' % unicode(exc))
+
+
+class YAMLParser(BaseParser):
+ """
+ Parses YAML-serialized data.
+ """
+
+ media_type = 'application/yaml'
+
+ def parse_stream(self, stream, **opts):
+ """
+ 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 yaml.safe_load(stream)
+ except (ValueError, yaml.parser.ParserError), exc:
+ raise ParseError('YAML parse error - %s' % unicode(exc))
+
+
+class PlainTextParser(BaseParser):
+ """
+ Plain text parser.
+ """
+
+ media_type = 'text/plain'
+
+ def parse_stream(self, stream, **opts):
+ """
+ Returns a 2-tuple of `(data, files)`.
+
+ `data` will simply be a string representing the body of the request.
+ `files` will always be `None`.
+ """
+ return stream.read()
+
+
+class FormParser(BaseParser):
+ """
+ Parser for form data.
+ """
+
+ media_type = 'application/x-www-form-urlencoded'
+
+ def parse_stream(self, stream, **opts):
+ """
+ Returns a 2-tuple of `(data, files)`.
+
+ `data` will be a :class:`QueryDict` containing all the form parameters.
+ `files` will always be :const:`None`.
+ """
+ data = QueryDict(stream.read())
+ return data
+
+
+class MultiPartParser(BaseParser):
+ """
+ Parser for multipart form data, which may include file data.
+ """
+
+ media_type = 'multipart/form-data'
+
+ def parse_stream(self, stream, **opts):
+ """
+ Returns a DataAndFiles object.
+
+ `.data` will be a `QueryDict` containing all the form parameters.
+ `.files` will be a `QueryDict` containing all the form files.
+ """
+ meta = opts['meta']
+ upload_handlers = opts['upload_handlers']
+ try:
+ parser = DjangoMultiPartParser(meta, stream, upload_handlers)
+ data, files = parser.parse()
+ return DataAndFiles(data, files)
+ except MultiPartParserError, exc:
+ raise ParseError('Multipart form parse error - %s' % unicode(exc))
+
+
+class XMLParser(BaseParser):
+ """
+ XML parser.
+ """
+
+ media_type = 'application/xml'
+
+ def parse_stream(self, stream, **opts):
+ try:
+ tree = ET.parse(stream)
+ except (ExpatError, ETParseError, ValueError), exc:
+ raise ParseError('XML parse error - %s' % unicode(exc))
+ data = self._xml_convert(tree.getroot())
+
+ return data
+
+ def _xml_convert(self, element):
+ """
+ convert the xml `element` into the corresponding python object
+ """
+
+ children = element.getchildren()
+
+ if len(children) == 0:
+ return self._type_convert(element.text)
+ else:
+ # if the fist child tag is list-item means all children are list-item
+ if children[0].tag == "list-item":
+ data = []
+ for child in children:
+ data.append(self._xml_convert(child))
+ else:
+ data = {}
+ for child in children:
+ data[child.tag] = self._xml_convert(child)
+
+ return data
+
+ def _type_convert(self, value):
+ """
+ Converts the value returned by the XMl parse into the equivalent
+ Python type
+ """
+ if value is None:
+ return value
+
+ try:
+ return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
+ except ValueError:
+ pass
+
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return decimal.Decimal(value)
+ except decimal.InvalidOperation:
+ pass
+
+ return value
+
+
+DEFAULT_PARSERS = (
+ JSONParser,
+ FormParser,
+ MultiPartParser,
+ XMLParser
+)
+
+if yaml:
+ DEFAULT_PARSERS += (YAMLParser, )
+else:
+ YAMLParser = None