Content Type tunneling

This commit is contained in:
tom christie tom@tomchristie.com 2011-01-26 20:31:47 +00:00
parent eff54c00d5
commit 8b89d7416c
5 changed files with 93 additions and 36 deletions

View File

@ -13,14 +13,14 @@ OBJECT_STORE_DIR = os.path.join(settings.MEDIA_ROOT, 'objectstore')
class ObjectStoreRoot(Resource):
"""Root of the Object Store API.
Allows the client to get a complete list of all the stored objects, or to create a new stored object."""
allowed_methods = ('GET', 'POST')
allowed_methods = anon_allowed_methods = ('GET', 'POST')
def get(self, request):
def get(self, request, auth):
"""Return a list of all the stored object URLs."""
keys = sorted(os.listdir(OBJECT_STORE_DIR))
return [self.reverse(StoredObject, key=key) for key in keys]
def post(self, request, content):
def post(self, request, auth, content):
"""Create a new stored object, with a unique key."""
key = str(uuid.uuid1())
pathname = os.path.join(OBJECT_STORE_DIR, key)
@ -31,22 +31,22 @@ class ObjectStoreRoot(Resource):
class StoredObject(Resource):
"""Represents a stored object.
The object may be any picklable content."""
allowed_methods = ('GET', 'PUT', 'DELETE')
allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
def get(self, request, key):
def get(self, request, auth, key):
"""Return a stored object, by unpickling the contents of a locally stored file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):
return Response(status.HTTP_404_NOT_FOUND)
return pickle.load(open(pathname, 'rb'))
def put(self, request, content, key):
def put(self, request, auth, content, key):
"""Update/create a stored object, by pickling the request content to a locally stored file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
pickle.dump(content, open(pathname, 'wb'))
return content
def delete(self, request, key):
def delete(self, request, auth, key):
"""Delete a stored object, by removing it's pickled file."""
pathname = os.path.join(OBJECT_STORE_DIR, key)
if not os.path.exists(pathname):

View File

@ -4,6 +4,7 @@ from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'pygments-example/', include('pygmentsapi.urls')),
(r'^blog-post-example/', include('blogpost.urls')),
(r'^object-store-example/', include('objectstore.urls')),
(r'^admin/doc/', include('django.contrib.admindocs.urls')),

View File

@ -1,8 +1,10 @@
from django.template import RequestContext, loader
from django import forms
from flywheel.response import NoContent
from utils import dict2xml
import string
try:
import json
except ImportError:
@ -20,25 +22,31 @@ class BaseEmitter(object):
raise Exception('emit() function on a subclass of BaseEmitter must be implemented')
from django import forms
class JSONForm(forms.Form):
_contenttype = forms.CharField(max_length=256, initial='application/json', label='Content Type')
_content = forms.CharField(label='Content', widget=forms.Textarea)
class DocumentingTemplateEmitter(BaseEmitter):
"""Emitter used to self-document the API"""
template = None
def emit(self, output=NoContent):
resource = self.resource
def _get_content(self, resource, output):
"""Get the content as if it had been emitted by a non-documenting emitter.
# Find the first valid emitter and emit the content. (Don't another documenting emitter.)
(Typically this will be the content as it would have been if the Resource had been
requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
# Find the first valid emitter and emit the content. (Don't use another documenting emitter.)
emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
if not emitters:
content = 'No emitters were found'
else:
content = emitters[0](resource).emit(output, verbose=True)
return '[No emitters were found]'
content = emitters[0](resource).emit(output, verbose=True)
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
return content
def _get_form_instance(self, resource):
# Get the form instance if we have one bound to the input
form_instance = resource.form_instance
@ -57,8 +65,45 @@ class DocumentingTemplateEmitter(BaseEmitter):
except:
pass
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
if not form_instance:
form_instance = JSONForm()
form_instance = self._get_generic_content_form(resource)
return form_instance
def _get_generic_content_form(self, resource):
"""Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
(Which are typically application/x-www-form-urlencoded)"""
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form):
def __init__(self, resource):
"""We don't know the names of the fields we want to set until the point the form is instantiated,
as they are determined by the Resource the form is being created against.
Add the fields dynamically."""
super(GenericContentForm, self).__init__()
contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
initial_contenttype = resource.default_parser.media_type
self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
choices=contenttype_choices,
initial=initial_contenttype)
self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
widget=forms.Textarea)
# If either of these reserved parameters are turned off then content tunneling is not possible
if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
return None
# Okey doke, let's do it
return GenericContentForm(resource)
def emit(self, output=NoContent):
content = self._get_content(self.resource, output)
form_instance = self._get_form_instance(self.resource)
template = loader.get_template(self.template)
context = RequestContext(self.resource.request, {

View File

@ -8,7 +8,7 @@ except ImportError:
# TODO: Make all parsers only list a single media_type, rather than a list
class BaseParser(object):
media_types = ()
media_type = None
def __init__(self, resource):
self.resource = resource
@ -18,7 +18,7 @@ class BaseParser(object):
class JSONParser(BaseParser):
media_types = ('application/xml',)
media_type = 'application/json'
def parse(self, input):
try:
@ -27,7 +27,7 @@ class JSONParser(BaseParser):
raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class XMLParser(BaseParser):
media_types = ('application/xml',)
media_type = 'application/xml'
class FormParser(BaseParser):
@ -35,7 +35,7 @@ class FormParser(BaseParser):
Return a dict containing a single value for each non-reserved parameter.
"""
media_types = ('application/x-www-form-urlencoded',)
media_type = 'application/x-www-form-urlencoded'
def parse(self, input):
# The FormParser doesn't parse the input as other parsers would, since Django's already done the

View File

@ -120,14 +120,16 @@ class Resource(object):
(This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
return self.emitters[0]
# TODO:
@property
def parsed_media_types(self):
"""Return an list of all the media types that this resource can emit."""
return [parser.media_type for parser in self.parsers]
#def parsed_media_types(self):
# """Return an list of all the media types that this resource can emit."""
# return [parser.media_type for parser in self.parsers]
#def default_parser(self):
# return self.parsers[0]
@property
def default_parser(self):
"""Return the resource's most prefered emitter.
(This has no behavioural effect, but is may be used by documenting emitters)"""
return self.parsers[0]
def reverse(self, view, *args, **kwargs):
@ -281,19 +283,28 @@ class Resource(object):
"""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."""
content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
raw_content = request.raw_post_data
split = content_type.split(';', 1)
if len(split) > 1:
content_type = split[0]
content_type = content_type.strip()
# If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
if (content_type == 'application/x-www-form-urlencoded' and
request.method == 'POST' and
self.CONTENTTYPE_PARAM and
self.CONTENT_PARAM and
request.POST.get(self.CONTENTTYPE_PARAM, None) and
request.POST.get(self.CONTENT_PARAM, None)):
raw_content = request.POST[self.CONTENT_PARAM]
content_type = request.POST[self.CONTENTTYPE_PARAM]
# Create a list of list of (media_type, Parser) tuples
media_type_parser_tuples = [[(media_type, parser) for media_type in parser.media_types] for parser in self.parsers]
# Flatten the list and turn it into a media_type -> Parser dict
media_type_to_parser = dict(chain.from_iterable(media_type_parser_tuples))
media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
try:
return media_type_to_parser[content_type]
return (media_type_to_parser[content_type], raw_content)
except KeyError:
raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
{'detail': 'Unsupported media type \'%s\'' % content_type})
@ -397,8 +408,8 @@ class Resource(object):
# Either generate the response data, deserializing and validating any request data
# TODO: Add support for message bodys on other HTTP methods, as it is valid.
if method in ('PUT', 'POST'):
parser = self.determine_parser(request)
data = parser(self).parse(request.raw_post_data)
(parser, raw_content) = self.determine_parser(request)
data = parser(self).parse(raw_content)
self.form_instance = self.get_form(data)
data = self.cleanup_request(data, self.form_instance)
response = func(request, auth_context, data, *args, **kwargs)