1. Miran Levar
  2. graph-viewer

Commits

Miran Levar  committed ceba36e

Added tastypie

  • Participants
  • Parent commits 91edd2e
  • Branches default

Comments (0)

Files changed (31)

File tastypie/__init__.py

View file
+__author__ = 'Daniel Lindsley, Cody Soyland, Matt Croydon'
+__version__ = (0, 9, 11)

File tastypie/admin.py

View file
+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

File tastypie/api.py

View file
+import warnings
+from django.conf.urls.defaults import *
+from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from tastypie.exceptions import NotRegistered, BadRequest
+from tastypie.serializers import Serializer
+from tastypie.utils import trailing_slash, is_valid_jsonp_callback_value
+from tastypie.utils.mime import determine_format, build_content_type
+
+
+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"):
+        self.api_name = api_name
+        self._registry = {}
+        self._canonicals = {}
+
+    def register(self, resource, canonical=True):
+        """
+        Registers an instance of a ``Resource`` subclass with the API.
+
+        Optionally accept a ``canonical`` argument, which indicates that the
+        resource being registered is the canonical variant. Defaults to
+        ``True``.
+        """
+        resource_name = getattr(resource._meta, 'resource_name', None)
+
+        if resource_name is None:
+            raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource)
+
+        self._registry[resource_name] = resource
+
+        if canonical is True:
+            if resource_name in self._canonicals:
+                warnings.warn("A new resource '%r' is replacing the existing canonical URL for '%s'." % (resource, resource_name), Warning, stacklevel=2)
+
+            self._canonicals[resource_name] = resource
+            # TODO: This is messy, but makes URI resolution on FK/M2M fields
+            #       work consistently.
+            resource._meta.api_name = self.api_name
+            resource.__class__.Meta.api_name = self.api_name
+
+    def unregister(self, resource_name):
+        """
+        If present, unregisters a resource from the API.
+        """
+        if resource_name in self._registry:
+            del(self._registry[resource_name])
+
+        if resource_name in self._canonicals:
+            del(self._canonicals[resource_name])
+
+    def canonical_resource_for(self, resource_name):
+        """
+        Returns the canonical resource for a given ``resource_name``.
+        """
+        if resource_name in self._canonicals:
+            return self._canonicals[resource_name]
+
+        raise NotRegistered("No resource was registered as canonical for '%s'." % resource_name)
+
+    def wrap_view(self, view):
+        def wrapper(request, *args, **kwargs):
+            return getattr(self, view)(request, *args, **kwargs)
+        return wrapper
+
+    def override_urls(self):
+        """
+        A hook for adding your own URLs or overriding the default URLs.
+        """
+        return []
+
+    @property
+    def urls(self):
+        """
+        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.override_urls() + 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.
+        """
+        serializer = Serializer()
+        available_resources = {}
+
+        if api_name is None:
+            api_name = self.api_name
+
+        for name in sorted(self._registry.keys()):
+            available_resources[name] = {
+                'list_endpoint': self._build_reverse_url("api_dispatch_list", kwargs={
+                    'api_name': api_name,
+                    'resource_name': name,
+                }),
+                'schema': self._build_reverse_url("api_get_schema", kwargs={
+                    'api_name': api_name,
+                    'resource_name': name,
+                }),
+            }
+
+        desired_format = determine_format(request, serializer)
+        options = {}
+
+        if 'text/javascript' in desired_format:
+            callback = request.GET.get('callback', 'callback')
+
+            if not is_valid_jsonp_callback_value(callback):
+                raise BadRequest('JSONP callback name is invalid.')
+
+            options['callback'] = callback
+
+        serialized = serializer.serialize(available_resources, desired_format, options)
+        return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
+
+    def _build_reverse_url(self, name, args=None, kwargs=None):
+        """
+        A convenience hook for overriding how URLs are built.
+
+        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):
+        super(NamespacedApi, self).__init__(api_name=api_name)
+        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)

File tastypie/authentication.py

View file
+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.utils.translation import ugettext as _
+from tastypie.http import HttpUnauthorized
+
+try:
+    from hashlib import sha1
+except ImportError:
+    import sha
+    sha1 = sha.sha
+
+try:
+    import python_digest
+except ImportError:
+    python_digest = None
+
+try:
+    import oauth2
+except ImportError:
+    oauth2 = None
+
+try:
+    import oauth_provider
+except ImportError:
+    oauth_provider = None
+
+
+class Authentication(object):
+    """
+    A simple base class to establish the protocol for auth.
+
+    By default, this indicates the user is always authenticated.
+    """
+    def is_authenticated(self, request, **kwargs):
+        """
+        Identifies if the user is authenticated to continue or not.
+
+        Should return either ``True`` if allowed, ``False`` if not or an
+        ``HttpResponse`` if you need something custom.
+        """
+        return True
+
+    def get_identifier(self, request):
+        """
+        Provides a unique string identifier for the requestor.
+
+        This implementation returns a combination of IP address and hostname.
+        """
+        return "%s_%s" % (request.META.get('REMOTE_ADDR', 'noaddr'), request.META.get('REMOTE_HOST', 'nohost'))
+
+
+class BasicAuthentication(Authentication):
+    """
+    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'):
+        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 != 'Basic':
+                return self._unauthorized()
+            user_pass = base64.b64decode(data)
+        except:
+            return self._unauthorized()
+
+        bits = user_pass.split(':', 1)
+
+        if len(bits) != 2:
+            return self._unauthorized()
+
+        if self.backend:
+            user = self.backend.authenticate(username=bits[0], password=bits[1])
+        else:
+            user = authenticate(username=bits[0], password=bits[1])
+
+        if user is None:
+            return self._unauthorized()
+
+        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 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.
+        """
+        from django.contrib.auth.models import User
+
+        username = request.GET.get('username') or request.POST.get('username')
+        api_key = request.GET.get('api_key') or request.POST.get('api_key')
+
+        if not username or not api_key:
+            return self._unauthorized()
+
+        try:
+            user = User.objects.get(username=username)
+        except (User.DoesNotExist, User.MultipleObjectsReturned):
+            return self._unauthorized()
+
+        request.user = user
+        return self.get_key(user, api_key)
+
+    def get_key(self, user, api_key):
+        """
+        Attempts to find the API key for the user. Uses ``ApiKey`` by default
+        but can be overridden.
+        """
+        from tastypie.models import ApiKey
+
+        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.
+        """
+        return request.REQUEST.get('username', 'nouser')
+
+
+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'):
+        self.backend = backend
+        self.realm = realm
+
+        if python_digest is None:
+            raise ImproperlyConfigured("The 'python_digest' package could not be imported. It is required for use with the 'DigestAuthentication' class.")
+
+    def _unauthorized(self):
+        response = HttpUnauthorized()
+        new_uuid = uuid.uuid4()
+        opaque = hmac.new(str(new_uuid), digestmod=sha1).hexdigest()
+        response['WWW-Authenticate'] = python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), self.realm, opaque, False)
+        return response
+
+    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 != 'Digest':
+                return self._unauthorized()
+        except:
+            return self._unauthorized()
+
+        digest_response = python_digest.parse_digest_credentials(request.META['HTTP_AUTHORIZATION'])
+
+        # FIXME: Should the nonce be per-user?
+        if not python_digest.validate_nonce(digest_response.nonce, getattr(settings, 'SECRET_KEY', '')):
+            return self._unauthorized()
+
+        user = self.get_user(digest_response.username)
+        api_key = self.get_key(user)
+
+        if user is False or api_key is False:
+            return self._unauthorized()
+
+        expected = python_digest.calculate_request_digest(
+            request.method,
+            python_digest.calculate_partial_digest(digest_response.username, self.realm, api_key),
+            digest_response)
+
+        if not digest_response.response == expected:
+            return self._unauthorized()
+
+        request.user = user
+        return True
+
+    def get_user(self, username):
+        from django.contrib.auth.models import User
+
+        try:
+            user = User.objects.get(username=username)
+        except (User.DoesNotExist, User.MultipleObjectsReturned):
+            return False
+
+        return user
+
+    def get_key(self, user):
+        """
+        Attempts to find the API key for the user. Uses ``ApiKey`` by default
+        but can be overridden.
+
+        Note that this behaves differently than the ``ApiKeyAuthentication``
+        method of the same name.
+        """
+        from tastypie.models import ApiKey
+
+        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):
+        super(OAuthAuthentication, self).__init__()
+
+        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, e:
+                return oauth_provider.utils.send_oauth_error(e)
+
+            if consumer and token:
+                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)

File tastypie/authorization.py

View file
+class Authorization(object):
+    """
+    A base class that provides no permissions checking.
+    """
+    def __get__(self, instance, owner):
+        """
+        Makes ``Authorization`` a descriptor of ``ResourceOptions`` and creates
+        a reference to the ``ResourceOptions`` object that may be used by
+        methods of ``Authorization``.
+        """
+        self.resource_meta = instance
+        return self
+
+    def is_authorized(self, request, object=None):
+        """
+        Checks if the user is authorized to perform the request. If ``object``
+        is provided, it can do additional row-level checks.
+
+        Should return either ``True`` if allowed, ``False`` if not or an
+        ``HttpResponse`` if you need something custom.
+        """
+        return True
+
+
+class ReadOnlyAuthorization(Authorization):
+    """
+    Default Authentication class for ``Resource`` objects.
+
+    Only allows GET requests.
+    """
+
+    def is_authorized(self, request, object=None):
+        """
+        Allow any ``GET`` request.
+        """
+        if request.method == 'GET':
+            return True
+        else:
+            return False
+
+
+class DjangoAuthorization(Authorization):
+    """
+    Uses permission checking from ``django.contrib.auth`` to map ``POST``,
+    ``PUT``, and ``DELETE`` to their equivalent django auth permissions.
+    """
+    def is_authorized(self, request, object=None):
+        # GET is always allowed
+        if request.method == 'GET':
+            return True
+
+        klass = self.resource_meta.object_class
+
+        # cannot check permissions if we don't know the model
+        if not klass or not getattr(klass, '_meta', None):
+            return True
+
+        permission_codes = {
+            'POST': '%s.add_%s',
+            'PUT': '%s.change_%s',
+            'DELETE': '%s.delete_%s',
+        }
+
+        # cannot map request method to permission code name
+        if request.method not in permission_codes:
+            return True
+
+        permission_code = permission_codes[request.method] % (
+            klass._meta.app_label,
+            klass._meta.module_name)
+
+        # user must be logged in to check permissions
+        # authentication backend must set request.user
+        if not hasattr(request, 'user'):
+            return False
+
+        return request.user.has_perm(permission_code)

File tastypie/bundle.py

View file
+from django.http import HttpRequest
+
+
+# In a separate file to avoid circular imports...
+class Bundle(object):
+    """
+    A small container for instances and converted data for the
+    ``dehydrate/hydrate`` cycle.
+
+    Necessary because the ``dehydrate/hydrate`` cycle needs to access data at
+    different points.
+    """
+    def __init__(self, obj=None, data=None, request=None, related_obj=None, related_name=None):
+        self.obj = obj
+        self.data = data or {}
+        self.request = request or HttpRequest()
+        self.related_obj = related_obj
+        self.related_name = related_name
+
+    def __repr__(self):
+        return "<Bundle for obj: '%s' and with data: '%s'>" % (self.obj, self.data)

File tastypie/cache.py

View file
+from django.core.cache import cache
+
+
+class NoCache(object):
+    """
+    A simplified, swappable base class for caching.
+    
+    Does nothing save for simulating the cache API.
+    """
+    def get(self, key):
+        """
+        Always returns ``None``.
+        """
+        return None
+    
+    def set(self, key, value, timeout=60):
+        """
+        No-op for setting values in the cache.
+        """
+        pass
+
+
+class SimpleCache(NoCache):
+    """
+    Uses Django's current ``CACHE_BACKEND`` to store cached data.
+    """
+    def get(self, key):
+        """
+        Gets a key from the cache. Returns ``None`` if the key is not found.
+        """
+        return cache.get(key)
+    
+    def set(self, key, value, timeout=60):
+        """
+        Sets a key-value in the cache.
+        
+        Optionally accepts a ``timeout`` in seconds. Defaults to ``60`` seconds.
+        """
+        cache.set(key, value, timeout)

File tastypie/constants.py

View file
+# 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

File tastypie/exceptions.py

View file
+from django.http import HttpResponse
+
+
+class TastypieError(Exception):
+    """A base exception for other tastypie-related errors."""
+    pass
+
+
+class HydrationError(TastypieError):
+    """Raised when there is an error hydrating data."""
+    pass
+
+
+class NotRegistered(TastypieError):
+    """
+    Raised when the requested resource isn't registered with the ``Api`` class.
+    """
+    pass
+
+
+class NotFound(TastypieError):
+    """
+    Raised when the resource/object in question can't be found.
+    """
+    pass
+
+
+class ApiFieldError(TastypieError):
+    """
+    Raised when there is a configuration error with a ``ApiField``.
+    """
+    pass
+
+
+class UnsupportedFormat(TastypieError):
+    """
+    Raised when an unsupported serialization format is requested.
+    """
+    pass
+
+
+class BadRequest(TastypieError):
+    """
+    A generalized exception for indicating incorrect request parameters.
+    
+    Handled specially in that the message tossed by this exception will be
+    presented to the end user.
+    """
+    pass
+
+
+class BlueberryFillingFound(TastypieError):
+    pass
+
+
+class InvalidFilterError(BadRequest):
+    """
+    Raised when the end user attempts to use a filter that has not be
+    explicitly allowed.
+    """
+    pass
+
+
+class InvalidSortError(TastypieError):
+    """
+    Raised when the end user attempts to sort on a field that has not be
+    explicitly allowed.
+    """
+    pass
+
+
+class ImmediateHttpResponse(TastypieError):
+    """
+    This exception is used to interrupt the flow of processing to immediately
+    return a custom HttpResponse.
+    
+    Common uses include::
+    
+        * for authentication (like digest/OAuth)
+        * for throttling
+    
+    """
+    response = HttpResponse("Nothing provided.")
+    
+    def __init__(self, response):
+        self.response = response

File tastypie/fields.py

View file
+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 tastypie.bundle import Bundle
+from tastypie.exceptions import ApiFieldError, NotFound
+from tastypie.utils import dict_strip_unicode_keys
+
+
+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):
+        """
+        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.
+        """
+        # 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
+
+        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):
+        """
+        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 bundle.data.has_key(self.instance_name):
+            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 unicode(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)
+
+
+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, basestring):
+            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 = 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, basestring):
+            match = DATETIME_REGEX.search(value)
+
+            if match:
+                data = match.groupdict()
+                return 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'):
+            try:
+                # Try to rip a date/datetime out of it.
+                value = parse(value)
+            except ValueError:
+                pass
+
+        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):
+        """
+        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.
+        """
+        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
+
+        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, basestring):
+            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):
+        """
+        Based on the ``full_resource``, returns either the endpoint or the data
+        from ``full_dehydrate`` for the related resource.
+        """
+        if not self.full:
+            # 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)
+            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
+
+        # We need to check to see if updates are allowed on the FK
+        # resource. If not, we'll just return a populated bundle instead
+        # of mistakenly updating something that should be read-only.
+        if not fk_resource.can_update():
+            return fk_resource.full_hydrate(fk_bundle)
+
+        try:
+            return fk_resource.obj_update(fk_bundle, **data)
+        except NotFound:
+            try:
+                # Attempt lookup by primary key
+                lookup_kwargs = dict((k, v) for k, v in data.iteritems() if getattr(fk_resource, k).unique)
+
+                if not lookup_kwargs:
+                    raise NotFound()
+
+                return fk_resource.obj_update(fk_bundle, **lookup_kwargs)
+            except NotFound:
+                return fk_resource.full_hydrate(fk_bundle)
+        except MultipleObjectsReturned:
+            return fk_resource.full_hydrate(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, basestring):
+            # 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 has was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: %s." % (self.instance_name, value))
+
+
+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):
+        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
+        )
+        self.fk_resource = None
+
+    def dehydrate(self, bundle):
+        try:
+            foreign_obj = getattr(bundle.obj, self.attribute)
+        except ObjectDoesNotExist:
+            foreign_obj = None
+
+        if not foreign_obj:
+            if not self.null:
+                raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (bundle.obj, self.attribute))
+
+            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)
+
+    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):
+        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
+        )
+        self.m2m_bundles = []
+
+    def dehydrate(self, bundle):
+        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
+
+        if isinstance(self.attribute, basestring):
+            the_m2ms = getattr(bundle.obj, self.attribute)
+        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." % (bundle.obj, self.attribute))
+
+            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))
+
+        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):
+        return self.convert(super(TimeField, self).dehydrate(obj))
+
+    def convert(self, value):
+        if isinstance(value, basestring):
+            return self.to_time(value)
+        return value
+
+    def to_time(self, s):
+        try:
+            dt = parse(s)
+        except ValueError, 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

File tastypie/http.py

View file
+"""
+The various HTTP responses for use in returning proper HTTP codes.
+"""
+from django.http import HttpResponse
+
+
+class HttpCreated(HttpResponse):
+    status_code = 201
+    
+    def __init__(self, *args, **kwargs):
+        location = ''
+
+        if 'location' in kwargs:
+            location = kwargs['location']
+            del(kwargs['location'])
+        
+        super(HttpCreated, self).__init__(*args, **kwargs)
+        self['Location'] = location
+
+
+class HttpAccepted(HttpResponse):
+    status_code = 202
+
+
+class HttpNoContent(HttpResponse):
+    status_code = 204
+
+
+class HttpMultipleChoices(HttpResponse):
+    status_code = 300
+
+
+class HttpSeeOther(HttpResponse):
+    status_code = 303
+
+
+class HttpNotModified(HttpResponse):
+    status_code = 304
+
+
+class HttpBadRequest(HttpResponse):
+    status_code = 400
+
+
+class HttpUnauthorized(HttpResponse):
+    status_code = 401
+
+
+class HttpForbidden(HttpResponse):
+    status_code = 403
+
+
+class HttpNotFound(HttpResponse):
+    status_code = 404
+
+
+class HttpMethodNotAllowed(HttpResponse):
+    status_code = 405
+
+
+class HttpConflict(HttpResponse):
+    status_code = 409
+
+
+class HttpGone(HttpResponse):
+    status_code = 410
+
+
+class HttpApplicationError(HttpResponse):
+    status_code = 500
+
+
+class HttpNotImplemented(HttpResponse):
+    status_code = 501
+

File tastypie/management/__init__.py

Empty file added.

File tastypie/management/commands/__init__.py

Empty file added.

File tastypie/management/commands/backfill_api_keys.py

View file
+from django.contrib.auth.models import User
+from django.core.management.base import NoArgsCommand
+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))
+        
+        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

File tastypie/migrations/0001_initial.py

View file
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+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'])),
+            ('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': {
+            'Meta': {'object_name': 'User'},
+            '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['auth.User']"})
+        }
+    }
+
+    complete_apps = ['tastypie']

File tastypie/migrations/__init__.py

Empty file added.

File tastypie/models.py

View file
+import datetime
+import hmac
+import time
+from django.conf import settings
+from django.db import models
+try:
+    from hashlib import sha1
+except ImportError:
+    import sha
+    sha1 = sha.sha
+
+
+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 __unicode__(self):
+        return u"%s @ %s" % (self.identifer, 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 django.conf import settings
+    from django.contrib.auth.models import User
+    
+    class ApiKey(models.Model):
+        user = models.OneToOneField(User, related_name='api_key')
+        key = models.CharField(max_length=256, blank=True, default='')
+        created = models.DateTimeField(default=datetime.datetime.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(str(new_uuid), digestmod=sha1).hexdigest()
+    
+    
+    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'))

File tastypie/paginator.py

View file
+from django.conf import settings
+from tastypie.exceptions import BadRequest
+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):
+        """
+        Instantiates the ``Paginator`` and allows for some configuration.
+        
+        The ``request_data`` argument ought to be a dictionary-like object.
+        May provide ``limit`` and/or ``offset`` to override the defaults.
+        Commonly provided ``request.GET``. Required.
+        
+        The ``objects`` should be a list-like object of ``Resources``.
+        This is typically a ``QuerySet`` but can be anything that
+        implements slicing. Required.
+        
+        Optionally accepts a ``limit`` argument, which specifies how many
+        items to show at a time. Defaults to ``None``, which is no limit.
+        
+        Optionally accepts an ``offset`` argument, which specifies where in
+        the ``objects`` to start displaying results from. Defaults to 0.
+        """
+        self.request_data = request_data
+        self.objects = objects
+        self.limit = limit
+        self.offset = offset
+        self.resource_uri = resource_uri
+    
+    def get_limit(self):
+        """
+        Determines the proper maximum number of results to return.
+        
+        In order of importance, it will use:
+        
+            * The user-requested ``limit`` from the GET parameters, if specified.
+            * The object-level ``limit`` if specified.
+            * ``settings.API_LIMIT_PER_PAGE`` if specified.
+        
+        Default is 20 per page.
+        """
+        limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
+        
+        if 'limit' in self.request_data:
+            limit = self.request_data['limit']
+        elif self.limit is not None:
+            limit = self.limit
+        
+        try:
+            limit = int(limit)
+        except ValueError:
+            raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer.")
+        
+        if limit < 0:
+            raise BadRequest("Invalid limit '%s' provided. Please provide an integer >= 0.")
+        
+        return limit
+    
+    def get_offset(self):
+        """
+        Determines the proper starting offset of results to return.
+        
+        It attempst to use the user-provided ``offset`` from the GET parameters,
+        if specified. Otherwise, it falls back to the object-level ``offset``.
+        
+        Default is 0.
+        """
+        offset = self.offset
+        
+        if 'offset' in self.request_data:
+            offset = self.request_data['offset']
+        
+        try:
+            offset = int(offset)
+        except ValueError:
+            raise BadRequest("Invalid offset '%s' provided. Please provide an integer.")
+        
+        if offset < 0:
+            raise BadRequest("Invalid offset '%s' provided. Please provide an integer >= 0.")
+        
+        return offset
+    
+    def get_slice(self, limit, offset):
+        """
+        Slices the result set to the specified ``limit`` & ``offset``.
+        """
+        # If it's zero, return everything.
+        if limit == 0:
+            return self.objects[offset:]
+        
+        return self.objects[offset:offset + limit]
+    
+    def get_count(self):
+        """
+        Returns a count of the total number of objects seen.
+        """
+        try:
+            return self.objects.count()
+        except (AttributeError, TypeError):
+            # If it's not a QuerySet (or it's ilk), fallback to ``len``.
+            return len(self.objects)
+
+    def get_previous(self, limit, offset):
+        """
+        If a previous page is available, will generate a URL to request that
+        page. If not available, this returns ``None``.
+        """
+        if offset - limit < 0:
+            return None
+        
+        return self._generate_uri(limit, offset-limit)
+
+    def get_next(self, limit, offset, count):
+        """
+        If a next page is available, will generate a URL to request that
+        page. If not available, this returns ``None``.
+        """
+        if offset + limit >= count:
+            return None
+        
+        return self._generate_uri(limit, offset+limit)
+
+    def _generate_uri(self, limit, offset):
+        if self.resource_uri is None:
+            return None
+        
+        request_params = dict([k, v.encode('utf-8')] for k, v in self.request_data.items())
+        request_params.update({'limit': limit, 'offset': offset})
+        return '%s?%s' % (
+            self.resource_uri,
+            urlencode(request_params)
+        )
+
+    def page(self):
+        """
+        Generates all pertinent data about the requested page.
+        
+        Handles getting the correct ``limit`` & ``offset``, then slices off
+        the correct set of results and returns all pertinent metadata.
+        """
+        limit = self.get_limit()
+        offset = self.get_offset()
+        count = self.get_count()
+        objects = self.get_slice(limit, offset)
+        meta = {
+            'offset': offset,
+            'limit': limit,
+            'total_count': count,
+        }
+        
+        if limit:
+            meta['previous'] = self.get_previous(limit, offset)
+            meta['next'] = self.get_next(limit, offset, count)
+
+        return {
+            'objects': objects,
+            'meta': meta,
+        }

File tastypie/resources.py

View file
+import logging
+import warnings
+import django
+from django.conf import settings
+from django.conf.urls.defaults import patterns, url
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError
+from django.core.urlresolvers import NoReverseMatch, reverse, resolve, Resolver404, get_script_prefix
+from django.db import transaction
+from django.db.models.sql.constants import QUERY_TERMS, LOOKUP_SEP
+from django.http import HttpResponse, HttpResponseNotFound
+from django.utils.cache import patch_cache_control
+from tastypie.authentication import Authentication
+from tastypie.authorization import ReadOnlyAuthorization
+from tastypie.bundle import Bundle
+from tastypie.cache import NoCache
+from tastypie.constants import ALL, ALL_WITH_RELATIONS
+from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse
+from tastypie import fields
+from tastypie import http
+from tastypie.paginator import Paginator
+from tastypie.serializers import Serializer
+from tastypie.throttle import BaseThrottle
+from tastypie.utils import is_valid_jsonp_callback_value, dict_strip_unicode_keys, trailing_slash
+from tastypie.utils.mime import determine_format, build_content_type
+from tastypie.validation import Validation
+try:
+    set
+except NameError:
+    from sets import Set as set
+# The ``copy`` module became function-friendly in Python 2.5 and
+# ``copycompat`` was added in post 1.1.1 Django (r11901)..
+try:
+    from django.utils.copycompat import deepcopy
+except ImportError:
+    from copy import deepcopy
+# If ``csrf_exempt`` isn't present, stub it.
+try:
+    from django.views.decorators.csrf import csrf_exempt
+except ImportError:
+    def csrf_exempt(func):
+        return func
+
+
+class NOT_AVAILABLE:
+    def __str__(self):
+        return 'No such data is available.'
+
+
+class ResourceOptions(object):
+    """
+    A configuration class for ``Resource``.
+
+    Provides sane defaults and the logic needed to augment these settings with
+    the internal ``class Meta`` used on ``Resource`` subclasses.
+    """
+    serializer = Serializer()
+    authentication = Authentication()
+    authorization = ReadOnlyAuthorization()
+    cache = NoCache()
+    throttle = BaseThrottle()
+    validation = Validation()
+    paginator_class = Paginator
+    allowed_methods = ['get', 'post', 'put', 'delete', 'patch']
+    list_allowed_methods = None
+    detail_allowed_methods = None
+    limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
+    api_name = None
+    resource_name = None
+    urlconf_namespace = None
+    default_format = 'application/json'
+    filtering = {}
+    ordering = []
+    object_class = None
+    queryset = None
+    fields = []
+    excludes = []
+    include_resource_uri = True