XML Parsers

This commit is contained in:
Tom Christie 2010-12-31 16:21:20 +00:00
parent c10a95de08
commit 48c7171aa0
6 changed files with 215 additions and 20 deletions

View File

@ -1,6 +1,7 @@
from django.template import RequestContext, loader from django.template import RequestContext, loader
from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.core.handlers.wsgi import STATUS_CODE_TEXT
import json import json
from utils import dict2xml
class BaseEmitter(object): class BaseEmitter(object):
def __init__(self, resource, request, status, headers): def __init__(self, resource, request, status, headers):
@ -38,7 +39,8 @@ class JSONEmitter(BaseEmitter):
return json.dumps(output) return json.dumps(output)
class XMLEmitter(BaseEmitter): class XMLEmitter(BaseEmitter):
pass def emit(self, output):
return dict2xml(output)
class HTMLEmitter(TemplatedEmitter): class HTMLEmitter(TemplatedEmitter):
template = 'emitter.html' template = 'emitter.html'

View File

@ -1,11 +1,15 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from rest import emitters, parsers, utils from rest import emitters, parsers, utils
from decimal import Decimal from decimal import Decimal
for (key, val) in STATUS_CODE_TEXT.items(): #
locals()["STATUS_%d_%s" % (key, val.replace(' ', '_'))] = key STATUS_400_BAD_REQUEST = 400
STATUS_405_METHOD_NOT_ALLOWED = 405
STATUS_406_NOT_ACCEPTABLE = 406
STATUS_415_UNSUPPORTED_MEDIA_TYPE = 415
STATUS_500_INTERNAL_SERVER_ERROR = 500
STATUS_501_NOT_IMPLEMENTED = 501
class ResourceException(Exception): class ResourceException(Exception):
@ -30,7 +34,8 @@ class Resource(object):
parsers = { 'application/json': parsers.JSONParser, parsers = { 'application/json': parsers.JSONParser,
'application/xml': parsers.XMLParser, 'application/xml': parsers.XMLParser,
'application/x-www-form-urlencoded': parsers.FormParser } 'application/x-www-form-urlencoded': parsers.FormParser,
'multipart/form-data': parsers.FormParser }
create_form = None create_form = None
update_form = None update_form = None
@ -74,11 +79,17 @@ class Resource(object):
def _determine_parser(self, request): def _determine_parser(self, request):
"""Return the appropriate parser for the input, given the client's 'Content-Type' header, """Return the appropriate parser for the input, given the client's 'Content-Type' header,
and the content types that this Resource knows how to parse.""" and the content types that this Resource knows how to parse."""
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
split = content_type.split(';', 1)
if len(split) > 1:
content_type = split[0]
content_type = content_type.strip()
try: try:
return self.parsers[request.META['CONTENT_TYPE']] return self.parsers[content_type]
except: except KeyError:
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE, raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
{'detail': 'Unsupported media type'}) {'detail': 'Unsupported content type \'%s\'' % content_type})
def _determine_emitter(self, request): def _determine_emitter(self, request):
"""Return the appropriate emitter for the output, given the client's 'Accept' header, """Return the appropriate emitter for the output, given the client's 'Accept' header,

View File

@ -1,5 +1,13 @@
# From piston... import re
import xml.etree.ElementTree as ET
from django.utils.encoding import smart_unicode
from django.utils.xmlutils import SimplerXMLGenerator
try:
import cStringIO as StringIO
except ImportError:
import StringIO
# From piston
def coerce_put_post(request): def coerce_put_post(request):
""" """
Django doesn't particularly understand REST. Django doesn't particularly understand REST.
@ -37,3 +45,126 @@ def coerce_put_post(request):
request.META['REQUEST_METHOD'] = 'PUT' request.META['REQUEST_METHOD'] = 'PUT'
request.PUT = request.POST request.PUT = request.POST
# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
#class object_dict(dict):
# """object view of dict, you can
# >>> a = object_dict()
# >>> a.fish = 'fish'
# >>> a['fish']
# 'fish'
# >>> a['water'] = 'water'
# >>> a.water
# 'water'
# >>> a.test = {'value': 1}
# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
# >>> a.test, a.test2.name, a.test2.value
# (1, 'test2', 2)
# """
# def __init__(self, initd=None):
# if initd is None:
# initd = {}
# dict.__init__(self, initd)
#
# def __getattr__(self, item):
# d = self.__getitem__(item)
# # if value is the only key in object, you can omit it
# if isinstance(d, dict) and 'value' in d and len(d) == 1:
# return d['value']
# else:
# return d
#
# def __setattr__(self, item, value):
# self.__setitem__(item, value)
# From xml2dict
class XML2Dict(object):
def __init__(self):
pass
def _parse_node(self, node):
node_tree = {}
# Save attrs and text, hope there will not be a child with same name
if node.text:
node_tree = node.text
for (k,v) in node.attrib.items():
k,v = self._namespace_split(k, v)
node_tree[k] = v
#Save childrens
for child in node.getchildren():
tag, tree = self._namespace_split(child.tag, self._parse_node(child))
if tag not in node_tree: # the first time, so store it in dict
node_tree[tag] = tree
continue
old = node_tree[tag]
if not isinstance(old, list):
node_tree.pop(tag)
node_tree[tag] = [old] # multi times, so change old dict to a list
node_tree[tag].append(tree) # add the new one
return node_tree
def _namespace_split(self, tag, value):
"""
Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
ns = http://cs.sfsu.edu/csc867/myscheduler
name = patients
"""
result = re.compile("\{(.*)\}(.*)").search(tag)
if result:
print tag
value.namespace, tag = result.groups()
return (tag, value)
def parse(self, file):
"""parse a xml file to a dict"""
f = open(file, 'r')
return self.fromstring(f.read())
def fromstring(self, s):
"""parse a string"""
t = ET.fromstring(s)
unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
return root_tree
def xml2dict(input):
return XML2Dict().fromstring(input)
# Piston:
class XMLEmitter():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
xml.startElement("resource", {})
self._to_xml(xml, item)
xml.endElement("resource")
elif isinstance(data, dict):
for key, value in data.iteritems():
xml.startElement(key, {})
self._to_xml(xml, value)
xml.endElement(key)
else:
xml.characters(smart_unicode(data))
def dict2xml(self, data):
stream = StringIO.StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
xml.startElement("content", {})
self._to_xml(xml, data)
xml.endElement("content")
xml.endDocument()
return stream.getvalue()
def dict2xml(input):
return XMLEmitter().dict2xml(input)

View File

@ -7,7 +7,9 @@ Replace these with more appropriate tests for your application.
from django.test import TestCase from django.test import TestCase
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from testapp.views import ReadOnlyResource, MirroringWriteResource from testapp import views
import json
from rest.utils import xml2dict, dict2xml
class AcceptHeaderTests(TestCase): class AcceptHeaderTests(TestCase):
def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True):
@ -18,7 +20,7 @@ class AcceptHeaderTests(TestCase):
if expect is None: if expect is None:
expect = mimetype expect = mimetype
resp = self.client.get(reverse(ReadOnlyResource), HTTP_ACCEPT=mimetype) resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT=mimetype)
if expect_match: if expect_match:
self.assertEquals(resp['content-type'], expect) self.assertEquals(resp['content-type'], expect)
@ -41,14 +43,63 @@ class AcceptHeaderTests(TestCase):
self.assert_accept_mimetype('application/invalid', expect_match=False) self.assert_accept_mimetype('application/invalid', expect_match=False)
def test_invalid_accept_header_returns_406(self): def test_invalid_accept_header_returns_406(self):
resp = self.client.get(reverse(ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid')
self.assertEquals(resp.status_code, 406) self.assertEquals(resp.status_code, 406)
class AllowedMethodsTests(TestCase): class AllowedMethodsTests(TestCase):
def test_write_on_read_only_resource_returns_405(self): def test_reading_read_only_allowed(self):
resp = self.client.put(reverse(ReadOnlyResource), {}) resp = self.client.get(reverse(views.ReadOnlyResource))
self.assertEquals(resp.status_code, 200)
def test_writing_read_only_not_allowed(self):
resp = self.client.put(reverse(views.ReadOnlyResource), {})
self.assertEquals(resp.status_code, 405) self.assertEquals(resp.status_code, 405)
def test_read_on_write_only_resource_returns_405(self): def test_reading_write_only_not_allowed(self):
resp = self.client.get(reverse(MirroringWriteResource)) resp = self.client.get(reverse(views.WriteOnlyResource))
self.assertEquals(resp.status_code, 405) self.assertEquals(resp.status_code, 405)
def test_writing_write_only_allowed(self):
resp = self.client.put(reverse(views.WriteOnlyResource), {})
self.assertEquals(resp.status_code, 200)
class EncodeDecodeTests(TestCase):
def setUp(self):
super(self.__class__, self).setUp()
self.input = {'a': 1, 'b': 'example'}
def test_encode_form_decode_json(self):
content = self.input
resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/json')
output = json.loads(resp.content)
self.assertEquals(self.input, output)
def test_encode_json_decode_json(self):
content = json.dumps(self.input)
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
output = json.loads(resp.content)
self.assertEquals(self.input, output)
def test_encode_xml_decode_json(self):
content = dict2xml(self.input)
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
output = json.loads(resp.content)
self.assertEquals(self.input, output)
def test_encode_form_decode_xml(self):
content = self.input
resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
output = xml2dict(resp.content)
self.assertEquals(self.input, output)
def test_encode_json_decode_xml(self):
content = json.dumps(self.input)
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
output = xml2dict(resp.content)
self.assertEquals(self.input, output)
def test_encode_xml_decode_xml(self):
content = dict2xml(self.input)
resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
output = xml2dict(resp.content)
self.assertEquals(self.input, output)

View File

@ -3,6 +3,6 @@ from django.conf.urls.defaults import patterns
urlpatterns = patterns('testapp.views', urlpatterns = patterns('testapp.views',
(r'^$', 'RootResource'), (r'^$', 'RootResource'),
(r'^read-only$', 'ReadOnlyResource'), (r'^read-only$', 'ReadOnlyResource'),
(r'^write-only$', 'MirroringWriteResource'), (r'^write-only$', 'WriteOnlyResource'),
(r'^read-write$', 'ReadWriteResource'), (r'^read-write$', 'ReadWriteResource'),
) )

View File

@ -8,7 +8,7 @@ class RootResource(Resource):
def read(self, headers={}, *args, **kwargs): def read(self, headers={}, *args, **kwargs):
return (200, {'read-only-api': self.reverse(ReadOnlyResource), return (200, {'read-only-api': self.reverse(ReadOnlyResource),
'write-only-api': self.reverse(MirroringWriteResource), 'write-only-api': self.reverse(WriteOnlyResource),
'read-write-api': self.reverse(ReadWriteResource)}, {}) 'read-write-api': self.reverse(ReadWriteResource)}, {})
@ -23,12 +23,12 @@ class ReadOnlyResource(Resource):
'ExampleDecimal': 1.0}, {}) 'ExampleDecimal': 1.0}, {})
class MirroringWriteResource(Resource): class WriteOnlyResource(Resource):
"""This is my docstring """This is my docstring
""" """
allowed_methods = ('PUT',) allowed_methods = ('PUT',)
def create(self, data, headers={}, *args, **kwargs): def update(self, data, headers={}, *args, **kwargs):
return (200, data, {}) return (200, data, {})