[snowy] Update piston to 92644a459862



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]