diff options
| author | tom christie tom@tomchristie.com | 2011-01-30 18:30:39 +0000 |
|---|---|---|
| committer | tom christie tom@tomchristie.com | 2011-01-30 18:30:39 +0000 |
| commit | 42f2f9b40d1295e18a5b720b0d1f6ad85e928d8a (patch) | |
| tree | 458f97992bcd1348a413d21a5925f933ba7f74e3 /flywheel/resource.py | |
| parent | 8a470f031eeccf45625c3e3e18a8743021b38d41 (diff) | |
| download | django-rest-framework-42f2f9b40d1295e18a5b720b0d1f6ad85e928d8a.tar.bz2 | |
Rename to django-rest-framework, get simpleexample working
Diffstat (limited to 'flywheel/resource.py')
| -rw-r--r-- | flywheel/resource.py | 451 |
1 files changed, 0 insertions, 451 deletions
diff --git a/flywheel/resource.py b/flywheel/resource.py deleted file mode 100644 index 2a8554f3..00000000 --- a/flywheel/resource.py +++ /dev/null @@ -1,451 +0,0 @@ -from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse -from django.http import HttpResponse - -from flywheel import emitters, parsers, authenticators -from flywheel.response import status, Response, ResponseException - -from decimal import Decimal -import re - -# TODO: Figure how out references and named urls need to work nicely -# TODO: POST on existing 404 URL, PUT on existing 404 URL -# -# NEXT: Exceptions on func() -> 500, tracebacks emitted if settings.DEBUG -# - -__all__ = ['Resource'] - - - -class Resource(object): - """Handles incoming requests and maps them to REST operations, - performing authentication, input deserialization, input validation, output serialization.""" - - # List of RESTful operations which may be performed on this resource. - allowed_methods = ('GET',) - anon_allowed_methods = () - - # List of emitters the resource can serialize the response with, ordered by preference - emitters = ( emitters.JSONEmitter, - emitters.DocumentingHTMLEmitter, - emitters.DocumentingXHTMLEmitter, - emitters.DocumentingPlainTextEmitter, - emitters.XMLEmitter ) - - # List of content-types the resource can read from - parsers = ( parsers.JSONParser, - parsers.XMLParser, - parsers.FormParser ) - - # List of all authenticating methods to attempt - authenticators = ( authenticators.UserLoggedInAuthenticator, - authenticators.BasicAuthenticator ) - - # Optional form for input validation and presentation of HTML formatted responses. - form = None - - # Map standard HTTP methods to function calls - callmap = { 'GET': 'get', 'POST': 'post', - 'PUT': 'put', 'DELETE': 'delete' } - - # Some reserved parameters to allow us to use standard HTML forms with our resource - # Override any/all of these with None to disable them, or override them with another value to rename them. - ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params - METHOD_PARAM = '_method' # Allow POST overloading in form params - CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms) - CONTENT_PARAM = '_content' # Allow override of body content in form params (allows sending arbitrary content with standard forms) - CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params - - - def __new__(cls, *args, **kwargs): - """Make the class callable so it can be used as a Django view.""" - self = object.__new__(cls) - if args: - request = args[0] - self.__init__(request) - return self._handle_request(request, *args[1:], **kwargs) - else: - self.__init__() - return self - - - def __init__(self, request=None): - """""" - # Setup the resource context - self.request = request - self.response = None - self.form_instance = None - - # These sets are determined now so that overridding classes can modify the various parameter names, - # or set them to None to disable them. - self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM)) - self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM)) - self.RESERVED_FORM_PARAMS.discard(None) - self.RESERVED_QUERY_PARAMS.discard(None) - - - @property - def name(self): - """Provide a name for the resource. - By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'.""" - class_name = self.__class__.__name__ - return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip() - - @property - def description(self): - """Provide a description for the resource. - By default this is the class's docstring with leading line spaces stripped.""" - return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__) - - @property - def emitted_media_types(self): - """Return an list of all the media types that this resource can emit.""" - return [emitter.media_type for emitter in self.emitters] - - @property - def default_emitter(self): - """Return the resource's most prefered emitter. - (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)""" - return self.emitters[0] - - @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] - - @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 get(self, request, auth, *args, **kwargs): - """Must be subclassed to be implemented.""" - self.not_implemented('GET') - - - def post(self, request, auth, content, *args, **kwargs): - """Must be subclassed to be implemented.""" - self.not_implemented('POST') - - - def put(self, request, auth, content, *args, **kwargs): - """Must be subclassed to be implemented.""" - self.not_implemented('PUT') - - - def delete(self, request, auth, *args, **kwargs): - """Must be subclassed to be implemented.""" - self.not_implemented('DELETE') - - - def reverse(self, view, *args, **kwargs): - """Return a fully qualified URI for a given view or resource. - Add the domain using the Sites framework if possible, otherwise fallback to using the current request.""" - return self.add_domain(reverse(view, args=args, kwargs=kwargs)) - - - def not_implemented(self, operation): - """Return an HTTP 500 server error if an operation is called which has been allowed by - allowed_methods, but which has not been implemented.""" - raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR, - {'detail': '%s operation on this resource has not been implemented' % (operation, )}) - - - def add_domain(self, path): - """Given a path, return an fully qualified URI. - Use the Sites framework if possible, otherwise fallback to using the domain from the current request.""" - - # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com' - # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html - try: - site = Site.objects.get_current() - if site.domain and site.domain != 'example.com': - return 'http://%s%s' % (site.domain, path) - except: - pass - - return self.request.build_absolute_uri(path) - - - def determine_method(self, request): - """Determine the HTTP method that this request should be treated as. - Allows PUT and DELETE tunneling via the _method parameter if METHOD_PARAM is set.""" - method = request.method.upper() - - if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM): - method = request.POST[self.METHOD_PARAM].upper() - - return method - - - def authenticate(self, request): - """Attempt to authenticate the request, returning an authentication context or None. - An authentication context may be any object, although in many cases it will be a User instance.""" - - # Attempt authentication against each authenticator in turn, - # and return None if no authenticators succeed in authenticating the request. - for authenticator in self.authenticators: - auth_context = authenticator(self).authenticate(request) - if auth_context: - return auth_context - - return None - - - def check_method_allowed(self, method, auth): - """Ensure the request method is permitted for this resource, raising a ResourceException if it is not.""" - - if not method in self.callmap.keys(): - raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED, - {'detail': 'Unknown or unsupported method \'%s\'' % method}) - - if not method in self.allowed_methods: - raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED, - {'detail': 'Method \'%s\' not allowed on this resource.' % method}) - - if auth is None and not method in self.anon_allowed_methods: - raise ResponseException(status.HTTP_403_FORBIDDEN, - {'detail': 'You do not have permission to access this resource. ' + - 'You may need to login or otherwise authenticate the request.'}) - - def get_form(self, data=None): - """Optionally return a Django Form instance, which may be used for validation - and/or rendered by an HTML/XHTML emitter. - - If data is not None the form will be bound to data.""" - - if self.form: - if data: - return self.form(data) - else: - return self.form() - return None - - - def cleanup_request(self, data, form_instance): - """Perform any resource-specific data deserialization and/or validation - after the initial HTTP content-type deserialization has taken place. - - Returns a tuple containing the cleaned up data, and optionally a form bound to that data. - - By default this uses form validation to filter the basic input into the required types.""" - - if form_instance is None: - return data - - # Default form validation does not check for additional invalid fields - non_existent_fields = [] - for key in set(data.keys()) - set(form_instance.fields.keys()): - non_existent_fields.append(key) - - if not form_instance.is_valid() or non_existent_fields: - if not form_instance.errors and not non_existent_fields: - # If no data was supplied the errors property will be None - details = 'No content was supplied' - - else: - # Add standard field errors - details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems() if key != '__all__') - - # Add any non-field errors - if form_instance.non_field_errors(): - details['errors'] = form_instance.non_field_errors() - - # Add any non-existent field errors - for key in non_existent_fields: - details[key] = ['This field does not exist'] - - # Bail. Note that we will still serialize this response with the appropriate content type - raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': details}) - - return form_instance.cleaned_data - - - def cleanup_response(self, data): - """Perform any resource-specific data filtering prior to the standard HTTP - content-type serialization. - - Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.""" - return data - - - def determine_parser(self, request): - """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_to_parser = dict([(parser.media_type, parser) for parser in self.parsers]) - - try: - 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}) - - - def determine_emitter(self, request): - """Return the appropriate emitter for the output, given the client's 'Accept' header, - and the content types that this Resource knows how to serve. - - See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html""" - - if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None): - # Use _accept parameter override - accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)] - elif request.META.has_key('HTTP_ACCEPT'): - # Use standard HTTP Accept negotiation - accept_list = request.META["HTTP_ACCEPT"].split(',') - else: - # No accept header specified - return self.default_emitter - - # Parse the accept header into a dict of {qvalue: set of media types} - # We ignore mietype parameters - accept_dict = {} - for token in accept_list: - components = token.split(';') - mimetype = components[0].strip() - qvalue = Decimal('1.0') - - if len(components) > 1: - # Parse items that have a qvalue eg text/html;q=0.9 - try: - (q, num) = components[-1].split('=') - if q == 'q': - qvalue = Decimal(num) - except: - # Skip malformed entries - continue - - if accept_dict.has_key(qvalue): - accept_dict[qvalue].add(mimetype) - else: - accept_dict[qvalue] = set((mimetype,)) - - # Convert to a list of sets ordered by qvalue (highest first) - accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)] - - for accept_set in accept_sets: - # Return any exact match - for emitter in self.emitters: - if emitter.media_type in accept_set: - return emitter - - # Return any subtype match - for emitter in self.emitters: - if emitter.media_type.split('/')[0] + '/*' in accept_set: - return emitter - - # Return default - if '*/*' in accept_set: - return self.default_emitter - - - raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE, - {'detail': 'Could not statisfy the client\'s Accept header', - 'available_types': self.emitted_media_types}) - - - def _handle_request(self, request, *args, **kwargs): - """This method is the core of Resource, through which all requests are passed. - - Broadly this consists of the following procedure: - - 0. ensure the operation is permitted - 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only) - 2. cleanup and validate request data (PUT/POST only) - 3. call the core method to get the response data - 4. cleanup the response data - 5. serialize response data into response content, using standard HTTP content negotiation - """ - emitter = None - method = self.determine_method(request) - - try: - # Before we attempt anything else determine what format to emit our response data with. - emitter = self.determine_emitter(request) - - # Authenticate the request, and store any context so that the resource operations can - # do more fine grained authentication if required. - # - # Typically the context will be a user, or None if this is an anonymous request, - # but it could potentially be more complex (eg the context of a request key which - # has been signed against a particular set of permissions) - auth_context = self.authenticate(request) - - # Ensure the requested operation is permitted on this resource - self.check_method_allowed(method, auth_context) - - # Get the appropriate create/read/update/delete function - func = getattr(self, self.callmap.get(method, None)) - - # 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, 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) - - else: - response = func(request, auth_context, *args, **kwargs) - - # Allow return value to be either Response, or an object, or None - if isinstance(response, Response): - self.response = response - elif response is not None: - self.response = Response(status.HTTP_200_OK, response) - else: - self.response = Response(status.HTTP_204_NO_CONTENT) - - # Pre-serialize filtering (eg filter complex objects into natively serializable types) - self.response.cleaned_content = self.cleanup_response(self.response.raw_content) - - - except ResponseException, exc: - self.response = exc.response - - # Fall back to the default emitter if we failed to perform content negotiation - if emitter is None: - emitter = self.default_emitter - - - # Always add these headers - self.response.headers['Allow'] = ', '.join(self.allowed_methods) - self.response.headers['Vary'] = 'Authenticate, Allow' - - # Serialize the response content - if self.response.has_content_body: - content = emitter(self).emit(output=self.response.cleaned_content) - else: - content = emitter(self).emit() - - # Build the HTTP Response - # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set - resp = HttpResponse(content, mimetype=emitter.media_type, status=self.response.status) - for (key, val) in self.response.headers.items(): - resp[key] = val - - return resp - |
