185 lines
6.6 KiB
Python
185 lines
6.6 KiB
Python
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)
|