[snowy] Update piston to 92644a459862
- From: Brad Taylor <btaylor src gnome org>
- To: svn-commits-list gnome org
- Subject: [snowy] Update piston to 92644a459862
- Date: Tue, 21 Jul 2009 20:27:34 +0000 (UTC)
commit 17845e37a399fae5619bb581b6c690451917efca
Author: Brad Taylor <brad getcoded net>
Date: Tue Jul 21 16:26:40 2009 -0400
Update piston to 92644a459862
This update includes fixes from Benoit Garret.
lib/piston/authentication.py | 35 +++--
lib/piston/doc.py | 82 +++++++++-
lib/piston/emitters.py | 126 +++++++++++++---
lib/piston/forms.py | 5 +-
lib/piston/handler.py | 19 ++-
lib/piston/middleware.py | 20 +++
lib/piston/oauth.py | 3 +-
lib/piston/resource.py | 101 ++++++++----
lib/piston/templates/documentation.html | 55 +++++++
lib/piston/templates/piston/authorize_token.html | 15 ++
lib/piston/utils.py | 186 +++++++++++++++++++---
11 files changed, 546 insertions(+), 101 deletions(-)
---
diff --git a/lib/piston/authentication.py b/lib/piston/authentication.py
index 2f325fa..c0bd1cc 100644
--- a/lib/piston/authentication.py
+++ b/lib/piston/authentication.py
@@ -1,3 +1,6 @@
+import binascii
+
+import oauth
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.decorators import login_required
@@ -8,9 +11,7 @@ from django.core.urlresolvers import get_callable
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.utils.importlib import import_module
-import oauth
from piston import forms
class NoAuthentication(object):
@@ -46,13 +47,16 @@ class HttpBasicAuthentication(object):
if not auth_string:
return False
- (authmeth, auth) = auth_string.split(" ", 1)
-
- if not authmeth.lower() == 'basic':
+ try:
+ (authmeth, auth) = auth_string.split(" ", 1)
+
+ if not authmeth.lower() == 'basic':
+ return False
+
+ auth = auth.strip().decode('base64')
+ (username, password) = auth.split(':', 1)
+ except (ValueError, binascii.Error):
return False
-
- auth = auth.strip().decode('base64')
- (username, password) = auth.split(':', 1)
request.user = self.auth_func(username=username, password=password) \
or AnonymousUser()
@@ -65,8 +69,6 @@ class HttpBasicAuthentication(object):
resp.status_code = 401
return resp
-DataStore = None
-
def load_data_store():
'''Load data store for OAuth Consumers, Tokens, Nonces and Resources
'''
@@ -75,8 +77,9 @@ def load_data_store():
# stolen from django.contrib.auth.load_backend
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
+
try:
- mod = import_module(module)
+ mod = __import__(module, {}, {}, attr)
except ImportError, e:
raise ImproperlyConfigured, 'Error importing OAuth data store %s: "%s"' % (module, e)
@@ -87,6 +90,9 @@ def load_data_store():
return cls
+# Set the datastore here.
+oauth_datastore = load_data_store()
+
def initialize_server_request(request):
"""
Shortcut for initialization.
@@ -97,11 +103,7 @@ def initialize_server_request(request):
query_string=request.environ.get('QUERY_STRING', ''))
if oauth_request:
- global DataStore
- if DataStore is None:
- DataStore = load_data_store()
-
- oauth_server = oauth.OAuthServer(DataStore(oauth_request))
+ oauth_server = oauth.OAuthServer(oauth_datastore(oauth_request))
oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
else:
@@ -143,6 +145,7 @@ def oauth_auth_view(request, token, callback, params):
'oauth_token': token.key,
'oauth_callback': callback,
})
+
return render_to_response('piston/authorize_token.html',
{ 'form': form }, RequestContext(request))
diff --git a/lib/piston/doc.py b/lib/piston/doc.py
index 02c54af..441702f 100644
--- a/lib/piston/doc.py
+++ b/lib/piston/doc.py
@@ -1,5 +1,11 @@
import inspect, handler
+from piston.handler import typemapper
+
+from django.core.urlresolvers import get_resolver, get_callable, get_script_prefix
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
def generate_doc(handler_cls):
"""
Returns a `HandlerDocumentation` object
@@ -72,19 +78,89 @@ class HandlerDocumentation(object):
met = getattr(self.handler, method)
stale = inspect.getmodule(met) is handler
- if met and (not stale or include_default):
- yield HandlerMethod(met, stale)
+ if not self.handler.is_anonymous:
+ if met and (not stale or include_default):
+ yield HandlerMethod(met, stale)
+ else:
+ if not stale or met.__name__ == "read" \
+ and 'GET' in self.allowed_methods:
+
+ yield HandlerMethod(met, stale)
+
+ def get_all_methods(self):
+ return self.get_methods(include_default=True)
@property
def is_anonymous(self):
- return False
+ return handler.is_anonymous
def get_model(self):
return getattr(self, 'model', None)
+
+ def get_doc(self):
+ return self.handler.__doc__
+ doc = property(get_doc)
+
@property
def name(self):
return self.handler.__name__
+ @property
+ def allowed_methods(self):
+ return self.handler.allowed_methods
+
+ def get_resource_uri_template(self):
+ """
+ URI template processor.
+
+ See http://bitworking.org/projects/URI-Templates/
+ """
+ def _convert(template, params=[]):
+ """URI template converter"""
+ paths = template % dict([p, "{%s}" % p] for p in params)
+ return u'%s%s' % (get_script_prefix(), paths)
+
+ try:
+ resource_uri = self.handler.resource_uri()
+
+ components = [None, [], {}]
+
+ for i, value in enumerate(resource_uri):
+ components[i] = value
+
+ lookup_view, args, kwargs = components
+ lookup_view = get_callable(lookup_view, True)
+
+ possibilities = get_resolver(None).reverse_dict.getlist(lookup_view)
+
+ for possibility, pattern in possibilities:
+ for result, params in possibility:
+ if args:
+ if len(args) != len(params):
+ continue
+ return _convert(result, params)
+ else:
+ if set(kwargs.keys()) != set(params):
+ continue
+ return _convert(result, params)
+ except:
+ return None
+
+ resource_uri_template = property(get_resource_uri_template)
+
def __repr__(self):
return u'<Documentation for "%s">' % self.name
+
+def documentation_view(request):
+ """
+ Generic documentation view. Generates documentation
+ from the handlers you've defined.
+ """
+ docs = [ ]
+
+ for handler, (model, anonymous) in typemapper.iteritems():
+ docs.append(generate_doc(handler))
+
+ return render_to_response('documentation.html',
+ { 'docs': docs }, RequestContext(request))
diff --git a/lib/piston/emitters.py b/lib/piston/emitters.py
index 7190ca1..e82670d 100644
--- a/lib/piston/emitters.py
+++ b/lib/piston/emitters.py
@@ -1,4 +1,6 @@
-import types, decimal, types, re, inspect
+from __future__ import generators
+
+import decimal, re, inspect
try:
# yaml isn't standard with python. It shouldn't be required if it
@@ -7,6 +9,16 @@ try:
except ImportError:
yaml = None
+# Fallback since `any` isn't in Python <2.5
+try:
+ any
+except NameError:
+ def any(iterable):
+ for element in iterable:
+ if element:
+ return True
+ return False
+
from django.db.models.query import QuerySet
from django.db.models import Model, permalink
from django.utils import simplejson
@@ -14,8 +26,9 @@ from django.utils.xmlutils import SimplerXMLGenerator
from django.utils.encoding import smart_unicode
from django.core.serializers.json import DateTimeAwareJSONEncoder
from django.http import HttpResponse
+from django.core import serializers
-from utils import HttpStatusCode
+from utils import HttpStatusCode, Mimer
try:
import cStringIO as StringIO
@@ -47,6 +60,19 @@ class Emitter(object):
if isinstance(self.data, Exception):
raise
+ def method_fields(self, data, fields):
+ if not data:
+ return { }
+
+ has = dir(data)
+ ret = dict()
+
+ for field in fields:
+ if field in has:
+ ret[field] = getattr(data, field)
+
+ return ret
+
def construct(self):
"""
Recursively serialize a lot of types, and
@@ -61,7 +87,9 @@ class Emitter(object):
"""
ret = None
- if isinstance(thing, (tuple, list, QuerySet)):
+ if isinstance(thing, QuerySet):
+ ret = _qs(thing, fields=fields)
+ elif isinstance(thing, (tuple, list)):
ret = _list(thing)
elif isinstance(thing, dict):
ret = _dict(thing)
@@ -70,10 +98,14 @@ class Emitter(object):
elif isinstance(thing, Model):
ret = _model(thing, fields=fields)
elif isinstance(thing, HttpResponse):
- raise HttpStatusCode(thing.content, code=thing.status_code)
- elif isinstance(thing, types.FunctionType):
+ raise HttpStatusCode(thing)
+ elif inspect.isfunction(thing):
if not inspect.getargspec(thing)[0]:
ret = _any(thing())
+ elif hasattr(thing, '__emittable__'):
+ f = thing.__emittable__
+ if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+ ret = _any(f())
else:
ret = smart_unicode(thing, strings_only=True)
@@ -103,9 +135,10 @@ class Emitter(object):
`exclude` on the handler (see `typemapper`.)
"""
ret = { }
+ handler = self.in_typemapper(type(data), self.anonymous)
+ get_absolute_uri = False
- if self.in_typemapper(type(data), self.anonymous) or fields:
-
+ if handler or fields:
v = lambda f: getattr(data, f.attname)
if not fields:
@@ -115,7 +148,10 @@ class Emitter(object):
"""
mapped = self.in_typemapper(type(data), self.anonymous)
get_fields = set(mapped.fields)
- exclude_fields = set(mapped.exclude)
+ exclude_fields = set(mapped.exclude).difference(get_fields)
+
+ if 'absolute_uri' in get_fields:
+ get_absolute_uri = True
if not get_fields:
get_fields = set([ f.attname.replace("_id", "", 1)
@@ -125,6 +161,7 @@ class Emitter(object):
for exclude in exclude_fields:
if isinstance(exclude, basestring):
get_fields.discard(exclude)
+
elif isinstance(exclude, re._pattern_type):
for field in get_fields.copy():
if exclude.match(field):
@@ -133,8 +170,10 @@ class Emitter(object):
else:
get_fields = set(fields)
+ met_fields = self.method_fields(handler, get_fields)
+
for f in data._meta.local_fields:
- if f.serialize:
+ if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
if not f.rel:
if f.attname in get_fields:
ret[f.attname] = _any(v(f))
@@ -145,14 +184,14 @@ class Emitter(object):
get_fields.remove(f.name)
for mf in data._meta.many_to_many:
- if mf.serialize:
+ if mf.serialize and mf.attname not in met_fields:
if mf.attname in get_fields:
ret[mf.name] = _m2m(data, mf)
get_fields.remove(mf.name)
# try to get the remainder of fields
for maybe_field in get_fields:
-
+
if isinstance(maybe_field, (list, tuple)):
model, fields = maybe_field
inst = getattr(data, model, None)
@@ -160,19 +199,31 @@ class Emitter(object):
if inst:
if hasattr(inst, 'all'):
ret[model] = _related(inst, fields)
+ elif callable(inst):
+ if len(inspect.getargspec(inst)[0]) == 1:
+ ret[model] = _any(inst(), fields)
else:
ret[model] = _model(inst, fields)
+ elif maybe_field in met_fields:
+ # Overriding normal field which has a "resource method"
+ # so you can alter the contents of certain fields without
+ # using different names.
+ ret[maybe_field] = _any(met_fields[maybe_field](data))
+
else:
maybe = getattr(data, maybe_field, None)
if maybe:
- if isinstance(maybe, (int, basestring)):
+ if callable(maybe):
+ if len(inspect.getargspec(maybe)[0]) == 1:
+ ret[maybe_field] = _any(maybe())
+ else:
ret[maybe_field] = _any(maybe)
else:
- handler_f = getattr(self.handler, maybe_field, None)
+ handler_f = getattr(handler or self.handler, maybe_field, None)
if handler_f:
- ret[maybe_field] = handler_f(data)
+ ret[maybe_field] = _any(handler_f(data))
else:
for f in data._meta.fields:
@@ -185,8 +236,8 @@ class Emitter(object):
ret[k] = _any(getattr(data, k))
# resouce uri
- if type(data) in self.typemapper.keys():
- handler = self.typemapper.get(type(data))
+ if self.in_typemapper(type(data), self.anonymous):
+ handler = self.in_typemapper(type(data), self.anonymous)
if hasattr(handler, 'resource_uri'):
url_id, fields = handler.resource_uri()
ret['resource_uri'] = permalink( lambda: (url_id,
@@ -197,12 +248,18 @@ class Emitter(object):
except: pass
# absolute uri
- if hasattr(data, 'get_absolute_url'):
+ if hasattr(data, 'get_absolute_url') and get_absolute_uri:
try: ret['absolute_uri'] = data.get_absolute_url()
except: pass
return ret
+ def _qs(data, fields=()):
+ """
+ Querysets.
+ """
+ return [ _any(v, fields) for v in data ]
+
def _list(data):
"""
Lists.
@@ -230,6 +287,15 @@ class Emitter(object):
"""
raise NotImplementedError("Please implement render.")
+ def stream_render(self, request, stream=True):
+ """
+ Tells our patched middleware not to look
+ at the contents, and returns a generator
+ rather than the buffered string. Should be
+ more memory friendly for large datasets.
+ """
+ yield self.render(request)
+
@classmethod
def get(cls, format):
"""
@@ -264,7 +330,9 @@ class XMLEmitter(Emitter):
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, {})
@@ -288,6 +356,7 @@ class XMLEmitter(Emitter):
return stream.getvalue()
Emitter.register('xml', XMLEmitter, 'text/xml; charset=utf-8')
+Mimer.register(lambda *a: None, ('text/xml',))
class JSONEmitter(Emitter):
"""
@@ -295,7 +364,7 @@ class JSONEmitter(Emitter):
"""
def render(self, request):
cb = request.GET.get('callback')
- seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder)
+ seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
# Callback
if cb:
@@ -304,6 +373,7 @@ class JSONEmitter(Emitter):
return seria
Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')
+Mimer.register(simplejson.loads, ('application/json',))
class YAMLEmitter(Emitter):
"""
@@ -315,6 +385,7 @@ class YAMLEmitter(Emitter):
if yaml: # Only register yaml if it was import successfully.
Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
+ Mimer.register(yaml.load, ('application/x-yaml',))
class PickleEmitter(Emitter):
"""
@@ -323,4 +394,21 @@ class PickleEmitter(Emitter):
def render(self, request):
return pickle.dumps(self.construct())
-Emitter.register('pickle', PickleEmitter, 'application/octet-stream')
+Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
+Mimer.register(pickle.loads, ('application/python-pickle',))
+
+class DjangoEmitter(Emitter):
+ """
+ Emitter for the Django serialized format.
+ """
+ def render(self, request, format='xml'):
+ if isinstance(self.data, HttpResponse):
+ return self.data
+ elif isinstance(self.data, (int, str)):
+ response = self.data
+ else:
+ response = serializers.serialize(format, self.data, indent=True)
+
+ return response
+
+Emitter.register('django', DjangoEmitter, 'text/xml; charset=utf-8')
diff --git a/lib/piston/forms.py b/lib/piston/forms.py
index 8f1f1d7..0cf9d4b 100644
--- a/lib/piston/forms.py
+++ b/lib/piston/forms.py
@@ -1,5 +1,4 @@
-import hmac
-import base64
+import hmac, base64
from django import forms
from django.conf import settings
@@ -24,7 +23,7 @@ class ModelForm(forms.ModelForm):
class OAuthAuthenticationForm(forms.Form):
oauth_token = forms.CharField(widget=forms.HiddenInput)
- oauth_callback = forms.URLField(widget=forms.HiddenInput)
+ oauth_callback = forms.CharField(widget=forms.HiddenInput)
authorize_access = forms.BooleanField(required=True)
csrf_signature = forms.CharField(widget=forms.HiddenInput)
diff --git a/lib/piston/handler.py b/lib/piston/handler.py
index f983c63..19acbee 100644
--- a/lib/piston/handler.py
+++ b/lib/piston/handler.py
@@ -1,4 +1,5 @@
-from piston.utils import rc
+from utils import rc
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
typemapper = { }
@@ -57,8 +58,18 @@ class BaseHandler(object):
def read(self, request, *args, **kwargs):
if not self.has_model():
return rc.NOT_IMPLEMENTED
-
- return self.model.objects.filter(*args, **kwargs)
+
+ pkfield = self.model._meta.pk.name
+
+ if pkfield in kwargs:
+ try:
+ return self.model.objects.get(pk=kwargs.get(pkfield))
+ except ObjectDoesNotExist:
+ return rc.NOT_FOUND
+ except MultipleObjectsReturned: # should never happen, since we're using a PK
+ return rc.BAD_REQUEST
+ else:
+ return self.model.objects.filter(*args, **kwargs)
def create(self, request, *args, **kwargs):
if not self.has_model():
@@ -73,6 +84,8 @@ class BaseHandler(object):
inst = self.model(**attrs)
inst.save()
return inst
+ except self.model.MultipleObjectsReturned:
+ return rc.DUPLICATE_ENTRY
def update(self, request, *args, **kwargs):
# TODO: This doesn't work automatically yet.
diff --git a/lib/piston/middleware.py b/lib/piston/middleware.py
new file mode 100644
index 0000000..6d1c155
--- /dev/null
+++ b/lib/piston/middleware.py
@@ -0,0 +1,20 @@
+from django.middleware.http import ConditionalGetMiddleware
+from django.middleware.common import CommonMiddleware
+
+def compat_middleware_factory(klass):
+ """
+ Class wrapper that only executes `process_response`
+ if `streaming` is not set on the `HttpResponse` object.
+ Django has a bad habbit of looking at the content,
+ which will prematurely exhaust the data source if we're
+ using generators or buffers.
+ """
+ class compatwrapper(klass):
+ def process_response(self, req, resp):
+ if not hasattr(resp, 'streaming'):
+ return klass.process_response(self, req, resp)
+ return resp
+ return compatwrapper
+
+ConditionalMiddlewareCompatProxy = compat_middleware_factory(ConditionalGetMiddleware)
+CommonMiddlewareCompatProxy = compat_middleware_factory(CommonMiddleware)
diff --git a/lib/piston/oauth.py b/lib/piston/oauth.py
index 6090800..1e50f1c 100644
--- a/lib/piston/oauth.py
+++ b/lib/piston/oauth.py
@@ -247,10 +247,11 @@ class OAuthRequest(object):
@staticmethod
def _split_header(header):
params = {}
+ header = header.replace('OAuth ', '', 1)
parts = header.split(',')
for param in parts:
# ignore realm parameter
- if param.find('OAuth realm') > -1:
+ if param.find('realm') > -1:
continue
# remove whitespace
param = param.strip()
diff --git a/lib/piston/resource.py b/lib/piston/resource.py
index bae36c2..d78121e 100644
--- a/lib/piston/resource.py
+++ b/lib/piston/resource.py
@@ -1,6 +1,7 @@
import sys, inspect
-from django.http import HttpResponse, Http404, HttpResponseNotAllowed, HttpResponseForbidden
+from django.http import (HttpResponse, Http404, HttpResponseNotAllowed,
+ HttpResponseForbidden, HttpResponseServerError)
from django.views.debug import ExceptionReporter
from django.views.decorators.vary import vary_on_headers
from django.conf import settings
@@ -9,16 +10,9 @@ from django.core.mail import send_mail, EmailMessage
from emitters import Emitter
from handler import typemapper
from doc import HandlerMethod
-from utils import coerce_put_post, FormValidationError, HttpStatusCode, rc, format_error
-
-class NoAuthentication(object):
- """
- Authentication handler that always returns
- True, so no authentication is needed, nor
- initiated (`challenge` is missing.)
- """
- def is_authenticated(self, request):
- return True
+from authentication import NoAuthentication
+from utils import coerce_put_post, FormValidationError, HttpStatusCode
+from utils import rc, format_error, translate_mime, MimerDataException
class Resource(object):
"""
@@ -45,6 +39,24 @@ class Resource(object):
# Erroring
self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
+ self.stream = getattr(settings, 'PISTON_STREAM_OUTPUT', False)
+
+ def determine_emitter(self, request, *args, **kwargs):
+ """
+ Function for determening which emitter to use
+ for output. It lives here so you can easily subclass
+ `Resource` in order to change how emission is detected.
+
+ You could also check for the `Accept` HTTP header here,
+ since that pretty much makes sense. Refer to `Mimer` for
+ that as well.
+ """
+ em = kwargs.pop('emitter_format', None)
+
+ if not em:
+ em = request.GET.get('format', 'json')
+
+ return em
@vary_on_headers('Authorization')
def __call__(self, request, *args, **kwargs):
@@ -52,33 +64,45 @@ class Resource(object):
NB: Sends a `Vary` header so we don't cache requests
that are different (OAuth stuff in `Authorization` header.)
"""
+ rm = request.method.upper()
+
+ # Django's internal mechanism doesn't pick up
+ # PUT request, so we trick it a little here.
+ if rm == "PUT":
+ coerce_put_post(request)
+
if not self.authentication.is_authenticated(request):
- if self.handler.anonymous and callable(self.handler.anonymous):
+ if hasattr(self.handler, 'anonymous') and \
+ callable(self.handler.anonymous) and \
+ rm in self.handler.anonymous.allowed_methods:
+
handler = self.handler.anonymous()
anonymous = True
else:
return self.authentication.challenge()
else:
handler = self.handler
- anonymous = False
+ anonymous = handler.is_anonymous
- rm = request.method.upper()
-
- # Django's internal mechanism doesn't pick up
- # PUT request, so we trick it a little here.
- if rm == "PUT":
- coerce_put_post(request)
+ # Translate nested datastructs into `request.data` here.
+ if rm in ('POST', 'PUT'):
+ try:
+ translate_mime(request)
+ except MimerDataException:
+ return rc.BAD_REQUEST
if not rm in handler.allowed_methods:
return HttpResponseNotAllowed(handler.allowed_methods)
- meth = getattr(handler, Resource.callmap.get(rm), None)
+ meth = getattr(handler, self.callmap.get(rm), None)
if not meth:
raise Http404
# Support emitter both through (?P<emitter_format>) and ?format=emitter.
- em_format = kwargs.pop('emitter_format', request.GET.get('format', 'json'))
+ em_format = self.determine_emitter(request, *args, **kwargs)
+
+ kwargs.pop('emitter_format', None)
# Clean up the request object a bit, since we might
# very well have `oauth_`-headers in there, and we
@@ -87,9 +111,9 @@ class Resource(object):
try:
result = meth(request, *args, **kwargs)
- except FormValidationError, form:
+ except FormValidationError, e:
# TODO: Use rc.BAD_REQUEST here
- return HttpResponse("Bad Request: %s" % form.errors, status=400)
+ return HttpResponse("Bad Request: %s" % e.form.errors, status=400)
except TypeError, e:
result = rc.BAD_REQUEST
hm = HandlerMethod(meth)
@@ -107,7 +131,8 @@ class Resource(object):
result.content = format_error(msg)
except HttpStatusCode, e:
- result = e
+ #result = e ## why is this being passed on and not just dealt with now?
+ return e.response
except Exception, e:
"""
On errors (like code errors), we'd like to be able to
@@ -123,24 +148,36 @@ class Resource(object):
If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
receive a basic "500 Internal Server Error" message.
"""
+ exc_type, exc_value, tb = sys.exc_info()
+ rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
if self.email_errors:
- exc_type, exc_value, tb = sys.exc_info()
- rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
-
self.email_exception(rep)
-
if self.display_errors:
- result = format_error('\n'.join(rep.format_exception()))
+ return HttpResponseServerError(
+ format_error('\n'.join(rep.format_exception())))
else:
raise
emitter, ct = Emitter.get(em_format)
srl = emitter(result, typemapper, handler, handler.fields, anonymous)
-
+
try:
- return HttpResponse(srl.render(request), mimetype=ct)
+ """
+ Decide whether or not we want a generator here,
+ or we just want to buffer up the entire result
+ before sending it to the client. Won't matter for
+ smaller datasets, but larger will have an impact.
+ """
+ if self.stream: stream = srl.stream_render(request)
+ else: stream = srl.render(request)
+
+ resp = HttpResponse(stream, mimetype=ct)
+
+ resp.streaming = self.stream
+
+ return resp
except HttpStatusCode, e:
- return HttpResponse(e.message, status=e.code)
+ return e.response
@staticmethod
def cleanup_request(request):
diff --git a/lib/piston/templates/documentation.html b/lib/piston/templates/documentation.html
new file mode 100644
index 0000000..d7b1830
--- /dev/null
+++ b/lib/piston/templates/documentation.html
@@ -0,0 +1,55 @@
+{% load markup %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+"http://www.w3.org/TR/html4/strict.dtd">
+<html>
+ <head>
+ <title>
+ Piston generated documentation
+ </title>
+ <style type="text/css">
+ body {
+ background: #fffff0;
+ font: 1em "Helvetica Neue", Verdana;
+ padding: 0 0 0 25px;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>API Documentation</h1>
+
+ {% for doc in docs %}
+
+ <h3>{{ doc.name|cut:"Handler" }}:</h3>
+
+ <p>
+ {{ doc.get_doc|default:""|restructuredtext }}
+ </p>
+
+ <p>
+ URL: <b>{{ doc.get_resource_uri_template }}</b>
+ </p>
+
+ <p>
+ Accepted methods: {% for meth in doc.allowed_methods %}<b>{{ meth }}</b>{% if not forloop.last %}, {% endif %}{% endfor %}
+ </p>
+
+ <dl>
+ {% for method in doc.get_all_methods %}
+
+ <dt>
+ method <i>{{ method.name }}</i>({{ method.signature }}){% if method.stale %} <i>- inherited</i>{% else %}:{% endif %}
+
+ </dt>
+
+ {% if method.get_doc %}
+ <dd>
+ {{ method.get_doc|default:""|restructuredtext }}
+ <dd>
+ {% endif %}
+
+ {% endfor %}
+ </dl>
+
+ {% endfor %}
+ </body>
+</html>
diff --git a/lib/piston/templates/piston/authorize_token.html b/lib/piston/templates/piston/authorize_token.html
new file mode 100644
index 0000000..dae840e
--- /dev/null
+++ b/lib/piston/templates/piston/authorize_token.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+"http://www.w3.org/TR/html4/strict.dtd">
+<html>
+ <head>
+ <title>Authorize Token</title>
+ </head>
+ <body>
+ <h1>Authorize Token</h1>
+
+ <form action="{% url piston.authentication.oauth_user_auth %}" method="POST">
+ {{ form.as_table }}
+ </form>
+
+ </body>
+</html>
diff --git a/lib/piston/utils.py b/lib/piston/utils.py
index a992ef2..b197ded 100644
--- a/lib/piston/utils.py
+++ b/lib/piston/utils.py
@@ -1,5 +1,4 @@
-from functools import wraps
-from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse
+from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from django.core.cache import cache
from django import get_version as django_version
@@ -7,7 +6,7 @@ from decorator import decorator
from datetime import datetime, timedelta
-__version__ = '0.2'
+__version__ = '0.2.2'
def get_version():
return __version__
@@ -16,31 +15,43 @@ def format_error(error):
return u"Piston/%s (Django %s) crash report:\n\n%s" % \
(get_version(), django_version(), error)
-def create_reply(message, status=200):
- return HttpResponse(message, status=status)
-
-class rc(object):
+class rc_factory(object):
"""
Status codes.
"""
- ALL_OK = create_reply('OK', status=200)
- CREATED = create_reply('Created', status=201)
- DELETED = create_reply('', status=204) # 204 says "Don't send a body!"
- BAD_REQUEST = create_reply('Bad Request', status=400)
- FORBIDDEN = create_reply('Forbidden', status=401)
- DUPLICATE_ENTRY = create_reply('Conflict/Duplicate', status=409)
- NOT_HERE = create_reply('Gone', status=410)
- NOT_IMPLEMENTED = create_reply('Not Implemented', status=501)
- THROTTLED = create_reply('Throttled', status=503)
+ CODES = dict(ALL_OK = ('OK', 200),
+ CREATED = ('Created', 201),
+ DELETED = ('', 204), # 204 says "Don't send a body!"
+ BAD_REQUEST = ('Bad Request', 400),
+ FORBIDDEN = ('Forbidden', 401),
+ NOT_FOUND = ('Not Found', 404),
+ DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
+ NOT_HERE = ('Gone', 410),
+ NOT_IMPLEMENTED = ('Not Implemented', 501),
+ THROTTLED = ('Throttled', 503))
+
+ def __getattr__(self, attr):
+ """
+ Returns a fresh `HttpResponse` when getting
+ an "attribute". This is backwards compatible
+ with 0.2, which is important.
+ """
+ try:
+ (r, c) = self.CODES.get(attr)
+ except TypeError:
+ raise AttributeError(attr)
+
+ return HttpResponse(r, content_type='text/plain', status=c)
+
+rc = rc_factory()
class FormValidationError(Exception):
def __init__(self, form):
self.form = form
class HttpStatusCode(Exception):
- def __init__(self, message, code=200):
- self.message = message
- self.code = code
+ def __init__(self, response):
+ self.response = response
def validate(v_form, operation='POST'):
@decorator
@@ -48,7 +59,6 @@ def validate(v_form, operation='POST'):
form = v_form(getattr(request, operation))
if form.is_valid():
-# kwa.update({ 'form': form })
return f(self, request, *a, **kwa)
else:
raise FormValidationError(form)
@@ -115,10 +125,138 @@ def throttle(max_requests, timeout=60*60, extra=''):
return wrap
def coerce_put_post(request):
+ """
+ Django doesn't particularly understand REST.
+ In case we send data over PUT, Django won't
+ actually look at the data and load it. We need
+ to twist its arm here.
+
+ The try/except abominiation here is due to a bug
+ in mod_python. This should fix it.
+ """
if request.method == "PUT":
- request.method = "POST"
- request._load_post_and_files()
- request.method = "PUT"
+ try:
+ request.method = "POST"
+ request._load_post_and_files()
+ request.method = "PUT"
+ except AttributeError:
+ request.META['REQUEST_METHOD'] = 'POST'
+ request._load_post_and_files()
+ request.META['REQUEST_METHOD'] = 'PUT'
+
request.PUT = request.POST
- del request._post
+
+class MimerDataException(Exception):
+ """
+ Raised if the content_type and data don't match
+ """
+ pass
+
+class Mimer(object):
+ TYPES = dict()
+
+ def __init__(self, request):
+ self.request = request
+
+ def is_multipart(self):
+ content_type = self.content_type()
+
+ if content_type is not None:
+ return content_type.lstrip().startswith('multipart')
+
+ return False
+
+ def loader_for_type(self, ctype):
+ """
+ Gets a function ref to deserialize content
+ for a certain mimetype.
+ """
+ for loadee, mimes in Mimer.TYPES.iteritems():
+ for mime in mimes:
+ if ctype.startswith(mime):
+ return loadee
+
+ def content_type(self):
+ """
+ Returns the content type of the request in all cases where it is
+ different than a submitted form - application/x-www-form-urlencoded
+ """
+ type_formencoded = "application/x-www-form-urlencoded"
+
+ ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
+
+ if ctype.startswith(type_formencoded):
+ return None
+
+ return ctype
+
+
+ def translate(self):
+ """
+ Will look at the `Content-type` sent by the client, and maybe
+ deserialize the contents into the format they sent. This will
+ work for JSON, YAML, XML and Pickle. Since the data is not just
+ key-value (and maybe just a list), the data will be placed on
+ `request.data` instead, and the handler will have to read from
+ there.
+
+ It will also set `request.content_type` so the handler has an easy
+ way to tell what's going on. `request.content_type` will always be
+ None for form-encoded and/or multipart form data (what your browser sends.)
+ """
+ ctype = self.content_type()
+ self.request.content_type = ctype
+
+ if not self.is_multipart() and ctype:
+ loadee = self.loader_for_type(ctype)
+
+ try:
+ self.request.data = loadee(self.request.raw_post_data)
+
+ # Reset both POST and PUT from request, as its
+ # misleading having their presence around.
+ self.request.POST = self.request.PUT = dict()
+ except (TypeError, ValueError):
+ raise MimerDataException
+
+ return self.request
+
+ @classmethod
+ def register(cls, loadee, types):
+ cls.TYPES[loadee] = types
+
+ @classmethod
+ def unregister(cls, loadee):
+ return cls.TYPES.pop(loadee)
+
+def translate_mime(request):
+ request = Mimer(request).translate()
+
+def require_mime(*mimes):
+ """
+ Decorator requiring a certain mimetype. There's a nifty
+ helper called `require_extended` below which requires everything
+ we support except for post-data via form.
+ """
+ @decorator
+ def wrap(f, self, request, *args, **kwargs):
+ m = Mimer(request)
+ realmimes = set()
+
+ rewrite = { 'json': 'application/json',
+ 'yaml': 'application/x-yaml',
+ 'xml': 'text/xml',
+ 'pickle': 'application/python-pickle' }
+
+ for idx, mime in enumerate(mimes):
+ realmimes.add(rewrite.get(mime, mime))
+
+ if not m.content_type() in realmimes:
+ return rc.BAD_REQUEST
+
+ return f(self, request, *args, **kwargs)
+ return wrap
+
+require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]