Skip to content
Browse files

What is and will never be.

  • Loading branch information...
1 parent bc39633 commit a15a6054d6e06380f940552b274e9ff6880edfe9 @toastdriven toastdriven committed Oct 18, 2011
View
107 tastypie/api.py
@@ -1,15 +1,11 @@
-import warnings
from django.conf.urls.defaults import *
-from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.http import HttpResponse
-from tastypie.exceptions import NotRegistered, BadRequest
-from tastypie.serializers import Serializer
-from tastypie.utils import trailing_slash, is_valid_jsonp_callback_value
-from tastypie.utils.mime import determine_format, build_content_type
+from piecrust.api import Api as PiecrustApi
+from tastypie.utils import trailing_slash
-class Api(object):
+class Api(PiecrustApi):
"""
Implements a registry to tie together the various resources that make up
an API.
@@ -21,66 +17,6 @@ class Api(object):
this is done with version numbers (i.e. ``v1``, ``v2``, etc.) but can
be named any string.
"""
- def __init__(self, api_name="v1"):
- self.api_name = api_name
- self._registry = {}
- self._canonicals = {}
-
- def register(self, resource, canonical=True):
- """
- Registers an instance of a ``Resource`` subclass with the API.
-
- Optionally accept a ``canonical`` argument, which indicates that the
- resource being registered is the canonical variant. Defaults to
- ``True``.
- """
- resource_name = getattr(resource._meta, 'resource_name', None)
-
- if resource_name is None:
- raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource)
-
- self._registry[resource_name] = resource
-
- if canonical is True:
- if resource_name in self._canonicals:
- warnings.warn("A new resource '%r' is replacing the existing canonical URL for '%s'." % (resource, resource_name), Warning, stacklevel=2)
-
- self._canonicals[resource_name] = resource
- # TODO: This is messy, but makes URI resolution on FK/M2M fields
- # work consistently.
- resource._meta.api_name = self.api_name
- resource.__class__.Meta.api_name = self.api_name
-
- def unregister(self, resource_name):
- """
- If present, unregisters a resource from the API.
- """
- if resource_name in self._registry:
- del(self._registry[resource_name])
-
- if resource_name in self._canonicals:
- del(self._canonicals[resource_name])
-
- def canonical_resource_for(self, resource_name):
- """
- Returns the canonical resource for a given ``resource_name``.
- """
- if resource_name in self._canonicals:
- return self._canonicals[resource_name]
-
- raise NotRegistered("No resource was registered as canonical for '%s'." % resource_name)
-
- def wrap_view(self, view):
- def wrapper(request, *args, **kwargs):
- return getattr(self, view)(request, *args, **kwargs)
- return wrapper
-
- def override_urls(self):
- """
- A hook for adding your own URLs or overriding the default URLs.
- """
- return []
-
@property
def urls(self):
"""
@@ -100,43 +36,6 @@ def urls(self):
)
return urlpatterns
- def top_level(self, request, api_name=None):
- """
- A view that returns a serialized list of all resources registers
- to the ``Api``. Useful for discovery.
- """
- serializer = Serializer()
- available_resources = {}
-
- if api_name is None:
- api_name = self.api_name
-
- for name in sorted(self._registry.keys()):
- available_resources[name] = {
- 'list_endpoint': self._build_reverse_url("api_dispatch_list", kwargs={
- 'api_name': api_name,
- 'resource_name': name,
- }),
- 'schema': self._build_reverse_url("api_get_schema", kwargs={
- 'api_name': api_name,
- 'resource_name': name,
- }),
- }
-
- desired_format = determine_format(request, serializer)
- options = {}
-
- if 'text/javascript' in desired_format:
- callback = request.GET.get('callback', 'callback')
-
- if not is_valid_jsonp_callback_value(callback):
- raise BadRequest('JSONP callback name is invalid.')
-
- options['callback'] = callback
-
- serialized = serializer.serialize(available_resources, desired_format, options)
- return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
-
def _build_reverse_url(self, name, args=None, kwargs=None):
"""
A convenience hook for overriding how URLs are built.
View
231 tastypie/authentication.py
@@ -1,61 +1,24 @@
-import base64
-import hmac
-import time
-import uuid
-
from django.conf import settings
from django.contrib.auth import authenticate
-from django.core.exceptions import ImproperlyConfigured
+from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
-from tastypie.http import HttpUnauthorized
-
-try:
- from hashlib import sha1
-except ImportError:
- import sha
- sha1 = sha.sha
-
-try:
- import python_digest
-except ImportError:
- python_digest = None
-
+from piecrust.authentication import Authentication
+from piecrust.authentication import BasicAuthentication as PiecrustBasicAuthentication
+from piecrust.authentication import ApiKeyAuthentication as PiecrustApiKeyAuthentication
+from piecrust.authentication import DigestAuthentication as PiecrustDigestAuthentication
+from piecrust.exceptions import ImproperlyConfigured, Unauthorized
+from tastypie.models import ApiKey
try:
import oauth2
except ImportError:
oauth2 = None
-
try:
import oauth_provider
except ImportError:
oauth_provider = None
-class Authentication(object):
- """
- A simple base class to establish the protocol for auth.
-
- By default, this indicates the user is always authenticated.
- """
- def is_authenticated(self, request, **kwargs):
- """
- Identifies if the user is authenticated to continue or not.
-
- Should return either ``True`` if allowed, ``False`` if not or an
- ``HttpResponse`` if you need something custom.
- """
- return True
-
- def get_identifier(self, request):
- """
- Provides a unique string identifier for the requestor.
-
- This implementation returns a combination of IP address and hostname.
- """
- return "%s_%s" % (request.META.get('REMOTE_ADDR', 'noaddr'), request.META.get('REMOTE_HOST', 'nohost'))
-
-
-class BasicAuthentication(Authentication):
+class BasicAuthentication(PiecrustBasicAuthentication):
"""
Handles HTTP Basic auth against a specific auth backend if provided,
or against all configured authentication backends using the
@@ -71,118 +34,53 @@ class BasicAuthentication(Authentication):
The realm to use in the ``HttpUnauthorized`` response. Default:
``django-tastypie``.
"""
- def __init__(self, backend=None, realm='django-tastypie'):
- self.backend = backend
- self.realm = realm
-
- def _unauthorized(self):
- response = HttpUnauthorized()
- # FIXME: Sanitize realm.
- response['WWW-Authenticate'] = 'Basic Realm="%s"' % self.realm
- return response
+ realm = 'django-tastypie'
- def is_authenticated(self, request, **kwargs):
+ def check_credentials(self, username, password):
"""
Checks a user's basic auth credentials against the current
Django auth backend.
-
- Should return either ``True`` if allowed, ``False`` if not or an
- ``HttpResponse`` if you need something custom.
"""
- if not request.META.get('HTTP_AUTHORIZATION'):
- return self._unauthorized()
-
- try:
- (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
- if auth_type != 'Basic':
- return self._unauthorized()
- user_pass = base64.b64decode(data)
- except:
- return self._unauthorized()
-
- bits = user_pass.split(':', 1)
-
- if len(bits) != 2:
- return self._unauthorized()
-
if self.backend:
- user = self.backend.authenticate(username=bits[0], password=bits[1])
+ user = self.backend.authenticate(username=username, password=password)
else:
- user = authenticate(username=bits[0], password=bits[1])
-
- if user is None:
- return self._unauthorized()
-
- request.user = user
- return True
+ user = authenticate(username=username, password=password)
- def get_identifier(self, request):
- """
- Provides a unique string identifier for the requestor.
+ if not user:
+ raise Unauthorized()
- This implementation returns the user's basic auth username.
- """
- return request.META.get('REMOTE_USER', 'nouser')
+ return user
-class ApiKeyAuthentication(Authentication):
+class ApiKeyAuthentication(PiecrustApiKeyAuthentication):
"""
Handles API key auth, in which a user provides a username & API key.
Uses the ``ApiKey`` model that ships with tastypie. If you wish to use
a different model, override the ``get_key`` method to perform the key check
as suits your needs.
"""
- def _unauthorized(self):
- return HttpUnauthorized()
-
- def is_authenticated(self, request, **kwargs):
+ def check_credentials(self, username, api_key):
"""
Finds the user and checks their API key.
Should return either ``True`` if allowed, ``False`` if not or an
``HttpResponse`` if you need something custom.
"""
- from django.contrib.auth.models import User
-
- username = request.GET.get('username') or request.POST.get('username')
- api_key = request.GET.get('api_key') or request.POST.get('api_key')
-
- if not username or not api_key:
- return self._unauthorized()
-
try:
user = User.objects.get(username=username)
except (User.DoesNotExist, User.MultipleObjectsReturned):
- return self._unauthorized()
-
- request.user = user
- return self.get_key(user, api_key)
-
- def get_key(self, user, api_key):
- """
- Attempts to find the API key for the user. Uses ``ApiKey`` by default
- but can be overridden.
- """
- from tastypie.models import ApiKey
+ raise Unauthorized("User does not exist.")
try:
ApiKey.objects.get(user=user, key=api_key)
except ApiKey.DoesNotExist:
- return self._unauthorized()
+ raise Unauthorized("Api key does not exist.")
- return True
-
- def get_identifier(self, request):
- """
- Provides a unique string identifier for the requestor.
-
- This implementation returns the user's username.
- """
- return request.REQUEST.get('username', 'nouser')
+ return user
-class DigestAuthentication(Authentication):
+class DigestAuthentication(PiecrustDigestAuthentication):
"""
Handles HTTP Digest auth against a specific auth backend if provided,
or against all configured authentication backends using the
@@ -199,99 +97,32 @@ class DigestAuthentication(Authentication):
The realm to use in the ``HttpUnauthorized`` response. Default:
``django-tastypie``.
"""
- def __init__(self, backend=None, realm='django-tastypie'):
- self.backend = backend
- self.realm = realm
-
- if python_digest is None:
- raise ImproperlyConfigured("The 'python_digest' package could not be imported. It is required for use with the 'DigestAuthentication' class.")
-
- def _unauthorized(self):
- response = HttpUnauthorized()
- new_uuid = uuid.uuid4()
- opaque = hmac.new(str(new_uuid), digestmod=sha1).hexdigest()
- response['WWW-Authenticate'] = python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), self.realm, opaque, False)
- return response
+ secret_key = getattr(settings, 'SECRET_KEY', '')
+ realm = 'django-tastypie'
- def is_authenticated(self, request, **kwargs):
+ def check_credentials(self, request, digest_response, username):
"""
Finds the user and checks their API key.
Should return either ``True`` if allowed, ``False`` if not or an
``HttpResponse`` if you need something custom.
"""
- if not request.META.get('HTTP_AUTHORIZATION'):
- return self._unauthorized()
-
- try:
- (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
-
- if auth_type != 'Digest':
- return self._unauthorized()
- except:
- return self._unauthorized()
-
- digest_response = python_digest.parse_digest_credentials(request.META['HTTP_AUTHORIZATION'])
-
- # FIXME: Should the nonce be per-user?
- if not python_digest.validate_nonce(digest_response.nonce, getattr(settings, 'SECRET_KEY', '')):
- return self._unauthorized()
-
- user = self.get_user(digest_response.username)
- api_key = self.get_key(user)
-
- if user is False or api_key is False:
- return self._unauthorized()
-
- expected = python_digest.calculate_request_digest(
- request.method,
- python_digest.calculate_partial_digest(digest_response.username, self.realm, api_key),
- digest_response)
-
- if not digest_response.response == expected:
- return self._unauthorized()
-
- request.user = user
- return True
-
- def get_user(self, username):
- from django.contrib.auth.models import User
-
try:
user = User.objects.get(username=username)
except (User.DoesNotExist, User.MultipleObjectsReturned):
- return False
-
- return user
-
- def get_key(self, user):
- """
- Attempts to find the API key for the user. Uses ``ApiKey`` by default
- but can be overridden.
-
- Note that this behaves differently than the ``ApiKeyAuthentication``
- method of the same name.
- """
- from tastypie.models import ApiKey
+ raise Unauthorized("No such user.")
try:
key = ApiKey.objects.get(user=user)
except ApiKey.DoesNotExist:
- return False
+ raise Unauthorized("No such API key.")
- return key.key
+ expected = self.calculate_request_digest(request, digest_response, username, key.key)
- def get_identifier(self, request):
- """
- Provides a unique string identifier for the requestor.
-
- This implementation returns the user's username.
- """
- if hasattr(request, 'user'):
- if hasattr(request.user, 'username'):
- return request.user.username
+ if not digest_response.response == expected:
+ raise Unauthorized("Digests did not match.")
- return 'nouser'
+ return user
class OAuthAuthentication(Authentication):
@@ -312,7 +143,7 @@ def __init__(self):
raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.")
def is_authenticated(self, request, **kwargs):
- from oauth_provider.store import store, InvalidTokenError
+ from oauth_provider.store import store
if self.is_valid_request(request):
oauth_request = oauth_provider.utils.get_oauth_request(request)
View
40 tastypie/authorization.py
@@ -1,42 +1,4 @@
-class Authorization(object):
- """
- A base class that provides no permissions checking.
- """
- def __get__(self, instance, owner):
- """
- Makes ``Authorization`` a descriptor of ``ResourceOptions`` and creates
- a reference to the ``ResourceOptions`` object that may be used by
- methods of ``Authorization``.
- """
- self.resource_meta = instance
- return self
-
- def is_authorized(self, request, object=None):
- """
- Checks if the user is authorized to perform the request. If ``object``
- is provided, it can do additional row-level checks.
-
- Should return either ``True`` if allowed, ``False`` if not or an
- ``HttpResponse`` if you need something custom.
- """
- return True
-
-
-class ReadOnlyAuthorization(Authorization):
- """
- Default Authentication class for ``Resource`` objects.
-
- Only allows GET requests.
- """
-
- def is_authorized(self, request, object=None):
- """
- Allow any ``GET`` request.
- """
- if request.method == 'GET':
- return True
- else:
- return False
+from piecrust.authorization import Authorization, ReadOnlyAuthorization
class DjangoAuthorization(Authorization):
View
22 tastypie/bundle.py
@@ -1,21 +1 @@
-from django.http import HttpRequest
-
-
-# In a separate file to avoid circular imports...
-class Bundle(object):
- """
- A small container for instances and converted data for the
- ``dehydrate/hydrate`` cycle.
-
- Necessary because the ``dehydrate/hydrate`` cycle needs to access data at
- different points.
- """
- def __init__(self, obj=None, data=None, request=None, related_obj=None, related_name=None):
- self.obj = obj
- self.data = data or {}
- self.request = request or HttpRequest()
- self.related_obj = related_obj
- self.related_name = related_name
-
- def __repr__(self):
- return "<Bundle for obj: '%s' and with data: '%s'>" % (self.obj, self.data)
+from piecrust.bundles import Bundle
View
26 tastypie/cache.py
@@ -1,23 +1,5 @@
from django.core.cache import cache
-
-
-class NoCache(object):
- """
- A simplified, swappable base class for caching.
-
- Does nothing save for simulating the cache API.
- """
- def get(self, key):
- """
- Always returns ``None``.
- """
- return None
-
- def set(self, key, value, timeout=60):
- """
- No-op for setting values in the cache.
- """
- pass
+from piecrust.cache import NoCache
class SimpleCache(NoCache):
@@ -29,11 +11,11 @@ def get(self, key):
Gets a key from the cache. Returns ``None`` if the key is not found.
"""
return cache.get(key)
-
+
def set(self, key, value, timeout=60):
"""
Sets a key-value in the cache.
-
+
Optionally accepts a ``timeout`` in seconds. Defaults to ``60`` seconds.
"""
- cache.set(key, value, timeout)
+ return cache.set(key, value, timeout)
View
5 tastypie/constants.py
@@ -1,4 +1 @@
-# Enable all basic ORM filters but do not allow filtering across relationships.
-ALL = 1
-# Enable all ORM filters, including across relationships
-ALL_WITH_RELATIONS = 2
+from piecrust.constants import ALL, ALL_WITH_RELATIONS
View
87 tastypie/exceptions.py
@@ -1,86 +1 @@
-from django.http import HttpResponse
-
-
-class TastypieError(Exception):
- """A base exception for other tastypie-related errors."""
- pass
-
-
-class HydrationError(TastypieError):
- """Raised when there is an error hydrating data."""
- pass
-
-
-class NotRegistered(TastypieError):
- """
- Raised when the requested resource isn't registered with the ``Api`` class.
- """
- pass
-
-
-class NotFound(TastypieError):
- """
- Raised when the resource/object in question can't be found.
- """
- pass
-
-
-class ApiFieldError(TastypieError):
- """
- Raised when there is a configuration error with a ``ApiField``.
- """
- pass
-
-
-class UnsupportedFormat(TastypieError):
- """
- Raised when an unsupported serialization format is requested.
- """
- pass
-
-
-class BadRequest(TastypieError):
- """
- A generalized exception for indicating incorrect request parameters.
-
- Handled specially in that the message tossed by this exception will be
- presented to the end user.
- """
- pass
-
-
-class BlueberryFillingFound(TastypieError):
- pass
-
-
-class InvalidFilterError(BadRequest):
- """
- Raised when the end user attempts to use a filter that has not be
- explicitly allowed.
- """
- pass
-
-
-class InvalidSortError(TastypieError):
- """
- Raised when the end user attempts to sort on a field that has not be
- explicitly allowed.
- """
- pass
-
-
-class ImmediateHttpResponse(TastypieError):
- """
- This exception is used to interrupt the flow of processing to immediately
- return a custom HttpResponse.
-
- Common uses include::
-
- * for authentication (like digest/OAuth)
- * for throttling
-
- """
- response = HttpResponse("Nothing provided.")
-
- def __init__(self, response):
- self.response = response
+from piecrust.exceptions import *
View
76 tastypie/http.py
@@ -1,75 +1 @@
-"""
-The various HTTP responses for use in returning proper HTTP codes.
-"""
-from django.http import HttpResponse
-
-
-class HttpCreated(HttpResponse):
- status_code = 201
-
- def __init__(self, *args, **kwargs):
- location = ''
-
- if 'location' in kwargs:
- location = kwargs['location']
- del(kwargs['location'])
-
- super(HttpCreated, self).__init__(*args, **kwargs)
- self['Location'] = location
-
-
-class HttpAccepted(HttpResponse):
- status_code = 202
-
-
-class HttpNoContent(HttpResponse):
- status_code = 204
-
-
-class HttpMultipleChoices(HttpResponse):
- status_code = 300
-
-
-class HttpSeeOther(HttpResponse):
- status_code = 303
-
-
-class HttpNotModified(HttpResponse):
- status_code = 304
-
-
-class HttpBadRequest(HttpResponse):
- status_code = 400
-
-
-class HttpUnauthorized(HttpResponse):
- status_code = 401
-
-
-class HttpForbidden(HttpResponse):
- status_code = 403
-
-
-class HttpNotFound(HttpResponse):
- status_code = 404
-
-
-class HttpMethodNotAllowed(HttpResponse):
- status_code = 405
-
-
-class HttpConflict(HttpResponse):
- status_code = 409
-
-
-class HttpGone(HttpResponse):
- status_code = 410
-
-
-class HttpApplicationError(HttpResponse):
- status_code = 500
-
-
-class HttpNotImplemented(HttpResponse):
- status_code = 501
-
+from piecrust.http import *
View
166 tastypie/paginator.py
@@ -1,170 +1,20 @@
from django.conf import settings
-from tastypie.exceptions import BadRequest
-from urllib import urlencode
+from piecrust.paginator import Paginator as PiecrustPaginator
-class Paginator(object):
+class Paginator(PiecrustPaginator):
"""
Limits result sets down to sane amounts for passing to the client.
-
+
This is used in place of Django's ``Paginator`` due to the way pagination
works. ``limit`` & ``offset`` (tastypie) are used in place of ``page``
(Django) so none of the page-related calculations are necessary.
-
+
This implementation also provides additional details like the
``total_count`` of resources seen and convenience links to the
``previous``/``next`` pages of data as available.
- """
- def __init__(self, request_data, objects, resource_uri=None, limit=None, offset=0):
- """
- Instantiates the ``Paginator`` and allows for some configuration.
-
- The ``request_data`` argument ought to be a dictionary-like object.
- May provide ``limit`` and/or ``offset`` to override the defaults.
- Commonly provided ``request.GET``. Required.
-
- The ``objects`` should be a list-like object of ``Resources``.
- This is typically a ``QuerySet`` but can be anything that
- implements slicing. Required.
-
- Optionally accepts a ``limit`` argument, which specifies how many
- items to show at a time. Defaults to ``None``, which is no limit.
-
- Optionally accepts an ``offset`` argument, which specifies where in
- the ``objects`` to start displaying results from. Defaults to 0.
- """
- self.request_data = request_data
- self.objects = objects
- self.limit = limit
- self.offset = offset
- self.resource_uri = resource_uri
-
- def get_limit(self):
- """
- Determines the proper maximum number of results to return.
-
- In order of importance, it will use:
-
- * The user-requested ``limit`` from the GET parameters, if specified.
- * The object-level ``limit`` if specified.
- * ``settings.API_LIMIT_PER_PAGE`` if specified.
-
- Default is 20 per page.
- """
- limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
-
- if 'limit' in self.request_data:
- limit = self.request_data['limit']
- elif self.limit is not None:
- limit = self.limit
-
- try:
- limit = int(limit)
- except ValueError:
- raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer.")
-
- if limit < 0:
- raise BadRequest("Invalid limit '%s' provided. Please provide an integer >= 0.")
-
- return limit
-
- def get_offset(self):
- """
- Determines the proper starting offset of results to return.
-
- It attempst to use the user-provided ``offset`` from the GET parameters,
- if specified. Otherwise, it falls back to the object-level ``offset``.
-
- Default is 0.
- """
- offset = self.offset
-
- if 'offset' in self.request_data:
- offset = self.request_data['offset']
-
- try:
- offset = int(offset)
- except ValueError:
- raise BadRequest("Invalid offset '%s' provided. Please provide an integer.")
-
- if offset < 0:
- raise BadRequest("Invalid offset '%s' provided. Please provide an integer >= 0.")
-
- return offset
-
- def get_slice(self, limit, offset):
- """
- Slices the result set to the specified ``limit`` & ``offset``.
- """
- # If it's zero, return everything.
- if limit == 0:
- return self.objects[offset:]
-
- return self.objects[offset:offset + limit]
-
- def get_count(self):
- """
- Returns a count of the total number of objects seen.
- """
- try:
- return self.objects.count()
- except (AttributeError, TypeError):
- # If it's not a QuerySet (or it's ilk), fallback to ``len``.
- return len(self.objects)
-
- def get_previous(self, limit, offset):
- """
- If a previous page is available, will generate a URL to request that
- page. If not available, this returns ``None``.
- """
- if offset - limit < 0:
- return None
-
- return self._generate_uri(limit, offset-limit)
- def get_next(self, limit, offset, count):
- """
- If a next page is available, will generate a URL to request that
- page. If not available, this returns ``None``.
- """
- if offset + limit >= count:
- return None
-
- return self._generate_uri(limit, offset+limit)
-
- def _generate_uri(self, limit, offset):
- if self.resource_uri is None:
- return None
-
- request_params = dict([k, v.encode('utf-8')] for k, v in self.request_data.items())
- request_params.update({'limit': limit, 'offset': offset})
- return '%s?%s' % (
- self.resource_uri,
- urlencode(request_params)
- )
-
- def page(self):
- """
- Generates all pertinent data about the requested page.
-
- Handles getting the correct ``limit`` & ``offset``, then slices off
- the correct set of results and returns all pertinent metadata.
- """
- limit = self.get_limit()
- offset = self.get_offset()
- count = self.get_count()
- objects = self.get_slice(limit, offset)
- meta = {
- 'offset': offset,
- 'limit': limit,
- 'total_count': count,
- }
-
- if limit:
- meta['previous'] = self.get_previous(limit, offset)
- meta['next'] = self.get_next(limit, offset, count)
-
- return {
- 'objects': objects,
- 'meta': meta,
- }
+ This is implemented solely for backward-compatibility on the ``limit``
+ setting.
+ """
+ limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
View
1,339 tastypie/resources.py
@@ -1,22 +1,17 @@
-import logging
import warnings
-import django
-from django.conf import settings
from django.conf.urls.defaults import patterns, url
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError
from django.core.urlresolvers import NoReverseMatch, reverse, resolve, Resolver404, get_script_prefix
from django.db import transaction
from django.db.models.sql.constants import QUERY_TERMS, LOOKUP_SEP
from django.http import HttpResponse, HttpResponseNotFound
from django.utils.cache import patch_cache_control
-from tastypie.authentication import Authentication
-from tastypie.authorization import ReadOnlyAuthorization
+from piecrust.constants import ALL, ALL_WITH_RELATIONS
+from piecrust.resources import DeclarativeMetaclass
+from piecrust.resources import Resource as PiecrustResource
from tastypie.bundle import Bundle
-from tastypie.cache import NoCache
-from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse
from tastypie import fields
-from tastypie import http
from tastypie.paginator import Paginator
from tastypie.serializers import Serializer
from tastypie.throttle import BaseThrottle
@@ -41,181 +36,20 @@ def csrf_exempt(func):
return func
-class ResourceOptions(object):
- """
- A configuration class for ``Resource``.
-
- Provides sane defaults and the logic needed to augment these settings with
- the internal ``class Meta`` used on ``Resource`` subclasses.
- """
- serializer = Serializer()
- authentication = Authentication()
- authorization = ReadOnlyAuthorization()
- cache = NoCache()
- throttle = BaseThrottle()
- validation = Validation()
- paginator_class = Paginator
- allowed_methods = ['get', 'post', 'put', 'delete', 'patch']
- list_allowed_methods = None
- detail_allowed_methods = None
- limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
- api_name = None
- resource_name = None
- urlconf_namespace = None
- default_format = 'application/json'
- filtering = {}
- ordering = []
- object_class = None
- queryset = None
- fields = []
- excludes = []
- include_resource_uri = True
- include_absolute_url = False
- always_return_data = False
-
- def __new__(cls, meta=None):
- overrides = {}
-
- # Handle overrides.
- if meta:
- for override_name in dir(meta):
- # No internals please.
- if not override_name.startswith('_'):
- overrides[override_name] = getattr(meta, override_name)
-
- allowed_methods = overrides.get('allowed_methods', ['get', 'post', 'put', 'delete', 'patch'])
-
- if overrides.get('list_allowed_methods', None) is None:
- overrides['list_allowed_methods'] = allowed_methods
-
- if overrides.get('detail_allowed_methods', None) is None:
- overrides['detail_allowed_methods'] = allowed_methods
-
- return object.__new__(type('ResourceOptions', (cls,), overrides))
-
-
-class DeclarativeMetaclass(type):
- def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = {}
- declared_fields = {}
-
- # Inherit any fields from parent(s).
- try:
- parents = [b for b in bases if issubclass(b, Resource)]
- # Simulate the MRO.
- parents.reverse()
-
- for p in parents:
- parent_fields = getattr(p, 'base_fields', {})
-
- for field_name, field_object in parent_fields.items():
- attrs['base_fields'][field_name] = deepcopy(field_object)
- except NameError:
- pass
-
- for field_name, obj in attrs.items():
- # Look for ``dehydrated_type`` instead of doing ``isinstance``,
- # which can break down if Tastypie is re-namespaced as something
- # else.
- if hasattr(obj, 'dehydrated_type'):
- field = attrs.pop(field_name)
- declared_fields[field_name] = field
-
- attrs['base_fields'].update(declared_fields)
- attrs['declared_fields'] = declared_fields
- new_class = super(DeclarativeMetaclass, cls).__new__(cls, name, bases, attrs)
- opts = getattr(new_class, 'Meta', None)
- new_class._meta = ResourceOptions(opts)
-
- if not getattr(new_class._meta, 'resource_name', None):
- # No ``resource_name`` provided. Attempt to auto-name the resource.
- class_name = new_class.__name__
- name_bits = [bit for bit in class_name.split('Resource') if bit]
- resource_name = ''.join(name_bits).lower()
- new_class._meta.resource_name = resource_name
-
- if getattr(new_class._meta, 'include_resource_uri', True):
- if not 'resource_uri' in new_class.base_fields:
- new_class.base_fields['resource_uri'] = fields.CharField(readonly=True)
- elif 'resource_uri' in new_class.base_fields and not 'resource_uri' in attrs:
- del(new_class.base_fields['resource_uri'])
-
- for field_name, field_object in new_class.base_fields.items():
- if hasattr(field_object, 'contribute_to_class'):
- field_object.contribute_to_class(new_class, field_name)
-
- return new_class
-
-
-class Resource(object):
- """
- Handles the data, request dispatch and responding to requests.
-
- Serialization/deserialization is handled "at the edges" (i.e. at the
- beginning/end of the request/response cycle) so that everything internally
- is Python data structures.
-
- This class tries to be non-model specific, so it can be hooked up to other
- data sources, such as search results, files, other data, etc.
- """
- __metaclass__ = DeclarativeMetaclass
+class Resource(PiecrustResource):
+ def handle_500(self, request, exception):
+ if hasattr(e, 'response'):
+ return e.response
- def __init__(self, api_name=None):
- self.fields = deepcopy(self.base_fields)
+ # A real, non-expected exception.
+ # Handle the case where the full traceback is more helpful
+ # than the serialized error.
+ if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False):
+ raise
- if not api_name is None:
- self._meta.api_name = api_name
-
- def __getattr__(self, name):
- if name in self.fields:
- return self.fields[name]
- raise AttributeError(name)
-
- def wrap_view(self, view):
- """
- Wraps methods so they can be called in a more functional way as well
- as handling exceptions better.
-
- Note that if ``BadRequest`` or an exception with a ``response`` attr
- are seen, there is special handling to either present a message back
- to the user or return the response traveling with the exception.
- """
- @csrf_exempt
- def wrapper(request, *args, **kwargs):
- try:
- callback = getattr(self, view)
- response = callback(request, *args, **kwargs)
-
-
- if request.is_ajax():
- # IE excessively caches XMLHttpRequests, so we're disabling
- # the browser cache here.
- # See http://www.enhanceie.com/ie/bugs.asp for details.
- patch_cache_control(response, no_cache=True)
-
- return response
- except (BadRequest, fields.ApiFieldError), e:
- return http.HttpBadRequest(e.args[0])
- except ValidationError, e:
- return http.HttpBadRequest(', '.join(e.messages))
- except Exception, e:
- if hasattr(e, 'response'):
- return e.response
-
- # A real, non-expected exception.
- # Handle the case where the full traceback is more helpful
- # than the serialized error.
- if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False):
- raise
-
- # Rather than re-raising, we're going to things similar to
- # what Django does. The difference is returning a serialized
- # error message.
- return self._handle_500(request, e)
-
- return wrapper
-
- def _handle_500(self, request, exception):
+ # Rather than re-raising, we're going to things similar to
+ # what Django does. The difference is returning a serialized
+ # error message.
import traceback
import sys
the_trace = '\n'.join(traceback.format_exception(*(sys.exc_info())))
@@ -229,9 +63,7 @@ def _handle_500(self, request, exception):
"error_message": unicode(exception),
"traceback": the_trace,
}
- desired_format = self.determine_format(request)
- serialized = self.serialize(request, data, desired_format)
- return response_class(content=serialized, content_type=build_content_type(desired_format))
+ return self.create_response(request, data, response_class=response_class)
# When DEBUG is False, send an error message to the admins (unless it's
# a 404, in which case we check the setting).
@@ -254,1144 +86,7 @@ def _handle_500(self, request, exception):
data = {
"error_message": getattr(settings, 'TASTYPIE_CANNED_ERROR', "Sorry, this request could not be processed. Please try again later."),
}
- desired_format = self.determine_format(request)
- serialized = self.serialize(request, data, desired_format)
- return response_class(content=serialized, content_type=build_content_type(desired_format))
-
- def _build_reverse_url(self, name, args=None, kwargs=None):
- """
- A convenience hook for overriding how URLs are built.
-
- See ``NamespacedModelResource._build_reverse_url`` for an example.
- """
- return reverse(name, args=args, kwargs=kwargs)
-
- def base_urls(self):
- """
- The standard URLs this ``Resource`` should respond to.
- """
- # Due to the way Django parses URLs, ``get_multiple`` won't work without
- # a trailing slash.
- return [
- url(r"^(?P<resource_name>%s)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_list'), name="api_dispatch_list"),
- url(r"^(?P<resource_name>%s)/schema%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_schema'), name="api_get_schema"),
- url(r"^(?P<resource_name>%s)/set/(?P<pk_list>\w[\w/;-]*)/$" % self._meta.resource_name, self.wrap_view('get_multiple'), name="api_get_multiple"),
- url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
- ]
-
- def override_urls(self):
- """
- A hook for adding your own URLs or overriding the default URLs.
- """
- return []
-
- @property
- def urls(self):
- """
- The endpoints this ``Resource`` responds to.
-
- Mostly a standard URLconf, this is suitable for either automatic use
- when registered with an ``Api`` class or for including directly in
- a URLconf should you choose to.
- """
- urls = self.override_urls() + self.base_urls()
- urlpatterns = patterns('',
- *urls
- )
- return urlpatterns
-
- def determine_format(self, request):
- """
- Used to determine the desired format.
-
- Largely relies on ``tastypie.utils.mime.determine_format`` but here
- as a point of extension.
- """
- return determine_format(request, self._meta.serializer, default_format=self._meta.default_format)
-
- def serialize(self, request, data, format, options=None):
- """
- Given a request, data and a desired format, produces a serialized
- version suitable for transfer over the wire.
-
- Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``.
- """
- options = options or {}
-
- if 'text/javascript' in format:
- # get JSONP callback name. default to "callback"
- callback = request.GET.get('callback', 'callback')
-
- if not is_valid_jsonp_callback_value(callback):
- raise BadRequest('JSONP callback name is invalid.')
-
- options['callback'] = callback
-
- return self._meta.serializer.serialize(data, format, options)
-
- def deserialize(self, request, data, format='application/json'):
- """
- Given a request, data and a format, deserializes the given data.
-
- It relies on the request properly sending a ``CONTENT_TYPE`` header,
- falling back to ``application/json`` if not provided.
-
- Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``.
- """
- deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', 'application/json'))
- return deserialized
-
- def alter_list_data_to_serialize(self, request, data):
- """
- A hook to alter list data just before it gets serialized & sent to the user.
-
- Useful for restructuring/renaming aspects of the what's going to be
- sent.
-
- Should accommodate for a list of objects, generally also including
- meta data.
- """
- return data
-
- def alter_detail_data_to_serialize(self, request, data):
- """
- A hook to alter detail data just before it gets serialized & sent to the user.
-
- Useful for restructuring/renaming aspects of the what's going to be
- sent.
-
- Should accommodate for receiving a single bundle of data.
- """
- return data
-
- def alter_deserialized_list_data(self, request, data):
- """
- A hook to alter list data just after it has been received from the user &
- gets deserialized.
-
- Useful for altering the user data before any hydration is applied.
- """
- return data
-
- def alter_deserialized_detail_data(self, request, data):
- """
- A hook to alter detail data just after it has been received from the user &
- gets deserialized.
-
- Useful for altering the user data before any hydration is applied.
- """
- return data
-
- def dispatch_list(self, request, **kwargs):
- """
- A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over
- the entire list of resources.
-
- Relies on ``Resource.dispatch`` for the heavy-lifting.
- """
- return self.dispatch('list', request, **kwargs)
-
- def dispatch_detail(self, request, **kwargs):
- """
- A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on
- a single resource.
-
- Relies on ``Resource.dispatch`` for the heavy-lifting.
- """
- return self.dispatch('detail', request, **kwargs)
-
- def dispatch(self, request_type, request, **kwargs):
- """
- Handles the common operations (allowed HTTP method, authentication,
- throttling, method lookup) surrounding most CRUD interactions.
- """
- allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None)
- request_method = self.method_check(request, allowed=allowed_methods)
-
- method = getattr(self, "%s_%s" % (request_method, request_type), None)
-
- if method is None:
- raise ImmediateHttpResponse(response=http.HttpNotImplemented())
-
- self.is_authenticated(request)
- self.is_authorized(request)
- self.throttle_check(request)
-
- # All clear. Process the request.
- request = convert_post_to_put(request)
- response = method(request, **kwargs)
-
- # Add the throttled request.
- self.log_throttled_access(request)
-
- # If what comes back isn't a ``HttpResponse``, assume that the
- # request was accepted and that some action occurred. This also
- # prevents Django from freaking out.
- if not isinstance(response, HttpResponse):
- return http.HttpNoContent()
-
- return response
-
- def remove_api_resource_names(self, url_dict):
- """
- Given a dictionary of regex matches from a URLconf, removes
- ``api_name`` and/or ``resource_name`` if found.
-
- This is useful for converting URLconf matches into something suitable
- for data lookup. For example::
-
- Model.objects.filter(**self.remove_api_resource_names(matches))
- """
- kwargs_subset = url_dict.copy()
-
- for key in ['api_name', 'resource_name']:
- try:
- del(kwargs_subset[key])
- except KeyError:
- pass
-
- return kwargs_subset
-
- def method_check(self, request, allowed=None):
- """
- Ensures that the HTTP method used on the request is allowed to be
- handled by the resource.
-
- Takes an ``allowed`` parameter, which should be a list of lowercase
- HTTP methods to check against. Usually, this looks like::
-
- # The most generic lookup.
- self.method_check(request, self._meta.allowed_methods)
-
- # A lookup against what's allowed for list-type methods.
- self.method_check(request, self._meta.list_allowed_methods)
-
- # A useful check when creating a new endpoint that only handles
- # GET.
- self.method_check(request, ['get'])
- """
- if allowed is None:
- allowed = []
-
- request_method = request.method.lower()
-
- if request_method == "options":
- allows = ','.join(map(str.upper, allowed))
- response = HttpResponse(allows)
- response['Allow'] = allows
- raise ImmediateHttpResponse(response=response)
-
- if not request_method in allowed:
- raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed())
-
- return request_method
-
- def is_authorized(self, request, object=None):
- """
- Handles checking of permissions to see if the user has authorization
- to GET, POST, PUT, or DELETE this resource. If ``object`` is provided,
- the authorization backend can apply additional row-level permissions
- checking.
- """
- auth_result = self._meta.authorization.is_authorized(request, object)
-
- if isinstance(auth_result, HttpResponse):
- raise ImmediateHttpResponse(response=auth_result)
-
- if not auth_result is True:
- raise ImmediateHttpResponse(response=http.HttpUnauthorized())
-
- def is_authenticated(self, request):
- """
- Handles checking if the user is authenticated and dealing with
- unauthenticated users.
-
- Mostly a hook, this uses class assigned to ``authentication`` from
- ``Resource._meta``.
- """
- # Authenticate the request as needed.
- auth_result = self._meta.authentication.is_authenticated(request)
-
- if isinstance(auth_result, HttpResponse):
- raise ImmediateHttpResponse(response=auth_result)
-
- if not auth_result is True:
- raise ImmediateHttpResponse(response=http.HttpUnauthorized())
-
- def throttle_check(self, request):
- """
- Handles checking if the user should be throttled.
-
- Mostly a hook, this uses class assigned to ``throttle`` from
- ``Resource._meta``.
- """
- identifier = self._meta.authentication.get_identifier(request)
-
- # Check to see if they should be throttled.
- if self._meta.throttle.should_be_throttled(identifier):
- # Throttle limit exceeded.
- raise ImmediateHttpResponse(response=http.HttpForbidden())
-
- def log_throttled_access(self, request):
- """
- Handles the recording of the user's access for throttling purposes.
-
- Mostly a hook, this uses class assigned to ``throttle`` from
- ``Resource._meta``.
- """
- request_method = request.method.lower()
- self._meta.throttle.accessed(self._meta.authentication.get_identifier(request), url=request.get_full_path(), request_method=request_method)
-
- def build_bundle(self, obj=None, data=None, request=None):
- """
- Given either an object, a data dictionary or both, builds a ``Bundle``
- for use throughout the ``dehydrate/hydrate`` cycle.
-
- If no object is provided, an empty object from
- ``Resource._meta.object_class`` is created so that attempts to access
- ``bundle.obj`` do not fail.
- """
- if obj is None:
- obj = self._meta.object_class()
-
- return Bundle(obj=obj, data=data, request=request)
-
- def build_filters(self, filters=None):
- """
- Allows for the filtering of applicable objects.
-
- This needs to be implemented at the user level.'
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- return filters
-
- def apply_sorting(self, obj_list, options=None):
- """
- Allows for the sorting of objects being returned.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- return obj_list
-
- # URL-related methods.
-
- def get_resource_uri(self, bundle_or_obj):
- """
- This needs to be implemented at the user level.
-
- A ``return reverse("api_dispatch_detail", kwargs={'resource_name':
- self.resource_name, 'pk': object.id})`` should be all that would
- be needed.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def get_resource_list_uri(self):
- """
- Returns a URL specific to this resource's list endpoint.
- """
- kwargs = {
- 'resource_name': self._meta.resource_name,
- }
-
- if self._meta.api_name is not None:
- kwargs['api_name'] = self._meta.api_name
-
- try:
- return self._build_reverse_url("api_dispatch_list", kwargs=kwargs)
- except NoReverseMatch:
- return None
-
- def get_via_uri(self, uri, request=None):
- """
- This pulls apart the salient bits of the URI and populates the
- resource via a ``obj_get``.
-
- Optionally accepts a ``request``.
-
- If you need custom behavior based on other portions of the URI,
- simply override this method.
- """
- prefix = get_script_prefix()
- chomped_uri = uri
-
- if prefix and chomped_uri.startswith(prefix):
- chomped_uri = chomped_uri[len(prefix)-1:]
-
- try:
- view, args, kwargs = resolve(chomped_uri)
- except Resolver404:
- raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)
-
- return self.obj_get(request=request, **self.remove_api_resource_names(kwargs))
-
- # Data preparation.
-
- def full_dehydrate(self, bundle):
- """
- Given a bundle with an object instance, extract the information from it
- to populate the resource.
- """
- # Dehydrate each field.
- for field_name, field_object in self.fields.items():
- # A touch leaky but it makes URI resolution work.
- if getattr(field_object, 'dehydrated_type', None) == 'related':
- field_object.api_name = self._meta.api_name
- field_object.resource_name = self._meta.resource_name
-
- bundle.data[field_name] = field_object.dehydrate(bundle)
-
- # Check for an optional method to do further dehydration.
- method = getattr(self, "dehydrate_%s" % field_name, None)
-
- if method:
- bundle.data[field_name] = method(bundle)
-
- bundle = self.dehydrate(bundle)
- return bundle
-
- def dehydrate(self, bundle):
- """
- A hook to allow a final manipulation of data once all fields/methods
- have built out the dehydrated data.
-
- Useful if you need to access more than one dehydrated field or want
- to annotate on additional data.
-
- Must return the modified bundle.
- """
- return bundle
-
- def full_hydrate(self, bundle):
- """
- Given a populated bundle, distill it and turn it back into
- a full-fledged object instance.
- """
- if bundle.obj is None:
- bundle.obj = self._meta.object_class()
-
- bundle = self.hydrate(bundle)
-
- for field_name, field_object in self.fields.items():
- if field_object.readonly is True:
- continue
-
- # Check for an optional method to do further hydration.
- method = getattr(self, "hydrate_%s" % field_name, None)
-
- if method:
- bundle = method(bundle)
-
- if field_object.attribute:
- value = field_object.hydrate(bundle)
-
- if value is not None or field_object.null:
- # We need to avoid populating M2M data here as that will
- # cause things to blow up.
- if not getattr(field_object, 'is_related', False):
- setattr(bundle.obj, field_object.attribute, value)
- elif not getattr(field_object, 'is_m2m', False):
- if value is not None:
- setattr(bundle.obj, field_object.attribute, value.obj)
- elif field_object.blank:
- continue
- elif field_object.null:
- setattr(bundle.obj, field_object.attribute, value)
-
- return bundle
-
- def hydrate(self, bundle):
- """
- A hook to allow a final manipulation of data once all fields/methods
- have built out the hydrated data.
-
- Useful if you need to access more than one hydrated field or want
- to annotate on additional data.
-
- Must return the modified bundle.
- """
- return bundle
-
- def hydrate_m2m(self, bundle):
- """
- Populate the ManyToMany data on the instance.
- """
- if bundle.obj is None:
- raise HydrationError("You must call 'full_hydrate' before attempting to run 'hydrate_m2m' on %r." % self)
-
- for field_name, field_object in self.fields.items():
- if not getattr(field_object, 'is_m2m', False):
- continue
-
- if field_object.attribute:
- # Note that we only hydrate the data, leaving the instance
- # unmodified. It's up to the user's code to handle this.
- # The ``ModelResource`` provides a working baseline
- # in this regard.
- bundle.data[field_name] = field_object.hydrate_m2m(bundle)
-
- for field_name, field_object in self.fields.items():
- if not getattr(field_object, 'is_m2m', False):
- continue
-
- method = getattr(self, "hydrate_%s" % field_name, None)
-
- if method:
- method(bundle)
-
- return bundle
-
- def build_schema(self):
- """
- Returns a dictionary of all the fields on the resource and some
- properties about those fields.
-
- Used by the ``schema/`` endpoint to describe what will be available.
- """
- data = {
- 'fields': {},
- 'default_format': self._meta.default_format,
- 'allowed_list_http_methods': self._meta.list_allowed_methods,
- 'allowed_detail_http_methods': self._meta.detail_allowed_methods,
- 'default_limit': self._meta.limit,
- }
-
- if self._meta.ordering:
- data['ordering'] = self._meta.ordering
-
- if self._meta.filtering:
- data['filtering'] = self._meta.filtering
-
- for field_name, field_object in self.fields.items():
- data['fields'][field_name] = {
- 'default': field_object.default,
- 'type': field_object.dehydrated_type,
- 'nullable': field_object.null,
- 'blank': field_object.blank,
- 'readonly': field_object.readonly,
- 'help_text': field_object.help_text,
- 'unique': field_object.unique,
- }
-
- return data
-
- def dehydrate_resource_uri(self, bundle):
- """
- For the automatically included ``resource_uri`` field, dehydrate
- the URI for the given bundle.
-
- Returns empty string if no URI can be generated.
- """
- try:
- return self.get_resource_uri(bundle)
- except NotImplementedError:
- return ''
- except NoReverseMatch:
- return ''
-
- def generate_cache_key(self, *args, **kwargs):
- """
- Creates a unique-enough cache key.
-
- This is based off the current api_name/resource_name/args/kwargs.
- """
- smooshed = []
-
- for key, value in kwargs.items():
- smooshed.append("%s=%s" % (key, value))
-
- # Use a list plus a ``.join()`` because it's faster than concatenation.
- return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), ':'.join(smooshed))
-
- # Data access methods.
-
- def get_object_list(self, request):
- """
- A hook to allow making returning the list of available objects.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def apply_authorization_limits(self, request, object_list):
- """
- Allows the ``Authorization`` class to further limit the object list.
- Also a hook to customize per ``Resource``.
- """
- if hasattr(self._meta.authorization, 'apply_limits'):
- object_list = self._meta.authorization.apply_limits(request, object_list)
-
- return object_list
-
- def can_create(self):
- """
- Checks to ensure ``post`` is within ``allowed_methods``.
- """
- allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods)
- return 'post' in allowed
-
- def can_update(self):
- """
- Checks to ensure ``put`` is within ``allowed_methods``.
-
- Used when hydrating related data.
- """
- allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods)
- return 'put' in allowed
-
- def can_delete(self):
- """
- Checks to ensure ``delete`` is within ``allowed_methods``.
- """
- allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods)
- return 'delete' in allowed
-
- def apply_filters(self, request, applicable_filters):
- """
- A hook to alter how the filters are applied to the object list.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def obj_get_list(self, request=None, **kwargs):
- """
- Fetches the list of objects available on the resource.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def cached_obj_get_list(self, request=None, **kwargs):
- """
- A version of ``obj_get_list`` that uses the cache as a means to get
- commonly-accessed data faster.
- """
- cache_key = self.generate_cache_key('list', **kwargs)
- obj_list = self._meta.cache.get(cache_key)
-
- if obj_list is None:
- obj_list = self.obj_get_list(request=request, **kwargs)
- self._meta.cache.set(cache_key, obj_list)
-
- return obj_list
-
- def obj_get(self, request=None, **kwargs):
- """
- Fetches an individual object on the resource.
-
- This needs to be implemented at the user level. If the object can not
- be found, this should raise a ``NotFound`` exception.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def cached_obj_get(self, request=None, **kwargs):
- """
- A version of ``obj_get`` that uses the cache as a means to get
- commonly-accessed data faster.
- """
- cache_key = self.generate_cache_key('detail', **kwargs)
- bundle = self._meta.cache.get(cache_key)
-
- if bundle is None:
- bundle = self.obj_get(request=request, **kwargs)
- self._meta.cache.set(cache_key, bundle)
-
- return bundle
-
- def obj_create(self, bundle, request=None, **kwargs):
- """
- Creates a new object based on the provided data.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def obj_update(self, bundle, request=None, **kwargs):
- """
- Updates an existing object (or creates a new object) based on the
- provided data.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def obj_delete_list(self, request=None, **kwargs):
- """
- Deletes an entire list of objects.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def obj_delete(self, request=None, **kwargs):
- """
- Deletes a single object.
-
- This needs to be implemented at the user level.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- def create_response(self, request, data, response_class=HttpResponse, **response_kwargs):
- """
- Extracts the common "which-format/serialize/return-response" cycle.
-
- Mostly a useful shortcut/hook.
- """
- desired_format = self.determine_format(request)
- serialized = self.serialize(request, data, desired_format)
- return response_class(content=serialized, content_type=build_content_type(desired_format), **response_kwargs)
-
- def is_valid(self, bundle, request=None):
- """
- Handles checking if the data provided by the user is valid.
-
- Mostly a hook, this uses class assigned to ``validation`` from
- ``Resource._meta``.
-
- If validation fails, an error is raised with the error messages
- serialized inside it.
- """
- errors = self._meta.validation.is_valid(bundle, request)
-
- if len(errors):
- if request:
- desired_format = self.determine_format(request)
- else:
- desired_format = self._meta.default_format
-
- serialized = self.serialize(request, errors, desired_format)
- response = http.HttpBadRequest(content=serialized, content_type=build_content_type(desired_format))
- raise ImmediateHttpResponse(response=response)
-
- def rollback(self, bundles):
- """
- Given the list of bundles, delete all objects pertaining to those
- bundles.
-
- This needs to be implemented at the user level. No exceptions should
- be raised if possible.
-
- ``ModelResource`` includes a full working version specific to Django's
- ``Models``.
- """
- raise NotImplementedError()
-
- # Views.
-
- def get_list(self, request, **kwargs):
- """
- Returns a serialized list of resources.
-
- Calls ``obj_get_list`` to provide the data, then handles that result
- set and serializes it.
-
- Should return a HttpResponse (200 OK).
- """
- # TODO: Uncached for now. Invalidation that works for everyone may be
- # impossible.
- objects = self.obj_get_list(request=request, **self.remove_api_resource_names(kwargs))
- sorted_objects = self.apply_sorting(objects, options=request.GET)
-
- paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_list_uri(), limit=self._meta.limit)
- to_be_serialized = paginator.page()
-
- # Dehydrate the bundles in preparation for serialization.
- bundles = [self.build_bundle(obj=obj, request=request) for obj in to_be_serialized['objects']]
- to_be_serialized['objects'] = [self.full_dehydrate(bundle) for bundle in bundles]
- to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized)
- return self.create_response(request, to_be_serialized)
-
- def get_detail(self, request, **kwargs):
- """
- Returns a single serialized resource.
-
- Calls ``cached_obj_get/obj_get`` to provide the data, then handles that result
- set and serializes it.
-
- Should return a HttpResponse (200 OK).
- """
- try:
- obj = self.cached_obj_get(request=request, **self.remove_api_resource_names(kwargs))
- except ObjectDoesNotExist:
- return http.HttpNotFound()
- except MultipleObjectsReturned:
- return http.HttpMultipleChoices("More than one resource is found at this URI.")
-
- bundle = self.build_bundle(obj=obj, request=request)
- bundle = self.full_dehydrate(bundle)
- bundle = self.alter_detail_data_to_serialize(request, bundle)
- return self.create_response(request, bundle)
-
- def put_list(self, request, **kwargs):
- """
- Replaces a collection of resources with another collection.
-
- Calls ``delete_list`` to clear out the collection then ``obj_create``
- with the provided the data to create the new collection.
-
- Return ``HttpNoContent`` (204 No Content) if
- ``Meta.always_return_data = False`` (default).
-
- Return ``HttpAccepted`` (202 Accepted) if
- ``Meta.always_return_data = True``.
- """
- deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
- deserialized = self.alter_deserialized_list_data(request, deserialized)
-
- if not 'objects' in deserialized:
- raise BadRequest("Invalid data sent.")
-
- self.obj_delete_list(request=request, **self.remove_api_resource_names(kwargs))
- bundles_seen = []
-
- for object_data in deserialized['objects']:
- bundle = self.build_bundle(data=dict_strip_unicode_keys(object_data), request=request)
-
- # Attempt to be transactional, deleting any previously created
- # objects if validation fails.
- try:
- self.is_valid(bundle, request)
- except ImmediateHttpResponse:
- self.rollback(bundles_seen)
- raise
-
- self.obj_create(bundle, request=request, **self.remove_api_resource_names(kwargs))
- bundles_seen.append(bundle)
-
- if not self._meta.always_return_data:
- return http.HttpNoContent()
- else:
- to_be_serialized = {}
- to_be_serialized['objects'] = [self.full_dehydrate(bundle) for bundle in bundles_seen]
- to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized)
- return self.create_response(request, to_be_serialized, response_class=http.HttpAccepted)
-
- def put_detail(self, request, **kwargs):
- """
- Either updates an existing resource or creates a new one with the
- provided data.
-
- Calls ``obj_update`` with the provided data first, but falls back to
- ``obj_create`` if the object does not already exist.
-
- If a new resource is created, return ``HttpCreated`` (201 Created).
- If ``Meta.always_return_data = True``, there will be a populated body
- of serialized data.
-
- If an existing resource is modified and
- ``Meta.always_return_data = False`` (default), return ``HttpNoContent``
- (204 No Content).
- If an existing resource is modified and
- ``Meta.always_return_data = True``, return ``HttpAccepted`` (202
- Accepted).
- """
- deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
- deserialized = self.alter_deserialized_detail_data(request, deserialized)
- bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request)
- self.is_valid(bundle, request)
-
- try:
- updated_bundle = self.obj_update(bundle, request=request, **self.remove_api_resource_names(kwargs))
-
- if not self._meta.always_return_data:
- return http.HttpNoContent()
- else:
- updated_bundle = self.full_dehydrate(updated_bundle)
- updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle)
- return self.create_response(request, updated_bundle, response_class=http.HttpAccepted)
- except (NotFound, MultipleObjectsReturned):
- updated_bundle = self.obj_create(bundle, request=request, **self.remove_api_resource_names(kwargs))
- location = self.get_resource_uri(updated_bundle)
-
- if not self._meta.always_return_data:
- return http.HttpCreated(location=location)
- else:
- updated_bundle = self.full_dehydrate(updated_bundle)
- updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle)
- return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location)
-
- def post_list(self, request, **kwargs):
- """
- Creates a new resource/object with the provided data.
-
- Calls ``obj_create`` with the provided data and returns a response
- with the new resource's location.
-
- If a new resource is created, return ``HttpCreated`` (201 Created).
- If ``Meta.always_return_data = True``, there will be a populated body
- of serialized data.
- """
- deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
- deserialized = self.alter_deserialized_detail_data(request, deserialized)
- bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request)
- self.is_valid(bundle, request)
- updated_bundle = self.obj_create(bundle, request=request, **self.remove_api_resource_names(kwargs))
- location = self.get_resource_uri(updated_bundle)
-
- if not self._meta.always_return_data:
- return http.HttpCreated(location=location)
- else:
- updated_bundle = self.full_dehydrate(updated_bundle)
- updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle)
- return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location)
-
- def post_detail(self, request, **kwargs):
- """
- Creates a new subcollection of the resource under a resource.
-
- This is not implemented by default because most people's data models
- aren't self-referential.
-
- If a new resource is created, return ``HttpCreated`` (201 Created).
- """
- return http.HttpNotImplemented()
-
- def delete_list(self, request, **kwargs):
- """
- Destroys a collection of resources/objects.
-
- Calls ``obj_delete_list``.
-
- If the resources are deleted, return ``HttpNoContent`` (204 No Content).
- """
- self.obj_delete_list(request=request, **self.remove_api_resource_names(kwargs))
- return http.HttpNoContent()
-
- def delete_detail(self, request, **kwargs):
- """
- Destroys a single resource/object.
-
- Calls ``obj_delete``.
-
- If the resource is deleted, return ``HttpNoContent`` (204 No Content).
- If the resource did not exist, return ``Http404`` (404 Not Found).
- """
- try:
- self.obj_delete(request=request, **self.remove_api_resource_names(kwargs))
- return http.HttpNoContent()
- except NotFound:
- return http.HttpNotFound()
-
- def patch_list(self, request, **kwargs):
- """
- Updates a collection in-place.
-
- The exact behavior of ``PATCH`` to a list resource is still the matter of
- some debate in REST circles, and the ``PATCH`` RFC isn't standard. So the
- behavior this method implements (described below) is something of a
- stab in the dark. It's mostly cribbed from GData, with a smattering
- of ActiveResource-isms and maybe even an original idea or two.
-
- The ``PATCH`` format is one that's similar to the response returned from
- a ``GET`` on a list resource::
-
- {
- "objects": [{object}, {object}, ...],
- "deleted_objects": ["URI", "URI", "URI", ...],
- }
-
- For each object in ``objects``:
-
- * If the dict does not have a ``resource_uri`` key then the item is
- considered "new" and is handled like a ``POST`` to the resource list.
-
- * If the dict has a ``resource_uri`` key and the ``resource_uri`` refers
- to an existing resource then the item is a update; it's treated
- like a ``PATCH`` to the corresponding resource detail.
-
- * If the dict has a ``resource_uri`` but the resource *doesn't* exist,
- then this is considered to be a create-via-``PUT``.
-
- Each entry in ``deleted_objects`` referes to a resource URI of an existing
- resource to be deleted; each is handled like a ``DELETE`` to the relevent
- resource.
-
- In any case:
-
- * If there's a resource URI it *must* refer to a resource of this
- type. It's an error to include a URI of a different resource.
-
- * ``PATCH`` is all or nothing. If a single sub-operation fails, the
- entire request will fail and all resources will be rolled back.
- """
- request = convert_post_to_patch(request)
- deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
-
- if "objects" not in deserialized:
- raise BadRequest("Invalid data sent.")
-
- if len(deserialized["objects"]) and 'put' not in self._meta.detail_allowed_methods:
- raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed())
-
- for data in deserialized["objects"]:
- # If there's a resource_uri then this is either an
- # update-in-place or a create-via-PUT.
- if "resource_uri" in data:
- uri = data.pop('resource_uri')
-
- try:
- obj = self.get_via_uri(uri, request=request)
-
- # The object does exist, so this is an update-in-place.
- bundle = self.build_bundle(obj=obj, request=request)
- bundle = self.full_dehydrate(bundle)
- bundle = self.alter_detail_data_to_serialize(request, bundle)
- self.update_in_place(request, bundle, data)
- except (ObjectDoesNotExist, MultipleObjectsReturned):
- # The object referenced by resource_uri doesn't exist,
- # so this is a create-by-PUT equivalent.
- data = self.alter_deserialized_detail_data(request, data)
- bundle = self.build_bundle(data=dict_strip_unicode_keys(data))
- bundle.obj.pk = obj.pk
- self.is_valid(bundle, request)
- self.obj_create(bundle, request=request)
- else:
- # There's no resource URI, so this is a create call just
- # like a POST to the list resource.
- data = self.alter_deserialized_detail_data(request, data)
- bundle = self.build_bundle(data=dict_strip_unicode_keys(data))
- self.is_valid(bundle, request)
- self.obj_create(bundle, request=request)
-
- if len(deserialized.get('deleted_objects', [])) and 'delete' not in self._meta.detail_allowed_methods:
- raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed())
-
- for uri in deserialized.get('deleted_objects', []):
- obj = self.get_via_uri(uri, request=request)
- self.obj_delete(request=request, _obj=obj)
-
- return http.HttpAccepted()
-
- def patch_detail(self, request, **kwargs):
- """
- Updates a resource in-place.
-
- Calls ``obj_update``.
-
- If the resource is updated, return ``HttpAccepted`` (202 Accepted).
- If the resource did not exist, return ``HttpNotFound`` (404 Not Found).
- """
- request = convert_post_to_patch(request)
-
- # We want to be able to validate the update, but we can't just pass
- # the partial data into the validator since all data needs to be
- # present. Instead, we basically simulate a PUT by pulling out the
- # original data and updating it in-place.
- # So first pull out the original object. This is essentially
- # ``get_detail``.
- try:
- obj = self.cached_obj_get(request=request, **self.remove_api_resource_names(kwargs))
- except ObjectDoesNotExist:
- return http.HttpNotFound()
- except MultipleObjectsReturned:
- return http.HttpMultipleChoices("More than one resource is found at this URI.")
-
- bundle = self.build_bundle(obj=obj, request=request)
- bundle = self.full_dehydrate(bundle)
- bundle = self.alter_detail_data_to_serialize(request, bundle)
-
- # Now update the bundle in-place.
- deserialized = self.deserialize(request, request.raw_post_data, format=request.META.get('CONTENT_TYPE', 'application/json'))
- self.update_in_place(request, bundle, deserialized)
- return http.HttpAccepted()
-
- def update_in_place(self, request, original_bundle, new_data):
- """
- Update the object in original_bundle in-place using new_data.
- """
- original_bundle.data.update(**dict_strip_unicode_keys(new_data))
-
- # Now we've got a bundle with the new data sitting in it and we're
- # we're basically in the same spot as a PUT request. SO the rest of this
- # function is cribbed from put_detail.
- self.alter_deserialized_detail_data(request, original_bundle.data)
- self.is_valid(original_bundle, request)
- return self.obj_update(original_bundle, request=request, pk=original_bundle.obj.pk)
-
- def get_schema(self, request, **kwargs):
- """
- Returns a serialized form of the schema of the resource.
-
- Calls ``build_schema`` to generate the data. This method only responds
- to HTTP GET.
-
- Should return a HttpResponse (200 OK).
- """
- self.method_check(request, allowed=['get'])
- self.is_authenticated(request)
- self.throttle_check(request)
- self.log_throttled_access(request)
- return self.create_response(request, self.build_schema())
-
- def get_multiple(self, request, **kwargs):
- """
- Returns a serialized list of resources based on the identifiers
- from the URL.
-
- Calls ``obj_get`` to fetch only the objects requested. This method
- only responds to HTTP GET.
-
- Should return a HttpResponse (200 OK).
- """
- self.method_check(request, allowed=['get'])
- self.is_authenticated(request)
- self.throttle_check(request)
-
- # Rip apart the list then iterate.
- obj_pks = kwargs.get('pk_list', '').split(';')
- objects = []
- not_found = []
-
- for pk in obj_pks:
- try:
- obj = self.obj_get(request, pk=pk)
- bundle = self.build_bundle(obj=obj, request=request)
- bundle = self.full_dehydrate(bundle)
- objects.append(bundle)
- except ObjectDoesNotExist:
- not_found.append(pk)
-
- object_list = {
- 'objects': objects,
- }
-
- if len(not_found):
- object_list['not_found'] = not_found
-
- self.log_throttled_access(request)
- return self.create_response(request, object_list)
+ return self.create_response(request, data, response_class=response_class)
class ModelDeclarativeMetaclass(DeclarativeMetaclass):
View
401 tastypie/serializers.py
@@ -1,419 +1,34 @@
-import datetime
-from StringIO import StringIO
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
from django.core.serializers import json
from django.utils import simplejson
from django.utils.encoding import force_unicode
-from tastypie.bundle import Bundle
-from tastypie.exceptions import UnsupportedFormat
-from tastypie.utils import format_datetime, format_date, format_time
-try:
- import lxml
- from lxml.etree import parse as parse_xml
- from lxml.etree import Element, tostring
-except ImportError:
- lxml = None
-try:
- import yaml
- from django.core.serializers import pyyaml
-except ImportError:
- yaml = None
-try:
- import biplist
-except ImportError:
- biplist = None
+from piecrust.serializers import Serializer as PiecrustSerializer