--- /dev/null
+import sys, inspect
+
+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
+from django.core.mail import send_mail, EmailMessage
+from django.db.models.query import QuerySet
+from django.http import Http404
+
+from emitters import Emitter
+from handler import typemapper
+from doc import HandlerMethod
+from authentication import NoAuthentication
+from utils import coerce_put_post, FormValidationError, HttpStatusCode
+from utils import rc, format_error, translate_mime, MimerDataException
+
+CHALLENGE = object()
+
+class Resource(object):
+ """
+ Resource. Create one for your URL mappings, just
+ like you would with Django. Takes one argument,
+ the handler. The second argument is optional, and
+ is an authentication handler. If not specified,
+ `NoAuthentication` will be used by default.
+ """
+ callmap = { 'GET': 'read', 'POST': 'create',
+ 'PUT': 'update', 'DELETE': 'delete' }
+
+ def __init__(self, handler, authentication=None):
+ if not callable(handler):
+ raise AttributeError, "Handler not callable."
+
+ self.handler = handler()
+
+ if not authentication:
+ self.authentication = (NoAuthentication(),)
+ elif isinstance(authentication, (list, tuple)):
+ self.authentication = authentication
+ else:
+ self.authentication = (authentication,)
+
+ # 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
+
+ @property
+ def anonymous(self):
+ """
+ Gets the anonymous handler. Also tries to grab a class
+ if the `anonymous` value is a string, so that we can define
+ anonymous handlers that aren't defined yet (like, when
+ you're subclassing your basehandler into an anonymous one.)
+ """
+ if hasattr(self.handler, 'anonymous'):
+ anon = self.handler.anonymous
+
+ if callable(anon):
+ return anon
+
+ for klass in typemapper.keys():
+ if anon == klass.__name__:
+ return klass
+
+ return None
+
+ def authenticate(self, request, rm):
+ actor, anonymous = False, True
+
+ for authenticator in self.authentication:
+ if not authenticator.is_authenticated(request):
+ if self.anonymous and \
+ rm in self.anonymous.allowed_methods:
+
+ actor, anonymous = self.anonymous(), True
+ else:
+ actor, anonymous = authenticator.challenge, CHALLENGE
+ else:
+ return self.handler, self.handler.is_anonymous
+
+ return actor, anonymous
+
+ @vary_on_headers('Authorization')
+ def __call__(self, request, *args, **kwargs):
+ """
+ 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)
+
+ actor, anonymous = self.authenticate(request, rm)
+
+ if anonymous is CHALLENGE:
+ return actor()
+ else:
+ handler = actor
+
+ # 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, self.callmap.get(rm), None)
+
+ if not meth:
+ raise Http404
+
+ # Support emitter both through (?P<emitter_format>) and ?format=emitter.
+ 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
+ # don't want to pass these along to the handler.
+ request = self.cleanup_request(request)
+
+ try:
+ result = meth(request, *args, **kwargs)
+ except FormValidationError, e:
+ resp = rc.BAD_REQUEST
+ resp.write(' '+str(e.form.errors))
+
+ return resp
+ except TypeError, e:
+ result = rc.BAD_REQUEST
+ hm = HandlerMethod(meth)
+ sig = hm.signature
+
+ msg = 'Method signature does not match.\n\n'
+
+ if sig:
+ msg += 'Signature should be: %s' % sig
+ else:
+ msg += 'Resource does not expect any parameters.'
+
+ if self.display_errors:
+ msg += '\n\nException was: %s' % str(e)
+
+ result.content = format_error(msg)
+ except Http404:
+ return rc.NOT_FOUND
+ except HttpStatusCode, e:
+ return e.response
+ except Exception, e:
+ """
+ On errors (like code errors), we'd like to be able to
+ give crash reports to both admins and also the calling
+ user. There's two setting parameters for this:
+
+ Parameters::
+ - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
+ error email to people in `settings.ADMINS`.
+ - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
+ to the caller, so he can tell you what error they got.
+
+ 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:
+ self.email_exception(rep)
+ if self.display_errors:
+ return HttpResponseServerError(
+ format_error('\n'.join(rep.format_exception())))
+ else:
+ raise
+
+ emitter, ct = Emitter.get(em_format)
+ fields = handler.fields
+ if hasattr(handler, 'list_fields') and (
+ isinstance(result, list) or isinstance(result, QuerySet)):
+ fields = handler.list_fields
+
+ srl = emitter(result, typemapper, handler, fields, anonymous)
+
+ try:
+ """
+ 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)
+
+ if not isinstance(stream, HttpResponse):
+ resp = HttpResponse(stream, mimetype=ct)
+ else:
+ resp = stream
+
+ resp.streaming = self.stream
+
+ return resp
+ except HttpStatusCode, e:
+ return e.response
+
+ @staticmethod
+ def cleanup_request(request):
+ """
+ Removes `oauth_` keys from various dicts on the
+ request object, and returns the sanitized version.
+ """
+ for method_type in ('GET', 'PUT', 'POST', 'DELETE'):
+ block = getattr(request, method_type, { })
+
+ if True in [ k.startswith("oauth_") for k in block.keys() ]:
+ sanitized = block.copy()
+
+ for k in sanitized.keys():
+ if k.startswith("oauth_"):
+ sanitized.pop(k)
+
+ setattr(request, method_type, sanitized)
+
+ return request
+
+ # --
+
+ def email_exception(self, reporter):
+ subject = "Piston crash report"
+ html = reporter.get_traceback_html()
+
+ message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
+ html, settings.SERVER_EMAIL,
+ [ admin[1] for admin in settings.ADMINS ])
+
+ message.content_subtype = 'html'
+ message.send(fail_silently=True)