Tweak parsers to take parser_context

This commit is contained in:
Tom Christie 2012-10-15 13:27:50 +01:00
parent e88ca9637b
commit 9c1fba3483
18 changed files with 119 additions and 125 deletions

View File

@ -60,6 +60,8 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
# API Reference
## BasicAuthentication
This policy uses [HTTP Basic Authentication][basicauth], signed against a user's username and password. Basic authentication is generally only appropriate for testing.
@ -113,7 +115,7 @@ If successfully authenticated, `SessionAuthentication` provides the following cr
* `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be `None`.
## Custom authentication policies
# Custom authentication
To implement a custom authentication policy, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.

View File

@ -37,7 +37,7 @@ Might recieve an error response indicating that the `DELETE` method is not allow
**Signature:** `APIException(detail=None)`
The base class for all exceptions raised inside REST framework.
The **base class** for all exceptions raised inside REST framework.
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.

View File

@ -100,7 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
## Creating custom pagination serializers
## Custom pagination serializers
To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return.

View File

@ -65,7 +65,7 @@ Parses `YAML` request content.
Parses REST framework's default style of `XML` request content.
Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, `SOAP`, and `XHTML`.
Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
@ -95,7 +95,19 @@ To implement a custom parser, you should override `BaseParser`, set the `.media_
The method should return the data that will be used to populate the `request.DATA` property.
For example:
The arguments passed to `.parse_stream()` are:
### stream
A stream-like object representing the body of the request.
### parser_context
If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. By default it includes the keys `'upload_handlers'` and `'meta'`, which contain the values of the `request.upload_handlers` and `request.meta` properties.
## Example
The following is an example plaintext parser that will populate the `request.DATA` property with a string representing the body of the request.
class PlainTextParser(BaseParser):
"""
@ -110,16 +122,6 @@ For example:
"""
return stream.read()
The arguments passed to `.parse_stream()` are:
### stream
A stream-like object representing the body of the request.
### parser_context
If supplied, this argument will be a dictionary containing any additional context that may be required to parse the request content. By default it includes the keys `'upload_handlers'` and `'meta'`, which contain the values of the `request.upload_handlers` and `request.meta` properties.
## Uploading file content
If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse_stream()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property.

View File

@ -54,6 +54,8 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
# API Reference
## IsAuthenticated
The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise.
@ -86,7 +88,7 @@ To use custom model permissions, override `DjangoModelPermissions` and set the `
The `DjangoModelPermissions` class also supports object-level permissions. Third-party authorization backends such as [django-guardian][guardian] that provide object-level permissions should work just fine with `DjangoModelPermissions` without any custom configuration required.
## Custom permissions
# Custom permissions
To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, view, obj=None)` method.

View File

@ -98,7 +98,7 @@ Renders the request data into `YAML`.
Renders REST framework's default style of `XML` response content.
Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, `SOAP`, and `XHTML`.
Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
@ -154,21 +154,6 @@ Renders data into HTML for the Browseable API. This renderer will determine whi
To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
For example:
from django.utils.encoding import smart_unicode
from rest_framework import renderers
class PlainText(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
if isinstance(data, basestring):
return data
return smart_unicode(data)
The arguments passed to the `.render()` method are:
### `data`
@ -184,6 +169,23 @@ Optional. If provided, this is the accepted media type, as determined by the con
Optional. If provided, this is a dictionary of contextual information provided by the view.
By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`.
## Example
The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response.
from django.utils.encoding import smart_unicode
from rest_framework import renderers
class PlainText(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
if isinstance(data, basestring):
return data
return smart_unicode(data)
---
# Advanced renderer usage

View File

@ -63,6 +63,8 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
# API Reference
## AnonRateThrottle
The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
@ -142,7 +144,7 @@ For example, given the following views...
User requests to either `ContactListView` or `ContactDetailView` would be restricted to a total of 1000 requests per-day. User requests to `UploadView` would be restricted to 20 requests per day.
## Custom throttles
# Custom throttles
To create a custom throttle, override `BaseThrottle` and implement `.allow_request(request, view)`. The method should return `True` if the request should be allowed, and `False` otherwise.

View File

@ -24,15 +24,15 @@ For a more thorough background, check out Klabnik's [Hypermedia API reading list
REST framework is an agnositic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style.
### What REST framework *does* provide.
## What REST framework *does* provide.
It is self evident that REST framework makes it possible to build Hypermedia APIs. The browseable API that it offers is built on HTML - the hypermedia language of the web.
REST framework also includes [serialization] and [parser]/[renderer] components that make it easy to build appropriate media types, [hyperlinked relations][fields] for building well-connected systems, and great support for [content negotiation][conneg].
### What REST framework *doesn't* provide.
## What REST framework *doesn't* provide.
What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include form descriptions, and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
[cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

View File

@ -1,10 +1,9 @@
"""
The :mod:`authentication` module provides a set of pluggable authentication classes.
Authentication behavior is provided by mixing the :class:`mixins.RequestMixin` class into a :class:`View` class.
Provides a set of pluggable authentication policies.
"""
from django.contrib.auth import authenticate
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.authtoken.models import Token
import base64
@ -17,25 +16,14 @@ class BaseAuthentication(object):
def authenticate(self, request):
"""
Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_
.. [*] The authentication context *will* typically be a :obj:`User`,
but it need not be. It can be any user-like object so long as the
permissions classes (see the :mod:`permissions` module) on the view can
handle the object and use it to determine if the request has the required
permissions or not.
This can be an important distinction if you're implementing some token
based authentication mechanism, where the authentication context
may be more involved than simply mapping to a :obj:`User`.
Authenticate the request and return a two-tuple of (user, token).
"""
return None
raise NotImplementedError(".authenticate() must be overridden.")
class BasicAuthentication(BaseAuthentication):
"""
Base class for HTTP Basic authentication.
Subclasses should implement `.authenticate_credentials()`.
HTTP Basic authentication against username/password.
"""
def authenticate(self, request):
@ -43,8 +31,6 @@ class BasicAuthentication(BaseAuthentication):
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2 and auth[0].lower() == "basic":
@ -54,21 +40,13 @@ class BasicAuthentication(BaseAuthentication):
return None
try:
userid, password = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2])
userid = smart_unicode(auth_parts[0])
password = smart_unicode(auth_parts[2])
except DjangoUnicodeDecodeError:
return None
return self.authenticate_credentials(userid, password)
def authenticate_credentials(self, userid, password):
"""
Given the Basic authentication userid and password, authenticate
and return a user instance.
"""
raise NotImplementedError('.authenticate_credentials() must be overridden')
class UserBasicAuthentication(BasicAuthentication):
def authenticate_credentials(self, userid, password):
"""
Authenticate the userid and password against username and password.
@ -85,8 +63,8 @@ class SessionAuthentication(BaseAuthentication):
def authenticate(self, request):
"""
Returns a :obj:`User` if the request session currently has a logged in user.
Otherwise returns :const:`None`.
Returns a `User` if the request session currently has a logged in user.
Otherwise returns `None`.
"""
# Get the underlying HttpRequest object

View File

@ -38,7 +38,7 @@ class BaseParser(object):
media_type = None
def parse(self, string_or_stream, **opts):
def parse(self, string_or_stream, parser_context=None):
"""
The main entry point to parsers. This is a light wrapper around
`parse_stream`, that instead handles both string and stream objects.
@ -47,9 +47,9 @@ class BaseParser(object):
stream = BytesIO(string_or_stream)
else:
stream = string_or_stream
return self.parse_stream(stream, **opts)
return self.parse_stream(stream, parser_context)
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Given a stream to read from, return the deserialized output.
Should return parsed data, or a DataAndFiles object consisting of the
@ -65,7 +65,7 @@ class JSONParser(BaseParser):
media_type = 'application/json'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Returns a 2-tuple of `(data, files)`.
@ -85,7 +85,7 @@ class YAMLParser(BaseParser):
media_type = 'application/yaml'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Returns a 2-tuple of `(data, files)`.
@ -105,7 +105,7 @@ class FormParser(BaseParser):
media_type = 'application/x-www-form-urlencoded'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Returns a 2-tuple of `(data, files)`.
@ -123,15 +123,16 @@ class MultiPartParser(BaseParser):
media_type = 'multipart/form-data'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Returns a DataAndFiles object.
`.data` will be a `QueryDict` containing all the form parameters.
`.files` will be a `QueryDict` containing all the form files.
"""
meta = opts['meta']
upload_handlers = opts['upload_handlers']
parser_context = parser_context or {}
meta = parser_context['meta']
upload_handlers = parser_context['upload_handlers']
try:
parser = DjangoMultiPartParser(meta, stream, upload_handlers)
data, files = parser.parse()
@ -147,7 +148,7 @@ class XMLParser(BaseParser):
media_type = 'application/xml'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
try:
tree = ET.parse(stream)
except (ExpatError, ETParseError, ValueError), exc:

View File

@ -1,8 +1,5 @@
"""
The :mod:`permissions` module bundles a set of permission classes that are used
for checking if a request passes a certain set of constraints.
Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` class into a :class:`View` class.
Provides a set of pluggable permission policies.
"""
@ -16,7 +13,7 @@ class BasePermission(object):
def has_permission(self, request, view, obj=None):
"""
Should simply return, or raise an :exc:`response.ImmediateResponse`.
Return `True` if permission is granted, `False` otherwise.
"""
raise NotImplementedError(".has_permission() must be overridden.")
@ -64,7 +61,8 @@ class DjangoModelPermissions(BasePermission):
It ensures that the user is authenticated, and has the appropriate
`add`/`change`/`delete` permissions on the model.
This permission should only be used on views with a `ModelResource`.
This permission will only be applied against view classes that
provide a `.model` attribute, such as the generic class-based views.
"""
# Map methods into required permission codes.
@ -92,7 +90,10 @@ class DjangoModelPermissions(BasePermission):
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, view, obj=None):
model_cls = view.model
model_cls = getattr(view, 'model', None)
if not model_cls:
return True
perms = self.get_required_permissions(request.method, model_cls)
if (request.user and

View File

@ -7,6 +7,7 @@ and providing forms and links depending on the allowed methods, renderers and pa
"""
import string
from django import forms
from django.http.multipartparser import parse_header
from django.template import RequestContext, loader
from django.utils import simplejson as json
from rest_framework.compat import yaml
@ -16,7 +17,6 @@ from rest_framework.request import clone_request
from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework.utils.mediatypes import get_media_type_params
from rest_framework import VERSION
from rest_framework import serializers, parsers
@ -58,7 +58,7 @@ class JSONRenderer(BaseRenderer):
if accepted_media_type:
# If the media type looks like 'application/json; indent=4',
# then pretty print the result.
params = get_media_type_params(accepted_media_type)
base_media_type, params = parse_header(accepted_media_type)
indent = params.get('indent', indent)
try:
indent = max(min(int(indent), 8), 0)

View File

@ -11,9 +11,18 @@ The wrapped request then offers a richer API, in particular :
"""
from StringIO import StringIO
from django.http.multipartparser import parse_header
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import is_form_media_type
def is_form_media_type(media_type):
"""
Return True if the media type is a valid form media type.
"""
base_media_type, params = parse_header(media_type)
return base_media_type == 'application/x-www-form-urlencoded' or \
base_media_type == 'multipart/form-data'
class Empty(object):
@ -35,7 +44,8 @@ def clone_request(request, method):
"""
ret = Request(request._request,
request.parsers,
request.authenticators)
request.authenticators,
request.parser_context)
ret._data = request._data
ret._files = request._files
ret._content_type = request._content_type
@ -65,20 +75,30 @@ class Request(object):
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
def __init__(self, request, parsers=None, authenticators=None,
negotiator=None):
negotiator=None, parser_context=None):
self._request = request
self.parsers = parsers or ()
self.authenticators = authenticators or ()
self.negotiator = negotiator or self._default_negotiator()
self.parser_context = parser_context
self._data = Empty
self._files = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
if self.parser_context is None:
self.parser_context = self._default_parser_context(request)
def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION()
def _default_parser_context(self, request):
return {
'upload_handlers': request.upload_handlers,
'meta': request.META,
}
@property
def method(self):
"""
@ -253,8 +273,7 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(self.content_type)
parsed = parser.parse(self.stream, meta=self.META,
upload_handlers=self.upload_handlers)
parsed = parser.parse(self.stream, self.parser_context)
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
try:

View File

@ -70,6 +70,7 @@ class Resource(ResourceMixin, views.APIView):
##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY #####
class ModelResource(ResourceMixin, views.APIView):
# TODO: Actually delegation won't work
root_class = generics.ListCreateAPIView
detail_class = generics.RetrieveUpdateDestroyAPIView

View File

@ -35,7 +35,7 @@ DEFAULTS = {
),
'DEFAULT_AUTHENTICATION': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.UserBasicAuthentication'
'rest_framework.authentication.BasicAuthentication'
),
'DEFAULT_PERMISSIONS': (),
'DEFAULT_THROTTLES': (),

View File

@ -27,7 +27,7 @@ factory = RequestFactory()
class PlainTextParser(BaseParser):
media_type = 'text/plain'
def parse_stream(self, stream, **opts):
def parse_stream(self, stream, parser_context=None):
"""
Returns a 2-tuple of `(data, files)`.

View File

@ -25,32 +25,6 @@ def media_type_matches(lhs, rhs):
return lhs.match(rhs)
def is_form_media_type(media_type):
"""
Return True if the media type is a valid form media type as defined by the HTML4 spec.
(NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here)
"""
media_type = _MediaType(media_type)
return media_type.full_type == 'application/x-www-form-urlencoded' or \
media_type.full_type == 'multipart/form-data'
def add_media_type_param(media_type, key, val):
"""
Add a key, value parameter to a media type string, and return the new media type string.
"""
media_type = _MediaType(media_type)
media_type.params[key] = val
return str(media_type)
def get_media_type_params(media_type):
"""
Return a dictionary of the parameters on the given media type.
"""
return _MediaType(media_type).params
def order_by_precedence(media_type_lst):
"""
Returns a list of sets of media type strings, ordered by precedence.

View File

@ -1,8 +1,5 @@
"""
The :mod:`views` module provides the Views you will most probably
be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour.
Provides an APIView class that is used as the base of all class-based views.
"""
import re
@ -159,9 +156,19 @@ class APIView(View):
"""
raise exceptions.Throttled(wait)
def get_parser_context(self, request):
"""
Returns a dict that is passed through to Parser.parse_stream(),
as the `parser_context` keyword argument.
"""
return {
'upload_handlers': request.upload_handlers,
'meta': request.META,
}
def get_renderer_context(self):
"""
Returns a dict that is passed through to the Renderer.render(),
Returns a dict that is passed through to Renderer.render(),
as the `renderer_context` keyword argument.
"""
# Note: Additionally 'response' will also be set on the context,
@ -253,10 +260,13 @@ class APIView(View):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator())
negotiator=self.get_content_negotiator(),
parser_context=parser_context)
def initial(self, request, *args, **kwargs):
"""