Removed local copy of tastypie

- Legacy-Id: 9562
This commit is contained in:
Henrik Levkowetz 2015-04-26 19:24:02 +00:00
parent e6518affe4
commit 4ec326a505
43 changed files with 0 additions and 7070 deletions

View file

@ -1,5 +0,0 @@
from __future__ import unicode_literals
__author__ = 'Daniel Lindsley & the Tastypie core team'
__version__ = (0, 12, 1)

View file

@ -1,20 +0,0 @@
from __future__ import unicode_literals
from django.conf import settings
from django.contrib import admin
if 'django.contrib.auth' in settings.INSTALLED_APPS:
from tastypie.models import ApiKey
class ApiKeyInline(admin.StackedInline):
model = ApiKey
extra = 0
ABSTRACT_APIKEY = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False)
if ABSTRACT_APIKEY and not isinstance(ABSTRACT_APIKEY, bool):
raise TypeError("'TASTYPIE_ABSTRACT_APIKEY' must be either 'True' "
"or 'False'.")
if not ABSTRACT_APIKEY:
admin.site.register(ApiKey)

View file

@ -1,184 +0,0 @@
from __future__ import unicode_literals
import warnings
from django.conf.urls import url, patterns, include
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseBadRequest
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
class Api(object):
"""
Implements a registry to tie together the various resources that make up
an API.
Especially useful for navigation, HATEOAS and for providing multiple
versions of your API.
Optionally supplying ``api_name`` allows you to name the API. Generally,
this is done with version numbers (i.e. ``v1``, ``v2``, etc.) but can
be named any string.
"""
def __init__(self, api_name="v1", serializer_class=Serializer):
self.api_name = api_name
self._registry = {}
self._canonicals = {}
self.serializer = serializer_class()
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):
try:
return getattr(self, view)(request, *args, **kwargs)
except BadRequest:
return HttpResponseBadRequest()
return wrapper
def override_urls(self):
"""
Deprecated. Will be removed by v1.0.0. Please use ``prepend_urls`` instead.
"""
return []
def prepend_urls(self):
"""
A hook for adding your own URLs or matching before the default URLs.
"""
return []
@property
def urls(self):
"""
Provides URLconf details for the ``Api`` and all registered
``Resources`` beneath it.
"""
pattern_list = [
url(r"^(?P<api_name>%s)%s$" % (self.api_name, trailing_slash()), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name),
]
for name in sorted(self._registry.keys()):
self._registry[name].api_name = self.api_name
pattern_list.append((r"^(?P<api_name>%s)/" % self.api_name, include(self._registry[name].urls)))
urlpatterns = self.prepend_urls()
overridden_urls = self.override_urls()
if overridden_urls:
warnings.warn("'override_urls' is a deprecated method & will be removed by v1.0.0. Please rename your method to ``prepend_urls``.")
urlpatterns += overridden_urls
urlpatterns += patterns('',
*pattern_list
)
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.
"""
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, self.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 = self.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.
See ``NamespacedApi._build_reverse_url`` for an example.
"""
return reverse(name, args=args, kwargs=kwargs)
class NamespacedApi(Api):
"""
An API subclass that respects Django namespaces.
"""
def __init__(self, api_name="v1", urlconf_namespace=None, **kwargs):
super(NamespacedApi, self).__init__(api_name=api_name, **kwargs)
self.urlconf_namespace = urlconf_namespace
def register(self, resource, canonical=True):
super(NamespacedApi, self).register(resource, canonical=canonical)
if canonical is True:
# Plop in the namespace here as well.
resource._meta.urlconf_namespace = self.urlconf_namespace
def _build_reverse_url(self, name, args=None, kwargs=None):
namespaced = "%s:%s" % (self.urlconf_namespace, name)
return reverse(namespaced, args=args, kwargs=kwargs)

View file

@ -1,523 +0,0 @@
from __future__ import unicode_literals
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.middleware.csrf import _sanitize_token, constant_time_compare
from django.utils.http import same_origin
from django.utils.translation import ugettext as _
from tastypie.http import HttpUnauthorized
from tastypie.compat import get_user_model, get_username_field
try:
from hashlib import sha1
except ImportError:
import sha
sha1 = sha.sha
try:
import python_digest
except ImportError:
python_digest = None
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 __init__(self, require_active=True):
self.require_active = require_active
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'))
def check_active(self, user):
"""
Ensures the user has an active account.
Optimized for the ``django.contrib.auth.models.User`` case.
"""
if not self.require_active:
# Ignore & move on.
return True
return user.is_active
class BasicAuthentication(Authentication):
"""
Handles HTTP Basic auth against a specific auth backend if provided,
or against all configured authentication backends using the
``authenticate`` method from ``django.contrib.auth``.
Optional keyword arguments:
``backend``
If specified, use a specific ``django.contrib.auth`` backend instead
of checking all backends specified in the ``AUTHENTICATION_BACKENDS``
setting.
``realm``
The realm to use in the ``HttpUnauthorized`` response. Default:
``django-tastypie``.
"""
def __init__(self, backend=None, realm='django-tastypie', **kwargs):
super(BasicAuthentication, self).__init__(**kwargs)
self.backend = backend
self.realm = realm
def _unauthorized(self):
response = HttpUnauthorized()
# FIXME: Sanitize realm.
response['WWW-Authenticate'] = 'Basic Realm="%s"' % self.realm
return response
def is_authenticated(self, request, **kwargs):
"""
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.lower() != 'basic':
return self._unauthorized()
user_pass = base64.b64decode(data).decode('utf-8')
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])
else:
user = authenticate(username=bits[0], password=bits[1])
if user is None:
return self._unauthorized()
if not self.check_active(user):
return False
request.user = user
return True
def get_identifier(self, request):
"""
Provides a unique string identifier for the requestor.
This implementation returns the user's basic auth username.
"""
return request.META.get('REMOTE_USER', 'nouser')
class ApiKeyAuthentication(Authentication):
"""
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 extract_credentials(self, request):
if request.META.get('HTTP_AUTHORIZATION') and request.META['HTTP_AUTHORIZATION'].lower().startswith('apikey '):
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
if auth_type.lower() != 'apikey':
raise ValueError("Incorrect authorization header.")
username, api_key = data.split(':', 1)
else:
username = request.GET.get('username') or request.POST.get('username')
api_key = request.GET.get('api_key') or request.POST.get('api_key')
return username, api_key
def is_authenticated(self, request, **kwargs):
"""
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.
"""
try:
username, api_key = self.extract_credentials(request)
except ValueError:
return self._unauthorized()
if not username or not api_key:
return self._unauthorized()
username_field = get_username_field()
User = get_user_model()
try:
lookup_kwargs = {username_field: username}
user = User.objects.get(**lookup_kwargs)
except (User.DoesNotExist, User.MultipleObjectsReturned):
return self._unauthorized()
if not self.check_active(user):
return False
key_auth_check = self.get_key(user, api_key)
if key_auth_check and not isinstance(key_auth_check, HttpUnauthorized):
request.user = user
return key_auth_check
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
try:
ApiKey.objects.get(user=user, key=api_key)
except ApiKey.DoesNotExist:
return self._unauthorized()
return True
def get_identifier(self, request):
"""
Provides a unique string identifier for the requestor.
This implementation returns the user's username.
"""
username, api_key = self.extract_credentials(request)
return username or 'nouser'
class SessionAuthentication(Authentication):
"""
An authentication mechanism that piggy-backs on Django sessions.
This is useful when the API is talking to Javascript on the same site.
Relies on the user being logged in through the standard Django login
setup.
Requires a valid CSRF token.
"""
def is_authenticated(self, request, **kwargs):
"""
Checks to make sure the user is logged in & has a Django session.
"""
# Cargo-culted from Django 1.3/1.4's ``django/middleware/csrf.py``.
# We can't just use what's there, since the return values will be
# wrong.
# We also can't risk accessing ``request.POST``, which will break with
# the serialized bodies.
if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
return request.user.is_authenticated()
if getattr(request, '_dont_enforce_csrf_checks', False):
return request.user.is_authenticated()
csrf_token = _sanitize_token(request.COOKIES.get(settings.CSRF_COOKIE_NAME, ''))
if request.is_secure():
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False
good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
return False
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
if not constant_time_compare(request_csrf_token, csrf_token):
return False
return request.user.is_authenticated()
def get_identifier(self, request):
"""
Provides a unique string identifier for the requestor.
This implementation returns the user's username.
"""
return getattr(request.user, get_username_field())
class DigestAuthentication(Authentication):
"""
Handles HTTP Digest auth against a specific auth backend if provided,
or against all configured authentication backends using the
``authenticate`` method from ``django.contrib.auth``. However, instead of
the user's password, their API key should be used.
Optional keyword arguments:
``backend``
If specified, use a specific ``django.contrib.auth`` backend instead
of checking all backends specified in the ``AUTHENTICATION_BACKENDS``
setting.
``realm``
The realm to use in the ``HttpUnauthorized`` response. Default:
``django-tastypie``.
"""
def __init__(self, backend=None, realm='django-tastypie', **kwargs):
super(DigestAuthentication, self).__init__(**kwargs)
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).encode('utf-8'), digestmod=sha1).hexdigest()
response['WWW-Authenticate'] = python_digest.build_digest_challenge(
timestamp=time.time(),
secret=getattr(settings, 'SECRET_KEY', ''),
realm=self.realm,
opaque=opaque,
stale=False
)
return response
def is_authenticated(self, request, **kwargs):
"""
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.lower() != '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()
if not self.check_active(user):
return False
request.user = user
return True
def get_user(self, username):
username_field = get_username_field()
User = get_user_model()
try:
lookup_kwargs = {username_field: username}
user = User.objects.get(**lookup_kwargs)
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
try:
key = ApiKey.objects.get(user=user)
except ApiKey.DoesNotExist:
return False
return 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
return 'nouser'
class OAuthAuthentication(Authentication):
"""
Handles OAuth, which checks a user's credentials against a separate service.
Currently verifies against OAuth 1.0a services.
This does *NOT* provide OAuth authentication in your API, strictly
consumption.
"""
def __init__(self, **kwargs):
super(OAuthAuthentication, self).__init__(**kwargs)
if oauth2 is None:
raise ImproperlyConfigured("The 'python-oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.")
if oauth_provider is None:
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
if self.is_valid_request(request):
oauth_request = oauth_provider.utils.get_oauth_request(request)
consumer = store.get_consumer(request, oauth_request, oauth_request.get_parameter('oauth_consumer_key'))
try:
token = store.get_access_token(request, oauth_request, consumer, oauth_request.get_parameter('oauth_token'))
except oauth_provider.store.InvalidTokenError:
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid access token: %s') % oauth_request.get_parameter('oauth_token')))
try:
self.validate_token(request, consumer, token)
except oauth2.Error as e:
return oauth_provider.utils.send_oauth_error(e)
if consumer and token:
if not self.check_active(token.user):
return False
request.user = token.user
return True
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('You are not allowed to access this resource.')))
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid request parameters.')))
def is_in(self, params):
"""
Checks to ensure that all the OAuth parameter names are in the
provided ``params``.
"""
from oauth_provider.consts import OAUTH_PARAMETERS_NAMES
for param_name in OAUTH_PARAMETERS_NAMES:
if param_name not in params:
return False
return True
def is_valid_request(self, request):
"""
Checks whether the required parameters are either in the HTTP
``Authorization`` header sent by some clients (the preferred method
according to OAuth spec) or fall back to ``GET/POST``.
"""
auth_params = request.META.get("HTTP_AUTHORIZATION", [])
return self.is_in(auth_params) or self.is_in(request.REQUEST)
def validate_token(self, request, consumer, token):
oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request)
return oauth_server.verify_request(oauth_request, consumer, token)
class MultiAuthentication(object):
"""
An authentication backend that tries a number of backends in order.
"""
def __init__(self, *backends, **kwargs):
super(MultiAuthentication, self).__init__(**kwargs)
self.backends = backends
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.
"""
unauthorized = False
for backend in self.backends:
check = backend.is_authenticated(request, **kwargs)
if check:
if isinstance(check, HttpUnauthorized):
unauthorized = unauthorized or check
else:
request._authentication_backend = backend
return check
return unauthorized
def get_identifier(self, request):
"""
Provides a unique string identifier for the requestor.
This implementation returns a combination of IP address and hostname.
"""
try:
return request._authentication_backend.get_identifier(request)
except AttributeError:
return 'nouser'

View file

@ -1,245 +0,0 @@
from __future__ import unicode_literals
from tastypie.exceptions import TastypieError, Unauthorized
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 apply_limits(self, request, object_list):
"""
Deprecated.
FIXME: REMOVE BEFORE 1.0
"""
raise TastypieError("Authorization classes no longer support `apply_limits`. Please update to using `read_list`.")
def read_list(self, object_list, bundle):
"""
Returns a list of all the objects a user is allowed to read.
Should return an empty list if none are allowed.
Returns the entire list by default.
"""
return object_list
def read_detail(self, object_list, bundle):
"""
Returns either ``True`` if the user is allowed to read the object in
question or throw ``Unauthorized`` if they are not.
Returns ``True`` by default.
"""
return True
def create_list(self, object_list, bundle):
"""
Unimplemented, as Tastypie never creates entire new lists, but
present for consistency & possible extension.
"""
raise NotImplementedError("Tastypie has no way to determine if all objects should be allowed to be created.")
def create_detail(self, object_list, bundle):
"""
Returns either ``True`` if the user is allowed to create the object in
question or throw ``Unauthorized`` if they are not.
Returns ``True`` by default.
"""
return True
def update_list(self, object_list, bundle):
"""
Returns a list of all the objects a user is allowed to update.
Should return an empty list if none are allowed.
Returns the entire list by default.
"""
return object_list
def update_detail(self, object_list, bundle):
"""
Returns either ``True`` if the user is allowed to update the object in
question or throw ``Unauthorized`` if they are not.
Returns ``True`` by default.
"""
return True
def delete_list(self, object_list, bundle):
"""
Returns a list of all the objects a user is allowed to delete.
Should return an empty list if none are allowed.
Returns the entire list by default.
"""
return object_list
def delete_detail(self, object_list, bundle):
"""
Returns either ``True`` if the user is allowed to delete the object in
question or throw ``Unauthorized`` if they are not.
Returns ``True`` by default.
"""
return True
class ReadOnlyAuthorization(Authorization):
"""
Default Authentication class for ``Resource`` objects.
Only allows ``GET`` requests.
"""
def read_list(self, object_list, bundle):
return object_list
def read_detail(self, object_list, bundle):
return True
def create_list(self, object_list, bundle):
return []
def create_detail(self, object_list, bundle):
raise Unauthorized("You are not allowed to access that resource.")
def update_list(self, object_list, bundle):
return []
def update_detail(self, object_list, bundle):
raise Unauthorized("You are not allowed to access that resource.")
def delete_list(self, object_list, bundle):
return []
def delete_detail(self, object_list, bundle):
raise Unauthorized("You are not allowed to access that resource.")
class DjangoAuthorization(Authorization):
"""
Uses permission checking from ``django.contrib.auth`` to map
``POST / PUT / DELETE / PATCH`` to their equivalent Django auth
permissions.
Both the list & detail variants simply check the model they're based
on, as that's all the more granular Django's permission setup gets.
"""
def base_checks(self, request, model_klass):
# If it doesn't look like a model, we can't check permissions.
if not model_klass or not getattr(model_klass, '_meta', None):
return False
# User must be logged in to check permissions.
if not hasattr(request, 'user'):
return False
return model_klass
def read_list(self, object_list, bundle):
klass = self.base_checks(bundle.request, object_list.model)
if klass is False:
return []
# GET-style methods are always allowed.
return object_list
def read_detail(self, object_list, bundle):
klass = self.base_checks(bundle.request, bundle.obj.__class__)
if klass is False:
raise Unauthorized("You are not allowed to access that resource.")
# GET-style methods are always allowed.
return True
def create_list(self, object_list, bundle):
klass = self.base_checks(bundle.request, object_list.model)
if klass is False:
return []
permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
return []
return object_list
def create_detail(self, object_list, bundle):
klass = self.base_checks(bundle.request, bundle.obj.__class__)
if klass is False:
raise Unauthorized("You are not allowed to access that resource.")
permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
raise Unauthorized("You are not allowed to access that resource.")
return True
def update_list(self, object_list, bundle):
klass = self.base_checks(bundle.request, object_list.model)
if klass is False:
return []
permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
return []
return object_list
def update_detail(self, object_list, bundle):
klass = self.base_checks(bundle.request, bundle.obj.__class__)
if klass is False:
raise Unauthorized("You are not allowed to access that resource.")
permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
raise Unauthorized("You are not allowed to access that resource.")
return True
def delete_list(self, object_list, bundle):
klass = self.base_checks(bundle.request, object_list.model)
if klass is False:
return []
permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
return []
return object_list
def delete_detail(self, object_list, bundle):
klass = self.base_checks(bundle.request, bundle.obj.__class__)
if klass is False:
raise Unauthorized("You are not allowed to access that resource.")
permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name)
if not bundle.request.user.has_perm(permission):
raise Unauthorized("You are not allowed to access that resource.")
return True

View file

@ -1,33 +0,0 @@
from __future__ import unicode_literals
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,
objects_saved=None,
related_objects_to_save=None,
):
self.obj = obj
self.data = data or {}
self.request = request or HttpRequest()
self.related_obj = related_obj
self.related_name = related_name
self.errors = {}
self.objects_saved = objects_saved or set()
self.related_objects_to_save = related_objects_to_save or {}
def __repr__(self):
return "<Bundle for obj: '%s' and with data: '%s'>" % (self.obj, self.data)

View file

@ -1,98 +0,0 @@
from __future__ import unicode_literals
from django.core.cache import get_cache
class NoCache(object):
"""
A simplified, swappable base class for caching.
Does nothing save for simulating the cache API.
"""
def __init__(self, varies=None, *args, **kwargs):
"""
Optionally accepts a ``varies`` list that will be used in the
Vary header. Defaults to ["Accept"].
"""
super(NoCache, self).__init__(*args, **kwargs)
self.varies = varies
if self.varies is None:
self.varies = ["Accept"]
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
def cacheable(self, request, response):
"""
Returns True or False if the request -> response is capable of being
cached.
"""
return bool(request.method == "GET" and response.status_code == 200)
def cache_control(self):
"""
No-op for returning values for cache-control
"""
return {
'no_cache': True,
}
class SimpleCache(NoCache):
"""
Uses Django's current ``CACHES`` configuration to store cached data.
"""
def __init__(self, cache_name='default', timeout=None, public=None,
private=None, *args, **kwargs):
"""
Optionally accepts a ``timeout`` in seconds for the resource's cache.
Defaults to ``60`` seconds.
"""
super(SimpleCache, self).__init__(*args, **kwargs)
self.cache = get_cache(cache_name)
self.timeout = timeout or self.cache.default_timeout
self.public = public
self.private = private
def get(self, key, **kwargs):
"""
Gets a key from the cache. Returns ``None`` if the key is not found.
"""
return self.cache.get(key, **kwargs)
def set(self, key, value, timeout=None):
"""
Sets a key-value in the cache.
Optionally accepts a ``timeout`` in seconds. Defaults to ``None`` which
uses the resource's default timeout.
"""
if timeout is None:
timeout = self.timeout
self.cache.set(key, value, timeout)
def cache_control(self):
control = {
'max_age': self.timeout,
's_maxage': self.timeout,
}
if self.public is not None:
control["public"] = self.public
if self.private is not None:
control["private"] = self.private
return control

View file

@ -1,25 +0,0 @@
from __future__ import unicode_literals
from django.conf import settings
import django
__all__ = ['get_user_model', 'get_username_field', 'AUTH_USER_MODEL']
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
# Django 1.5+ compatibility
if django.VERSION >= (1, 5):
def get_user_model():
from django.contrib.auth import get_user_model as django_get_user_model
return django_get_user_model()
def get_username_field():
return get_user_model().USERNAME_FIELD
else:
def get_user_model():
from django.contrib.auth.models import User
return User
def get_username_field():
return 'username'

View file

@ -1,6 +0,0 @@
from __future__ import unicode_literals
# 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

View file

@ -1,55 +0,0 @@
from __future__ import unicode_literals
from functools import partial
from tastypie import fields
from tastypie.resources import Resource
from tastypie.exceptions import ApiFieldError
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from .resources import GenericResource
class GenericForeignKeyField(fields.ToOneField):
"""
Provides access to GenericForeignKey objects from the django content_types
framework.
"""
def __init__(self, to, attribute, **kwargs):
if not isinstance(to, dict):
raise ValueError('to field must be a dictionary in GenericForeignKeyField')
if len(to) <= 0:
raise ValueError('to field must have some values')
for k, v in to.items():
if not issubclass(k, models.Model) or not issubclass(v, Resource):
raise ValueError('to field must map django models to tastypie resources')
super(GenericForeignKeyField, self).__init__(to, attribute, **kwargs)
def get_related_resource(self, related_instance):
self._to_class = self.to.get(type(related_instance), None)
if self._to_class is None:
raise TypeError('no resource for model %s' % type(related_instance))
return super(GenericForeignKeyField, self).get_related_resource(related_instance)
@property
def to_class(self):
if self._to_class and not issubclass(GenericResource, self._to_class):
return self._to_class
return partial(GenericResource, resources=self.to.values())
def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None):
try:
obj = fk_resource.get_via_uri(uri, request=request)
fk_resource = self.get_related_resource(obj)
return super(GenericForeignKeyField, self).resource_from_uri(fk_resource, uri, request, related_obj, related_name)
except ObjectDoesNotExist:
raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri)
def build_related_resource(self, *args, **kwargs):
self._to_class = None
return super(GenericForeignKeyField, self).build_related_resource(*args, **kwargs)

View file

@ -1,42 +0,0 @@
from __future__ import unicode_literals
from tastypie.bundle import Bundle
from tastypie.resources import ModelResource
from tastypie.exceptions import NotFound
from django.core.urlresolvers import resolve, Resolver404, get_script_prefix
class GenericResource(ModelResource):
"""
Provides a stand-in resource for GFK relations.
"""
def __init__(self, resources, *args, **kwargs):
self.resource_mapping = dict((r._meta.resource_name, r) for r in resources)
return super(GenericResource, self).__init__(*args, **kwargs)
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)
resource_name = kwargs['resource_name']
resource_class = self.resource_mapping[resource_name]
except (Resolver404, KeyError):
raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)
parent_resource = resource_class(api_name=self._meta.api_name)
kwargs = parent_resource.remove_api_resource_names(kwargs)
bundle = Bundle(request=request)
return parent_resource.obj_get(bundle, **kwargs)

View file

@ -1,77 +0,0 @@
# See COPYING file in this directory.
# Some code originally from django-boundaryservice
from __future__ import unicode_literals
import json
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
from django.contrib.gis.db.models import GeometryField
from django.contrib.gis.geos import GEOSGeometry
from tastypie.fields import ApiField, CharField
from tastypie import resources
class GeometryApiField(ApiField):
"""
Custom ApiField for dealing with data from GeometryFields (by serializing
them as GeoJSON).
"""
dehydrated_type = 'geometry'
help_text = 'Geometry data.'
def hydrate(self, bundle):
value = super(GeometryApiField, self).hydrate(bundle)
if value is None:
return value
return json.dumps(value)
def dehydrate(self, obj, for_list=False):
return self.convert(super(GeometryApiField, self).dehydrate(obj))
def convert(self, value):
if value is None:
return None
if isinstance(value, dict):
return value
# Get ready-made geojson serialization and then convert it _back_ to
# a Python object so that tastypie can serialize it as part of the
# bundle.
return json.loads(value.geojson)
class ModelResource(resources.ModelResource):
"""
ModelResource subclass that handles geometry fields as GeoJSON.
"""
@classmethod
def api_field_from_django_field(cls, f, default=CharField):
"""
Overrides default field handling to support custom GeometryApiField.
"""
if isinstance(f, GeometryField):
return GeometryApiField
return super(ModelResource, cls).api_field_from_django_field(f, default)
def filter_value_to_python(self, value, field_name, filters, filter_expr,
filter_type):
value = super(ModelResource, self).filter_value_to_python(
value, field_name, filters, filter_expr, filter_type)
# If we are filtering on a GeometryApiField then we should try
# and convert this to a GEOSGeometry object. The conversion
# will fail if we don't have value JSON, so in that case we'll
# just return ``value`` as normal.
if isinstance(self.fields[field_name], GeometryApiField):
try:
value = GEOSGeometry(unquote(value))
except ValueError:
pass
return value

View file

@ -1,101 +0,0 @@
from __future__ import unicode_literals
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 Unauthorized(TastypieError):
"""
Raised when the request object is not accessible to the user.
This is different than the ``tastypie.http.HttpUnauthorized`` & is handled
differently internally.
"""
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(BadRequest):
"""
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
@property
def response(self):
return self._response

View file

@ -1,907 +0,0 @@
from __future__ import unicode_literals
import datetime
from dateutil.parser import parse
from decimal import Decimal
import re
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.utils import datetime_safe, importlib
from django.utils import six
from tastypie.bundle import Bundle
from tastypie.exceptions import ApiFieldError, NotFound
from tastypie.utils import dict_strip_unicode_keys, make_aware
class NOT_PROVIDED:
def __str__(self):
return 'No default provided.'
DATE_REGEX = re.compile('^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}).*?$')
DATETIME_REGEX = re.compile('^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(T|\s+)(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}).*?$')
# All the ApiField variants.
class ApiField(object):
"""The base implementation of a field used by the resources."""
dehydrated_type = 'string'
help_text = ''
def __init__(self, attribute=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, unique=False, help_text=None, use_in='all'):
"""
Sets up the field. This is generally called when the containing
``Resource`` is initialized.
Optionally accepts an ``attribute``, which should be a string of
either an instance attribute or callable off the object during the
``dehydrate`` or push data onto an object during the ``hydrate``.
Defaults to ``None``, meaning data will be manually accessed.
Optionally accepts a ``default``, which provides default data when the
object being ``dehydrated``/``hydrated`` has no data on the field.
Defaults to ``NOT_PROVIDED``.
Optionally accepts a ``null``, which indicated whether or not a
``None`` is allowable data on the field. Defaults to ``False``.
Optionally accepts a ``blank``, which indicated whether or not
data may be omitted on the field. Defaults to ``False``.
Optionally accepts a ``readonly``, which indicates whether the field
is used during the ``hydrate`` or not. Defaults to ``False``.
Optionally accepts a ``unique``, which indicates if the field is a
unique identifier for the object.
Optionally accepts ``help_text``, which lets you provide a
human-readable description of the field exposed at the schema level.
Defaults to the per-Field definition.
Optionally accepts ``use_in``. This may be one of ``list``, ``detail``
``all`` or a callable which accepts a ``bundle`` and returns
``True`` or ``False``. Indicates wheather this field will be included
during dehydration of a list of objects or a single object. If ``use_in``
is a callable, and returns ``True``, the field will be included during
dehydration.
Defaults to ``all``.
"""
# Track what the index thinks this field is called.
self.instance_name = None
self._resource = None
self.attribute = attribute
self._default = default
self.null = null
self.blank = blank
self.readonly = readonly
self.value = None
self.unique = unique
self.use_in = 'all'
if use_in in ['all', 'detail', 'list'] or callable(use_in):
self.use_in = use_in
if help_text:
self.help_text = help_text
def contribute_to_class(self, cls, name):
# Do the least we can here so that we don't hate ourselves in the
# morning.
self.instance_name = name
self._resource = cls
def has_default(self):
"""Returns a boolean of whether this field has a default value."""
return self._default is not NOT_PROVIDED
@property
def default(self):
"""Returns the default value for the field."""
if callable(self._default):
return self._default()
return self._default
def dehydrate(self, bundle, for_list=True):
"""
Takes data from the provided object and prepares it for the
resource.
"""
if self.attribute is not None:
# Check for `__` in the field for looking through the relation.
attrs = self.attribute.split('__')
current_object = bundle.obj
for attr in attrs:
previous_object = current_object
current_object = getattr(current_object, attr, None)
if current_object is None:
if self.has_default():
current_object = self._default
# Fall out of the loop, given any further attempts at
# accesses will fail miserably.
break
elif self.null:
current_object = None
# Fall out of the loop, given any further attempts at
# accesses will fail miserably.
break
else:
raise ApiFieldError("The object '%r' has an empty attribute '%s' and doesn't allow a default or null value." % (previous_object, attr))
if callable(current_object):
current_object = current_object()
return self.convert(current_object)
if self.has_default():
return self.convert(self.default)
else:
return None
def convert(self, value):
"""
Handles conversion between the data found and the type of the field.
Extending classes should override this method and provide correct
data coercion.
"""
return value
def hydrate(self, bundle):
"""
Takes data stored in the bundle for the field and returns it. Used for
taking simple data and building a instance object.
"""
if self.readonly:
return None
if not self.instance_name in bundle.data:
if getattr(self, 'is_related', False) and not getattr(self, 'is_m2m', False):
# We've got an FK (or alike field) & a possible parent object.
# Check for it.
if bundle.related_obj and bundle.related_name in (self.attribute, self.instance_name):
return bundle.related_obj
if self.blank:
return None
elif self.attribute and getattr(bundle.obj, self.attribute, None):
return getattr(bundle.obj, self.attribute)
elif self.instance_name and hasattr(bundle.obj, self.instance_name):
return getattr(bundle.obj, self.instance_name)
elif self.has_default():
if callable(self._default):
return self._default()
return self._default
elif self.null:
return None
else:
raise ApiFieldError("The '%s' field has no data and doesn't allow a default or null value." % self.instance_name)
return bundle.data[self.instance_name]
class CharField(ApiField):
"""
A text field of arbitrary length.
Covers both ``models.CharField`` and ``models.TextField``.
"""
dehydrated_type = 'string'
help_text = 'Unicode string data. Ex: "Hello World"'
def convert(self, value):
if value is None:
return None
return six.text_type(value)
class FileField(ApiField):
"""
A file-related field.
Covers both ``models.FileField`` and ``models.ImageField``.
"""
dehydrated_type = 'string'
help_text = 'A file URL as a string. Ex: "http://media.example.com/media/photos/my_photo.jpg"'
def convert(self, value):
if value is None:
return None
try:
# Try to return the URL if it's a ``File``, falling back to the string
# itself if it's been overridden or is a default.
return getattr(value, 'url', value)
except ValueError:
return None
class IntegerField(ApiField):
"""
An integer field.
Covers ``models.IntegerField``, ``models.PositiveIntegerField``,
``models.PositiveSmallIntegerField`` and ``models.SmallIntegerField``.
"""
dehydrated_type = 'integer'
help_text = 'Integer data. Ex: 2673'
def convert(self, value):
if value is None:
return None
return int(value)
class FloatField(ApiField):
"""
A floating point field.
"""
dehydrated_type = 'float'
help_text = 'Floating point numeric data. Ex: 26.73'
def convert(self, value):
if value is None:
return None
return float(value)
class DecimalField(ApiField):
"""
A decimal field.
"""
dehydrated_type = 'decimal'
help_text = 'Fixed precision numeric data. Ex: 26.73'
def convert(self, value):
if value is None:
return None
return Decimal(value)
def hydrate(self, bundle):
value = super(DecimalField, self).hydrate(bundle)
if value and not isinstance(value, Decimal):
value = Decimal(value)
return value
class BooleanField(ApiField):
"""
A boolean field.
Covers both ``models.BooleanField`` and ``models.NullBooleanField``.
"""
dehydrated_type = 'boolean'
help_text = 'Boolean data. Ex: True'
def convert(self, value):
if value is None:
return None
return bool(value)
class ListField(ApiField):
"""
A list field.
"""
dehydrated_type = 'list'
help_text = "A list of data. Ex: ['abc', 26.73, 8]"
def convert(self, value):
if value is None:
return None
return list(value)
class DictField(ApiField):
"""
A dictionary field.
"""
dehydrated_type = 'dict'
help_text = "A dictionary of data. Ex: {'price': 26.73, 'name': 'Daniel'}"
def convert(self, value):
if value is None:
return None
return dict(value)
class DateField(ApiField):
"""
A date field.
"""
dehydrated_type = 'date'
help_text = 'A date as a string. Ex: "2010-11-10"'
def convert(self, value):
if value is None:
return None
if isinstance(value, six.string_types):
match = DATE_REGEX.search(value)
if match:
data = match.groupdict()
return datetime_safe.date(int(data['year']), int(data['month']), int(data['day']))
else:
raise ApiFieldError("Date provided to '%s' field doesn't appear to be a valid date string: '%s'" % (self.instance_name, value))
return value
def hydrate(self, bundle):
value = super(DateField, self).hydrate(bundle)
if value and not hasattr(value, 'year'):
try:
# Try to rip a date/datetime out of it.
value = make_aware(parse(value))
if hasattr(value, 'hour'):
value = value.date()
except ValueError:
pass
return value
class DateTimeField(ApiField):
"""
A datetime field.
"""
dehydrated_type = 'datetime'
help_text = 'A date & time as a string. Ex: "2010-11-10T03:07:43"'
def convert(self, value):
if value is None:
return None
if isinstance(value, six.string_types):
match = DATETIME_REGEX.search(value)
if match:
data = match.groupdict()
return make_aware(datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second'])))
else:
raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
return value
def hydrate(self, bundle):
value = super(DateTimeField, self).hydrate(bundle)
if value and not hasattr(value, 'year'):
if isinstance(value, six.string_types):
try:
# Try to rip a date/datetime out of it.
value = make_aware(parse(value))
except (ValueError, TypeError):
raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
else:
raise ApiFieldError("Datetime provided to '%s' field must be a string: %s" % (self.instance_name, value))
return value
class RelatedField(ApiField):
"""
Provides access to data that is related within the database.
The ``RelatedField`` base class is not intended for direct use but provides
functionality that ``ToOneField`` and ``ToManyField`` build upon.
The contents of this field actually point to another ``Resource``,
rather than the related object. This allows the field to represent its data
in different ways.
The abstractions based around this are "leaky" in that, unlike the other
fields provided by ``tastypie``, these fields don't handle arbitrary objects
very well. The subclasses use Django's ORM layer to make things go, though
there is no ORM-specific code at this level.
"""
dehydrated_type = 'related'
is_related = True
self_referential = False
help_text = 'A related resource. Can be either a URI or set of nested resource data.'
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
"""
Builds the field and prepares it to access to related data.
The ``to`` argument should point to a ``Resource`` class, NOT
to a ``Model``. Required.
The ``attribute`` argument should specify what field/callable points to
the related data on the instance object. Required.
Optionally accepts a ``related_name`` argument. Currently unused, as
unlike Django's ORM layer, reverse relations between ``Resource``
classes are not automatically created. Defaults to ``None``.
Optionally accepts a ``null``, which indicated whether or not a
``None`` is allowable data on the field. Defaults to ``False``.
Optionally accepts a ``blank``, which indicated whether or not
data may be omitted on the field. Defaults to ``False``.
Optionally accepts a ``readonly``, which indicates whether the field
is used during the ``hydrate`` or not. Defaults to ``False``.
Optionally accepts a ``full``, which indicates how the related
``Resource`` will appear post-``dehydrate``. If ``False``, the
related ``Resource`` will appear as a URL to the endpoint of that
resource. If ``True``, the result of the sub-resource's
``dehydrate`` will be included in full.
Optionally accepts a ``unique``, which indicates if the field is a
unique identifier for the object.
Optionally accepts ``help_text``, which lets you provide a
human-readable description of the field exposed at the schema level.
Defaults to the per-Field definition.
Optionally accepts ``use_in``. This may be one of ``list``, ``detail``
``all`` or a callable which accepts a ``bundle`` and returns
``True`` or ``False``. Indicates wheather this field will be included
during dehydration of a list of objects or a single object. If ``use_in``
is a callable, and returns ``True``, the field will be included during
dehydration.
Defaults to ``all``.
Optionally accepts a ``full_list``, which indicated whether or not
data should be fully dehydrated when the request is for a list of
resources. Accepts ``True``, ``False`` or a callable that accepts
a bundle and returns ``True`` or ``False``. Depends on ``full``
being ``True``. Defaults to ``True``.
Optionally accepts a ``full_detail``, which indicated whether or not
data should be fully dehydrated when then request is for a single
resource. Accepts ``True``, ``False`` or a callable that accepts a
bundle and returns ``True`` or ``False``.Depends on ``full``
being ``True``. Defaults to ``True``.
"""
self.instance_name = None
self._resource = None
self.to = to
self.attribute = attribute
self.related_name = related_name
self._default = default
self.null = null
self.blank = blank
self.readonly = readonly
self.full = full
self.api_name = None
self.resource_name = None
self.unique = unique
self._to_class = None
self.use_in = 'all'
self.full_list = full_list
self.full_detail = full_detail
if use_in in ['all', 'detail', 'list'] or callable(use_in):
self.use_in = use_in
if self.to == 'self':
self.self_referential = True
self._to_class = self.__class__
if help_text:
self.help_text = help_text
def contribute_to_class(self, cls, name):
super(RelatedField, self).contribute_to_class(cls, name)
# Check if we're self-referential and hook it up.
# We can't do this quite like Django because there's no ``AppCache``
# here (which I think we should avoid as long as possible).
if self.self_referential or self.to == 'self':
self._to_class = cls
def get_related_resource(self, related_instance):
"""
Instaniates the related resource.
"""
related_resource = self.to_class()
# Fix the ``api_name`` if it's not present.
if related_resource._meta.api_name is None:
if self._resource and not self._resource._meta.api_name is None:
related_resource._meta.api_name = self._resource._meta.api_name
# Try to be efficient about DB queries.
related_resource.instance = related_instance
return related_resource
@property
def to_class(self):
# We need to be lazy here, because when the metaclass constructs the
# Resources, other classes may not exist yet.
# That said, memoize this so we never have to relookup/reimport.
if self._to_class:
return self._to_class
if not isinstance(self.to, six.string_types):
self._to_class = self.to
return self._to_class
# It's a string. Let's figure it out.
if '.' in self.to:
# Try to import.
module_bits = self.to.split('.')
module_path, class_name = '.'.join(module_bits[:-1]), module_bits[-1]
module = importlib.import_module(module_path)
else:
# We've got a bare class name here, which won't work (No AppCache
# to rely on). Try to throw a useful error.
raise ImportError("Tastypie requires a Python-style path (<module.module.Class>) to lazy load related resources. Only given '%s'." % self.to)
self._to_class = getattr(module, class_name, None)
if self._to_class is None:
raise ImportError("Module '%s' does not appear to have a class called '%s'." % (module_path, class_name))
return self._to_class
def dehydrate_related(self, bundle, related_resource, for_list=True):
"""
Based on the ``full_resource``, returns either the endpoint or the data
from ``full_dehydrate`` for the related resource.
"""
should_dehydrate_full_resource = self.should_full_dehydrate(bundle, for_list=for_list)
if not should_dehydrate_full_resource:
# Be a good netizen.
return related_resource.get_resource_uri(bundle)
else:
# ZOMG extra data and big payloads.
bundle = related_resource.build_bundle(
obj=related_resource.instance,
request=bundle.request,
objects_saved=bundle.objects_saved
)
return related_resource.full_dehydrate(bundle)
def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None):
"""
Given a URI is provided, the related resource is attempted to be
loaded based on the identifiers in the URI.
"""
try:
obj = fk_resource.get_via_uri(uri, request=request)
bundle = fk_resource.build_bundle(
obj=obj,
request=request
)
return fk_resource.full_dehydrate(bundle)
except ObjectDoesNotExist:
raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri)
def resource_from_data(self, fk_resource, data, request=None, related_obj=None, related_name=None):
"""
Given a dictionary-like structure is provided, a fresh related
resource is created using that data.
"""
# Try to hydrate the data provided.
data = dict_strip_unicode_keys(data)
fk_bundle = fk_resource.build_bundle(
data=data,
request=request
)
if related_obj:
fk_bundle.related_obj = related_obj
fk_bundle.related_name = related_name
unique_keys = dict((k, v) for k, v in data.items() if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique))
# If we have no unique keys, we shouldn't go look for some resource that
# happens to match other kwargs. In the case of a create, it might be the
# completely wrong resource.
# We also need to check to see if updates are allowed on the FK resource.
if unique_keys and fk_resource.can_update():
try:
return fk_resource.obj_update(fk_bundle, skip_errors=True, **data)
except (NotFound, TypeError):
try:
# Attempt lookup by primary key
return fk_resource.obj_update(fk_bundle, skip_errors=True, **unique_keys)
except NotFound:
pass
except MultipleObjectsReturned:
pass
# If we shouldn't update a resource, or we couldn't find a matching
# resource we'll just return a populated bundle instead
# of mistakenly updating something that should be read-only.
fk_bundle = fk_resource.full_hydrate(fk_bundle)
fk_resource.is_valid(fk_bundle)
return fk_bundle
def resource_from_pk(self, fk_resource, obj, request=None, related_obj=None, related_name=None):
"""
Given an object with a ``pk`` attribute, the related resource
is attempted to be loaded via that PK.
"""
bundle = fk_resource.build_bundle(
obj=obj,
request=request
)
return fk_resource.full_dehydrate(bundle)
def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
"""
Returns a bundle of data built by the related resource, usually via
``hydrate`` with the data provided.
Accepts either a URI, a data dictionary (or dictionary-like structure)
or an object with a ``pk``.
"""
self.fk_resource = self.to_class()
kwargs = {
'request': request,
'related_obj': related_obj,
'related_name': related_name,
}
if isinstance(value, Bundle):
# Already hydrated, probably nested bundles. Just return.
return value
elif isinstance(value, six.string_types):
# We got a URI. Load the object and assign it.
return self.resource_from_uri(self.fk_resource, value, **kwargs)
elif hasattr(value, 'items'):
# We've got a data dictionary.
# Since this leads to creation, this is the only one of these
# methods that might care about "parent" data.
return self.resource_from_data(self.fk_resource, value, **kwargs)
elif hasattr(value, 'pk'):
# We've got an object with a primary key.
return self.resource_from_pk(self.fk_resource, value, **kwargs)
else:
raise ApiFieldError("The '%s' field was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: %s." % (self.instance_name, value))
def should_full_dehydrate(self, bundle, for_list):
"""
Based on the ``full``, ``list_full`` and ``detail_full`` returns ``True`` or ``False``
indicating weather the resource should be fully dehydrated.
"""
should_dehydrate_full_resource = False
if self.full:
is_details_view = not for_list
if is_details_view:
if (not callable(self.full_detail) and self.full_detail) or (callable(self.full_detail) and self.full_detail(bundle)):
should_dehydrate_full_resource = True
else:
if (not callable(self.full_list) and self.full_list) or (callable(self.full_list) and self.full_list(bundle)):
should_dehydrate_full_resource = True
return should_dehydrate_full_resource
class ToOneField(RelatedField):
"""
Provides access to related data via foreign key.
This subclass requires Django's ORM layer to work properly.
"""
help_text = 'A single related resource. Can be either a URI or set of nested resource data.'
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED,
null=False, blank=False, readonly=False, full=False,
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
super(ToOneField, self).__init__(
to, attribute, related_name=related_name, default=default,
null=null, blank=blank, readonly=readonly, full=full,
unique=unique, help_text=help_text, use_in=use_in,
full_list=full_list, full_detail=full_detail
)
self.fk_resource = None
def dehydrate(self, bundle, for_list=True):
foreign_obj = None
error_to_raise = None
if isinstance(self.attribute, six.string_types):
attrs = self.attribute.split('__')
foreign_obj = bundle.obj
for attr in attrs:
previous_obj = foreign_obj
try:
foreign_obj = getattr(foreign_obj, attr, None)
except ObjectDoesNotExist:
foreign_obj = None
elif callable(self.attribute):
previous_obj = bundle.obj
foreign_obj = self.attribute(bundle)
if not foreign_obj:
if not self.null:
if callable(self.attribute):
raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj))
else:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource, for_list=for_list)
def hydrate(self, bundle):
value = super(ToOneField, self).hydrate(bundle)
if value is None:
return value
return self.build_related_resource(value, request=bundle.request)
class ForeignKey(ToOneField):
"""
A convenience subclass for those who prefer to mirror ``django.db.models``.
"""
pass
class OneToOneField(ToOneField):
"""
A convenience subclass for those who prefer to mirror ``django.db.models``.
"""
pass
class ToManyField(RelatedField):
"""
Provides access to related data via a join table.
This subclass requires Django's ORM layer to work properly.
Note that the ``hydrate`` portions of this field are quite different than
any other field. ``hydrate_m2m`` actually handles the data and relations.
This is due to the way Django implements M2M relationships.
"""
is_m2m = True
help_text = 'Many related resources. Can be either a list of URIs or list of individually nested resource data.'
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED,
null=False, blank=False, readonly=False, full=False,
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
super(ToManyField, self).__init__(
to, attribute, related_name=related_name, default=default,
null=null, blank=blank, readonly=readonly, full=full,
unique=unique, help_text=help_text, use_in=use_in,
full_list=full_list, full_detail=full_detail
)
self.m2m_bundles = []
def dehydrate(self, bundle, for_list=True):
if not bundle.obj or not bundle.obj.pk:
if not self.null:
raise ApiFieldError("The model '%r' does not have a primary key and can not be used in a ToMany context." % bundle.obj)
return []
the_m2ms = None
previous_obj = bundle.obj
attr = self.attribute
if isinstance(self.attribute, six.string_types):
attrs = self.attribute.split('__')
the_m2ms = bundle.obj
for attr in attrs:
previous_obj = the_m2ms
try:
the_m2ms = getattr(the_m2ms, attr, None)
except ObjectDoesNotExist:
the_m2ms = None
if not the_m2ms:
break
elif callable(self.attribute):
the_m2ms = self.attribute(bundle)
if not the_m2ms:
if not self.null:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
return []
self.m2m_resources = []
m2m_dehydrated = []
# TODO: Also model-specific and leaky. Relies on there being a
# ``Manager`` there.
for m2m in the_m2ms.all():
m2m_resource = self.get_related_resource(m2m)
m2m_bundle = Bundle(obj=m2m, request=bundle.request)
self.m2m_resources.append(m2m_resource)
m2m_dehydrated.append(self.dehydrate_related(m2m_bundle, m2m_resource, for_list=for_list))
return m2m_dehydrated
def hydrate(self, bundle):
pass
def hydrate_m2m(self, bundle):
if self.readonly:
return None
if bundle.data.get(self.instance_name) is None:
if self.blank:
return []
elif self.null:
return []
else:
raise ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name)
m2m_hydrated = []
for value in bundle.data.get(self.instance_name):
if value is None:
continue
kwargs = {
'request': bundle.request,
}
if self.related_name:
kwargs['related_obj'] = bundle.obj
kwargs['related_name'] = self.related_name
m2m_hydrated.append(self.build_related_resource(value, **kwargs))
return m2m_hydrated
class ManyToManyField(ToManyField):
"""
A convenience subclass for those who prefer to mirror ``django.db.models``.
"""
pass
class OneToManyField(ToManyField):
"""
A convenience subclass for those who prefer to mirror ``django.db.models``.
"""
pass
class TimeField(ApiField):
dehydrated_type = 'time'
help_text = 'A time as string. Ex: "20:05:23"'
def dehydrate(self, obj, for_list=True):
return self.convert(super(TimeField, self).dehydrate(obj))
def convert(self, value):
if isinstance(value, six.string_types):
return self.to_time(value)
return value
def to_time(self, s):
try:
dt = parse(s)
except (ValueError, TypeError) as e:
raise ApiFieldError(str(e))
else:
return datetime.time(dt.hour, dt.minute, dt.second)
def hydrate(self, bundle):
value = super(TimeField, self).hydrate(bundle)
if value and not isinstance(value, datetime.time):
value = self.to_time(value)
return value

View file

@ -1,80 +0,0 @@
"""
The various HTTP responses for use in returning proper HTTP codes.
"""
from __future__ import unicode_literals
from django.http import HttpResponse
class HttpCreated(HttpResponse):
status_code = 201
def __init__(self, *args, **kwargs):
location = kwargs.pop('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 HttpUnprocessableEntity(HttpResponse):
status_code = 422
class HttpTooManyRequests(HttpResponse):
status_code = 429
class HttpApplicationError(HttpResponse):
status_code = 500
class HttpNotImplemented(HttpResponse):
status_code = 501

View file

@ -1,30 +0,0 @@
from __future__ import print_function
from __future__ import unicode_literals
from django.core.management.base import NoArgsCommand
from tastypie.compat import get_user_model
from tastypie.models import ApiKey
class Command(NoArgsCommand):
help = "Goes through all users and adds API keys for any that don't have one."
def handle_noargs(self, **options):
"""Goes through all users and adds API keys for any that don't have one."""
self.verbosity = int(options.get('verbosity', 1))
User = get_user_model()
for user in User.objects.all().iterator():
try:
api_key = ApiKey.objects.get(user=user)
if not api_key.key:
# Autogenerate the key.
api_key.save()
if self.verbosity >= 1:
print(u"Generated a new key for '%s'" % user.username)
except ApiKey.DoesNotExist:
api_key = ApiKey.objects.create(user=user)
if self.verbosity >= 1:
print(u"Created a new key for '%s'" % user.username)

View file

@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
import tastypie.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ApiAccess',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('identifier', models.CharField(max_length=255)),
('url', models.CharField(default='', max_length=255, blank=True)),
('request_method', models.CharField(default='', max_length=10, blank=True)),
('accessed', models.PositiveIntegerField()),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ApiKey',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('key', models.CharField(default='', max_length=128, db_index=True, blank=True)),
('created', models.DateTimeField(default=tastypie.utils.timezone.now)),
('user', models.OneToOneField(related_name='api_key', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
]

View file

@ -1,63 +0,0 @@
from __future__ import unicode_literals
import hmac
import time
from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from tastypie.utils import now
try:
from hashlib import sha1
except ImportError:
import sha
sha1 = sha.sha
@python_2_unicode_compatible
class ApiAccess(models.Model):
"""A simple model for use with the ``CacheDBThrottle`` behaviors."""
identifier = models.CharField(max_length=255)
url = models.CharField(max_length=255, blank=True, default='')
request_method = models.CharField(max_length=10, blank=True, default='')
accessed = models.PositiveIntegerField()
def __str__(self):
return "%s @ %s" % (self.identifier, self.accessed)
def save(self, *args, **kwargs):
self.accessed = int(time.time())
return super(ApiAccess, self).save(*args, **kwargs)
if 'django.contrib.auth' in settings.INSTALLED_APPS:
import uuid
from tastypie.compat import AUTH_USER_MODEL
class ApiKey(models.Model):
user = models.OneToOneField(AUTH_USER_MODEL, related_name='api_key')
key = models.CharField(max_length=128, blank=True, default='', db_index=True)
created = models.DateTimeField(default=now)
def __unicode__(self):
return u"%s for %s" % (self.key, self.user)
def save(self, *args, **kwargs):
if not self.key:
self.key = self.generate_key()
return super(ApiKey, self).save(*args, **kwargs)
def generate_key(self):
# Get a random UUID.
new_uuid = uuid.uuid4()
# Hmac that beast.
return hmac.new(new_uuid.bytes, digestmod=sha1).hexdigest()
class Meta:
abstract = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False)
def create_api_key(sender, **kwargs):
"""
A signal for hooking up automatic ``ApiKey`` creation.
"""
if kwargs.get('created') is True:
ApiKey.objects.create(user=kwargs.get('instance'))

View file

@ -1,209 +0,0 @@
from __future__ import unicode_literals
from django.conf import settings
from django.utils import six
from tastypie.exceptions import BadRequest
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
class Paginator(object):
"""
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, max_limit=1000, collection_name='objects'):
"""
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.
Optionally accepts a ``max_limit`` argument, which the upper bound
limit. Defaults to ``1000``. If you set it to 0 or ``None``, no upper
bound will be enforced.
"""
self.request_data = request_data
self.objects = objects
self.limit = limit
self.max_limit = max_limit
self.offset = offset
self.resource_uri = resource_uri
self.collection_name = collection_name
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 = self.request_data.get('limit', self.limit)
if limit is None:
limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
try:
limit = int(limit)
except ValueError:
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer." % limit)
if limit < 0:
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer >= 0." % limit)
if self.max_limit and (not limit or limit > self.max_limit):
# If it's more than the max, we're only going to return the max.
# This is to prevent excessive DB (or other) load.
return self.max_limit
return limit
def get_offset(self):
"""
Determines the proper starting offset of results to return.
It attempts 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." % offset)
if offset < 0:
raise BadRequest("Invalid offset '%s' provided. Please provide a positive integer >= 0." % offset)
return offset
def get_slice(self, limit, offset):
"""
Slices the result set to the specified ``limit`` & ``offset``.
"""
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
try:
# QueryDict has a urlencode method that can handle multiple values for the same key
request_params = self.request_data.copy()
if 'limit' in request_params:
del request_params['limit']
if 'offset' in request_params:
del request_params['offset']
request_params.update({'limit': limit, 'offset': offset})
encoded_params = request_params.urlencode()
except AttributeError:
request_params = {}
for k, v in self.request_data.items():
if isinstance(v, six.text_type):
request_params[k] = v.encode('utf-8')
else:
request_params[k] = v
if 'limit' in request_params:
del request_params['limit']
if 'offset' in request_params:
del request_params['offset']
request_params.update({'limit': limit, 'offset': offset})
encoded_params = urlencode(request_params)
return '%s?%s' % (
self.resource_uri,
encoded_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 {
self.collection_name: objects,
'meta': meta,
}

File diff suppressed because it is too large Load diff

View file

@ -1,519 +0,0 @@
from __future__ import unicode_literals
import datetime
import re
import django
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils import six
from django.utils.encoding import force_text, smart_bytes
from django.core.serializers import json as djangojson
from tastypie.bundle import Bundle
from tastypie.exceptions import BadRequest, UnsupportedFormat
from tastypie.utils import format_datetime, format_date, format_time, make_naive
try:
import defusedxml.lxml as lxml
from defusedxml.common import DefusedXmlException
from defusedxml.lxml import parse as parse_xml
from lxml.etree import Element, tostring, LxmlError, XMLParser
except ImportError:
lxml = None
try:
import yaml
from django.core.serializers import pyyaml
except ImportError:
yaml = None
try:
import biplist
except ImportError:
biplist = None
import json
XML_ENCODING = re.compile('<\?xml.*?\?>', re.IGNORECASE)
# Ugh & blah.
# So doing a regular dump is generally fine, since Tastypie doesn't usually
# serialize advanced types. *HOWEVER*, it will dump out Python Unicode strings
# as a custom YAML tag, which of course ``yaml.safe_load`` can't handle.
if yaml is not None:
from yaml.constructor import SafeConstructor
from yaml.loader import Reader, Scanner, Parser, Composer, Resolver
class TastypieConstructor(SafeConstructor):
def construct_yaml_unicode_dammit(self, node):
value = self.construct_scalar(node)
try:
return value.encode('ascii')
except UnicodeEncodeError:
return value
TastypieConstructor.add_constructor(u'tag:yaml.org,2002:python/unicode', TastypieConstructor.construct_yaml_unicode_dammit)
class TastypieLoader(Reader, Scanner, Parser, Composer, TastypieConstructor, Resolver):
def __init__(self, stream):
Reader.__init__(self, stream)
Scanner.__init__(self)
Parser.__init__(self)
Composer.__init__(self)
TastypieConstructor.__init__(self)
Resolver.__init__(self)
class Serializer(object):
"""
A swappable class for serialization.
This handles most types of data as well as the following output formats::
* json
* jsonp (Disabled by default)
* xml
* yaml
* html
* plist (see http://explorapp.com/biplist/)
It was designed to make changing behavior easy, either by overridding the
various format methods (i.e. ``to_json``), by changing the
``formats/content_types`` options or by altering the other hook methods.
"""
formats = ['json', 'xml', 'yaml', 'html', 'plist']
content_types = {'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'plist': 'application/x-plist'}
def __init__(self, formats=None, content_types=None, datetime_formatting=None):
if datetime_formatting is not None:
self.datetime_formatting = datetime_formatting
else:
self.datetime_formatting = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601')
self.supported_formats = []
if content_types is not None:
self.content_types = content_types
if formats is not None:
self.formats = formats
if self.formats is Serializer.formats and hasattr(settings, 'TASTYPIE_DEFAULT_FORMATS'):
# We want TASTYPIE_DEFAULT_FORMATS to override unmodified defaults but not intentational changes
# on Serializer subclasses:
self.formats = settings.TASTYPIE_DEFAULT_FORMATS
if not isinstance(self.formats, (list, tuple)):
raise ImproperlyConfigured('Formats should be a list or tuple, not %r' % self.formats)
for format in self.formats:
try:
self.supported_formats.append(self.content_types[format])
except KeyError:
raise ImproperlyConfigured("Content type for specified type '%s' not found. Please provide it at either the class level or via the arguments." % format)
def get_mime_for_format(self, format):
"""
Given a format, attempts to determine the correct MIME type.
If not available on the current ``Serializer``, returns
``application/json`` by default.
"""
try:
return self.content_types[format]
except KeyError:
return 'application/json'
def format_datetime(self, data):
"""
A hook to control how datetimes are formatted.
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
Default is ``iso-8601``, which looks like "2010-12-16T03:02:14".
"""
data = make_naive(data)
if self.datetime_formatting == 'rfc-2822':
return format_datetime(data)
if self.datetime_formatting == 'iso-8601-strict':
# Remove microseconds to strictly adhere to iso-8601
data = data - datetime.timedelta(microseconds = data.microsecond)
return data.isoformat()
def format_date(self, data):
"""
A hook to control how dates are formatted.
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
Default is ``iso-8601``, which looks like "2010-12-16".
"""
if self.datetime_formatting == 'rfc-2822':
return format_date(data)
return data.isoformat()
def format_time(self, data):
"""
A hook to control how times are formatted.
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
Default is ``iso-8601``, which looks like "03:02:14".
"""
if self.datetime_formatting == 'rfc-2822':
return format_time(data)
if self.datetime_formatting == 'iso-8601-strict':
# Remove microseconds to strictly adhere to iso-8601
data = (datetime.datetime.combine(datetime.date(1,1,1),data) - datetime.timedelta(microseconds = data.microsecond)).time()
return data.isoformat()
def serialize(self, bundle, format='application/json', options=None):
"""
Given some data and a format, calls the correct method to serialize
the data and returns the result.
"""
desired_format = None
if options is None:
options = {}
for short_format, long_format in self.content_types.items():
if format == long_format:
if hasattr(self, "to_%s" % short_format):
desired_format = short_format
break
if desired_format is None:
raise UnsupportedFormat("The format indicated '%s' had no available serialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format)
serialized = getattr(self, "to_%s" % desired_format)(bundle, options)
return serialized
def deserialize(self, content, format='application/json'):
"""
Given some data and a format, calls the correct method to deserialize
the data and returns the result.
"""
desired_format = None
format = format.split(';')[0]
for short_format, long_format in self.content_types.items():
if format == long_format:
if hasattr(self, "from_%s" % short_format):
desired_format = short_format
break
if desired_format is None:
raise UnsupportedFormat("The format indicated '%s' had no available deserialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format)
if isinstance(content, six.binary_type):
content = force_text(content)
deserialized = getattr(self, "from_%s" % desired_format)(content)
return deserialized
def to_simple(self, data, options):
"""
For a piece of data, attempts to recognize it and provide a simplified
form of something complex.
This brings complex Python data structures down to native types of the
serialization format(s).
"""
if isinstance(data, (list, tuple)):
return [self.to_simple(item, options) for item in data]
if isinstance(data, dict):
return dict((key, self.to_simple(val, options)) for (key, val) in data.items())
elif isinstance(data, Bundle):
return dict((key, self.to_simple(val, options)) for (key, val) in data.data.items())
elif hasattr(data, 'dehydrated_type'):
if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False:
if data.full:
return self.to_simple(data.fk_resource, options)
else:
return self.to_simple(data.value, options)
elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True:
if data.full:
return [self.to_simple(bundle, options) for bundle in data.m2m_bundles]
else:
return [self.to_simple(val, options) for val in data.value]
else:
return self.to_simple(data.value, options)
elif isinstance(data, datetime.datetime):
return self.format_datetime(data)
elif isinstance(data, datetime.date):
return self.format_date(data)
elif isinstance(data, datetime.time):
return self.format_time(data)
elif isinstance(data, bool):
return data
elif isinstance(data, (six.integer_types, float)):
return data
elif data is None:
return None
else:
return force_text(data)
def to_etree(self, data, options=None, name=None, depth=0):
"""
Given some data, converts that data to an ``etree.Element`` suitable
for use in the XML output.
"""
if isinstance(data, (list, tuple)):
element = Element(name or 'objects')
if name:
element = Element(name)
element.set('type', 'list')
else:
element = Element('objects')
for item in data:
element.append(self.to_etree(item, options, depth=depth+1))
element[:] = sorted(element, key=lambda x: x.tag)
elif isinstance(data, dict):
if depth == 0:
element = Element(name or 'response')
else:
element = Element(name or 'object')
element.set('type', 'hash')
for (key, value) in data.items():
element.append(self.to_etree(value, options, name=key, depth=depth+1))
element[:] = sorted(element, key=lambda x: x.tag)
elif isinstance(data, Bundle):
element = Element(name or 'object')
for field_name, field_object in data.data.items():
element.append(self.to_etree(field_object, options, name=field_name, depth=depth+1))
element[:] = sorted(element, key=lambda x: x.tag)
elif hasattr(data, 'dehydrated_type'):
if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False:
if data.full:
return self.to_etree(data.fk_resource, options, name, depth+1)
else:
return self.to_etree(data.value, options, name, depth+1)
elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True:
if data.full:
element = Element(name or 'objects')
for bundle in data.m2m_bundles:
element.append(self.to_etree(bundle, options, bundle.resource_name, depth+1))
else:
element = Element(name or 'objects')
for value in data.value:
element.append(self.to_etree(value, options, name, depth=depth+1))
else:
return self.to_etree(data.value, options, name)
else:
element = Element(name or 'value')
simple_data = self.to_simple(data, options)
data_type = get_type_string(simple_data)
if data_type != 'string':
element.set('type', get_type_string(simple_data))
if data_type != 'null':
if isinstance(simple_data, six.text_type):
element.text = simple_data
else:
element.text = force_text(simple_data)
return element
def from_etree(self, data):
"""
Not the smartest deserializer on the planet. At the request level,
it first tries to output the deserialized subelement called "object"
or "objects" and falls back to deserializing based on hinted types in
the XML element attribute "type".
"""
if data.tag == 'request':
# if "object" or "objects" exists, return deserialized forms.
elements = data.getchildren()
for element in elements:
if element.tag in ('object', 'objects'):
return self.from_etree(element)
return dict((element.tag, self.from_etree(element)) for element in elements)
elif data.tag == 'object' or data.get('type') == 'hash':
return dict((element.tag, self.from_etree(element)) for element in data.getchildren())
elif data.tag == 'objects' or data.get('type') == 'list':
return [self.from_etree(element) for element in data.getchildren()]
else:
type_string = data.get('type')
if type_string in ('string', None):
return data.text
elif type_string == 'integer':
return int(data.text)
elif type_string == 'float':
return float(data.text)
elif type_string == 'boolean':
if data.text == 'True':
return True
else:
return False
else:
return None
def to_json(self, data, options=None):
"""
Given some Python data, produces JSON output.
"""
options = options or {}
data = self.to_simple(data, options)
return djangojson.json.dumps(data, cls=djangojson.DjangoJSONEncoder, sort_keys=True, ensure_ascii=False, indent=2)
def from_json(self, content):
"""
Given some JSON data, returns a Python dictionary of the decoded data.
"""
try:
return json.loads(content)
except ValueError:
raise BadRequest
def to_jsonp(self, data, options=None):
"""
Given some Python data, produces JSON output wrapped in the provided
callback.
Due to a difference between JSON and Javascript, two
newline characters, \u2028 and \u2029, need to be escaped.
See http://timelessrepo.com/json-isnt-a-javascript-subset for
details.
"""
options = options or {}
json = self.to_json(data, options)
json = json.replace(u'\u2028', u'\\u2028').replace(u'\u2029', u'\\u2029')
return u'%s(%s)' % (options['callback'], json)
def to_xml(self, data, options=None):
"""
Given some Python data, produces XML output.
"""
options = options or {}
if lxml is None:
raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.")
return tostring(self.to_etree(data, options), xml_declaration=True, encoding='utf-8')
def from_xml(self, content, forbid_dtd=True, forbid_entities=True):
"""
Given some XML data, returns a Python dictionary of the decoded data.
By default XML entity declarations and DTDs will raise a BadRequest
exception content but subclasses may choose to override this if
necessary.
"""
if lxml is None:
raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.")
try:
# Stripping the encoding declaration. Because lxml.
# See http://lxml.de/parsing.html, "Python unicode strings".
content = XML_ENCODING.sub('', content)
parsed = parse_xml(
six.StringIO(content),
forbid_dtd=forbid_dtd,
forbid_entities=forbid_entities
)
except (LxmlError, DefusedXmlException):
raise BadRequest()
return self.from_etree(parsed.getroot())
def to_yaml(self, data, options=None):
"""
Given some Python data, produces YAML output.
"""
options = options or {}
if yaml is None:
raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.")
return yaml.dump(self.to_simple(data, options))
def from_yaml(self, content):
"""
Given some YAML data, returns a Python dictionary of the decoded data.
"""
if yaml is None:
raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.")
return yaml.load(content, Loader=TastypieLoader)
def to_plist(self, data, options=None):
"""
Given some Python data, produces binary plist output.
"""
options = options or {}
if biplist is None:
raise ImproperlyConfigured("Usage of the plist aspects requires biplist.")
return biplist.writePlistToString(self.to_simple(data, options))
def from_plist(self, content):
"""
Given some binary plist data, returns a Python dictionary of the decoded data.
"""
if biplist is None:
raise ImproperlyConfigured("Usage of the plist aspects requires biplist.")
if isinstance(content, six.text_type):
content = smart_bytes(content)
return biplist.readPlistFromString(content)
def to_html(self, data, options=None):
"""
Reserved for future usage.
The desire is to provide HTML output of a resource, making an API
available to a browser. This is on the TODO list but not currently
implemented.
"""
options = options or {}
return 'Sorry, not implemented yet. Please append "?format=json" to your URL.'
def from_html(self, content):
"""
Reserved for future usage.
The desire is to handle form-based (maybe Javascript?) input, making an
API available to a browser. This is on the TODO list but not currently
implemented.
"""
pass
def get_type_string(data):
"""
Translates a Python data type into a string format.
"""
data_type = type(data)
if data_type in six.integer_types:
return 'integer'
elif data_type == float:
return 'float'
elif data_type == bool:
return 'boolean'
elif data_type in (list, tuple):
return 'list'
elif data_type == dict:
return 'hash'
elif data is None:
return 'null'
elif isinstance(data, six.string_types):
return 'string'

View file

@ -1,97 +0,0 @@
# encoding: utf-8
from __future__ import unicode_literals
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from tastypie.compat import AUTH_USER_MODEL
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'ApiAccess'
db.create_table('tastypie_apiaccess', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('identifier', self.gf('django.db.models.fields.CharField')(max_length=255)),
('url', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)),
('request_method', self.gf('django.db.models.fields.CharField')(default='', max_length=10, blank=True)),
('accessed', self.gf('django.db.models.fields.PositiveIntegerField')()),
))
db.send_create_signal('tastypie', ['ApiAccess'])
# Adding model 'ApiKey'
db.create_table('tastypie_apikey', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='api_key', unique=True, to=orm[AUTH_USER_MODEL])),
('key', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
))
db.send_create_signal('tastypie', ['ApiKey'])
def backwards(self, orm):
# Deleting model 'ApiAccess'
db.delete_table('tastypie_apiaccess')
# Deleting model 'ApiKey'
db.delete_table('tastypie_apikey')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
AUTH_USER_MODEL: {
'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'tastypie.apiaccess': {
'Meta': {'object_name': 'ApiAccess'},
'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}),
'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
},
'tastypie.apikey': {
'Meta': {'object_name': 'ApiKey'},
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL})
}
}
complete_apps = ['tastypie']

View file

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from tastypie.compat import AUTH_USER_MODEL
class Migration(SchemaMigration):
def forwards(self, orm):
if not db.backend_name in ('mysql', 'sqlite'):
# Adding index on 'ApiKey', fields ['key']
db.create_index('tastypie_apikey', ['key'])
def backwards(self, orm):
if not db.backend_name in ('mysql', 'sqlite'):
# Removing index on 'ApiKey', fields ['key']
db.delete_index('tastypie_apikey', ['key'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
AUTH_USER_MODEL: {
'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'tastypie.apiaccess': {
'Meta': {'object_name': 'ApiAccess'},
'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}),
'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
},
'tastypie.apikey': {
'Meta': {'object_name': 'ApiKey'},
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 5, 0, 0)'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL})
}
}
complete_apps = ['tastypie']

View file

@ -1,29 +0,0 @@
<html>
<head>
<title>{% block title %}API for example.com{% endblock %}</title>
<style type="text/css">
* { margin: 0; padding: 0; }
body { background-color: #DDDDDD; font-family: Georgia, Times New Roman, sans-serif; text-align: center; }
#page_wrapper { margin: 0 auto; text-align: left; width: 800px; }
</style>
</head>
<body>
<div id="page_wrapper">
<h1>API for example.com</h1>
<section class="resources_nav">
{% block resources_nav %}
<ul>
{% for resource in resources_nav_items %}
<li><a href="{# % url api_resource_list % #}">{{ resource }}</a></li>
{% endfor %}
</ul>
{% endblock %}
</section>
<section class="content">
{% block content %}{% endblock %}
</section>
</div>
</body>
</html>

View file

@ -1,4 +0,0 @@
{% extends "tastypie/basic.html" %}
{% block content %}
{% endblock %}

View file

@ -1,4 +0,0 @@
{% extends "tastypie/basic.html" %}
{% block content %}
{% endblock %}

View file

@ -1,526 +0,0 @@
from __future__ import unicode_literals
import time
from django.conf import settings
from django.test import TestCase
from django.test.client import FakePayload, Client
from django.utils.encoding import force_text
from tastypie.serializers import Serializer
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
class TestApiClient(object):
def __init__(self, serializer=None):
"""
Sets up a fresh ``TestApiClient`` instance.
If you are employing a custom serializer, you can pass the class to the
``serializer=`` kwarg.
"""
self.client = Client()
self.serializer = serializer
if not self.serializer:
self.serializer = Serializer()
def get_content_type(self, short_format):
"""
Given a short name (such as ``json`` or ``xml``), returns the full content-type
for it (``application/json`` or ``application/xml`` in this case).
"""
return self.serializer.content_types.get(short_format, 'json')
def get(self, uri, format='json', data=None, authentication=None, **kwargs):
"""
Performs a simulated ``GET`` request to the provided URI.
Optionally accepts a ``data`` kwarg, which in the case of ``GET``, lets you
send along ``GET`` parameters. This is useful when testing filtering or other
things that read off the ``GET`` params. Example::
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.get('/api/v1/entry/1/', data={'format': 'json', 'title__startswith': 'a', 'limit': 20, 'offset': 60})
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other ``**kwargs`` passed in get passed through to the Django
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
"""
content_type = self.get_content_type(format)
kwargs['HTTP_ACCEPT'] = content_type
# GET & DELETE are the only times we don't serialize the data.
if data is not None:
kwargs['data'] = data
if authentication is not None:
kwargs['HTTP_AUTHORIZATION'] = authentication
return self.client.get(uri, **kwargs)
def post(self, uri, format='json', data=None, authentication=None, **kwargs):
"""
Performs a simulated ``POST`` request to the provided URI.
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``POST`` the
``data`` gets serialized & sent as the body instead of becoming part of the URI.
Example::
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.post('/api/v1/entry/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other ``**kwargs`` passed in get passed through to the Django
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
"""
content_type = self.get_content_type(format)
kwargs['content_type'] = content_type
if data is not None:
kwargs['data'] = self.serializer.serialize(data, format=content_type)
if authentication is not None:
kwargs['HTTP_AUTHORIZATION'] = authentication
return self.client.post(uri, **kwargs)
def put(self, uri, format='json', data=None, authentication=None, **kwargs):
"""
Performs a simulated ``PUT`` request to the provided URI.
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PUT`` the
``data`` gets serialized & sent as the body instead of becoming part of the URI.
Example::
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.put('/api/v1/entry/1/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other ``**kwargs`` passed in get passed through to the Django
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
"""
content_type = self.get_content_type(format)
kwargs['content_type'] = content_type
if data is not None:
kwargs['data'] = self.serializer.serialize(data, format=content_type)
if authentication is not None:
kwargs['HTTP_AUTHORIZATION'] = authentication
return self.client.put(uri, **kwargs)
def patch(self, uri, format='json', data=None, authentication=None, **kwargs):
"""
Performs a simulated ``PATCH`` request to the provided URI.
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PATCH`` the
``data`` gets serialized & sent as the body instead of becoming part of the URI.
Example::
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.patch('/api/v1/entry/1/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other ``**kwargs`` passed in get passed through to the Django
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
"""
content_type = self.get_content_type(format)
kwargs['content_type'] = content_type
if data is not None:
kwargs['data'] = self.serializer.serialize(data, format=content_type)
if authentication is not None:
kwargs['HTTP_AUTHORIZATION'] = authentication
# This hurts because Django doesn't support PATCH natively.
parsed = urlparse(uri)
r = {
'CONTENT_LENGTH': len(kwargs['data']),
'CONTENT_TYPE': content_type,
'PATH_INFO': self.client._get_path(parsed),
'QUERY_STRING': parsed[4],
'REQUEST_METHOD': 'PATCH',
'wsgi.input': FakePayload(kwargs['data']),
}
r.update(kwargs)
return self.client.request(**r)
def delete(self, uri, format='json', data=None, authentication=None, **kwargs):
"""
Performs a simulated ``DELETE`` request to the provided URI.
Optionally accepts a ``data`` kwarg, which in the case of ``DELETE``, lets you
send along ``DELETE`` parameters. This is useful when testing filtering or other
things that read off the ``DELETE`` params. Example::
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.delete('/api/v1/entry/1/', data={'format': 'json'})
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other ``**kwargs`` passed in get passed through to the Django
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
"""
content_type = self.get_content_type(format)
kwargs['content_type'] = content_type
# GET & DELETE are the only times we don't serialize the data.
if data is not None:
kwargs['data'] = data
if authentication is not None:
kwargs['HTTP_AUTHORIZATION'] = authentication
return self.client.delete(uri, **kwargs)
class ResourceTestCase(TestCase):
"""
A useful base class for the start of testing Tastypie APIs.
"""
def setUp(self):
super(ResourceTestCase, self).setUp()
self.serializer = Serializer()
self.api_client = TestApiClient()
def get_credentials(self):
"""
A convenience method for the user as a way to shorten up the
often repetitious calls to create the same authentication.
Raises ``NotImplementedError`` by default.
Usage::
class MyResourceTestCase(ResourceTestCase):
def get_credentials(self):
return self.create_basic('daniel', 'pass')
# Then the usual tests...
"""
raise NotImplementedError("You must return the class for your Resource to test.")
def create_basic(self, username, password):
"""
Creates & returns the HTTP ``Authorization`` header for use with BASIC
Auth.
"""
import base64
return 'Basic %s' % base64.b64encode(':'.join([username, password]).encode('utf-8')).decode('utf-8')
def create_apikey(self, username, api_key):
"""
Creates & returns the HTTP ``Authorization`` header for use with
``ApiKeyAuthentication``.
"""
return 'ApiKey %s:%s' % (username, api_key)
def create_digest(self, username, api_key, method, uri):
"""
Creates & returns the HTTP ``Authorization`` header for use with Digest
Auth.
"""
from tastypie.authentication import hmac, sha1, uuid, python_digest
new_uuid = uuid.uuid4()
opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest().decode('utf-8')
return python_digest.build_authorization_request(
username,
method.upper(),
uri,
1, # nonce_count
digest_challenge=python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), 'django-tastypie', opaque, False),
password=api_key
)
def create_oauth(self, user):
"""
Creates & returns the HTTP ``Authorization`` header for use with Oauth.
"""
from oauth_provider.models import Consumer, Token, Resource
# Necessary setup for ``oauth_provider``.
resource, _ = Resource.objects.get_or_create(url='test', defaults={
'name': 'Test Resource'
})
consumer, _ = Consumer.objects.get_or_create(key='123', defaults={
'name': 'Test',
'description': 'Testing...'
})
token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={
'consumer': consumer,
'resource': resource,
'secret': '',
'user': user,
})
# Then generate the header.
oauth_data = {
'oauth_consumer_key': '123',
'oauth_nonce': 'abc',
'oauth_signature': '&',
'oauth_signature_method': 'PLAINTEXT',
'oauth_timestamp': str(int(time.time())),
'oauth_token': 'foo',
}
return 'OAuth %s' % ','.join([key+'='+value for key, value in oauth_data.items()])
def assertHttpOK(self, resp):
"""
Ensures the response is returning a HTTP 200.
"""
return self.assertEqual(resp.status_code, 200)
def assertHttpCreated(self, resp):
"""
Ensures the response is returning a HTTP 201.
"""
return self.assertEqual(resp.status_code, 201)
def assertHttpAccepted(self, resp):
"""
Ensures the response is returning either a HTTP 202 or a HTTP 204.
"""
return self.assertIn(resp.status_code, [202, 204])
def assertHttpMultipleChoices(self, resp):
"""
Ensures the response is returning a HTTP 300.
"""
return self.assertEqual(resp.status_code, 300)
def assertHttpSeeOther(self, resp):
"""
Ensures the response is returning a HTTP 303.
"""
return self.assertEqual(resp.status_code, 303)
def assertHttpNotModified(self, resp):
"""
Ensures the response is returning a HTTP 304.
"""
return self.assertEqual(resp.status_code, 304)
def assertHttpBadRequest(self, resp):
"""
Ensures the response is returning a HTTP 400.
"""
return self.assertEqual(resp.status_code, 400)
def assertHttpUnauthorized(self, resp):
"""
Ensures the response is returning a HTTP 401.
"""
return self.assertEqual(resp.status_code, 401)
def assertHttpForbidden(self, resp):
"""
Ensures the response is returning a HTTP 403.
"""
return self.assertEqual(resp.status_code, 403)
def assertHttpNotFound(self, resp):
"""
Ensures the response is returning a HTTP 404.
"""
return self.assertEqual(resp.status_code, 404)
def assertHttpMethodNotAllowed(self, resp):
"""
Ensures the response is returning a HTTP 405.
"""
return self.assertEqual(resp.status_code, 405)
def assertHttpConflict(self, resp):
"""
Ensures the response is returning a HTTP 409.
"""
return self.assertEqual(resp.status_code, 409)
def assertHttpGone(self, resp):
"""
Ensures the response is returning a HTTP 410.
"""
return self.assertEqual(resp.status_code, 410)
def assertHttpUnprocessableEntity(self, resp):
"""
Ensures the response is returning a HTTP 422.
"""
return self.assertEqual(resp.status_code, 422)
def assertHttpTooManyRequests(self, resp):
"""
Ensures the response is returning a HTTP 429.
"""
return self.assertEqual(resp.status_code, 429)
def assertHttpApplicationError(self, resp):
"""
Ensures the response is returning a HTTP 500.
"""
return self.assertEqual(resp.status_code, 500)
def assertHttpNotImplemented(self, resp):
"""
Ensures the response is returning a HTTP 501.
"""
return self.assertEqual(resp.status_code, 501)
def assertValidJSON(self, data):
"""
Given the provided ``data`` as a string, ensures that it is valid JSON &
can be loaded properly.
"""
# Just try the load. If it throws an exception, the test case will fail.
self.serializer.from_json(data)
def assertValidXML(self, data):
"""
Given the provided ``data`` as a string, ensures that it is valid XML &
can be loaded properly.
"""
# Just try the load. If it throws an exception, the test case will fail.
self.serializer.from_xml(data)
def assertValidYAML(self, data):
"""
Given the provided ``data`` as a string, ensures that it is valid YAML &
can be loaded properly.
"""
# Just try the load. If it throws an exception, the test case will fail.
self.serializer.from_yaml(data)
def assertValidPlist(self, data):
"""
Given the provided ``data`` as a string, ensures that it is valid
binary plist & can be loaded properly.
"""
# Just try the load. If it throws an exception, the test case will fail.
self.serializer.from_plist(data)
def assertValidJSONResponse(self, resp):
"""
Given a ``HttpResponse`` coming back from using the ``client``, assert that
you get back:
* An HTTP 200
* The correct content-type (``application/json``)
* The content is valid JSON
"""
self.assertHttpOK(resp)
self.assertTrue(resp['Content-Type'].startswith('application/json'))
self.assertValidJSON(force_text(resp.content))
def assertValidXMLResponse(self, resp):
"""
Given a ``HttpResponse`` coming back from using the ``client``, assert that
you get back:
* An HTTP 200
* The correct content-type (``application/xml``)
* The content is valid XML
"""
self.assertHttpOK(resp)
self.assertTrue(resp['Content-Type'].startswith('application/xml'))
self.assertValidXML(force_text(resp.content))
def assertValidYAMLResponse(self, resp):
"""
Given a ``HttpResponse`` coming back from using the ``client``, assert that
you get back:
* An HTTP 200
* The correct content-type (``text/yaml``)
* The content is valid YAML
"""
self.assertHttpOK(resp)
self.assertTrue(resp['Content-Type'].startswith('text/yaml'))
self.assertValidYAML(force_text(resp.content))
def assertValidPlistResponse(self, resp):
"""
Given a ``HttpResponse`` coming back from using the ``client``, assert that
you get back:
* An HTTP 200
* The correct content-type (``application/x-plist``)
* The content is valid binary plist data
"""
self.assertHttpOK(resp)
self.assertTrue(resp['Content-Type'].startswith('application/x-plist'))
self.assertValidPlist(force_text(resp.content))
def deserialize(self, resp):
"""
Given a ``HttpResponse`` coming back from using the ``client``, this method
checks the ``Content-Type`` header & attempts to deserialize the data based on
that.
It returns a Python datastructure (typically a ``dict``) of the serialized data.
"""
return self.serializer.deserialize(resp.content, format=resp['Content-Type'])
def serialize(self, data, format='application/json'):
"""
Given a Python datastructure (typically a ``dict``) & a desired content-type,
this method will return a serialized string of that data.
"""
return self.serializer.serialize(data, format=format)
def assertKeys(self, data, expected):
"""
This method ensures that the keys of the ``data`` match up to the keys of
``expected``.
It covers the (extremely) common case where you want to make sure the keys of
a response match up to what is expected. This is typically less fragile than
testing the full structure, which can be prone to data changes.
"""
self.assertEqual(sorted(data.keys()), sorted(expected))

View file

@ -1,130 +0,0 @@
from __future__ import unicode_literals
import time
from django.core.cache import cache
class BaseThrottle(object):
"""
A simplified, swappable base class for throttling.
Does nothing save for simulating the throttling API and implementing
some common bits for the subclasses.
Accepts a number of optional kwargs::
* ``throttle_at`` - the number of requests at which the user should
be throttled. Default is 150 requests.
* ``timeframe`` - the length of time (in seconds) in which the user
make up to the ``throttle_at`` requests. Default is 3600 seconds (
1 hour).
* ``expiration`` - the length of time to retain the times the user
has accessed the api in the cache. Default is 604800 (1 week).
"""
def __init__(self, throttle_at=150, timeframe=3600, expiration=None):
self.throttle_at = throttle_at
# In seconds, please.
self.timeframe = timeframe
if expiration is None:
# Expire in a week.
expiration = 604800
self.expiration = int(expiration)
def convert_identifier_to_key(self, identifier):
"""
Takes an identifier (like a username or IP address) and converts it
into a key usable by the cache system.
"""
bits = []
for char in identifier:
if char.isalnum() or char in ['_', '.', '-']:
bits.append(char)
safe_string = ''.join(bits)
return "%s_accesses" % safe_string
def should_be_throttled(self, identifier, **kwargs):
"""
Returns whether or not the user has exceeded their throttle limit.
Always returns ``False``, as this implementation does not actually
throttle the user.
"""
return False
def accessed(self, identifier, **kwargs):
"""
Handles recording the user's access.
Does nothing in this implementation.
"""
pass
class CacheThrottle(BaseThrottle):
"""
A throttling mechanism that uses just the cache.
"""
def should_be_throttled(self, identifier, **kwargs):
"""
Returns whether or not the user has exceeded their throttle limit.
Maintains a list of timestamps when the user accessed the api within
the cache.
Returns ``False`` if the user should NOT be throttled or ``True`` if
the user should be throttled.
"""
key = self.convert_identifier_to_key(identifier)
# Weed out anything older than the timeframe.
minimum_time = int(time.time()) - int(self.timeframe)
times_accessed = [access for access in cache.get(key, []) if access >= minimum_time]
cache.set(key, times_accessed, self.expiration)
if len(times_accessed) >= int(self.throttle_at):
# Throttle them.
return True
# Let them through.
return False
def accessed(self, identifier, **kwargs):
"""
Handles recording the user's access.
Stores the current timestamp in the "accesses" list within the cache.
"""
key = self.convert_identifier_to_key(identifier)
times_accessed = cache.get(key, [])
times_accessed.append(int(time.time()))
cache.set(key, times_accessed, self.expiration)
class CacheDBThrottle(CacheThrottle):
"""
A throttling mechanism that uses the cache for actual throttling but
writes-through to the database.
This is useful for tracking/aggregating usage through time, to possibly
build a statistics interface or a billing mechanism.
"""
def accessed(self, identifier, **kwargs):
"""
Handles recording the user's access.
Does everything the ``CacheThrottle`` class does, plus logs the
access within the database using the ``ApiAccess`` model.
"""
# Do the import here, instead of top-level, so that the model is
# only required when using this throttling mechanism.
from tastypie.models import ApiAccess
super(CacheDBThrottle, self).accessed(identifier, **kwargs)
# Write out the access to the DB for logging purposes.
ApiAccess.objects.create(
identifier=identifier,
url=kwargs.get('url', ''),
request_method=kwargs.get('request_method', '')
)

View file

@ -1,5 +0,0 @@
from tastypie.utils.dict import dict_strip_unicode_keys
from tastypie.utils.formatting import mk_datetime, format_datetime, format_date, format_time
from tastypie.utils.urls import trailing_slash
from tastypie.utils.validate_jsonp import is_valid_jsonp_callback_value
from tastypie.utils.timezone import now, make_aware, make_naive, aware_date, aware_datetime

View file

@ -1,19 +0,0 @@
from django.utils.encoding import smart_bytes
from django.utils import six
def dict_strip_unicode_keys(uni_dict):
"""
Converts a dict of unicode keys into a dict of ascii keys.
Useful for converting a dict to a kwarg-able format.
"""
if six.PY3:
return uni_dict
data = {}
for key, value in uni_dict.items():
data[smart_bytes(key)] = value
return data

View file

@ -1,37 +0,0 @@
from __future__ import unicode_literals
import email
import datetime
import time
from django.utils import dateformat
from tastypie.utils.timezone import make_aware, make_naive, aware_datetime
# Try to use dateutil for maximum date-parsing niceness. Fall back to
# hard-coded RFC2822 parsing if that's not possible.
try:
from dateutil.parser import parse as mk_datetime
except ImportError:
def mk_datetime(string):
return make_aware(datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string))))
def format_datetime(dt):
"""
RFC 2822 datetime formatter
"""
return dateformat.format(make_naive(dt), 'r')
def format_date(d):
"""
RFC 2822 date formatter
"""
# workaround because Django's dateformat utility requires a datetime
# object (not just date)
dt = aware_datetime(d.year, d.month, d.day, 0, 0, 0)
return dateformat.format(dt, 'j M Y')
def format_time(t):
"""
RFC 2822 time formatter
"""
# again, workaround dateformat input requirement
dt = aware_datetime(2000, 1, 1, t.hour, t.minute, t.second)
return dateformat.format(dt, 'H:i:s O')

View file

@ -1,63 +0,0 @@
from __future__ import unicode_literals
import mimeparse
from tastypie.exceptions import BadRequest
def determine_format(request, serializer, default_format='application/json'):
"""
Tries to "smartly" determine which output format is desired.
First attempts to find a ``format`` override from the request and supplies
that if found.
If no request format was demanded, it falls back to ``mimeparse`` and the
``Accepts`` header, allowing specification that way.
If still no format is found, returns the ``default_format`` (which defaults
to ``application/json`` if not provided).
NOTE: callers *must* be prepared to handle BadRequest exceptions due to
malformed HTTP request headers!
"""
# First, check if they forced the format.
if request.GET.get('format'):
if request.GET['format'] in serializer.formats:
return serializer.get_mime_for_format(request.GET['format'])
# If callback parameter is present, use JSONP.
if 'callback' in request.GET:
return serializer.get_mime_for_format('jsonp')
# Try to fallback on the Accepts header.
if request.META.get('HTTP_ACCEPT', '*/*') != '*/*':
formats = list(serializer.supported_formats) or []
# Reverse the list, because mimeparse is weird like that. See also
# https://github.com/toastdriven/django-tastypie/issues#issue/12 for
# more information.
formats.reverse()
try:
best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT'])
except ValueError:
raise BadRequest('Invalid Accept header')
if best_format:
return best_format
# No valid 'Accept' header/formats. Sane default.
return default_format
def build_content_type(format, encoding='utf-8'):
"""
Appends character encoding to the provided format if not already present.
"""
if 'charset' in format:
return format
if format in ('application/json', 'text/javascript'):
return format
return "%s; charset=%s" % (format, encoding)

View file

@ -1,37 +0,0 @@
from __future__ import unicode_literals
import datetime
from django.conf import settings
try:
from django.utils import timezone
def make_aware(value):
if getattr(settings, "USE_TZ", False) and timezone.is_naive(value):
default_tz = timezone.get_default_timezone()
value = timezone.make_aware(value, default_tz)
return value
def make_naive(value):
if getattr(settings, "USE_TZ", False) and timezone.is_aware(value):
default_tz = timezone.get_default_timezone()
value = timezone.make_naive(value, default_tz)
return value
def now():
d = timezone.now()
if d.tzinfo:
return timezone.localtime(timezone.now())
return d
except ImportError:
now = datetime.datetime.now
make_aware = make_naive = lambda x: x
def aware_date(*args, **kwargs):
return make_aware(datetime.date(*args, **kwargs))
def aware_datetime(*args, **kwargs):
return make_aware(datetime.datetime(*args, **kwargs))

View file

@ -1,9 +0,0 @@
from __future__ import unicode_literals
from django.conf import settings
def trailing_slash():
if getattr(settings, 'TASTYPIE_ALLOW_MISSING_SLASH', False):
return '/?'
return '/'

View file

@ -1,211 +0,0 @@
# -*- coding: utf-8 -*-
# Placed into the Public Domain by tav <tav@espians.com>
# Modified for Python 3 compatibility.
"""Validate Javascript Identifiers for use as JSON-P callback parameters."""
from __future__ import unicode_literals
import re
from unicodedata import category
from django.utils import six
# ------------------------------------------------------------------------------
# javascript identifier unicode categories and "exceptional" chars
# ------------------------------------------------------------------------------
valid_jsid_categories_start = frozenset([
'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl'
])
valid_jsid_categories = frozenset([
'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc'
])
valid_jsid_chars = ('$', '_')
# ------------------------------------------------------------------------------
# regex to find array[index] patterns
# ------------------------------------------------------------------------------
array_index_regex = re.compile(r'\[[0-9]+\]$')
has_valid_array_index = array_index_regex.search
replace_array_index = array_index_regex.sub
# ------------------------------------------------------------------------------
# javascript reserved words -- including keywords and null/boolean literals
# ------------------------------------------------------------------------------
is_reserved_js_word = frozenset([
'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double',
'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float',
'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof',
'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private',
'protected', 'public', 'return', 'short', 'static', 'super', 'switch',
'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try',
'typeof', 'var', 'void', 'volatile', 'while', 'with',
# potentially reserved in a future version of the ES5 standard
# 'let', 'yield'
]).__contains__
# ------------------------------------------------------------------------------
# the core validation functions
# ------------------------------------------------------------------------------
def is_valid_javascript_identifier(identifier, escape=r'\\u', ucd_cat=category):
"""Return whether the given ``id`` is a valid Javascript identifier."""
if not identifier:
return False
if not isinstance(identifier, six.text_type):
try:
identifier = six.text_type(identifier, 'utf-8')
except UnicodeDecodeError:
return False
if escape in identifier:
new = []; add_char = new.append
split_id = identifier.split(escape)
add_char(split_id.pop(0))
for segment in split_id:
if len(segment) < 4:
return False
try:
add_char(unichr(int('0x' + segment[:4], 16)))
except Exception:
return False
add_char(segment[4:])
identifier = u''.join(new)
if is_reserved_js_word(identifier):
return False
first_char = identifier[0]
if not ((first_char in valid_jsid_chars) or
(ucd_cat(first_char) in valid_jsid_categories_start)):
return False
for char in identifier[1:]:
if not ((char in valid_jsid_chars) or
(ucd_cat(char) in valid_jsid_categories)):
return False
return True
def is_valid_jsonp_callback_value(value):
"""Return whether the given ``value`` can be used as a JSON-P callback."""
for identifier in value.split(u'.'):
while '[' in identifier:
if not has_valid_array_index(identifier):
return False
identifier = replace_array_index(u'', identifier)
if not is_valid_javascript_identifier(identifier):
return False
return True
# ------------------------------------------------------------------------------
# test
# ------------------------------------------------------------------------------
def test():
"""
The function ``is_valid_javascript_identifier`` validates a given identifier
according to the latest draft of the ECMAScript 5 Specification:
>>> is_valid_javascript_identifier('hello')
True
>>> is_valid_javascript_identifier('alert()')
False
>>> is_valid_javascript_identifier('a-b')
False
>>> is_valid_javascript_identifier('23foo')
False
>>> is_valid_javascript_identifier('foo23')
True
>>> is_valid_javascript_identifier('$210')
True
>>> is_valid_javascript_identifier(u'Stra\u00dfe')
True
>>> is_valid_javascript_identifier(r'\u0062') # u'b'
True
>>> is_valid_javascript_identifier(r'\u0020')
False
>>> is_valid_javascript_identifier('_bar')
True
>>> is_valid_javascript_identifier('some_var')
True
>>> is_valid_javascript_identifier('$')
True
But ``is_valid_jsonp_callback_value`` is the function you want to use for
validating JSON-P callback parameter values:
>>> is_valid_jsonp_callback_value('somevar')
True
>>> is_valid_jsonp_callback_value('function')
False
>>> is_valid_jsonp_callback_value(' somevar')
False
It supports the possibility of '.' being present in the callback name, e.g.
>>> is_valid_jsonp_callback_value('$.ajaxHandler')
True
>>> is_valid_jsonp_callback_value('$.23')
False
As well as the pattern of providing an array index lookup, e.g.
>>> is_valid_jsonp_callback_value('array_of_functions[42]')
True
>>> is_valid_jsonp_callback_value('array_of_functions[42][1]')
True
>>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')
True
>>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]')
False
>>> is_valid_jsonp_callback_value('array_of_functions[]')
False
>>> is_valid_jsonp_callback_value('array_of_functions["key"]')
False
Enjoy!
"""
if __name__ == '__main__':
import doctest
doctest.testmod()

View file

@ -1,110 +0,0 @@
from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.forms import ModelForm
from django.forms.models import model_to_dict
class Validation(object):
"""
A basic validation stub that does no validation.
"""
def __init__(self, **kwargs):
pass
def is_valid(self, bundle, request=None):
"""
Performs a check on the data within the bundle (and optionally the
request) to ensure it is valid.
Should return a dictionary of error messages. If the dictionary has
zero items, the data is considered valid. If there are errors, keys
in the dictionary should be field names and the values should be a list
of errors, even if there is only one.
"""
return {}
class FormValidation(Validation):
"""
A validation class that uses a Django ``Form`` to validate the data.
This class **DOES NOT** alter the data sent, only verifies it. If you
want to alter the data, please use the ``CleanedDataFormValidation`` class
instead.
This class requires a ``form_class`` argument, which should be a Django
``Form`` (or ``ModelForm``, though ``save`` will never be called) class.
This form will be used to validate the data in ``bundle.data``.
"""
def __init__(self, **kwargs):
if not 'form_class' in kwargs:
raise ImproperlyConfigured("You must provide a 'form_class' to 'FormValidation' classes.")
self.form_class = kwargs.pop('form_class')
super(FormValidation, self).__init__(**kwargs)
def form_args(self, bundle):
data = bundle.data
# Ensure we get a bound Form, regardless of the state of the bundle.
if data is None:
data = {}
kwargs = {'data': {}}
if hasattr(bundle.obj, 'pk'):
if issubclass(self.form_class, ModelForm):
kwargs['instance'] = bundle.obj
kwargs['data'] = model_to_dict(bundle.obj)
kwargs['data'].update(data)
return kwargs
def is_valid(self, bundle, request=None):
"""
Performs a check on ``bundle.data``to ensure it is valid.
If the form is valid, an empty list (all valid) will be returned. If
not, a list of errors will be returned.
"""
form = self.form_class(**self.form_args(bundle))
if form.is_valid():
return {}
# The data is invalid. Let's collect all the error messages & return
# them.
return form.errors
class CleanedDataFormValidation(FormValidation):
"""
A validation class that uses a Django ``Form`` to validate the data.
This class **ALTERS** data sent by the user!!!
This class requires a ``form_class`` argument, which should be a Django
``Form`` (or ``ModelForm``, though ``save`` will never be called) class.
This form will be used to validate the data in ``bundle.data``.
"""
def is_valid(self, bundle, request=None):
"""
Checks ``bundle.data``to ensure it is valid & replaces it with the
cleaned results.
If the form is valid, an empty list (all valid) will be returned. If
not, a list of errors will be returned.
"""
form = self.form_class(**self.form_args(bundle))
if form.is_valid():
# We're different here & relying on having a reference to the same
# bundle the rest of the process is using.
bundle.data = form.cleaned_data
return {}
# The data is invalid. Let's collect all the error messages & return
# them.
return form.errors