Files
2011-06-08 15:37:01 -04:00

305 lines
10 KiB
Python

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 EmailMultiAlternatives
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
from utils import head_guard
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',
'HEAD': 'meta' }
def __init__(self, handler, authentication=None):
if not callable(handler):
raise AttributeError, "Handler not callable."
self.handler = handler()
self.csrf_exempt = getattr(self.handler, 'csrf_exempt', True)
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
def form_validation_response(self, e):
"""
Method to return form validation error information.
You will probably want to override this in your own
`Resource` subclass.
"""
resp = rc.BAD_REQUEST
resp.write(' '+str(e.form.errors))
return resp
@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
@head_guard
@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 hasattr(request, 'data'):
if rm == 'POST':
request.data = request.POST
else:
request.data = request.PUT
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 Exception, e:
result = self.error_handler(e, request, meth, em_format)
try:
emitter, ct = Emitter.get(em_format)
fields = handler.fields
if hasattr(handler, 'list_fields') and isinstance(result, (list, tuple, QuerySet)):
fields = handler.list_fields
except ValueError:
result = rc.BAD_REQUEST
result.content = "Invalid output format specified '%s'." % em_format
return result
status_code = 200
# If we're looking at a response object which contains non-string
# content, then assume we should use the emitter to format that
# content
if isinstance(result, HttpResponse) and not result._is_string:
status_code = result.status_code
# Note: We can't use result.content here because that method attempts
# to convert the content into a string which we don't want.
# when _is_string is False _container is the raw data
result = result._container
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, status=status_code)
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()
email_text = "This message is only visible in HTML view."
email = EmailMultiAlternatives(subject, email_text, settings.SERVER_EMAIL, [ admin[1] for admin in settings.ADMINS ])
email.attach_alternative(html, "text/html")
email.send(fail_silently=True)
def error_handler(self, e, request, meth, em_format):
"""
Override this method to add handling of errors customized for your
needs
"""
if isinstance(e, FormValidationError):
return self.form_validation_response(e)
elif isinstance(e, TypeError):
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)
return result
elif isinstance(e, Http404):
return rc.NOT_FOUND
elif isinstance(e, HttpStatusCode):
return e.response
else:
"""
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