Table Of Contents¶
Welcome to Tastypie!¶
Tastypie is an webservice API framework for Django. It provides a convenient, yet powerful and highly customizable, abstraction for creating REST-style interfaces.
Getting Started with Tastypie¶
Tastypie is a reusable app (that is, it relies only on it’s own code and focuses on providing just a REST-style API) and is suitable for providing an API to any application without having to modify the sources of that app.
Not everyone’s needs are the same, so Tastypie goes out of its way to provide plenty of hooks for overriding or extending how it works.
Note
If you hit a stumbling block, you can join #tastypie on irc.freenode.net to get help.
This tutorial assumes that you have a basic understanding of Django as well as how proper REST-style APIs ought to work. We will only explain the portions of the code that are Tastypie-specific in any kind of depth.
For example purposes, we’ll be adding an API to a simple blog application.
Here is myapp/models.py
:
import datetime
from django.contrib.auth.models import User
from django.db import models
from django.template.defaultfilters import slugify
class Entry(models.Model):
user = models.ForeignKey(User)
pub_date = models.DateTimeField(default=datetime.datetime.now)
title = models.CharField(max_length=200)
slug = models.SlugField()
body = models.TextField()
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
# For automatic slug generation.
if not self.slug:
self.slug = slugify(self.title)[:50]
return super(Entry, self).save(*args, **kwargs)
With that, we’ll move on to installing and configuring Tastypie.
Installation¶
Installing Tastypie is as simple as checking out the source and adding it to
your project or PYTHONPATH
.
- Download the dependencies:
- Python 2.4+
- Django 1.0+ (tested on Django 1.1+)
mimeparse
0.1.3+ (http://code.google.com/p/mimeparse/)
- Older versions will work, but their behavior on JSON/JSONP is a touch wonky.
dateutil
(http://labix.org/python-dateutil)- OPTIONAL -
lxml
(http://codespeak.net/lxml/) if using the XML serializer- OPTIONAL -
pyyaml
(http://pyyaml.org/) if using the YAML serializer- OPTIONAL -
uuid
(present in 2.5+, downloadable from http://pypi.python.org/pypi/uuid/) if using theApiKey
authentication
Configuration¶
The only mandatory configuration is adding 'tastypie'
to your
INSTALLED_APPS
. This isn’t strictly necessary, as Tastypie has only one
non-required model, but may ease usage.
You have the option to set up a number of settings (see Tastypie Settings) but they all have sane defaults and are not required unless you need to tweak their values.
Creating Resources¶
REST-style architecture talks about resources, so unsurprisingly integrating
with Tastypie involves creating Resource
classes.
For our simple application, we’ll create a file for these in myapp/api.py
,
though they can live anywhere in your application:
# myapp/api.py
from tastypie.resources import ModelResource
from myapp.models import Entry
class EntryResource(ModelResource):
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
This class, by virtue of being a ModelResource
subclass, will introspect all non-relational fields on the Entry
model and
create it’s own ApiFields
that map to those fields,
much like the way Django’s ModelForm
class introspects.
Note
The resource_name
within the Meta
class is optional. If not
provided, it is automatically generated off the classname, removing any
instances of Resource
and lowercasing the string. So
EntryResource
would become just entry
.
We’ve included the resource_name
attribute in this example for clarity,
especially when looking at the URLs, but you should feel free to omit it if
you’re comfortable with the automatic behavior.
Hooking Up The Resource(s)¶
Now that we have our EntryResource
, we can hook it up in our URLconf. To
do this, we simply instantiate the resource in our URLconf and hook up its
urls
:
# urls.py
from django.conf.urls.defaults import *
from myapp.api import EntryResource
entry_resource = EntryResource()
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(entry_resource.urls)),
)
Now it’s just a matter of firing up server (./manage.py runserver
) and
going to http://127.0.0.1:8000/api/entry/?format=json. You should get back a
list of Entry
-like objects.
Note
The ?format=json
is an override required to make things look decent
in the browser (accept headers vary between browsers). Tastypie properly
handles the Accept
header. So the following will work properly:
curl -H "Accept: application/json" http://127.0.0.1:8000/api/entry/
But if you’re sure you want something else (or want to test in a browser),
Tastypie lets you specify ?format=...
when you really want to force
a certain type.
At this point, a bunch of other URLs are also available. Try out any/all of the following (assuming you have at least three records in the database):
With just seven lines of code, we have a full working REST interface to our
Entry
model. In addition, full GET/POST/PUT/DELETE support is already
there, so it’s possible to really work with all of the data. Well, almost.
You see, you’ll note that not quite all of our data is there. Markedly absent
is the user
field, which is a ForeignKey
to Django’s User
model.
Tastypie does NOT introspect related data because it has no way to know
how you want to represent that data.
And since that relation isn’t there, any attempt to POST/PUT new data will
fail, because no user
is present, which is a required field on the model.
This is easy to fix, but we’ll need to flesh out our API a little more.
Creating More Resources¶
In order to handle our user
relation, we’ll need to create a
UserResource
and tell the EntryResource
to use it. So we’ll modify
myapp/api.py
to match the following code:
# myapp/api.py
from django.contrib.auth.models import User
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
We simply created a new ModelResource
subclass
called UserResource
. Then we added a field to EntryResource
that
specified that the user
field points to a UserResource
for that data.
Now we should be able to get all of the fields back in our response. But since we have another full, working resource on our hands, we should hook that up to our API as well. And there’s a better way to do it.
Adding To The Api¶
Tastypie ships with an Api
class, which lets you bind
multiple Resources
together to form a
coherent API. Adding it to the mix is simple.
We’ll go back to our URLconf (urls.py
) and change it to match the
following:
# urls.py
from django.conf.urls.defaults import *
from tastypie.api import Api
from myapp.api import EntryResource, UserResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(v1_api.urls)),
)
Note that we’re now creating an Api
instance,
registering our EntryResource
and UserResource
instances with it and
that we’ve modified the urls to now point to v1_api.urls
.
This makes even more data accessible, so if we start up the runserver
again, the following URLs should work:
- http://127.0.0.1:8000/api/v1/?format=json
- http://127.0.0.1:8000/api/v1/user/?format=json
- http://127.0.0.1:8000/api/v1/user/1/?format=json
- http://127.0.0.1:8000/api/v1/user/schema/?format=json
- http://127.0.0.1:8000/api/v1/user/set/1;3/?format=json
- http://127.0.0.1:8000/api/v1/entry/?format=json
- http://127.0.0.1:8000/api/v1/entry/1/?format=json
- http://127.0.0.1:8000/api/v1/entry/schema/?format=json
- http://127.0.0.1:8000/api/v1/entry/set/1;3/?format=json
Additionally, the representations out of EntryResource
will now include
the user
field and point to an endpoint like /api/v1/users/1/
to access
that user’s data. And full POST/PUT delete support should now work.
But there’s several new problems. One is that our new UserResource
leaks
too much data, including fields like email
, password
, is_active
and
is_staff
. Another is that we may not want to allow end users to alter
User
data. Both of these problems are easily fixed as well.
Limiting Data And Access¶
Cutting out the email
, password
, is_active
and is_staff
fields
is easy to do. We simply modify our UserResource
code to match the
following:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
The excludes
directive tells UserResource
which fields not to include
in the output. If you’d rather whitelist fields, you could do:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
fields = ['username', 'first_name', 'last_name', 'last_login']
Now that the undesirable fields are no longer included, we can look at limiting
access. This is also easy and involves making our UserResource
look like:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
allowed_methods = ['get']
Now only HTTP GET requests will be allowed on /api/v1/user/
endpoints. If
you require more granular control, both list_allowed_methods
and
detail_allowed_methods
options are supported.
Beyond The Basics¶
We now have a full working API for our application. But Tastypie supports many more features, like:
- Authentication / Authorization
- Caching
- Throttling
- Resources (filtering & sorting)
- Serialization
Tastypie is also very easy to override and extend. For some common patterns and approaches, you should refer to the Tastypie Cookbook documentation.
Interacting With The API¶
Now that you’ve got a shiny new REST-style API in place, let’s demonstrate how to interact with it. We’ll assume that you have cURL installed on your system (generally available on most modern Mac & Linux machines), but any tool that allows you to control headers & bodies on requests will do.
We’ll assume that we’re interacting with the following Tastypie code:
# myapp/api/resources.py
from django.contrib.auth.models import User
from tastypie.authorization import Authorization
from tastypie import fields
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
filtering = {
'username': ALL,
}
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
authorization = Authorization()
filtering = {
'user': ALL_WITH_RELATIONS,
'pub_date': ['exact', 'lt', 'lte', 'gte', 'gt'],
}
# urls.py
from django.conf.urls.defaults import *
from tastypie.api import Api
from myapp.api.resources import EntryResource, UserResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(v1_api.urls)),
)
Let’s fire up a shell & start exploring the API!
Front Matter¶
Tastypie tries to treat all clients & all serialization types as equally as
possible. It also tries to be a good ‘Net citizen & respects the HTTP method
used as well as the Accepts
headers sent. Between these two, you control
all interactions with Tastypie through relatively few endpoints.
Warning
Should you try these URLs in your browser, be warned you WILL need to
append ?format=json
(or xml
or yaml
) to the URL. Your browser
requests application/xml
before application/json
, so you’ll always
get back XML if you don’t specify it.
That’s also why it’s recommended that you explore via curl, because you avoid your browser’s opinionated requests & get something closer to what any programmatic clients will get.
Fetching Data¶
Since reading data out of an API is a very common activity (and the easiest type of request to make), we’ll start there. Tastypie tries to expose various parts of the API & interlink things within the API (HATEOAS).
Api-Wide¶
We’ll start at the highest level:
curl http://localhost:8000/api/v1/
You’ll get back something like:
{
"entry": {
"list_endpoint": "/api/v1/entry/",
"schema": "/api/v1/entry/schema/"
},
"user": {
"list_endpoint": "/api/v1/user/",
"schema": "/api/v1/user/schema/"
}
}
This lists out all the different Resource
classes you registered in your
URLconf with the API. Each one is listed by the resource_name
you gave it
and provides the list_endpoint
& the schema
for the resource.
Note that these links try to direct you to other parts of the API, to make exploration/discovery easier. We’ll use these URLs in the next several sections.
To demonstrate another format, you could run the following to get the XML variant of the same information:
curl -H "Accept: application/xml" http://localhost:8000/api/v1/
To which you’d receive:
<?xml version="1.0" encoding="utf-8"?>
<response>
<entry type="hash">
<list_endpoint>/api/v1/entry/</list_endpoint>
<schema>/api/v1/entry/schema/</schema>
</entry>
<user type="hash">
<list_endpoint>/api/v1/user/</list_endpoint>
<schema>/api/v1/user/schema/</schema>
</user>
</response>
We’ll stick to JSON for the rest of this document, but using XML should be OK to do at any time.
Inspecting The Resource’s Schema¶
Since the api-wide view gave us a schema
URL, let’s inspect that next.
We’ll use the entry
resource. Again, a simple GET request by curl:
curl http://localhost:8000/api/v1/entry/schema/
This time, we get back a lot more data:
{
"default_format": "application/json",
"fields": {
"body": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"id": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"pub_date": {
"help_text": "A date & time as a string. Ex: \"2010-11-10T03:07:43\"",
"nullable": false,
"readonly": false,
"type": "datetime"
},
"resource_uri": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": true,
"type": "string"
},
"slug": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"title": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"user": {
"help_text": "A single related resource. Can be either a URI or set of nested resource data.",
"nullable": false,
"readonly": false,
"type": "related"
}
},
"filtering": {
"pub_date": ["exact", "lt", "lte", "gte", "gt"],
"user": 2
}
}
This lists out the default_format
this resource responds with, the
fields
on the resource & the filtering
options available. This
information can be used to prepare the other aspects of the code for the
data it can obtain & ways to filter the resources.
Getting A Collection Of Resources¶
Let’s get down to fetching live data. From the api-wide view, we’ll hit
the list_endpoint
for entry
:
curl http://localhost:8000/api/v1/entry/
We get back data that looks like:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 3
},
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "Well, it's been awhile and I still haven't updated. ",
"id": "2",
"pub_date": "2011-05-21T00:46:58",
"resource_uri": "/api/v1/entry/2/",
"slug": "second-post",
"title": "Second Post",
"user": "/api/v1/user/1/"
},
{
"body": "I'm really excited to get started with this new blog. It's gonna be great!",
"id": "3",
"pub_date": "2011-05-20T00:47:30",
"resource_uri": "/api/v1/entry/3/",
"slug": "my-blog",
"title": "My Blog",
"user": "/api/v1/user/2/"
}]
}
Some things to note:
- By default, you get a paginated set of objects (20 per page is the default).
- In the
meta
, you get aprevious
&next
. If available, these are URIs to the previous & next pages.- You get a list of resources/objects under the
objects
key.- Each resources/object has a
resource_uri
field that points to the detail view for that object.- The foreign key to
User
is represented as a URI by default. If you’re looking for the fullUserResource
to be embedded in this view, you’ll need to addfull=True
to thefields.ToOneField
.
If you want to skip paginating, simply run:
curl http://localhost:8000/api/v1/entry/?limit=0
Be warned this will return all objects, so it may be a CPU/IO-heavy operation on large datasets.
Let’s try filtering on the resource. Since we know we can filter on the
user
, we’ll fetch all posts by the daniel
user with:
curl http://localhost:8000/api/v1/entry/?user__username=daniel
We get back what we asked for:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 2
},
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "Well, it's been awhile and I still haven't updated. ",
"id": "2",
"pub_date": "2011-05-21T00:46:58",
"resource_uri": "/api/v1/entry/2/",
"slug": "second-post",
"title": "Second Post",
"user": "/api/v1/user/1/"
}]
}
Where there were three posts before, now there are only two.
Getting A Detail Resource¶
Since each resource/object in the list view had a resource_uri
, let’s
explore what’s there:
curl http://localhost:8000/api/v1/entry/1/
We get back a similar set of data that we received from the list view:
{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
}
Where this proves useful (for example) is present in the data we got back. We
know the URI of the User
associated with this blog entry. Let’s run:
curl http://localhost:8000/api/v1/user/1/
Without ever seeing any aspect of the UserResource
& just following the URI
given, we get back:
{
"date_joined": "2011-05-20T00:42:14.990617",
"first_name": "",
"id": "1",
"last_login": "2011-05-20T00:44:57.510066",
"last_name": "",
"resource_uri": "/api/v1/user/1/",
"username": "daniel"
}
Selecting A Subset Of Resources¶
Sometimes you may want back more than one record, but not an entire list view
nor do you want to do multiple requests. Tastypie includes a “set” view, which
lets you cherry-pick the objects you want. For example, if we just want the
first & third Entry
resources, we’d run:
curl "http://localhost:8000/api/v1/entry/set/1;3/"
Note
Quotes are needed in this case because of the semicolon delimiter between primary keys. Without the quotes, bash tries to split it into two statements. No extraordinary quoting will be necessary in your application (unless your API client is written in bash :D).
And we get back just those two objects:
{
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "I'm really excited to get started with this new blog. It's gonna be great!",
"id": "3",
"pub_date": "2011-05-20T00:47:30",
"resource_uri": "/api/v1/entry/3/",
"slug": "my-blog",
"title": "My Blog",
"user": "/api/v1/user/2/"
}]
}
Note that, like the list view, you get back a list of objects
. Unlike the
list view, there is NO pagination applied to these objects. You asked for
them, you’re going to get them all.
Sending Data¶
Tastypie also gives you full write capabilities in the API. Since the
EntryResource
has the no-limits Authentication
& Authorization
on
it, we can freely write data.
Warning
Note that this is a huge security hole as well. Don’t put unauthorized write-enabled resources on the Internet, because someone will trash your data.
This is why ReadOnlyAuthorization
is the default in Tastypie & why you
must override to provide more access.
The good news is that there are no new URLs to learn. The “list” & “detail”
URLs we’ve been using to fetch data ALSO support the
POST
/PUT
/DELETE
HTTP methods.
Creating A New Resource (POST)¶
Let’s add a new entry. To create new data, we’ll switch from GET
requests
to the familiar POST
request.
To create new resources/objects, you will POST
to the list endpoint of
a resource. Trying to POST
to a detail endpoint has a different meaning in
the REST mindset (meaning to add a resource as a child of a resource of the
same type).
As with all Tastypie requests, the headers we request are important. Since we’ve been using primarily JSON throughout, let’s send a new entry in JSON format:
curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post", "user": "/api/v1/user/1/"}' http://localhost:8000/api/v1/entry/
The Content-Type
header here informs Tastypie that we’re sending it JSON.
We send the data as a JSON-serialized body (NOT as form-data in the form of
URL parameters). What we get back is the following response:
HTTP/1.0 201 CREATED
Date: Fri, 20 May 2011 06:48:36 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Location: http://localhost:8000/api/v1/entry/4/
You’ll also note that we get a correct HTTP status code back (201) & a
Location
header, which gives us the URI to our newly created resource.
Passing --dump-header -
is important, because it gives you all the headers
as well as the status code. When things go wrong, this will be useful
information to help with debugging. For instance, if we send a request without
a user
:
curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post"}' http://localhost:8000/api/v1/entry/
We get back:
HTTP/1.0 400 BAD REQUEST
Date: Fri, 20 May 2011 06:53:02 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
The 'user' field has no data and doesn't allow a default or null value.
Updating An Existing Resource (PUT)¶
You might have noticed that we made some typos when we submitted the POST
request. We can fix this using a PUT
request to the detail endpoint (modify
this instance of a resource).
curl –dump-header - -H “Content-Type: application/json” -X PUT –data ‘{“body”: “This will probably be my last post.”, “pub_date”: “2011-05-22T00:46:38”, “slug”: “another-post”, “title”: “Another Post”, “user”: “/api/v1/user/1/”}’ http://localhost:8000/api/v1/entry/4/
After fixing up the body
, we get back:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:13:21 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
We get a 204 status code, meaning our update was successful. We don’t get
a Location
header back because we did the PUT
on a detail URL, which
presumably did not change.
Updating A Whole Collection Of Resources (PUT)¶
You can also, in rare circumstances, update an entire collection of objects.
By sending a PUT
request to the list view of a resource, you can replace
the entire collection.
Warning
This deletes all of the objects first, then creates the objects afresh. This is done because determining which objects are the same is actually difficult to get correct in the general case for all people.
Send a request like:
curl --dump-header - -H "Content-Type: application/json" -X PUT --data '{"objects": [{"body": "Welcome to my blog!","id": "1","pub_date": "2011-05-20T00:46:38","resource_uri": "/api/v1/entry/1/","slug": "first-post","title": "First Post","user": "/api/v1/user/1/"},{"body": "I'm really excited to get started with this new blog. It's gonna be great!","id": "3","pub_date": "2011-05-20T00:47:30","resource_uri": "/api/v1/entry/3/","slug": "my-blog","title": "My Blog","user": "/api/v1/user/2/"}]}' http://localhost:8000/api/v1/entry/
And you’ll get back a response like:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:13:21 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
Deleting Data¶
No CRUD setup would be complete without the ability to delete resources/objects.
Deleting also requires significantly less complicated requests than
POST
/PUT
.
Deleting A Single Resource¶
We’ve decided that we don’t like the entry we added & edited earlier. Let’s delete it (but leave the other objects alone):
curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/4/
Once again, we get back the “Accepted” response of a 204:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:28:01 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
If we request that resource, we get a 410 to show it’s no longer there:
curl --dump-header - http://localhost:8000/api/v1/entry/4/
HTTP/1.0 410 GONE
Date: Fri, 20 May 2011 07:29:02 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Additionally, if we try to run the DELETE
again (using the same original
command), we get the “Gone” response again:
HTTP/1.0 410 GONE
Date: Fri, 20 May 2011 07:30:00 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Deleting A Whole Collection Of Resources¶
Finally, it’s possible to remove an entire collection of resources. This is
as destructive as it sounds. Once again, we use the DELETE
method, this
time on the entire list endpoint:
curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/
As a response, we get:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:32:51 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
Hitting the list view:
curl --dump-header - http://localhost:8000/api/v1/entry/
Gives us a 200 but no objects:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 0
},
"objects": []
}
You Did It!¶
That’s a whirlwind tour of interacting with a Tastypie API. There’s additional functionality present, such as:
POST
/PUT
the other supported content-types- More filtering/
order_by
/limit
/offset
tricks - Using overridden URLconfs to support complex or non-PK lookups
- Authentication
But this grounds you in the basics & hopefully clarifies usage/debugging better.
Tastypie Settings¶
This is a comprehensive list of the settings Tastypie recognizes.
API_LIMIT_PER_PAGE
¶
Optional
This setting controls the default number of records Tastypie will show in a list view.
This is only used when a user does not specify a limit
GET parameter and
the Resource
subclass has not overridden the number to be shown.
An example:
API_LIMIT_PER_PAGE = 50
Defaults to 20.
TASTYPIE_FULL_DEBUG
¶
Optional
This setting controls what the behavior is when an unhandled exception occurs.
If set to True
and settings.DEBUG = True
, the standard Django
technical 500 is displayed.
If not set or set to False
, Tastypie will return a serialized response.
If settings.DEBUG
is True
, you’ll get the actual exception message plus
a traceback. If settings.DEBUG
is False
, Tastypie will call
mail_admins()
and provide a canned error message (which you can override
with TASTYPIE_CANNED_ERROR
) in the response.
An example:
TASTYPIE_FULL_DEBUG = True
Defaults to False
.
TASTYPIE_CANNED_ERROR
¶
Optional
This setting allows you to override the canned error response when an
unhandled exception is raised and settings.DEBUG
is False
.
An example:
TASTYPIE_CANNED_ERROR = "Oops, we broke it!"
Defaults to "Sorry, this request could not be processed. Please try again later."
.
TASTYPIE_ALLOW_MISSING_SLASH
¶
Optional
This setting allows your URLs to be missing the final slash. Useful for integrating with other systems.
You must also have settings.APPEND_SLASH = False
so that Django does not
emit HTTP 302 redirects.
Warning
This setting causes the Resource.get_multiple()
method to fail. If you
need this method, you will have to override the URLconf to meet your needs.
An example:
TASTYPIE_ALLOW_MISSING_SLASH = True
Defaults to False
.
TASTYPIE_DATETIME_FORMATTING
¶
Optional
This setting allows you to globally choose what format your datetime/date/time
data will be formatted in. Valid options are iso-8601
& rfc-2822
.
An example:
TASTYPIE_DATETIME_FORMATTING = 'rfc-2822'
Defaults to iso-8601
.
Using Tastypie With Non-ORM Data Sources¶
Much of this documentation demonstrates the use of Tastypie with Django’s ORM. You might think that Tastypie depended on the ORM, when in fact, it was purpose-built to handle non-ORM data. This documentation should help you get started providing APIs using other data sources.
Virtually all of the code that makes Tastypie actually process requests &
return data is within the Resource
class. ModelResource
is actually a
light wrapper around Resource
that provides ORM-specific access. The
methods that ModelResource
overrides are the same ones you’ll need to
override when hooking up your data source.
Approach¶
When working with Resource
, many things are handled for you. All the
authentication/authorization/caching/serialization/throttling bits should work
as normal and Tastypie can support all the REST-style methods. Schemas &
discovery views all work the same as well.
What you don’t get out of the box are the fields you’re choosing to expose & the lowest level data access methods. If you want a full read-write API, there are nine methods you need to implement. They are:
get_resource_uri
get_object_list
obj_get_list
obj_get
obj_create
obj_update
obj_delete_list
obj_delete
rollback
If read-only is all you’re exposing, you can cut that down to four methods to override.
Using Riak for MessageResource¶
As an example, we’ll take integrating with Riak (a Dynamo-like NoSQL store) since it has both a simple API and demonstrate what hooking up to a non-relational datastore looks like:
# We need a generic object to shove data in/get data from.
# Riak generally just tosses around dictionaries, so we'll lightly
# wrap that.
class RiakObject(object):
def __init__(self, initial=None):
self.__dict__['_data'] = {}
if hasattr(initial, 'items'):
self.__dict__['_data'] = initial
def __getattr__(self, name):
return self._data.get(name, None)
def __setattr__(self, name, value):
self.__dict__['_data'][name] = value
def to_dict(self):
return self._data
class MessageResource(Resource):
# Just like a Django ``Form`` or ``Model``, we're defining all the
# fields we're going to handle with the API here.
uuid = fields.CharField(attribute='uuid')
user_uuid = fields.CharField(attribute='user_uuid')
message = fields.CharField(attribute='message')
created = fields.IntegerField(attribute='created')
class Meta:
resource_name = 'riak'
object_class = RiakObject
authorization = Authorization()
# Specific to this resource, just to get the needed Riak bits.
def _client(self):
return riak.RiakClient()
def _bucket(self):
client = self._client()
# Note that we're hard-coding the bucket to use. Fine for
# example purposes, but you'll want to abstract this.
return client.bucket('messages')
# The following methods will need overriding regardless of your
# data source.
def get_resource_uri(self, bundle_or_obj):
kwargs = {
'resource_name': self._meta.resource_name,
}
if isinstance(bundle_or_obj, Bundle):
kwargs['pk'] = bundle_or_obj.obj.uuid
else:
kwargs['pk'] = bundle_or_obj.uuid
if self._meta.api_name is not None:
kwargs['api_name'] = self._meta.api_name
return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
def get_object_list(self, request):
query = self._client().add('messages')
query.map("function(v) { var data = JSON.parse(v.values[0].data); return [[v.key, data]]; }")
results = []
for result in query.run():
new_obj = RiakObject(initial=result[1])
new_obj.uuid = result[0]
results.append(new_obj)
return results
def obj_get_list(self, request=None, **kwargs):
# Filtering disabled for brevity...
return self.get_object_list(request)
def obj_get(self, request=None, **kwargs):
bucket = self._bucket()
message = bucket.get(kwargs['pk'])
return RiakObject(initial=message.get_data())
def obj_create(self, bundle, request=None, **kwargs):
bundle.obj = RiakObject(initial=kwargs)
bundle = self.full_hydrate(bundle)
bucket = self._bucket()
new_message = bucket.new(bundle.obj.uuid, data=bundle.obj.to_dict())
new_message.store()
return bundle
def obj_update(self, bundle, request=None, **kwargs):
return self.obj_create(bundle, request, **kwargs)
def obj_delete_list(self, request=None, **kwargs):
bucket = self._bucket()
for key in bucket.get_keys():
obj = bucket.get(key)
obj.delete()
def obj_delete(self, request=None, **kwargs):
bucket = self._bucket()
obj = bucket.get(kwargs['pk'])
obj.delete()
def rollback(self, bundles):
pass
This represents a full, working, Riak-powered API endpoint. All REST-style actions (GET/POST/PUT/DELETE) all work correctly. The only shortcut taken in this example was skipping filter-abilty, as adding in the MapReduce bits would have decreased readability.
All said and done, just nine methods needed overriding, eight of which were highly specific to how data access is done.
Resources¶
In terms of a REST-style architecture, a “resource” is a collection of similar
data. This data could be a table of a database, a collection of other resources
or a similar form of data storage. In Tastypie, these resources are generally
intermediaries between the end user & objects, usually Django models. As such,
Resource
(and its model-specific twin ModelResource
) form the heart of
Tastypie’s functionality.
Quick Start¶
A sample resource definition might look something like:
from django.contrib.auth.models import User
from tastypie import fields
from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
list_allowed_methods = ['get', 'post']
detail_allowed_methods = ['get', 'post', 'put', 'delete']
resource_name = 'myapp/entry'
authorization = DjangoAuthorization()
filtering = {
'slug': ALL,
'user': ALL_WITH_RELATIONS,
'created': ['exact', 'range', 'gt', 'gte', 'lt', 'lte'],
}
Why Class-Based?¶
Using class-based resources make it easier to extend/modify the code to meet your needs. APIs are rarely a one-size-fits-all problem space, so Tastypie tries to get the fundamentals right and provide you with enough hooks to customize things to work your way.
As is standard, this raises potential problems for thread-safety. Tastypie has been designed to minimize the possibility of data “leaking” between threads. This does however sometimes introduce some small complexities & you should be careful not to store state on the instances if you’re going to be using the code in a threaded environment.
Why Resource
vs. ModelResource
?¶
Make no mistake that Django models are far and away the most popular source of
data. However, in practice, there are many times where the ORM isn’t the data
source. Hooking up things like a NoSQL store (see Using Tastypie With Non-ORM Data Sources),
a search solution like Haystack or even managed filesystem data are all good
use cases for Resource
knowing nothing about the ORM.
Flow Through The Request/Response Cycle¶
Tastypie can be thought of as a set of class-based views that provide the API functionality. As such, many part of the request/response cycle are standard Django behaviors. For instance, all routing/middleware/response-handling aspects are the same as a typical Django app. Where it differs is in the view itself.
As an example, we’ll walk through what a GET request to a list endpoint (say
/api/v1/user/?format=json
) looks like:
The
Resource.urls
are checked by Django’s url resolvers.On a match for the list view,
Resource.wrap_view('dispatch_list')
is called.wrap_view
provides basic error handling & allows for returning serialized errors.Because
dispatch_list
was passed towrap_view
,Resource.dispatch_list
is called next. This is a thin wrapper aroundResource.dispatch
.dispatch
does a bunch of heavy lifting. It ensures:- the requested HTTP method is in
allowed_methods
(method_check
), - the class has a method that can handle the request (
get_list
), - the user is authenticated (
is_authenticated
), - the user is authorized (
is_authorized
), - & the user has not exceeded their throttle (
throttle_check
).
At this point,
dispatch
actually calls the requested method (get_list
).- the requested HTTP method is in
get_list
does the actual work of the API. It does:- A fetch of the available objects via
Resource.obj_get_list
. In the case ofModelResource
, this builds the ORM filters to apply (ModelResource.build_filters
). It then gets theQuerySet
viaModelResource.get_object_list
(which performsResource.apply_authorization_limits
to possibly limit the set the user can work with) and applies the built filters to it. - It then sorts the objects based on user input
(
ModelResource.apply_sorting
). - Then it paginates the results using the supplied
Paginator
& pulls out the data to be serialized. - The objects in the page have
full_dehydrate
applied to each of them, causing Tastypie to translate the raw object data into the fields the endpoint supports. - Finally, it calls
Resource.create_response
.
- A fetch of the available objects via
create_response
is a shortcut method that:- Determines the desired response format (
Resource.determine_format
), - Serializes the data given to it in the proper format,
- And returns a Django
HttpResponse
(200 OK) with the serialized data.
- Determines the desired response format (
We bubble back up the call stack to
dispatch
. The last thingdispatch
does is potentially store that a request occurred for future throttling (Resource.log_throttled_access
) then either returns theHttpResponse
or wraps whatever data came back in a response (so Django doesn’t freak out).
Processing on other endpoints or using the other HTTP methods results in a
similar cycle, usually differing only in what “actual work” method gets called
(which follows the format of “<http_method>_<list_or_detail>"). In the case
of POST/PUT, the ``hydrate
cycle additionally takes place and is used to take
the user data & convert it to raw data for storage.
What Are Bundles?¶
Bundles are a small abstraction that allow Tastypie to pass data between
resources. This allows us not to depend on passing request
to every single
method (especially in places where this would be overkill). It also allows
resources to work with data coming into the application paired together with
an unsaved instance of the object in question.
Think of it as package of user data & an object instance (either of which are optionally present).
Why Resource URIs?¶
Resource URIs play a heavy role in how Tastypie delivers data. This can seem
very different from other solutions which simply inline related data. Though
Tastypie can inline data like that (using full=True
on the field with the
relation), the default is to provide URIs.
URIs are useful because it results in smaller payloads, letting you fetch only the data that is important to you. You can imagine an instance where an object has thousands of related items that you may not be interested in.
URIs are also very cache-able, because the data at each endpoint is less likely to frequently change.
And URIs encourage proper use of each endpoint to display the data that endpoint covers.
Ideology aside, you should use whatever suits you. If you prefer fewer requests
& fewer endpoints, use of full=True
is available, but be aware of the
consequences of each approach.
Advanced Data Preparation¶
Tastypie uses a “dehydrate” cycle to prepare data for serialization & a “hydrate” cycle to take data sent to it & turn that back into useful Python objects.
Within these cycles, there are several points of customization if you need them.
dehydrate
¶
dehydrate_FOO
¶
hydrate
¶
hydrate_FOO
¶
Reverse “Relationships”¶
Unlike Django’s ORM, Tastypie does not automatically create reverse relations. This is because there is substantial technical complexity involved, as well as perhaps unintentionally exposing related data in an incorrect way to the end user of the API.
However, it is still possible to create reverse relations. Instead of handing
the ToOneField
or ToManyField
a class, pass them a string that
represents the full path to the desired class. Implementing a reverse
relationship looks like so:
# myapp/api/resources.py
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Note, Comment
class NoteResource(ModelResource):
comments = fields.ToManyField('myapp.api.resources.CommentResource', 'comments')
class Meta:
queryset = Note.objects.all()
class CommentResource(ModelResource):
note = fields.ToOneField(NoteResource, 'notes')
class Meta:
queryset = Comment.objects.all()
Warning
Unlike Django, you can’t use just the class name (i.e. 'CommentResource'
),
even if it’s in the same module. Tastypie (intentionally) lacks a construct
like the AppCache
which makes that sort of thing work in Django. Sorry.
Tastypie also supports self-referential relations. If you assume we added the
appropriate self-referential ForeignKey
to the Note
model, implementing
a similar relation in Tastypie would look like:
# myapp/api/resources.py
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Note
class NoteResource(ModelResource):
sub_notes = fields.ToManyField('self', 'notes')
class Meta:
queryset = Note.objects.all()
Resource Options (AKA Meta
)¶
The inner Meta
class allows for class-level configuration of how the
Resource
should behave. The following options are available:
serializer
¶
Controls which serializer class theResource
should use. Default istastypie.serializers.Serializer()
.
authentication
¶
Controls which authentication class theResource
should use. Default istastypie.authentication.Authentication()
.
authorization
¶
Controls which authorization class theResource
should use. Default istastypie.authorization.ReadOnlyAuthorization()
.
validation
¶
Controls which validation class theResource
should use. Default istastypie.validation.Validation()
.
paginator_class
¶
Controls which paginator class theResource
should use. Default istastypie.paginator.Paginator()
.
Note
This is different than the other options in that you supply a class rather than an instance. This is done because the Paginator has some per-request initialization options.
cache
¶
Controls which cache class theResource
should use. Default istastypie.cache.NoCache()
.
throttle
¶
Controls which throttle class theResource
should use. Default istastypie.throttle.BaseThrottle()
.
allowed_methods
¶
Controls what list & detail REST methods the
Resource
should respond to. Default isNone
, which means delegate to the more specificlist_allowed_methods
&detail_allowed_methods
options.You may specify a list like
['get', 'post', 'put', 'delete']
as a shortcut to prevent having to specify the other options.
list_allowed_methods
¶
Controls what list REST methods theResource
should respond to. Default is['get', 'post', 'put', 'delete']
.
detail_allowed_methods
¶
Controls what detail REST methods theResource
should respond to. Default is['get', 'post', 'put', 'delete']
.
limit
¶
Controls what how many results theResource
will show at a time. Default is either theAPI_LIMIT_PER_PAGE
setting (if provided) or20
if not specified.
api_name
¶
An override for theResource
to use when generating resource URLs. Default isNone
.
resource_name
¶
An override for the
Resource
to use when generating resource URLs. Default isNone
.If not provided, the
Resource
orModelResource
will attempt to name itself. This means a lowercase version of the classname preceding the wordResource
if present (i.e.SampleContentResource
would becomesamplecontent
).
default_format
¶
Specifies the default serialization format theResource
should use if one is not requested (usually by theAccept
header orformat
GET parameter). Default isapplication/json
.
filtering
¶
Provides a list of fields that the
Resource
will accept client filtering on. Default is{}
.Keys should be the fieldnames as strings while values should be a list of accepted filter types.
ordering
¶
Specifies the what fields the
Resource
should should allow ordering on. Default is[]
.Values should be the fieldnames as strings. When provided to the
Resource
by theorder_by
GET parameter, you can specify either thefieldname
(ascending order) or-fieldname
(descending order).
object_class
¶
Provides the
Resource
with the object that serves as the data source. Default isNone
.In the case of
ModelResource
, this is automatically populated by thequeryset
option and is the model class.
queryset
¶
Provides the
Resource
with the set of Django models to respond with. Default isNone
.Unused by
Resource
but present for consistency.
fields
¶
Controls what introspected fields theResource
should include. A whitelist of fields. Default is[]
.
excludes
¶
Controls what introspected fields theResource
should NOT include. A blacklist of fields. Default is[]
.
include_resource_uri
¶
Specifies if theResource
should include an extra field that displays the detail URL (within the api) for that resource. Default isTrue
.
include_absolute_url
¶
Specifies if theResource
should include an extra field that displays theget_absolute_url
for that object (on the site proper). Default isFalse
.
Basic Filtering¶
ModelResource
provides a basic Django ORM filter
interface. Simply list the resource fields which you’d like to filter on and
the allowed expression in a filtering property of your resource’s Meta
class:
from tastypie.constants import ALL, ALL_WITH_RELATIONS
class MyResource(ModelResource):
class Meta:
filtering = {
"slug": ('exact', 'startswith',),
"title": ALL,
}
Valid filtering values are: Django ORM filters (e.g. startswith
,
exact
, lte
, etc. or the ALL
or ALL_WITH_RELATIONS
constants
defined in tastypie.constants
.
These filters will be extracted from URL query strings using the same double-underscore syntax as the Django ORM:
/api/v1/myresource/?slug=myslug
/api/v1/myresource/?slug__startswith=test
Advanced Filtering¶
If you need to filter things other than ORM resources or wish to apply
additional constraints (e.g. text filtering using django-haystack
<http://haystacksearch.org> rather than simple database queries) your
Resource
may define a custom
build_filters()
method which allows you to
filter the queryset before processing a request:
from haystack.query import SearchQuerySet
class MyResource(Resource):
def build_filters(self, filters=None):
if filters is None:
filters = {}
orm_filters = super(MyResource, self).build_filters(filters)
if "q" in filters:
sqs = SearchQuerySet().auto_query(filters['q'])
orm_filters = {"pk__in": [ i.pk for i in sqs ]}
return orm_filters
Resource
Methods¶
Handles the data, request dispatch and responding to requests.
Serialization/deserialization is handled “at the edges” (i.e. at the beginning/end of the request/response cycle) so that everything internally is Python data structures.
This class tries to be non-model specific, so it can be hooked up to other data sources, such as search results, files, other data, etc.
wrap_view
¶
-
Resource.
wrap_view
(self, view)¶
Wraps methods so they can be called in a more functional way as well as handling exceptions better.
Note that if BadRequest
or an exception with a response
attr are seen,
there is special handling to either present a message back to the user or
return the response traveling with the exception.
base_urls
¶
-
Resource.
base_urls
(self)¶
The standard URLs this Resource
should respond to. These include the
list, detail, schema & multiple endpoints by default.
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
override_urls
¶
-
Resource.
override_urls
(self)¶
A hook for adding your own URLs or overriding the default URLs. Useful for
adding custom endpoints or overriding the built-in ones (from base_urls
).
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
urls
¶
-
Resource.
urls
(self)¶
Property
The endpoints this Resource
responds to. A combination of base_urls
&
override_urls
.
Mostly a standard URLconf, this is suitable for either automatic use
when registered with an Api
class or for including directly in
a URLconf should you choose to.
determine_format
¶
-
Resource.
determine_format
(self, request)¶
Used to determine the desired format.
Largely relies on tastypie.utils.mime.determine_format
but here
as a point of extension.
serialize
¶
-
Resource.
serialize
(self, request, data, format, options=None)¶
Given a request, data and a desired format, produces a serialized version suitable for transfer over the wire.
Mostly a hook, this uses the Serializer
from Resource._meta
.
deserialize
¶
-
Resource.
deserialize
(self, request, data, format='application/json')¶
Given a request, data and a format, deserializes the given data.
It relies on the request properly sending a CONTENT_TYPE
header,
falling back to application/json
if not provided.
Mostly a hook, this uses the Serializer
from Resource._meta
.
alter_list_data_to_serialize
¶
-
Resource.
alter_list_data_to_serialize
(self, request, data)¶
A hook to alter list data just before it gets serialized & sent to the user.
Useful for restructuring/renaming aspects of the what’s going to be sent.
Should accommodate for a list of objects, generally also including meta data.
alter_detail_data_to_serialize
¶
-
Resource.
alter_detail_data_to_serialize
(self, request, data)¶
A hook to alter detail data just before it gets serialized & sent to the user.
Useful for restructuring/renaming aspects of the what’s going to be sent.
Should accommodate for receiving a single bundle of data.
alter_deserialized_list_data
¶
-
Resource.
alter_deserialized_list_data
(self, request, data)¶
A hook to alter list data just after it has been received from the user & gets deserialized.
Useful for altering the user data before any hydration is applied.
alter_deserialized_detail_data
¶
-
Resource.
alter_deserialized_detail_data
(self, request, data)¶
A hook to alter detail data just after it has been received from the user & gets deserialized.
Useful for altering the user data before any hydration is applied.
dispatch_list
¶
-
Resource.
dispatch_list
(self, request, **kwargs)¶
A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over the entire list of resources.
Relies on Resource.dispatch
for the heavy-lifting.
dispatch_detail
¶
-
Resource.
dispatch_detail
(self, request, **kwargs)¶
A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on a single resource.
Relies on Resource.dispatch
for the heavy-lifting.
dispatch
¶
-
Resource.
dispatch
(self, request_type, request, **kwargs)¶
Handles the common operations (allowed HTTP method, authentication, throttling, method lookup) surrounding most CRUD interactions.
remove_api_resource_names
¶
-
Resource.
remove_api_resource_names
(self, url_dict)¶
Given a dictionary of regex matches from a URLconf, removes
api_name
and/or resource_name
if found.
This is useful for converting URLconf matches into something suitable for data lookup. For example:
Model.objects.filter(**self.remove_api_resource_names(matches))
method_check
¶
-
Resource.
method_check
(self, request, allowed=None)¶
Ensures that the HTTP method used on the request is allowed to be handled by the resource.
Takes an allowed
parameter, which should be a list of lowercase
HTTP methods to check against. Usually, this looks like:
# The most generic lookup.
self.method_check(request, self._meta.allowed_methods)
# A lookup against what's allowed for list-type methods.
self.method_check(request, self._meta.list_allowed_methods)
# A useful check when creating a new endpoint that only handles
# GET.
self.method_check(request, ['get'])
is_authorized
¶
Handles checking of permissions to see if the user has authorization
to GET, POST, PUT, or DELETE this resource. If object
is provided,
the authorization backend can apply additional row-level permissions
checking.
is_authenticated
¶
-
Resource.
is_authenticated
(self, request)¶
Handles checking if the user is authenticated and dealing with unauthenticated users.
Mostly a hook, this uses class assigned to authentication
from
Resource._meta
.
throttle_check
¶
-
Resource.
throttle_check
(self, request)¶
Handles checking if the user should be throttled.
Mostly a hook, this uses class assigned to throttle
from
Resource._meta
.
log_throttled_access
¶
-
Resource.
log_throttled_access
(self, request)¶
Handles the recording of the user’s access for throttling purposes.
Mostly a hook, this uses class assigned to throttle
from
Resource._meta
.
build_bundle
¶
-
Resource.
build_bundle
(self, obj=None, data=None)¶
Given either an object, a data dictionary or both, builds a Bundle
for use throughout the dehydrate/hydrate
cycle.
If no object is provided, an empty object from
Resource._meta.object_class
is created so that attempts to access
bundle.obj
do not fail.
build_filters
¶
-
Resource.
build_filters
(self, filters=None)¶
Allows for the filtering of applicable objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
apply_sorting
¶
-
Resource.
apply_sorting
(self, obj_list, options=None)¶
Allows for the sorting of objects being returned.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
get_resource_uri
¶
-
Resource.
get_resource_uri
(self, bundle_or_obj)¶
This needs to be implemented at the user level.
A return reverse("api_dispatch_detail", kwargs={'resource_name':
self.resource_name, 'pk': object.id})
should be all that would
be needed.
ModelResource
includes a full working version specific to Django’s
Models
.
get_resource_list_uri
¶
-
Resource.
get_resource_list_uri
(self)¶
Returns a URL specific to this resource’s list endpoint.
get_via_uri
¶
-
Resource.
get_via_uri
(self, uri)¶
This pulls apart the salient bits of the URI and populates the
resource via a obj_get
.
If you need custom behavior based on other portions of the URI, simply override this method.
full_dehydrate
¶
-
Resource.
full_dehydrate
(self, obj)¶
Given an object instance, extract the information from it to populate the resource.
dehydrate
¶
-
Resource.
dehydrate
(self, bundle)¶
A hook to allow a final manipulation of data once all fields/methods have built out the dehydrated data.
Useful if you need to access more than one dehydrated field or want to annotate on additional data.
Must return the modified bundle.
full_hydrate
¶
-
Resource.
full_hydrate
(self, bundle)¶
Given a populated bundle, distill it and turn it back into a full-fledged object instance.
hydrate
¶
-
Resource.
hydrate
(self, bundle)¶
A hook to allow a final manipulation of data once all fields/methods have built out the hydrated data.
Useful if you need to access more than one hydrated field or want to annotate on additional data.
Must return the modified bundle.
build_schema
¶
-
Resource.
build_schema
(self)¶
Returns a dictionary of all the fields on the resource and some properties about those fields.
Used by the schema/
endpoint to describe what will be available.
dehydrate_resource_uri
¶
-
Resource.
dehydrate_resource_uri
(self, bundle)¶
For the automatically included resource_uri
field, dehydrate
the URI for the given bundle.
Returns empty string if no URI can be generated.
generate_cache_key
¶
-
Resource.
generate_cache_key
(self, *args, **kwargs)¶
Creates a unique-enough cache key.
This is based off the current api_name/resource_name/args/kwargs.
get_object_list
¶
-
Resource.
get_object_list
(self, request)¶
A hook to allow making returning the list of available objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
apply_authorization_limits
¶
Allows the Authorization
class to further limit the object list.
Also a hook to customize per Resource
.
Calls Authorization.apply_limits
if available.
can_update
¶
-
Resource.
can_update
(self)¶
Checks to ensure put
is within allowed_methods
.
Used when hydrating related data.
obj_get_list
¶
-
Resource.
obj_get_list
(self, request=None, **kwargs)¶
Fetches the list of objects available on the resource.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
cached_obj_get_list
¶
-
Resource.
cached_obj_get_list
(self, request=None, **kwargs)¶
A version of obj_get_list
that uses the cache as a means to get
commonly-accessed data faster.
obj_get
¶
-
Resource.
obj_get
(self, request=None, **kwargs)¶
Fetches an individual object on the resource.
This needs to be implemented at the user level. If the object can not
be found, this should raise a NotFound
exception.
ModelResource
includes a full working version specific to Django’s
Models
.
cached_obj_get
¶
-
Resource.
cached_obj_get
(self, request=None, **kwargs)¶
A version of obj_get
that uses the cache as a means to get
commonly-accessed data faster.
obj_create
¶
-
Resource.
obj_create
(self, bundle, request=None, **kwargs)¶
Creates a new object based on the provided data.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_update
¶
-
Resource.
obj_update
(self, bundle, request=None, **kwargs)¶
Updates an existing object (or creates a new object) based on the provided data.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_delete_list
¶
-
Resource.
obj_delete_list
(self, request=None, **kwargs)¶
Deletes an entire list of objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_delete
¶
-
Resource.
obj_delete
(self, request=None, **kwargs)¶
Deletes a single object.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
create_response
¶
-
Resource.
create_response
(self, request, data)¶
Extracts the common “which-format/serialize/return-response” cycle.
Mostly a useful shortcut/hook.
is_valid
¶
-
Resource.
is_valid
(self, bundle, request=None)¶
Handles checking if the data provided by the user is valid.
Mostly a hook, this uses class assigned to validation
from
Resource._meta
.
If validation fails, an error is raised with the error messages serialized inside it.
rollback
¶
-
Resource.
rollback
(self, bundles)¶
Given the list of bundles, delete all objects pertaining to those bundles.
This needs to be implemented at the user level. No exceptions should be raised if possible.
ModelResource
includes a full working version specific to Django’s
Models
.
get_list
¶
-
Resource.
get_list
(self, request, **kwargs)¶
Returns a serialized list of resources.
Calls obj_get_list
to provide the data, then handles that result
set and serializes it.
Should return a HttpResponse (200 OK).
get_detail
¶
-
Resource.
get_detail
(self, request, **kwargs)¶
Returns a single serialized resource.
Calls cached_obj_get/obj_get
to provide the data, then handles that result
set and serializes it.
Should return a HttpResponse (200 OK).
put_list
¶
-
Resource.
put_list
(self, request, **kwargs)¶
Replaces a collection of resources with another collection.
Calls delete_list
to clear out the collection then obj_create
with the provided the data to create the new collection.
Return HttpAccepted
(204 No Content).
put_detail
¶
-
Resource.
put_detail
(self, request, **kwargs)¶
Either updates an existing resource or creates a new one with the provided data.
Calls obj_update
with the provided data first, but falls back to
obj_create
if the object does not already exist.
If a new resource is created, return HttpCreated
(201 Created).
If an existing resource is modified, return HttpAccepted
(204 No Content).
post_list
¶
-
Resource.
post_list
(self, request, **kwargs)¶
Creates a new resource/object with the provided data.
Calls obj_create
with the provided data and returns a response
with the new resource’s location.
If a new resource is created, return HttpCreated
(201 Created).
post_detail
¶
-
Resource.
post_detail
(self, request, **kwargs)¶
Creates a new subcollection of the resource under a resource.
This is not implemented by default because most people’s data models aren’t self-referential.
If a new resource is created, return HttpCreated
(201 Created).
delete_list
¶
-
Resource.
delete_list
(self, request, **kwargs)¶
Destroys a collection of resources/objects.
Calls obj_delete_list
.
If the resources are deleted, return HttpAccepted
(204 No Content).
delete_detail
¶
-
Resource.
delete_detail
(self, request, **kwargs)¶
Destroys a single resource/object.
Calls obj_delete
.
If the resource is deleted, return HttpAccepted
(204 No Content).
If the resource did not exist, return HttpGone
(410 Gone).
ModelResource
Methods¶
A subclass of Resource
designed to work with Django’s Models
.
This class will introspect a given Model
and build a field list based
on the fields found on the model (excluding relational fields).
Given that it is aware of Django’s ORM, it also handles the CRUD data operations of the resource.
should_skip_field
¶
-
ModelResource.
should_skip_field
(cls, field)¶
Class method
Given a Django model field, return if it should be included in the contributed ApiFields.
api_field_from_django_field
¶
-
ModelResource.
api_field_from_django_field
(cls, f, default=CharField)¶
Class method
Returns the field type that would likely be associated with each Django type.
get_fields
¶
-
ModelResource.
get_fields
(cls, fields=None, excludes=None)¶
Class method
Given any explicit fields to include and fields to exclude, add additional fields based on the associated model.
check_filtering
¶
-
ModelResource.
check_filtering
(self, field_name, filter_type='exact', filter_bits=None)¶
Given a field name, a optional filter type and an optional list of additional relations, determine if a field can be filtered on.
If a filter does not meet the needed conditions, it should raise an
InvalidFilterError
.
If the filter meets the conditions, a list of attribute names (not field names) will be returned.
build_filters
¶
-
ModelResource.
build_filters
(self, filters=None)¶
Given a dictionary of filters, create the necessary ORM-level filters.
Keys should be resource fields, NOT model fields.
Valid values are either a list of Django filter types (i.e.
['startswith', 'exact', 'lte']
), the ALL
constant or the
ALL_WITH_RELATIONS
constant.
At the declarative level:
filtering = {
'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'],
'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'],
'resource_field_name_3': ALL,
'resource_field_name_4': ALL_WITH_RELATIONS,
...
}
Accepts the filters as a dict. None
by default, meaning no filters.
apply_sorting
¶
-
ModelResource.
apply_sorting
(self, obj_list, options=None)¶
Given a dictionary of options, apply some ORM-level sorting to the
provided QuerySet
.
Looks for the order_by
key and handles either ascending (just the
field name) or descending (the field name with a -
in front).
The field name should be the resource field, NOT model field.
get_object_list
¶
-
ModelResource.
get_object_list
(self, request)¶
A ORM-specific implementation of get_object_list
.
Returns a QuerySet
that may have been limited by other overrides.
obj_get_list
¶
-
ModelResource.
obj_get_list
(self, filters=None, **kwargs)¶
A ORM-specific implementation of obj_get_list
.
Takes an optional filters
dictionary, which can be used to narrow
the query.
obj_get
¶
-
ModelResource.
obj_get
(self, **kwargs)¶
A ORM-specific implementation of obj_get
.
Takes optional kwargs
, which are used to narrow the query to find
the instance.
obj_create
¶
-
ModelResource.
obj_create
(self, bundle, **kwargs)¶
A ORM-specific implementation of obj_create
.
obj_update
¶
-
ModelResource.
obj_update
(self, bundle, **kwargs)¶
A ORM-specific implementation of obj_update
.
obj_delete_list
¶
-
ModelResource.
obj_delete_list
(self, **kwargs)¶
A ORM-specific implementation of obj_delete_list
.
Takes optional kwargs
, which can be used to narrow the query.
obj_delete
¶
-
ModelResource.
obj_delete
(self, **kwargs)¶
A ORM-specific implementation of obj_delete
.
Takes optional kwargs
, which are used to narrow the query to find
the instance.
rollback
¶
-
ModelResource.
rollback
(self, bundles)¶
A ORM-specific implementation of rollback
.
Given the list of bundles, delete all models pertaining to those bundles.
save_m2m
¶
-
ModelResource.
save_m2m
(self, bundle)¶
Handles the saving of related M2M data.
Due to the way Django works, the M2M data must be handled after the
main instance, which is why this isn’t a part of the main save
bits.
Currently slightly inefficient in that it will clear out the whole relation and recreate the related data as needed.
Api¶
In terms of a REST-style architecture, the “api” is a collection of resources.
In Tastypie, the Api
gathers together the Resources
& provides a nice
way to use them as a set. It handles many of the URLconf details for you,
provides a helpful “top-level” view to show what endpoints are available &
some extra URL resolution juice.
Quick Start¶
A sample api definition might look something like (usually located in a URLconf):
from tastypie.api import Api
from myapp.api.resources import UserResource, EntryResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
# Standard bits...
urlpatterns = patterns('',
(r'^api/', include(v1_api.urls)),
)
Api
Methods¶
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.
register
¶
-
Api.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
.
canonical_resource_for
¶
-
Api.canonical_resource_for(self, resource_name):
Returns the canonical resource for a given resource_name
.
override_urls
¶
-
Api.override_urls(self):
A hook for adding your own URLs or overriding the default URLs. Useful for adding custom endpoints or overriding the built-in ones.
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
urls
¶
-
Api.urls(self):
Property
Provides URLconf details for the Api
and all registered
Resources
beneath it.
top_level
¶
-
Api.top_level(self, request, api_name=None):
A view that returns a serialized list of all resources registers
to the Api
. Useful for discovery.
Resource Fields¶
When designing an API, an important component is defining the representation
of the data you’re presenting. Like Django models, you can control the
representation of a Resource
using fields. There are a variety of fields
for various types of data.
Quick Start¶
For the impatient:
import datetime
from tastypie import fields
from tastypie.resources import Resource
from myapp.api.resources import ProfileResource, NoteResource
class PersonResource(Resource):
name = fields.CharField(attribute='name')
age = fields.IntegerField(attribute='years_old', null=True)
created = fields.DateTimeField(readonly=True, help_text='When the person was created', default=datetime.datetime.now)
is_active = fields.BooleanField(default=True)
profile = fields.ToOneField(ProfileResource, 'profile')
notes = fields.ToManyField(NoteResource, 'notes', full=True)
Standard Data Fields¶
All standard data fields have a common base class ApiField
which handles
the basic implementation details.
Note
You should not use the ApiField
class directly. Please use one of the
subclasses that is more correct for your data.
Common Field Options¶
All ApiField
objects accept the following options.
attribute
¶
-
ApiField.
attribute
¶
A string naming an instance attribute of the object wrapped by the Resource. The
attribute will be accessed during the dehydrate
or or written during the hydrate
.
Defaults to None
, meaning data will be manually accessed.
default
¶
-
ApiField.
default
¶
Provides default data when the object being dehydrated
/hydrated
has no data on
the field.
Defaults to tastypie.fields.NOT_PROVIDED
.
null
¶
-
ApiField.
null
¶
Indicates whether or not a None
is allowable data on the field. Defaults to
False
.
Field Types¶
DateField
¶
A date field.
DateTimeField
¶
A datetime field.
DecimalField
¶
A decimal field.
DictField
¶
A dictionary field.
FloatField
¶
A floating point field.
IntegerField
¶
An integer field.
Covers models.IntegerField
, models.PositiveIntegerField
,
models.PositiveSmallIntegerField
and models.SmallIntegerField
.
ListField
¶
A list field.
Relationship Fields¶
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.
Common Field Options¶
In addition to the common attributes for all ApiField, relationship fields accept the following.
Field Types¶
ToOneField
¶
Provides access to related data via foreign key.
This subclass requires Django’s ORM layer to work properly.
OneToOneField
¶
An alias to ToOneField
for those who prefer to mirror django.db.models
.
ForeignKey
¶
An alias to ToOneField
for those who prefer to mirror django.db.models
.
ToManyField
¶
Provides access to related data via a join table.
This subclass requires Django’s ORM layer to work properly.
This field also has special behavior when dealing with attribute
in that
it can take a callable. For instance, if you need to filter the reverse
relation, you can do something like:
subjects = fields.ToManyField(SubjectResource, ToManyField(SubjectResource, attribute=lambda bundle: Subject.objects.filter(notes=bundle.obj, name__startswith='Personal'))
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.
ManyToManyField
¶
An alias to ToManyField
for those who prefer to mirror django.db.models
.
OneToManyField
¶
An alias to ToManyField
for those who prefer to mirror django.db.models
.
Caching¶
When adding an API to your site, it’s important to understand that most consumers of the API will not be people, but instead machines. This means that the traditional “fetch-read-click” cycle is no longer measured in minutes but in seconds or milliseconds.
As such, caching is a very important part of the deployment of your API. Tastypie ships with two classes to make working with caching easier. These caches store at the object level, reducing access time on the database.
However, it’s worth noting that these do NOT cache serialized representations. For heavy traffic, we’d encourage the use of a caching proxy, especially Varnish, as it shines under this kind of usage. It’s far faster than Django views and already neatly handles most situations.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.cache import SimpleCache
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
cache = SimpleCache()
Caching Options¶
Tastypie ships with the following Cache
classes:
NoCache
¶
The no-op cache option, this does no caching but serves as an api-compatible plug. Very useful for development.
SimpleCache
¶
This option does basic object caching, attempting to find the object in the
cache & writing the object to the cache. It uses Django’s current
CACHE_BACKEND
to store cached data.
Implementing Your Own Cache¶
Implementing your own Cache
class is as simple as subclassing NoCache
and overriding the get
& set
methods. For example, a json-backed
cache might look like:
import json
from django.conf import settings
from tastypie.cache import NoCache
class JSONCache(NoCache):
def _load(self):
data_file = open(settings.TASTYPIE_JSON_CACHE, 'r')
return json.load(data_file)
def _save(self, data):
data_file = open(settings.TASTYPIE_JSON_CACHE, 'w')
return json.dump(data, data_file)
def get(self, key):
data = self._load()
return data.get(key, None)
def set(self, key, value, timeout=60):
data = self._load()
data[key] = value
self._save(data)
Note that this is NOT necessarily an optimal solution, but is simply
demonstrating how one might go about implementing your own Cache
.
Validation¶
Validation allows you to ensure that the data being submitted by the user is appropriate for storage. This can range from simple type checking on up to complex validation that compares different fields together.
If the data is valid, an empty dictionary is returned and processing continues as normal. If the data is invalid, a dictionary of error messages (keys being the field names, values being a list of error messages). This will be immediately returned to the user, serialized in the format they requested.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.validation import Validation
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
validation = Validation()
Validation Options¶
Tastypie ships with the following Validation
classes:
Validation
¶
The no-op validation option, the data submitted is always considered to be valid.
This is the default class hooked up to Resource/ModelResource
.
FormValidation
¶
A more complex form of validation, this class accepts a form_class
argument
to its constructor. You supply a Django Form
(or ModelForm
, though
save
will never get called) and Tastypie will verify the data
in the
Bundle
against the form.
Warning
Data in the bundle must line up with the fieldnames in the Form
. If they
do not, you’ll need to either munge the data or change your form.
Usage looks like:
from django import forms
class NoteForm(forms.Form):
title = forms.CharField(max_length=100)
slug = forms.CharField(max_length=50)
content = forms.CharField(required=False, widget=forms.Textarea)
is_active = forms.BooleanField()
form = FormValidation(form_class=NoteForm)
Implementing Your Own Validation¶
Implementing your own Validation
classes is a simple process. The
constructor can take whatever **kwargs
it needs (if any). The only other
method to implement is the is_valid
method:
from tastypie.validation import Validation
class AwesomeValidation(Validation):
def is_valid(self, bundle, request=None):
if not bundle.data:
return {'__all__': 'Not quite what I had in mind.'}
errors = {}
for key, value in bundle.data.items():
if not isinstance(value, basestring):
continue
if not 'awesome' in value:
errors[key] = ['NOT ENOUGH AWESOME. NEEDS MORE.']
return errors
Under this validation, every field that’s a string is checked for the word ‘awesome’. If it’s not in the string, it’s an error.
Authentication / Authorization¶
Authentication & authorization make up the components needed to verify that a certain user has access to the API and what they can do with it.
Authentication answers the question “can they see this data?” This usually involves requiring credentials, such as an API key or username/password.
Authorization answers the question “what objects can they modify?” This usually involves checking permissions, but is open to other implementations.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
Authentication Options¶
Tastypie ships with the following Authentication
classes:
Authentication
¶
The no-op authentication option, the client is always allowed through. Very useful for development and read-only APIs.
BasicAuthentication
¶
This authentication scheme uses HTTP Basic Auth to check a user’s credentials.
The username is their django.contrib.auth.models.User
username (assuming
it is present) and their password should also correspond to that entry.
Warning
If you’re using Apache & mod_wsgi
, you will need to enable
WSGIPassAuthorization On
. See this post for details.
ApiKeyAuthentication
¶
As an alternative to requiring sensitive data like a password, the
ApiKeyAuthentication
allows you to collect just username & a
machine-generated api key. Tastypie ships with a special Model
just for
this purpose, so you’ll need to ensure tastypie
is in INSTALLED_APPS
.
DigestAuthentication
¶
This authentication scheme uses HTTP Digest Auth to check a user’s
credentials. The username is their django.contrib.auth.models.User
username (assuming it is present) and their password should be their
machine-generated api key. As with ApiKeyAuthentication, tastypie
should be included in INSTALLED_APPS
.
Warning
If you’re using Apache & mod_wsgi
, you will need to enable
WSGIPassAuthorization On
. See this post for details (even though it
only mentions Basic auth).
Authorization Options¶
Tastypie ships with the following Authorization
classes:
Authorization
¶
The no-op authorization option, no permissions checks are performed.
Warning
This is a potentially dangerous option, as it means ANY recognized user can modify ANY data they encounter in the API. Be careful who you trust.
ReadOnlyAuthorization
¶
This authorization class only permits reading data, regardless of what the
Resource
might think is allowed. This is the default Authorization
class and the safe option.
DjangoAuthorization
¶
The most advanced form of authorization, this checks the permission a user
has granted to them (via django.contrib.auth.models.Permission
). In
conjunction with the admin, this is a very effective means of control.
Implementing Your Own Authentication/Authorization¶
Implementing your own Authentication/Authorization
classes is a simple
process. Authentication
has two methods to override (one of which is
optional but recommended to be customized) and Authorization
has just one
required method and one optional method:
from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
class SillyAuthentication(Authentication):
def is_authenticated(self, request, **kwargs):
if 'daniel' in request.user.username:
return True
return False
# Optional but recommended
def get_identifier(self, request):
return request.user.username
class SillyAuthorization(Authorization):
def is_authorized(self, request, object=None):
if request.user.date_joined.year == 2010:
return True
else:
return False
# Optional but useful for advanced limiting, such as per user.
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
return object_list.filter(author__username=request.user.username)
return object_list.none()
Under this scheme, only users with ‘daniel’ in their username will be allowed in, and only those who joined the site in 2010 will be allowed to affect data.
If the optional apply_limits
method is included, each user that fits the
above criteria will only be able to access their own records.
Serialization¶
Serialization can be one of the most contentious areas of an API. Everyone has their own requirements, their own preferred output format & the desire to have control over what is returned.
As a result, Tastypie ships with a serializer that tries to meet the basic needs of most use cases, and the flexibility to go outside of that when you need to.
The default Serializer
supports the following formats:
- json
- jsonp
- xml
- yaml
- html
Usage¶
Using this class is simple. It is the default option on all Resource
classes unless otherwise specified. The following code is a no-op, but
demonstrate how you could use your own serializer:
from django.contrib.auth.models import User
from tastypie.resources import ModelResource
from tastypie.serializers import Serializer
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
serializer = Serializer()
Implementing Your Own Serializer¶
There are several different use cases here. We’ll cover simple examples of wanting a tweaked format & adding a different format.
To tweak a format, simply override it’s to_<format>
& from_<format>
methods. So adding the server time to all output might look like so:
import time
from tastypie.serializers import Serializer
class CustomJSONSerializer(Serializer):
def to_json(self, data, options=None):
options = options or {}
data = self.to_simple(data, options)
# Add in the current time.
data['requested_time'] = time.time()
return simplejson.dumps(data, cls=json.DjangoJSONEncoder, sort_keys=True)
def from_json(self, content):
data = simplejson.loads(content)
if 'requested_time' in data:
# Log the request here...
pass
return data
In the case of adding a different format, let’s say you want to add a CSV
output option to the existing set. Your Serializer
subclass might look
like:
import csv
import StringIO
from tastypie.serializers import Serializer
class CSVSerializer(Serializer):
formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'csv']
content_types = {
'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'csv': 'text/csv',
}
def to_csv(self, data, options=None):
options = options or {}
data = self.to_simple(data, options)
raw_data = StringIO.StringIO()
# Untested, so this might not work exactly right.
for item in data:
writer = csv.DictWriter(raw_data, item.keys(), extrasaction='ignore')
writer.write(item)
return raw_data
def from_csv(self, content):
raw_data = StringIO.StringIO(content)
data = []
# Untested, so this might not work exactly right.
for item in csv.DictReader(raw_data):
data.append(item)
return data
Serializer
Methods¶
A swappable class for serialization.
This handles most types of data as well as the following output formats:
* json
* jsonp
* xml
* yaml
* html
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.
get_mime_for_format
¶
-
Serializer.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.
format_datetime
¶
-
Serializer.format_datetime(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”.
format_date
¶
-
Serializer.format_date(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”.
format_time
¶
-
Serializer.format_time(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”.
serialize
¶
-
Serializer.serialize(self, bundle, format='application/json', options={}):
Given some data and a format, calls the correct method to serialize the data and returns the result.
deserialize
¶
-
Serializer.deserialize(self, content, format='application/json'):
Given some data and a format, calls the correct method to deserialize the data and returns the result.
to_simple
¶
-
Serializer.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).
to_etree
¶
-
Serializer.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.
from_etree
¶
-
Serializer.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”.
to_json
¶
-
Serializer.to_json(self, data, options=None):
Given some Python data, produces JSON output.
from_json
¶
-
Serializer.from_json(self, content):
Given some JSON data, returns a Python dictionary of the decoded data.
to_jsonp
¶
-
Serializer.to_jsonp(self, data, options=None):
Given some Python data, produces JSON output wrapped in the provided callback.
from_xml
¶
-
Serializer.from_xml(self, content):
Given some XML data, returns a Python dictionary of the decoded data.
to_yaml
¶
-
Serializer.to_yaml(self, data, options=None):
Given some Python data, produces YAML output.
from_yaml
¶
-
Serializer.from_yaml(self, content):
Given some YAML data, returns a Python dictionary of the decoded data.
to_html
¶
-
Serializer.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.
from_html
¶
-
Serializer.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.
Throttling¶
Sometimes, the client on the other end may request data too frequently or you have a business use case that dictates that the client should be limited to a certain number of requests per hour.
For this, Tastypie includes throttling as a way to limit the number of requests in a timeframe.
Usage¶
To specify a throttle, add the Throttle
class to the Meta
class on the
Resource
:
from django.contrib.auth.models import User
from tastypie.resources import ModelResource
from tastypie.throttle import BaseThrottle
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
throttle = BaseThrottle(throttle_at=100)
Throttle Options¶
Each of the Throttle
classes accepts the following initialization
arguments:
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 thethrottle_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).
Tastypie ships with the following Throttle
classes:
BaseThrottle
¶
The no-op throttle option, this does no throttling but implements much of the common logic and serves as an api-compatible plug. Very useful for development.
CacheThrottle
¶
This uses just the cache to manage throttling. Fast but prone to cache misses and/or cache restarts.
CacheDBThrottle
¶
A write-through option that uses the cache first & foremost, but also writes through to the database to persist access times. Useful for logging client accesses & with RAM-only caches.
Implementing Your Own Throttle¶
Writing a Throttle
class is not quite as simple as the other components.
There are two important methods, should_be_throttled
& accessed
. The
should_be_throttled
method dictates whether or not the client should be
throttled. The accessed
method allows for the recording of the hit to the
API.
An example of a subclass might be:
import random
from tastypie.throttle import BaseThrottle
class RandomThrottle(BaseThrottle):
def should_be_throttled(self, identifier, **kwargs):
if random.randint(0, 10) % 2 == 0:
return True
return False
def accessed(self, identifier, **kwargs):
pass
This throttle class would pick a random number between 0 & 10. If the number is even, their request is allowed through; otherwise, their request is throttled & rejected.
Tastypie Cookbook¶
Adding Custom Values¶
You might encounter cases where you wish to include additional data in a
response which is not obtained from a field or method on your model. You can
easily extend the dehydrate()
method to
provide additional values:
class MyModelResource(Resource):
class Meta:
qs = MyModel.objects.all()
def dehydrate(self, bundle):
bundle.data['custom_field'] = "Whatever you want"
return bundle
Using Your Resource
In Regular Views¶
In addition to using your resource classes to power the API, you can also use them to write other parts of your application, such as your views. For instance, if you wanted to encode user information in the page for some Javascript’s use, you could do the following:
# views.py
from django.shortcuts import render_to_response
from myapp.api.resources import UserResource
def user_detail(request, username):
ur = UserResource()
user = ur.obj_get_detail(username=username)
# Other things get prepped to go into the context then...
return render_to_response('myapp/user_detail.html', {
# Other things here.
"user_json": ur.serialize(None, ur.full_dehydrate(obj=user), 'application/json'),
})
Using Non-PK Data For Your URLs¶
By convention, ModelResource``s usually expose the detail endpoints utilizing
the primary key of the ``Model
they represent. However, this is not a strict
requirement. Each URL can take other named URLconf parameters that can be used
for the lookup.
For example, if you want to expose User
resources by username, you can do
something like the following:
# myapp/api/resources.py
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<username>[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]
The added URLconf matches before the standard URLconf included by default & matches on the username provided in the URL.
Nested Resources¶
You can also do “nested resources” (resources within another related resource)
by lightly overriding the override_urls
method & adding on a new method to
handle the children:
class ParentResource(ModelResource):
children = fields.ToManyField(ChildResource, 'children')
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)/children%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_children'), name="api_get_children"),
]
def get_children(self, request, **kwargs):
try:
obj = self.cached_obj_get(request=request, **self.remove_api_resource_names(kwargs))
except ObjectDoesNotExist:
return HttpGone()
except MultipleObjectsReturned:
return HttpMultipleChoices("More than one resource is found at this URI.")
child_resource = ChildResource()
return child_resource.get_detail(request, parent_id=obj.pk)
Another alternative approach is to override the dispatch
method:
# myapp/api/resources.py
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
def dispatch(self, request_type, request, **kwargs):
username = kwargs.pop('username')
kwargs['user'] = get_object_or_404(User, username=username)
return super(EntryResource, self).dispatch(request_type, request, **kwargs)
# urls.py
from django.conf.urls.defaults import *
from myapp.api import EntryResource
entry_resource = EntryResource()
urlpatterns = patterns('',
# The normal jazz here, then...
(r'^api/(?P<username>\w+)/', include(entry_resource.urls)),
)
Adding Search Functionality¶
Another common request is being able to integrate search functionality. This
approach uses Haystack, though you could hook it up to any search technology.
We leave the CRUD methods of the resource alone, choosing to add a new endpoint
at /api/v1/notes/search/
:
from django.conf.urls.defaults import *
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from haystack.query import SearchQuerySet
from tastypie.resources import ModelResource
from tastypie.utils import trailing_slash
from notes.models import Note
class NoteResource(ModelResource):
class Meta:
queryset = Note.objects.all()
resource_name = 'notes'
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/search%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_search'), name="api_get_search"),
]
def get_search(self, request, **kwargs):
self.method_check(request, allowed=['get'])
self.is_authenticated(request)
self.throttle_check(request)
# Do the query.
sqs = SearchQuerySet().models(Note).load_all().auto_query(request.GET.get('q', ''))
paginator = Paginator(sqs, 20)
try:
page = paginator.page(int(request.GET.get('page', 1)))
except InvalidPage:
raise Http404("Sorry, no results on that page.")
objects = []
for result in page.object_list:
bundle = self.full_dehydrate(result.object)
objects.append(bundle)
object_list = {
'objects': objects,
}
self.log_throttled_access(request)
return self.create_response(request, object_list)
Creating per-user resources¶
One might want to create an API which will require every user to authenticate and every user will be working only with objects associated with him. Let’s see how to implement it for two basic operations: listing and creation of an object.
For listing we want to list only objects for which ‘user’ field matches
‘request.user’. This could be done my applying filter in apply_authorization_limits
method of your resource.
For creating we’d have to wrap obj_create
method of ModelResource
. Then the
resulting code will look something like:
# myapp/api/resources.py
class EnvironmentResource(ModelResource):
class Meta:
queryset = Environment.objects.all()
resource_name = 'environment'
list_allowed_methods = ['get', 'post']
authentication = ApiKeyAuthentication()
authorization = Authorization()
def obj_create(self, bundle, request=None, **kwargs):
return super(EnvironmentResource, self).obj_create(bundle, request, user=request.user)
def apply_authorization_limits(self, request, object_list):
return object_list.filter(user=request.user)
Debugging Tastypie¶
There are some common problems people run into when using Tastypie for the first time. Some of the common problems and things to try appear below.
“I’m getting XML output in my browser but I want JSON output!”¶
This is actually not a bug and JSON support is present in your Resource
.
This issue is that Tastypie respects the Accept
header your browser sends.
Most browsers send something like:
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Note that application/xml
comes first, which is a format that Tastypie
handles by default, hence why you receive XML.
If you use curl
from the command line, you should receive JSON by default:
curl http://localhost:8000/api/v1/
If you want JSON in the browser, simply append ?format=json
to your URL.
Tastypie always respects this override first, before it falls back to the
Accept
header.
Sites Using Tastypie¶
The following sites are a partial list of people using Tastypie. I’m always
interested in adding more sites, so please find me (daniellindsley
) via
IRC or start a mailing list thread.
LJWorld Marketplace¶
Forkinit¶
Read-only API access to recipes.
Read The Docs¶
A hosted documentation site, primarily for Python docs. General purpose read-write access.
Luzme¶
An e-book search site that lets you fetch pricing information.
Getting Help¶
There are two primary ways of getting help. We have a mailing list hosted at Google (http://groups.google.com/group/django-tastypie/) and an IRC channel (#tastypie on irc.freenode.net) to get help, want to bounce idea or generally shoot the breeze.
Quick Start¶
Add
tastypie
toINSTALLED_APPS
.Create an
api
directory in your app with a bare__init__.py
.Create an
<my_app>/api/resources.py
file and place the following in it:from tastypie.resources import ModelResource from my_app.models import MyModel class MyModelResource(ModelResource): class Meta: queryset = MyModel.objects.all() allowed_methods = ['get']
In your root URLconf, add the following code (around where the admin code might be):
from tastypie.api import Api from my_app.api.resources import MyModelResource v1_api = Api(api_name='v1') v1_api.register(MyModelResource()) urlpatterns = patterns('', # ...more URLconf bits here... # Then add: (r'^api/', include(v1_api.urls)), )
Hit http://localhost:8000/api/v1/?format=json in your browser!
Requirements¶
Tastypie requires the following modules. If you use Pip, you can install
the necessary bits via the included requirements.txt
:
- Python 2.4+
- Django 1.0+
- mimeparse 0.1.3+ (http://code.google.com/p/mimeparse/)
- Older versions will work, but their behavior on JSON/JSONP is a touch wonky.
- dateutil (http://labix.org/python-dateutil)
- lxml (http://codespeak.net/lxml/) if using the XML serializer
- pyyaml (http://pyyaml.org/) if using the YAML serializer
If you choose to use Python 2.4, be warned that you will also need to grab the following modules:
- uuid (present in 2.5+, downloadable from http://pypi.python.org/pypi/uuid/) if using the
ApiKey
authentication
Getting Started with Tastypie¶
Tastypie is a reusable app (that is, it relies only on it’s own code and focuses on providing just a REST-style API) and is suitable for providing an API to any application without having to modify the sources of that app.
Not everyone’s needs are the same, so Tastypie goes out of its way to provide plenty of hooks for overriding or extending how it works.
Note
If you hit a stumbling block, you can join #tastypie on irc.freenode.net to get help.
This tutorial assumes that you have a basic understanding of Django as well as how proper REST-style APIs ought to work. We will only explain the portions of the code that are Tastypie-specific in any kind of depth.
For example purposes, we’ll be adding an API to a simple blog application.
Here is myapp/models.py
:
import datetime
from django.contrib.auth.models import User
from django.db import models
from django.template.defaultfilters import slugify
class Entry(models.Model):
user = models.ForeignKey(User)
pub_date = models.DateTimeField(default=datetime.datetime.now)
title = models.CharField(max_length=200)
slug = models.SlugField()
body = models.TextField()
def __unicode__(self):
return self.title
def save(self, *args, **kwargs):
# For automatic slug generation.
if not self.slug:
self.slug = slugify(self.title)[:50]
return super(Entry, self).save(*args, **kwargs)
With that, we’ll move on to installing and configuring Tastypie.
Installation¶
Installing Tastypie is as simple as checking out the source and adding it to
your project or PYTHONPATH
.
- Download the dependencies:
- Python 2.4+
- Django 1.0+ (tested on Django 1.1+)
mimeparse
0.1.3+ (http://code.google.com/p/mimeparse/)
- Older versions will work, but their behavior on JSON/JSONP is a touch wonky.
dateutil
(http://labix.org/python-dateutil)- OPTIONAL -
lxml
(http://codespeak.net/lxml/) if using the XML serializer- OPTIONAL -
pyyaml
(http://pyyaml.org/) if using the YAML serializer- OPTIONAL -
uuid
(present in 2.5+, downloadable from http://pypi.python.org/pypi/uuid/) if using theApiKey
authentication
Configuration¶
The only mandatory configuration is adding 'tastypie'
to your
INSTALLED_APPS
. This isn’t strictly necessary, as Tastypie has only one
non-required model, but may ease usage.
You have the option to set up a number of settings (see Tastypie Settings) but they all have sane defaults and are not required unless you need to tweak their values.
Creating Resources¶
REST-style architecture talks about resources, so unsurprisingly integrating
with Tastypie involves creating Resource
classes.
For our simple application, we’ll create a file for these in myapp/api.py
,
though they can live anywhere in your application:
# myapp/api.py
from tastypie.resources import ModelResource
from myapp.models import Entry
class EntryResource(ModelResource):
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
This class, by virtue of being a ModelResource
subclass, will introspect all non-relational fields on the Entry
model and
create it’s own ApiFields
that map to those fields,
much like the way Django’s ModelForm
class introspects.
Note
The resource_name
within the Meta
class is optional. If not
provided, it is automatically generated off the classname, removing any
instances of Resource
and lowercasing the string. So
EntryResource
would become just entry
.
We’ve included the resource_name
attribute in this example for clarity,
especially when looking at the URLs, but you should feel free to omit it if
you’re comfortable with the automatic behavior.
Hooking Up The Resource(s)¶
Now that we have our EntryResource
, we can hook it up in our URLconf. To
do this, we simply instantiate the resource in our URLconf and hook up its
urls
:
# urls.py
from django.conf.urls.defaults import *
from myapp.api import EntryResource
entry_resource = EntryResource()
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(entry_resource.urls)),
)
Now it’s just a matter of firing up server (./manage.py runserver
) and
going to http://127.0.0.1:8000/api/entry/?format=json. You should get back a
list of Entry
-like objects.
Note
The ?format=json
is an override required to make things look decent
in the browser (accept headers vary between browsers). Tastypie properly
handles the Accept
header. So the following will work properly:
curl -H "Accept: application/json" http://127.0.0.1:8000/api/entry/
But if you’re sure you want something else (or want to test in a browser),
Tastypie lets you specify ?format=...
when you really want to force
a certain type.
At this point, a bunch of other URLs are also available. Try out any/all of the following (assuming you have at least three records in the database):
With just seven lines of code, we have a full working REST interface to our
Entry
model. In addition, full GET/POST/PUT/DELETE support is already
there, so it’s possible to really work with all of the data. Well, almost.
You see, you’ll note that not quite all of our data is there. Markedly absent
is the user
field, which is a ForeignKey
to Django’s User
model.
Tastypie does NOT introspect related data because it has no way to know
how you want to represent that data.
And since that relation isn’t there, any attempt to POST/PUT new data will
fail, because no user
is present, which is a required field on the model.
This is easy to fix, but we’ll need to flesh out our API a little more.
Creating More Resources¶
In order to handle our user
relation, we’ll need to create a
UserResource
and tell the EntryResource
to use it. So we’ll modify
myapp/api.py
to match the following code:
# myapp/api.py
from django.contrib.auth.models import User
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
We simply created a new ModelResource
subclass
called UserResource
. Then we added a field to EntryResource
that
specified that the user
field points to a UserResource
for that data.
Now we should be able to get all of the fields back in our response. But since we have another full, working resource on our hands, we should hook that up to our API as well. And there’s a better way to do it.
Adding To The Api¶
Tastypie ships with an Api
class, which lets you bind
multiple Resources
together to form a
coherent API. Adding it to the mix is simple.
We’ll go back to our URLconf (urls.py
) and change it to match the
following:
# urls.py
from django.conf.urls.defaults import *
from tastypie.api import Api
from myapp.api import EntryResource, UserResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(v1_api.urls)),
)
Note that we’re now creating an Api
instance,
registering our EntryResource
and UserResource
instances with it and
that we’ve modified the urls to now point to v1_api.urls
.
This makes even more data accessible, so if we start up the runserver
again, the following URLs should work:
- http://127.0.0.1:8000/api/v1/?format=json
- http://127.0.0.1:8000/api/v1/user/?format=json
- http://127.0.0.1:8000/api/v1/user/1/?format=json
- http://127.0.0.1:8000/api/v1/user/schema/?format=json
- http://127.0.0.1:8000/api/v1/user/set/1;3/?format=json
- http://127.0.0.1:8000/api/v1/entry/?format=json
- http://127.0.0.1:8000/api/v1/entry/1/?format=json
- http://127.0.0.1:8000/api/v1/entry/schema/?format=json
- http://127.0.0.1:8000/api/v1/entry/set/1;3/?format=json
Additionally, the representations out of EntryResource
will now include
the user
field and point to an endpoint like /api/v1/users/1/
to access
that user’s data. And full POST/PUT delete support should now work.
But there’s several new problems. One is that our new UserResource
leaks
too much data, including fields like email
, password
, is_active
and
is_staff
. Another is that we may not want to allow end users to alter
User
data. Both of these problems are easily fixed as well.
Limiting Data And Access¶
Cutting out the email
, password
, is_active
and is_staff
fields
is easy to do. We simply modify our UserResource
code to match the
following:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
The excludes
directive tells UserResource
which fields not to include
in the output. If you’d rather whitelist fields, you could do:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
fields = ['username', 'first_name', 'last_name', 'last_login']
Now that the undesirable fields are no longer included, we can look at limiting
access. This is also easy and involves making our UserResource
look like:
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
allowed_methods = ['get']
Now only HTTP GET requests will be allowed on /api/v1/user/
endpoints. If
you require more granular control, both list_allowed_methods
and
detail_allowed_methods
options are supported.
Beyond The Basics¶
We now have a full working API for our application. But Tastypie supports many more features, like:
- Authentication / Authorization
- Caching
- Throttling
- Resources (filtering & sorting)
- Serialization
Tastypie is also very easy to override and extend. For some common patterns and approaches, you should refer to the Tastypie Cookbook documentation.
Interacting With The API¶
Now that you’ve got a shiny new REST-style API in place, let’s demonstrate how to interact with it. We’ll assume that you have cURL installed on your system (generally available on most modern Mac & Linux machines), but any tool that allows you to control headers & bodies on requests will do.
We’ll assume that we’re interacting with the following Tastypie code:
# myapp/api/resources.py
from django.contrib.auth.models import User
from tastypie.authorization import Authorization
from tastypie import fields
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'user'
excludes = ['email', 'password', 'is_active', 'is_staff', 'is_superuser']
filtering = {
'username': ALL,
}
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
authorization = Authorization()
filtering = {
'user': ALL_WITH_RELATIONS,
'pub_date': ['exact', 'lt', 'lte', 'gte', 'gt'],
}
# urls.py
from django.conf.urls.defaults import *
from tastypie.api import Api
from myapp.api.resources import EntryResource, UserResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
urlpatterns = patterns('',
# The normal jazz here...
(r'^blog/', include('myapp.urls')),
(r'^api/', include(v1_api.urls)),
)
Let’s fire up a shell & start exploring the API!
Front Matter¶
Tastypie tries to treat all clients & all serialization types as equally as
possible. It also tries to be a good ‘Net citizen & respects the HTTP method
used as well as the Accepts
headers sent. Between these two, you control
all interactions with Tastypie through relatively few endpoints.
Warning
Should you try these URLs in your browser, be warned you WILL need to
append ?format=json
(or xml
or yaml
) to the URL. Your browser
requests application/xml
before application/json
, so you’ll always
get back XML if you don’t specify it.
That’s also why it’s recommended that you explore via curl, because you avoid your browser’s opinionated requests & get something closer to what any programmatic clients will get.
Fetching Data¶
Since reading data out of an API is a very common activity (and the easiest type of request to make), we’ll start there. Tastypie tries to expose various parts of the API & interlink things within the API (HATEOAS).
Api-Wide¶
We’ll start at the highest level:
curl http://localhost:8000/api/v1/
You’ll get back something like:
{
"entry": {
"list_endpoint": "/api/v1/entry/",
"schema": "/api/v1/entry/schema/"
},
"user": {
"list_endpoint": "/api/v1/user/",
"schema": "/api/v1/user/schema/"
}
}
This lists out all the different Resource
classes you registered in your
URLconf with the API. Each one is listed by the resource_name
you gave it
and provides the list_endpoint
& the schema
for the resource.
Note that these links try to direct you to other parts of the API, to make exploration/discovery easier. We’ll use these URLs in the next several sections.
To demonstrate another format, you could run the following to get the XML variant of the same information:
curl -H "Accept: application/xml" http://localhost:8000/api/v1/
To which you’d receive:
<?xml version="1.0" encoding="utf-8"?>
<response>
<entry type="hash">
<list_endpoint>/api/v1/entry/</list_endpoint>
<schema>/api/v1/entry/schema/</schema>
</entry>
<user type="hash">
<list_endpoint>/api/v1/user/</list_endpoint>
<schema>/api/v1/user/schema/</schema>
</user>
</response>
We’ll stick to JSON for the rest of this document, but using XML should be OK to do at any time.
Inspecting The Resource’s Schema¶
Since the api-wide view gave us a schema
URL, let’s inspect that next.
We’ll use the entry
resource. Again, a simple GET request by curl:
curl http://localhost:8000/api/v1/entry/schema/
This time, we get back a lot more data:
{
"default_format": "application/json",
"fields": {
"body": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"id": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"pub_date": {
"help_text": "A date & time as a string. Ex: \"2010-11-10T03:07:43\"",
"nullable": false,
"readonly": false,
"type": "datetime"
},
"resource_uri": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": true,
"type": "string"
},
"slug": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"title": {
"help_text": "Unicode string data. Ex: \"Hello World\"",
"nullable": false,
"readonly": false,
"type": "string"
},
"user": {
"help_text": "A single related resource. Can be either a URI or set of nested resource data.",
"nullable": false,
"readonly": false,
"type": "related"
}
},
"filtering": {
"pub_date": ["exact", "lt", "lte", "gte", "gt"],
"user": 2
}
}
This lists out the default_format
this resource responds with, the
fields
on the resource & the filtering
options available. This
information can be used to prepare the other aspects of the code for the
data it can obtain & ways to filter the resources.
Getting A Collection Of Resources¶
Let’s get down to fetching live data. From the api-wide view, we’ll hit
the list_endpoint
for entry
:
curl http://localhost:8000/api/v1/entry/
We get back data that looks like:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 3
},
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "Well, it's been awhile and I still haven't updated. ",
"id": "2",
"pub_date": "2011-05-21T00:46:58",
"resource_uri": "/api/v1/entry/2/",
"slug": "second-post",
"title": "Second Post",
"user": "/api/v1/user/1/"
},
{
"body": "I'm really excited to get started with this new blog. It's gonna be great!",
"id": "3",
"pub_date": "2011-05-20T00:47:30",
"resource_uri": "/api/v1/entry/3/",
"slug": "my-blog",
"title": "My Blog",
"user": "/api/v1/user/2/"
}]
}
Some things to note:
- By default, you get a paginated set of objects (20 per page is the default).
- In the
meta
, you get aprevious
&next
. If available, these are URIs to the previous & next pages.- You get a list of resources/objects under the
objects
key.- Each resources/object has a
resource_uri
field that points to the detail view for that object.- The foreign key to
User
is represented as a URI by default. If you’re looking for the fullUserResource
to be embedded in this view, you’ll need to addfull=True
to thefields.ToOneField
.
If you want to skip paginating, simply run:
curl http://localhost:8000/api/v1/entry/?limit=0
Be warned this will return all objects, so it may be a CPU/IO-heavy operation on large datasets.
Let’s try filtering on the resource. Since we know we can filter on the
user
, we’ll fetch all posts by the daniel
user with:
curl http://localhost:8000/api/v1/entry/?user__username=daniel
We get back what we asked for:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 2
},
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "Well, it's been awhile and I still haven't updated. ",
"id": "2",
"pub_date": "2011-05-21T00:46:58",
"resource_uri": "/api/v1/entry/2/",
"slug": "second-post",
"title": "Second Post",
"user": "/api/v1/user/1/"
}]
}
Where there were three posts before, now there are only two.
Getting A Detail Resource¶
Since each resource/object in the list view had a resource_uri
, let’s
explore what’s there:
curl http://localhost:8000/api/v1/entry/1/
We get back a similar set of data that we received from the list view:
{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
}
Where this proves useful (for example) is present in the data we got back. We
know the URI of the User
associated with this blog entry. Let’s run:
curl http://localhost:8000/api/v1/user/1/
Without ever seeing any aspect of the UserResource
& just following the URI
given, we get back:
{
"date_joined": "2011-05-20T00:42:14.990617",
"first_name": "",
"id": "1",
"last_login": "2011-05-20T00:44:57.510066",
"last_name": "",
"resource_uri": "/api/v1/user/1/",
"username": "daniel"
}
Selecting A Subset Of Resources¶
Sometimes you may want back more than one record, but not an entire list view
nor do you want to do multiple requests. Tastypie includes a “set” view, which
lets you cherry-pick the objects you want. For example, if we just want the
first & third Entry
resources, we’d run:
curl "http://localhost:8000/api/v1/entry/set/1;3/"
Note
Quotes are needed in this case because of the semicolon delimiter between primary keys. Without the quotes, bash tries to split it into two statements. No extraordinary quoting will be necessary in your application (unless your API client is written in bash :D).
And we get back just those two objects:
{
"objects": [{
"body": "Welcome to my blog!",
"id": "1",
"pub_date": "2011-05-20T00:46:38",
"resource_uri": "/api/v1/entry/1/",
"slug": "first-post",
"title": "First Post",
"user": "/api/v1/user/1/"
},
{
"body": "I'm really excited to get started with this new blog. It's gonna be great!",
"id": "3",
"pub_date": "2011-05-20T00:47:30",
"resource_uri": "/api/v1/entry/3/",
"slug": "my-blog",
"title": "My Blog",
"user": "/api/v1/user/2/"
}]
}
Note that, like the list view, you get back a list of objects
. Unlike the
list view, there is NO pagination applied to these objects. You asked for
them, you’re going to get them all.
Sending Data¶
Tastypie also gives you full write capabilities in the API. Since the
EntryResource
has the no-limits Authentication
& Authorization
on
it, we can freely write data.
Warning
Note that this is a huge security hole as well. Don’t put unauthorized write-enabled resources on the Internet, because someone will trash your data.
This is why ReadOnlyAuthorization
is the default in Tastypie & why you
must override to provide more access.
The good news is that there are no new URLs to learn. The “list” & “detail”
URLs we’ve been using to fetch data ALSO support the
POST
/PUT
/DELETE
HTTP methods.
Creating A New Resource (POST)¶
Let’s add a new entry. To create new data, we’ll switch from GET
requests
to the familiar POST
request.
To create new resources/objects, you will POST
to the list endpoint of
a resource. Trying to POST
to a detail endpoint has a different meaning in
the REST mindset (meaning to add a resource as a child of a resource of the
same type).
As with all Tastypie requests, the headers we request are important. Since we’ve been using primarily JSON throughout, let’s send a new entry in JSON format:
curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post", "user": "/api/v1/user/1/"}' http://localhost:8000/api/v1/entry/
The Content-Type
header here informs Tastypie that we’re sending it JSON.
We send the data as a JSON-serialized body (NOT as form-data in the form of
URL parameters). What we get back is the following response:
HTTP/1.0 201 CREATED
Date: Fri, 20 May 2011 06:48:36 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Location: http://localhost:8000/api/v1/entry/4/
You’ll also note that we get a correct HTTP status code back (201) & a
Location
header, which gives us the URI to our newly created resource.
Passing --dump-header -
is important, because it gives you all the headers
as well as the status code. When things go wrong, this will be useful
information to help with debugging. For instance, if we send a request without
a user
:
curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"body": "This will prbbly be my lst post.", "pub_date": "2011-05-22T00:46:38", "slug": "another-post", "title": "Another Post"}' http://localhost:8000/api/v1/entry/
We get back:
HTTP/1.0 400 BAD REQUEST
Date: Fri, 20 May 2011 06:53:02 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
The 'user' field has no data and doesn't allow a default or null value.
Updating An Existing Resource (PUT)¶
You might have noticed that we made some typos when we submitted the POST
request. We can fix this using a PUT
request to the detail endpoint (modify
this instance of a resource).
curl –dump-header - -H “Content-Type: application/json” -X PUT –data ‘{“body”: “This will probably be my last post.”, “pub_date”: “2011-05-22T00:46:38”, “slug”: “another-post”, “title”: “Another Post”, “user”: “/api/v1/user/1/”}’ http://localhost:8000/api/v1/entry/4/
After fixing up the body
, we get back:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:13:21 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
We get a 204 status code, meaning our update was successful. We don’t get
a Location
header back because we did the PUT
on a detail URL, which
presumably did not change.
Updating A Whole Collection Of Resources (PUT)¶
You can also, in rare circumstances, update an entire collection of objects.
By sending a PUT
request to the list view of a resource, you can replace
the entire collection.
Warning
This deletes all of the objects first, then creates the objects afresh. This is done because determining which objects are the same is actually difficult to get correct in the general case for all people.
Send a request like:
curl --dump-header - -H "Content-Type: application/json" -X PUT --data '{"objects": [{"body": "Welcome to my blog!","id": "1","pub_date": "2011-05-20T00:46:38","resource_uri": "/api/v1/entry/1/","slug": "first-post","title": "First Post","user": "/api/v1/user/1/"},{"body": "I'm really excited to get started with this new blog. It's gonna be great!","id": "3","pub_date": "2011-05-20T00:47:30","resource_uri": "/api/v1/entry/3/","slug": "my-blog","title": "My Blog","user": "/api/v1/user/2/"}]}' http://localhost:8000/api/v1/entry/
And you’ll get back a response like:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:13:21 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
Deleting Data¶
No CRUD setup would be complete without the ability to delete resources/objects.
Deleting also requires significantly less complicated requests than
POST
/PUT
.
Deleting A Single Resource¶
We’ve decided that we don’t like the entry we added & edited earlier. Let’s delete it (but leave the other objects alone):
curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/4/
Once again, we get back the “Accepted” response of a 204:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:28:01 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
If we request that resource, we get a 410 to show it’s no longer there:
curl --dump-header - http://localhost:8000/api/v1/entry/4/
HTTP/1.0 410 GONE
Date: Fri, 20 May 2011 07:29:02 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Additionally, if we try to run the DELETE
again (using the same original
command), we get the “Gone” response again:
HTTP/1.0 410 GONE
Date: Fri, 20 May 2011 07:30:00 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Type: text/html; charset=utf-8
Deleting A Whole Collection Of Resources¶
Finally, it’s possible to remove an entire collection of resources. This is
as destructive as it sounds. Once again, we use the DELETE
method, this
time on the entire list endpoint:
curl --dump-header - -H "Content-Type: application/json" -X DELETE http://localhost:8000/api/v1/entry/
As a response, we get:
HTTP/1.0 204 NO CONTENT
Date: Fri, 20 May 2011 07:32:51 GMT
Server: WSGIServer/0.1 Python/2.7
Content-Length: 0
Content-Type: text/html; charset=utf-8
Hitting the list view:
curl --dump-header - http://localhost:8000/api/v1/entry/
Gives us a 200 but no objects:
{
"meta": {
"limit": 20,
"next": null,
"offset": 0,
"previous": null,
"total_count": 0
},
"objects": []
}
You Did It!¶
That’s a whirlwind tour of interacting with a Tastypie API. There’s additional functionality present, such as:
POST
/PUT
the other supported content-types- More filtering/
order_by
/limit
/offset
tricks - Using overridden URLconfs to support complex or non-PK lookups
- Authentication
But this grounds you in the basics & hopefully clarifies usage/debugging better.
Tastypie Settings¶
This is a comprehensive list of the settings Tastypie recognizes.
API_LIMIT_PER_PAGE
¶
Optional
This setting controls the default number of records Tastypie will show in a list view.
This is only used when a user does not specify a limit
GET parameter and
the Resource
subclass has not overridden the number to be shown.
An example:
API_LIMIT_PER_PAGE = 50
Defaults to 20.
TASTYPIE_FULL_DEBUG
¶
Optional
This setting controls what the behavior is when an unhandled exception occurs.
If set to True
and settings.DEBUG = True
, the standard Django
technical 500 is displayed.
If not set or set to False
, Tastypie will return a serialized response.
If settings.DEBUG
is True
, you’ll get the actual exception message plus
a traceback. If settings.DEBUG
is False
, Tastypie will call
mail_admins()
and provide a canned error message (which you can override
with TASTYPIE_CANNED_ERROR
) in the response.
An example:
TASTYPIE_FULL_DEBUG = True
Defaults to False
.
TASTYPIE_CANNED_ERROR
¶
Optional
This setting allows you to override the canned error response when an
unhandled exception is raised and settings.DEBUG
is False
.
An example:
TASTYPIE_CANNED_ERROR = "Oops, we broke it!"
Defaults to "Sorry, this request could not be processed. Please try again later."
.
TASTYPIE_ALLOW_MISSING_SLASH
¶
Optional
This setting allows your URLs to be missing the final slash. Useful for integrating with other systems.
You must also have settings.APPEND_SLASH = False
so that Django does not
emit HTTP 302 redirects.
Warning
This setting causes the Resource.get_multiple()
method to fail. If you
need this method, you will have to override the URLconf to meet your needs.
An example:
TASTYPIE_ALLOW_MISSING_SLASH = True
Defaults to False
.
TASTYPIE_DATETIME_FORMATTING
¶
Optional
This setting allows you to globally choose what format your datetime/date/time
data will be formatted in. Valid options are iso-8601
& rfc-2822
.
An example:
TASTYPIE_DATETIME_FORMATTING = 'rfc-2822'
Defaults to iso-8601
.
Using Tastypie With Non-ORM Data Sources¶
Much of this documentation demonstrates the use of Tastypie with Django’s ORM. You might think that Tastypie depended on the ORM, when in fact, it was purpose-built to handle non-ORM data. This documentation should help you get started providing APIs using other data sources.
Virtually all of the code that makes Tastypie actually process requests &
return data is within the Resource
class. ModelResource
is actually a
light wrapper around Resource
that provides ORM-specific access. The
methods that ModelResource
overrides are the same ones you’ll need to
override when hooking up your data source.
Approach¶
When working with Resource
, many things are handled for you. All the
authentication/authorization/caching/serialization/throttling bits should work
as normal and Tastypie can support all the REST-style methods. Schemas &
discovery views all work the same as well.
What you don’t get out of the box are the fields you’re choosing to expose & the lowest level data access methods. If you want a full read-write API, there are nine methods you need to implement. They are:
get_resource_uri
get_object_list
obj_get_list
obj_get
obj_create
obj_update
obj_delete_list
obj_delete
rollback
If read-only is all you’re exposing, you can cut that down to four methods to override.
Using Riak for MessageResource¶
As an example, we’ll take integrating with Riak (a Dynamo-like NoSQL store) since it has both a simple API and demonstrate what hooking up to a non-relational datastore looks like:
# We need a generic object to shove data in/get data from.
# Riak generally just tosses around dictionaries, so we'll lightly
# wrap that.
class RiakObject(object):
def __init__(self, initial=None):
self.__dict__['_data'] = {}
if hasattr(initial, 'items'):
self.__dict__['_data'] = initial
def __getattr__(self, name):
return self._data.get(name, None)
def __setattr__(self, name, value):
self.__dict__['_data'][name] = value
def to_dict(self):
return self._data
class MessageResource(Resource):
# Just like a Django ``Form`` or ``Model``, we're defining all the
# fields we're going to handle with the API here.
uuid = fields.CharField(attribute='uuid')
user_uuid = fields.CharField(attribute='user_uuid')
message = fields.CharField(attribute='message')
created = fields.IntegerField(attribute='created')
class Meta:
resource_name = 'riak'
object_class = RiakObject
authorization = Authorization()
# Specific to this resource, just to get the needed Riak bits.
def _client(self):
return riak.RiakClient()
def _bucket(self):
client = self._client()
# Note that we're hard-coding the bucket to use. Fine for
# example purposes, but you'll want to abstract this.
return client.bucket('messages')
# The following methods will need overriding regardless of your
# data source.
def get_resource_uri(self, bundle_or_obj):
kwargs = {
'resource_name': self._meta.resource_name,
}
if isinstance(bundle_or_obj, Bundle):
kwargs['pk'] = bundle_or_obj.obj.uuid
else:
kwargs['pk'] = bundle_or_obj.uuid
if self._meta.api_name is not None:
kwargs['api_name'] = self._meta.api_name
return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
def get_object_list(self, request):
query = self._client().add('messages')
query.map("function(v) { var data = JSON.parse(v.values[0].data); return [[v.key, data]]; }")
results = []
for result in query.run():
new_obj = RiakObject(initial=result[1])
new_obj.uuid = result[0]
results.append(new_obj)
return results
def obj_get_list(self, request=None, **kwargs):
# Filtering disabled for brevity...
return self.get_object_list(request)
def obj_get(self, request=None, **kwargs):
bucket = self._bucket()
message = bucket.get(kwargs['pk'])
return RiakObject(initial=message.get_data())
def obj_create(self, bundle, request=None, **kwargs):
bundle.obj = RiakObject(initial=kwargs)
bundle = self.full_hydrate(bundle)
bucket = self._bucket()
new_message = bucket.new(bundle.obj.uuid, data=bundle.obj.to_dict())
new_message.store()
return bundle
def obj_update(self, bundle, request=None, **kwargs):
return self.obj_create(bundle, request, **kwargs)
def obj_delete_list(self, request=None, **kwargs):
bucket = self._bucket()
for key in bucket.get_keys():
obj = bucket.get(key)
obj.delete()
def obj_delete(self, request=None, **kwargs):
bucket = self._bucket()
obj = bucket.get(kwargs['pk'])
obj.delete()
def rollback(self, bundles):
pass
This represents a full, working, Riak-powered API endpoint. All REST-style actions (GET/POST/PUT/DELETE) all work correctly. The only shortcut taken in this example was skipping filter-abilty, as adding in the MapReduce bits would have decreased readability.
All said and done, just nine methods needed overriding, eight of which were highly specific to how data access is done.
Resources¶
In terms of a REST-style architecture, a “resource” is a collection of similar
data. This data could be a table of a database, a collection of other resources
or a similar form of data storage. In Tastypie, these resources are generally
intermediaries between the end user & objects, usually Django models. As such,
Resource
(and its model-specific twin ModelResource
) form the heart of
Tastypie’s functionality.
Quick Start¶
A sample resource definition might look something like:
from django.contrib.auth.models import User
from tastypie import fields
from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from myapp.models import Entry
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
list_allowed_methods = ['get', 'post']
detail_allowed_methods = ['get', 'post', 'put', 'delete']
resource_name = 'myapp/entry'
authorization = DjangoAuthorization()
filtering = {
'slug': ALL,
'user': ALL_WITH_RELATIONS,
'created': ['exact', 'range', 'gt', 'gte', 'lt', 'lte'],
}
Why Class-Based?¶
Using class-based resources make it easier to extend/modify the code to meet your needs. APIs are rarely a one-size-fits-all problem space, so Tastypie tries to get the fundamentals right and provide you with enough hooks to customize things to work your way.
As is standard, this raises potential problems for thread-safety. Tastypie has been designed to minimize the possibility of data “leaking” between threads. This does however sometimes introduce some small complexities & you should be careful not to store state on the instances if you’re going to be using the code in a threaded environment.
Why Resource
vs. ModelResource
?¶
Make no mistake that Django models are far and away the most popular source of
data. However, in practice, there are many times where the ORM isn’t the data
source. Hooking up things like a NoSQL store (see Using Tastypie With Non-ORM Data Sources),
a search solution like Haystack or even managed filesystem data are all good
use cases for Resource
knowing nothing about the ORM.
Flow Through The Request/Response Cycle¶
Tastypie can be thought of as a set of class-based views that provide the API functionality. As such, many part of the request/response cycle are standard Django behaviors. For instance, all routing/middleware/response-handling aspects are the same as a typical Django app. Where it differs is in the view itself.
As an example, we’ll walk through what a GET request to a list endpoint (say
/api/v1/user/?format=json
) looks like:
The
Resource.urls
are checked by Django’s url resolvers.On a match for the list view,
Resource.wrap_view('dispatch_list')
is called.wrap_view
provides basic error handling & allows for returning serialized errors.Because
dispatch_list
was passed towrap_view
,Resource.dispatch_list
is called next. This is a thin wrapper aroundResource.dispatch
.dispatch
does a bunch of heavy lifting. It ensures:- the requested HTTP method is in
allowed_methods
(method_check
), - the class has a method that can handle the request (
get_list
), - the user is authenticated (
is_authenticated
), - the user is authorized (
is_authorized
), - & the user has not exceeded their throttle (
throttle_check
).
At this point,
dispatch
actually calls the requested method (get_list
).- the requested HTTP method is in
get_list
does the actual work of the API. It does:- A fetch of the available objects via
Resource.obj_get_list
. In the case ofModelResource
, this builds the ORM filters to apply (ModelResource.build_filters
). It then gets theQuerySet
viaModelResource.get_object_list
(which performsResource.apply_authorization_limits
to possibly limit the set the user can work with) and applies the built filters to it. - It then sorts the objects based on user input
(
ModelResource.apply_sorting
). - Then it paginates the results using the supplied
Paginator
& pulls out the data to be serialized. - The objects in the page have
full_dehydrate
applied to each of them, causing Tastypie to translate the raw object data into the fields the endpoint supports. - Finally, it calls
Resource.create_response
.
- A fetch of the available objects via
create_response
is a shortcut method that:- Determines the desired response format (
Resource.determine_format
), - Serializes the data given to it in the proper format,
- And returns a Django
HttpResponse
(200 OK) with the serialized data.
- Determines the desired response format (
We bubble back up the call stack to
dispatch
. The last thingdispatch
does is potentially store that a request occurred for future throttling (Resource.log_throttled_access
) then either returns theHttpResponse
or wraps whatever data came back in a response (so Django doesn’t freak out).
Processing on other endpoints or using the other HTTP methods results in a
similar cycle, usually differing only in what “actual work” method gets called
(which follows the format of “<http_method>_<list_or_detail>"). In the case
of POST/PUT, the ``hydrate
cycle additionally takes place and is used to take
the user data & convert it to raw data for storage.
What Are Bundles?¶
Bundles are a small abstraction that allow Tastypie to pass data between
resources. This allows us not to depend on passing request
to every single
method (especially in places where this would be overkill). It also allows
resources to work with data coming into the application paired together with
an unsaved instance of the object in question.
Think of it as package of user data & an object instance (either of which are optionally present).
Why Resource URIs?¶
Resource URIs play a heavy role in how Tastypie delivers data. This can seem
very different from other solutions which simply inline related data. Though
Tastypie can inline data like that (using full=True
on the field with the
relation), the default is to provide URIs.
URIs are useful because it results in smaller payloads, letting you fetch only the data that is important to you. You can imagine an instance where an object has thousands of related items that you may not be interested in.
URIs are also very cache-able, because the data at each endpoint is less likely to frequently change.
And URIs encourage proper use of each endpoint to display the data that endpoint covers.
Ideology aside, you should use whatever suits you. If you prefer fewer requests
& fewer endpoints, use of full=True
is available, but be aware of the
consequences of each approach.
Advanced Data Preparation¶
Tastypie uses a “dehydrate” cycle to prepare data for serialization & a “hydrate” cycle to take data sent to it & turn that back into useful Python objects.
Within these cycles, there are several points of customization if you need them.
dehydrate
¶
dehydrate_FOO
¶
hydrate
¶
hydrate_FOO
¶
Reverse “Relationships”¶
Unlike Django’s ORM, Tastypie does not automatically create reverse relations. This is because there is substantial technical complexity involved, as well as perhaps unintentionally exposing related data in an incorrect way to the end user of the API.
However, it is still possible to create reverse relations. Instead of handing
the ToOneField
or ToManyField
a class, pass them a string that
represents the full path to the desired class. Implementing a reverse
relationship looks like so:
# myapp/api/resources.py
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Note, Comment
class NoteResource(ModelResource):
comments = fields.ToManyField('myapp.api.resources.CommentResource', 'comments')
class Meta:
queryset = Note.objects.all()
class CommentResource(ModelResource):
note = fields.ToOneField(NoteResource, 'notes')
class Meta:
queryset = Comment.objects.all()
Warning
Unlike Django, you can’t use just the class name (i.e. 'CommentResource'
),
even if it’s in the same module. Tastypie (intentionally) lacks a construct
like the AppCache
which makes that sort of thing work in Django. Sorry.
Tastypie also supports self-referential relations. If you assume we added the
appropriate self-referential ForeignKey
to the Note
model, implementing
a similar relation in Tastypie would look like:
# myapp/api/resources.py
from tastypie import fields
from tastypie.resources import ModelResource
from myapp.models import Note
class NoteResource(ModelResource):
sub_notes = fields.ToManyField('self', 'notes')
class Meta:
queryset = Note.objects.all()
Resource Options (AKA Meta
)¶
The inner Meta
class allows for class-level configuration of how the
Resource
should behave. The following options are available:
serializer
¶
Controls which serializer class theResource
should use. Default istastypie.serializers.Serializer()
.
authentication
¶
Controls which authentication class theResource
should use. Default istastypie.authentication.Authentication()
.
authorization
¶
Controls which authorization class theResource
should use. Default istastypie.authorization.ReadOnlyAuthorization()
.
validation
¶
Controls which validation class theResource
should use. Default istastypie.validation.Validation()
.
paginator_class
¶
Controls which paginator class theResource
should use. Default istastypie.paginator.Paginator()
.
Note
This is different than the other options in that you supply a class rather than an instance. This is done because the Paginator has some per-request initialization options.
cache
¶
Controls which cache class theResource
should use. Default istastypie.cache.NoCache()
.
throttle
¶
Controls which throttle class theResource
should use. Default istastypie.throttle.BaseThrottle()
.
allowed_methods
¶
Controls what list & detail REST methods the
Resource
should respond to. Default isNone
, which means delegate to the more specificlist_allowed_methods
&detail_allowed_methods
options.You may specify a list like
['get', 'post', 'put', 'delete']
as a shortcut to prevent having to specify the other options.
list_allowed_methods
¶
Controls what list REST methods theResource
should respond to. Default is['get', 'post', 'put', 'delete']
.
detail_allowed_methods
¶
Controls what detail REST methods theResource
should respond to. Default is['get', 'post', 'put', 'delete']
.
limit
¶
Controls what how many results theResource
will show at a time. Default is either theAPI_LIMIT_PER_PAGE
setting (if provided) or20
if not specified.
api_name
¶
An override for theResource
to use when generating resource URLs. Default isNone
.
resource_name
¶
An override for the
Resource
to use when generating resource URLs. Default isNone
.If not provided, the
Resource
orModelResource
will attempt to name itself. This means a lowercase version of the classname preceding the wordResource
if present (i.e.SampleContentResource
would becomesamplecontent
).
default_format
¶
Specifies the default serialization format theResource
should use if one is not requested (usually by theAccept
header orformat
GET parameter). Default isapplication/json
.
filtering
¶
Provides a list of fields that the
Resource
will accept client filtering on. Default is{}
.Keys should be the fieldnames as strings while values should be a list of accepted filter types.
ordering
¶
Specifies the what fields the
Resource
should should allow ordering on. Default is[]
.Values should be the fieldnames as strings. When provided to the
Resource
by theorder_by
GET parameter, you can specify either thefieldname
(ascending order) or-fieldname
(descending order).
object_class
¶
Provides the
Resource
with the object that serves as the data source. Default isNone
.In the case of
ModelResource
, this is automatically populated by thequeryset
option and is the model class.
queryset
¶
Provides the
Resource
with the set of Django models to respond with. Default isNone
.Unused by
Resource
but present for consistency.
fields
¶
Controls what introspected fields theResource
should include. A whitelist of fields. Default is[]
.
excludes
¶
Controls what introspected fields theResource
should NOT include. A blacklist of fields. Default is[]
.
include_resource_uri
¶
Specifies if theResource
should include an extra field that displays the detail URL (within the api) for that resource. Default isTrue
.
include_absolute_url
¶
Specifies if theResource
should include an extra field that displays theget_absolute_url
for that object (on the site proper). Default isFalse
.
Basic Filtering¶
ModelResource
provides a basic Django ORM filter
interface. Simply list the resource fields which you’d like to filter on and
the allowed expression in a filtering property of your resource’s Meta
class:
from tastypie.constants import ALL, ALL_WITH_RELATIONS
class MyResource(ModelResource):
class Meta:
filtering = {
"slug": ('exact', 'startswith',),
"title": ALL,
}
Valid filtering values are: Django ORM filters (e.g. startswith
,
exact
, lte
, etc. or the ALL
or ALL_WITH_RELATIONS
constants
defined in tastypie.constants
.
These filters will be extracted from URL query strings using the same double-underscore syntax as the Django ORM:
/api/v1/myresource/?slug=myslug
/api/v1/myresource/?slug__startswith=test
Advanced Filtering¶
If you need to filter things other than ORM resources or wish to apply
additional constraints (e.g. text filtering using django-haystack
<http://haystacksearch.org> rather than simple database queries) your
Resource
may define a custom
build_filters()
method which allows you to
filter the queryset before processing a request:
from haystack.query import SearchQuerySet
class MyResource(Resource):
def build_filters(self, filters=None):
if filters is None:
filters = {}
orm_filters = super(MyResource, self).build_filters(filters)
if "q" in filters:
sqs = SearchQuerySet().auto_query(filters['q'])
orm_filters = {"pk__in": [ i.pk for i in sqs ]}
return orm_filters
Resource
Methods¶
Handles the data, request dispatch and responding to requests.
Serialization/deserialization is handled “at the edges” (i.e. at the beginning/end of the request/response cycle) so that everything internally is Python data structures.
This class tries to be non-model specific, so it can be hooked up to other data sources, such as search results, files, other data, etc.
wrap_view
¶
-
Resource.
wrap_view
(self, view)¶
Wraps methods so they can be called in a more functional way as well as handling exceptions better.
Note that if BadRequest
or an exception with a response
attr are seen,
there is special handling to either present a message back to the user or
return the response traveling with the exception.
base_urls
¶
-
Resource.
base_urls
(self)¶
The standard URLs this Resource
should respond to. These include the
list, detail, schema & multiple endpoints by default.
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
override_urls
¶
-
Resource.
override_urls
(self)¶
A hook for adding your own URLs or overriding the default URLs. Useful for
adding custom endpoints or overriding the built-in ones (from base_urls
).
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
urls
¶
-
Resource.
urls
(self)¶
Property
The endpoints this Resource
responds to. A combination of base_urls
&
override_urls
.
Mostly a standard URLconf, this is suitable for either automatic use
when registered with an Api
class or for including directly in
a URLconf should you choose to.
determine_format
¶
-
Resource.
determine_format
(self, request)¶
Used to determine the desired format.
Largely relies on tastypie.utils.mime.determine_format
but here
as a point of extension.
serialize
¶
-
Resource.
serialize
(self, request, data, format, options=None)¶
Given a request, data and a desired format, produces a serialized version suitable for transfer over the wire.
Mostly a hook, this uses the Serializer
from Resource._meta
.
deserialize
¶
-
Resource.
deserialize
(self, request, data, format='application/json')¶
Given a request, data and a format, deserializes the given data.
It relies on the request properly sending a CONTENT_TYPE
header,
falling back to application/json
if not provided.
Mostly a hook, this uses the Serializer
from Resource._meta
.
alter_list_data_to_serialize
¶
-
Resource.
alter_list_data_to_serialize
(self, request, data)¶
A hook to alter list data just before it gets serialized & sent to the user.
Useful for restructuring/renaming aspects of the what’s going to be sent.
Should accommodate for a list of objects, generally also including meta data.
alter_detail_data_to_serialize
¶
-
Resource.
alter_detail_data_to_serialize
(self, request, data)¶
A hook to alter detail data just before it gets serialized & sent to the user.
Useful for restructuring/renaming aspects of the what’s going to be sent.
Should accommodate for receiving a single bundle of data.
alter_deserialized_list_data
¶
-
Resource.
alter_deserialized_list_data
(self, request, data)¶
A hook to alter list data just after it has been received from the user & gets deserialized.
Useful for altering the user data before any hydration is applied.
alter_deserialized_detail_data
¶
-
Resource.
alter_deserialized_detail_data
(self, request, data)¶
A hook to alter detail data just after it has been received from the user & gets deserialized.
Useful for altering the user data before any hydration is applied.
dispatch_list
¶
-
Resource.
dispatch_list
(self, request, **kwargs)¶
A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over the entire list of resources.
Relies on Resource.dispatch
for the heavy-lifting.
dispatch_detail
¶
-
Resource.
dispatch_detail
(self, request, **kwargs)¶
A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on a single resource.
Relies on Resource.dispatch
for the heavy-lifting.
dispatch
¶
-
Resource.
dispatch
(self, request_type, request, **kwargs)¶
Handles the common operations (allowed HTTP method, authentication, throttling, method lookup) surrounding most CRUD interactions.
remove_api_resource_names
¶
-
Resource.
remove_api_resource_names
(self, url_dict)¶
Given a dictionary of regex matches from a URLconf, removes
api_name
and/or resource_name
if found.
This is useful for converting URLconf matches into something suitable for data lookup. For example:
Model.objects.filter(**self.remove_api_resource_names(matches))
method_check
¶
-
Resource.
method_check
(self, request, allowed=None)¶
Ensures that the HTTP method used on the request is allowed to be handled by the resource.
Takes an allowed
parameter, which should be a list of lowercase
HTTP methods to check against. Usually, this looks like:
# The most generic lookup.
self.method_check(request, self._meta.allowed_methods)
# A lookup against what's allowed for list-type methods.
self.method_check(request, self._meta.list_allowed_methods)
# A useful check when creating a new endpoint that only handles
# GET.
self.method_check(request, ['get'])
is_authorized
¶
Handles checking of permissions to see if the user has authorization
to GET, POST, PUT, or DELETE this resource. If object
is provided,
the authorization backend can apply additional row-level permissions
checking.
is_authenticated
¶
-
Resource.
is_authenticated
(self, request)¶
Handles checking if the user is authenticated and dealing with unauthenticated users.
Mostly a hook, this uses class assigned to authentication
from
Resource._meta
.
throttle_check
¶
-
Resource.
throttle_check
(self, request)¶
Handles checking if the user should be throttled.
Mostly a hook, this uses class assigned to throttle
from
Resource._meta
.
log_throttled_access
¶
-
Resource.
log_throttled_access
(self, request)¶
Handles the recording of the user’s access for throttling purposes.
Mostly a hook, this uses class assigned to throttle
from
Resource._meta
.
build_bundle
¶
-
Resource.
build_bundle
(self, obj=None, data=None)¶
Given either an object, a data dictionary or both, builds a Bundle
for use throughout the dehydrate/hydrate
cycle.
If no object is provided, an empty object from
Resource._meta.object_class
is created so that attempts to access
bundle.obj
do not fail.
build_filters
¶
-
Resource.
build_filters
(self, filters=None)¶
Allows for the filtering of applicable objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
apply_sorting
¶
-
Resource.
apply_sorting
(self, obj_list, options=None)¶
Allows for the sorting of objects being returned.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
get_resource_uri
¶
-
Resource.
get_resource_uri
(self, bundle_or_obj)¶
This needs to be implemented at the user level.
A return reverse("api_dispatch_detail", kwargs={'resource_name':
self.resource_name, 'pk': object.id})
should be all that would
be needed.
ModelResource
includes a full working version specific to Django’s
Models
.
get_resource_list_uri
¶
-
Resource.
get_resource_list_uri
(self)¶
Returns a URL specific to this resource’s list endpoint.
get_via_uri
¶
-
Resource.
get_via_uri
(self, uri)¶
This pulls apart the salient bits of the URI and populates the
resource via a obj_get
.
If you need custom behavior based on other portions of the URI, simply override this method.
full_dehydrate
¶
-
Resource.
full_dehydrate
(self, obj)¶
Given an object instance, extract the information from it to populate the resource.
dehydrate
¶
-
Resource.
dehydrate
(self, bundle)¶
A hook to allow a final manipulation of data once all fields/methods have built out the dehydrated data.
Useful if you need to access more than one dehydrated field or want to annotate on additional data.
Must return the modified bundle.
full_hydrate
¶
-
Resource.
full_hydrate
(self, bundle)¶
Given a populated bundle, distill it and turn it back into a full-fledged object instance.
hydrate
¶
-
Resource.
hydrate
(self, bundle)¶
A hook to allow a final manipulation of data once all fields/methods have built out the hydrated data.
Useful if you need to access more than one hydrated field or want to annotate on additional data.
Must return the modified bundle.
build_schema
¶
-
Resource.
build_schema
(self)¶
Returns a dictionary of all the fields on the resource and some properties about those fields.
Used by the schema/
endpoint to describe what will be available.
dehydrate_resource_uri
¶
-
Resource.
dehydrate_resource_uri
(self, bundle)¶
For the automatically included resource_uri
field, dehydrate
the URI for the given bundle.
Returns empty string if no URI can be generated.
generate_cache_key
¶
-
Resource.
generate_cache_key
(self, *args, **kwargs)¶
Creates a unique-enough cache key.
This is based off the current api_name/resource_name/args/kwargs.
get_object_list
¶
-
Resource.
get_object_list
(self, request)¶
A hook to allow making returning the list of available objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
apply_authorization_limits
¶
Allows the Authorization
class to further limit the object list.
Also a hook to customize per Resource
.
Calls Authorization.apply_limits
if available.
can_update
¶
-
Resource.
can_update
(self)¶
Checks to ensure put
is within allowed_methods
.
Used when hydrating related data.
obj_get_list
¶
-
Resource.
obj_get_list
(self, request=None, **kwargs)¶
Fetches the list of objects available on the resource.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
cached_obj_get_list
¶
-
Resource.
cached_obj_get_list
(self, request=None, **kwargs)¶
A version of obj_get_list
that uses the cache as a means to get
commonly-accessed data faster.
obj_get
¶
-
Resource.
obj_get
(self, request=None, **kwargs)¶
Fetches an individual object on the resource.
This needs to be implemented at the user level. If the object can not
be found, this should raise a NotFound
exception.
ModelResource
includes a full working version specific to Django’s
Models
.
cached_obj_get
¶
-
Resource.
cached_obj_get
(self, request=None, **kwargs)¶
A version of obj_get
that uses the cache as a means to get
commonly-accessed data faster.
obj_create
¶
-
Resource.
obj_create
(self, bundle, request=None, **kwargs)¶
Creates a new object based on the provided data.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_update
¶
-
Resource.
obj_update
(self, bundle, request=None, **kwargs)¶
Updates an existing object (or creates a new object) based on the provided data.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_delete_list
¶
-
Resource.
obj_delete_list
(self, request=None, **kwargs)¶
Deletes an entire list of objects.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
obj_delete
¶
-
Resource.
obj_delete
(self, request=None, **kwargs)¶
Deletes a single object.
This needs to be implemented at the user level.
ModelResource
includes a full working version specific to Django’s
Models
.
create_response
¶
-
Resource.
create_response
(self, request, data)¶
Extracts the common “which-format/serialize/return-response” cycle.
Mostly a useful shortcut/hook.
is_valid
¶
-
Resource.
is_valid
(self, bundle, request=None)¶
Handles checking if the data provided by the user is valid.
Mostly a hook, this uses class assigned to validation
from
Resource._meta
.
If validation fails, an error is raised with the error messages serialized inside it.
rollback
¶
-
Resource.
rollback
(self, bundles)¶
Given the list of bundles, delete all objects pertaining to those bundles.
This needs to be implemented at the user level. No exceptions should be raised if possible.
ModelResource
includes a full working version specific to Django’s
Models
.
get_list
¶
-
Resource.
get_list
(self, request, **kwargs)¶
Returns a serialized list of resources.
Calls obj_get_list
to provide the data, then handles that result
set and serializes it.
Should return a HttpResponse (200 OK).
get_detail
¶
-
Resource.
get_detail
(self, request, **kwargs)¶
Returns a single serialized resource.
Calls cached_obj_get/obj_get
to provide the data, then handles that result
set and serializes it.
Should return a HttpResponse (200 OK).
put_list
¶
-
Resource.
put_list
(self, request, **kwargs)¶
Replaces a collection of resources with another collection.
Calls delete_list
to clear out the collection then obj_create
with the provided the data to create the new collection.
Return HttpAccepted
(204 No Content).
put_detail
¶
-
Resource.
put_detail
(self, request, **kwargs)¶
Either updates an existing resource or creates a new one with the provided data.
Calls obj_update
with the provided data first, but falls back to
obj_create
if the object does not already exist.
If a new resource is created, return HttpCreated
(201 Created).
If an existing resource is modified, return HttpAccepted
(204 No Content).
post_list
¶
-
Resource.
post_list
(self, request, **kwargs)¶
Creates a new resource/object with the provided data.
Calls obj_create
with the provided data and returns a response
with the new resource’s location.
If a new resource is created, return HttpCreated
(201 Created).
post_detail
¶
-
Resource.
post_detail
(self, request, **kwargs)¶
Creates a new subcollection of the resource under a resource.
This is not implemented by default because most people’s data models aren’t self-referential.
If a new resource is created, return HttpCreated
(201 Created).
delete_list
¶
-
Resource.
delete_list
(self, request, **kwargs)¶
Destroys a collection of resources/objects.
Calls obj_delete_list
.
If the resources are deleted, return HttpAccepted
(204 No Content).
delete_detail
¶
-
Resource.
delete_detail
(self, request, **kwargs)¶
Destroys a single resource/object.
Calls obj_delete
.
If the resource is deleted, return HttpAccepted
(204 No Content).
If the resource did not exist, return HttpGone
(410 Gone).
ModelResource
Methods¶
A subclass of Resource
designed to work with Django’s Models
.
This class will introspect a given Model
and build a field list based
on the fields found on the model (excluding relational fields).
Given that it is aware of Django’s ORM, it also handles the CRUD data operations of the resource.
should_skip_field
¶
-
ModelResource.
should_skip_field
(cls, field)¶
Class method
Given a Django model field, return if it should be included in the contributed ApiFields.
api_field_from_django_field
¶
-
ModelResource.
api_field_from_django_field
(cls, f, default=CharField)¶
Class method
Returns the field type that would likely be associated with each Django type.
get_fields
¶
-
ModelResource.
get_fields
(cls, fields=None, excludes=None)¶
Class method
Given any explicit fields to include and fields to exclude, add additional fields based on the associated model.
check_filtering
¶
-
ModelResource.
check_filtering
(self, field_name, filter_type='exact', filter_bits=None)¶
Given a field name, a optional filter type and an optional list of additional relations, determine if a field can be filtered on.
If a filter does not meet the needed conditions, it should raise an
InvalidFilterError
.
If the filter meets the conditions, a list of attribute names (not field names) will be returned.
build_filters
¶
-
ModelResource.
build_filters
(self, filters=None)¶
Given a dictionary of filters, create the necessary ORM-level filters.
Keys should be resource fields, NOT model fields.
Valid values are either a list of Django filter types (i.e.
['startswith', 'exact', 'lte']
), the ALL
constant or the
ALL_WITH_RELATIONS
constant.
At the declarative level:
filtering = {
'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'],
'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'],
'resource_field_name_3': ALL,
'resource_field_name_4': ALL_WITH_RELATIONS,
...
}
Accepts the filters as a dict. None
by default, meaning no filters.
apply_sorting
¶
-
ModelResource.
apply_sorting
(self, obj_list, options=None)¶
Given a dictionary of options, apply some ORM-level sorting to the
provided QuerySet
.
Looks for the order_by
key and handles either ascending (just the
field name) or descending (the field name with a -
in front).
The field name should be the resource field, NOT model field.
get_object_list
¶
-
ModelResource.
get_object_list
(self, request)¶
A ORM-specific implementation of get_object_list
.
Returns a QuerySet
that may have been limited by other overrides.
obj_get_list
¶
-
ModelResource.
obj_get_list
(self, filters=None, **kwargs)¶
A ORM-specific implementation of obj_get_list
.
Takes an optional filters
dictionary, which can be used to narrow
the query.
obj_get
¶
-
ModelResource.
obj_get
(self, **kwargs)¶
A ORM-specific implementation of obj_get
.
Takes optional kwargs
, which are used to narrow the query to find
the instance.
obj_create
¶
-
ModelResource.
obj_create
(self, bundle, **kwargs)¶
A ORM-specific implementation of obj_create
.
obj_update
¶
-
ModelResource.
obj_update
(self, bundle, **kwargs)¶
A ORM-specific implementation of obj_update
.
obj_delete_list
¶
-
ModelResource.
obj_delete_list
(self, **kwargs)¶
A ORM-specific implementation of obj_delete_list
.
Takes optional kwargs
, which can be used to narrow the query.
obj_delete
¶
-
ModelResource.
obj_delete
(self, **kwargs)¶
A ORM-specific implementation of obj_delete
.
Takes optional kwargs
, which are used to narrow the query to find
the instance.
rollback
¶
-
ModelResource.
rollback
(self, bundles)¶
A ORM-specific implementation of rollback
.
Given the list of bundles, delete all models pertaining to those bundles.
save_m2m
¶
-
ModelResource.
save_m2m
(self, bundle)¶
Handles the saving of related M2M data.
Due to the way Django works, the M2M data must be handled after the
main instance, which is why this isn’t a part of the main save
bits.
Currently slightly inefficient in that it will clear out the whole relation and recreate the related data as needed.
Api¶
In terms of a REST-style architecture, the “api” is a collection of resources.
In Tastypie, the Api
gathers together the Resources
& provides a nice
way to use them as a set. It handles many of the URLconf details for you,
provides a helpful “top-level” view to show what endpoints are available &
some extra URL resolution juice.
Quick Start¶
A sample api definition might look something like (usually located in a URLconf):
from tastypie.api import Api
from myapp.api.resources import UserResource, EntryResource
v1_api = Api(api_name='v1')
v1_api.register(UserResource())
v1_api.register(EntryResource())
# Standard bits...
urlpatterns = patterns('',
(r'^api/', include(v1_api.urls)),
)
Api
Methods¶
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.
register
¶
-
Api.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
.
canonical_resource_for
¶
-
Api.canonical_resource_for(self, resource_name):
Returns the canonical resource for a given resource_name
.
override_urls
¶
-
Api.override_urls(self):
A hook for adding your own URLs or overriding the default URLs. Useful for adding custom endpoints or overriding the built-in ones.
Should return a list of individual URLconf lines (NOT wrapped in
patterns
).
urls
¶
-
Api.urls(self):
Property
Provides URLconf details for the Api
and all registered
Resources
beneath it.
top_level
¶
-
Api.top_level(self, request, api_name=None):
A view that returns a serialized list of all resources registers
to the Api
. Useful for discovery.
Resource Fields¶
When designing an API, an important component is defining the representation
of the data you’re presenting. Like Django models, you can control the
representation of a Resource
using fields. There are a variety of fields
for various types of data.
Quick Start¶
For the impatient:
import datetime
from tastypie import fields
from tastypie.resources import Resource
from myapp.api.resources import ProfileResource, NoteResource
class PersonResource(Resource):
name = fields.CharField(attribute='name')
age = fields.IntegerField(attribute='years_old', null=True)
created = fields.DateTimeField(readonly=True, help_text='When the person was created', default=datetime.datetime.now)
is_active = fields.BooleanField(default=True)
profile = fields.ToOneField(ProfileResource, 'profile')
notes = fields.ToManyField(NoteResource, 'notes', full=True)
Standard Data Fields¶
All standard data fields have a common base class ApiField
which handles
the basic implementation details.
Note
You should not use the ApiField
class directly. Please use one of the
subclasses that is more correct for your data.
Common Field Options¶
All ApiField
objects accept the following options.
attribute
¶
-
ApiField.
attribute
¶
A string naming an instance attribute of the object wrapped by the Resource. The
attribute will be accessed during the dehydrate
or or written during the hydrate
.
Defaults to None
, meaning data will be manually accessed.
default
¶
-
ApiField.
default
¶
Provides default data when the object being dehydrated
/hydrated
has no data on
the field.
Defaults to tastypie.fields.NOT_PROVIDED
.
null
¶
-
ApiField.
null
¶
Indicates whether or not a None
is allowable data on the field. Defaults to
False
.
Field Types¶
DateField
¶
A date field.
DateTimeField
¶
A datetime field.
DecimalField
¶
A decimal field.
DictField
¶
A dictionary field.
FloatField
¶
A floating point field.
IntegerField
¶
An integer field.
Covers models.IntegerField
, models.PositiveIntegerField
,
models.PositiveSmallIntegerField
and models.SmallIntegerField
.
ListField
¶
A list field.
Relationship Fields¶
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.
Common Field Options¶
In addition to the common attributes for all ApiField, relationship fields accept the following.
Field Types¶
ToOneField
¶
Provides access to related data via foreign key.
This subclass requires Django’s ORM layer to work properly.
OneToOneField
¶
An alias to ToOneField
for those who prefer to mirror django.db.models
.
ForeignKey
¶
An alias to ToOneField
for those who prefer to mirror django.db.models
.
ToManyField
¶
Provides access to related data via a join table.
This subclass requires Django’s ORM layer to work properly.
This field also has special behavior when dealing with attribute
in that
it can take a callable. For instance, if you need to filter the reverse
relation, you can do something like:
subjects = fields.ToManyField(SubjectResource, ToManyField(SubjectResource, attribute=lambda bundle: Subject.objects.filter(notes=bundle.obj, name__startswith='Personal'))
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.
ManyToManyField
¶
An alias to ToManyField
for those who prefer to mirror django.db.models
.
OneToManyField
¶
An alias to ToManyField
for those who prefer to mirror django.db.models
.
Authentication / Authorization¶
Authentication & authorization make up the components needed to verify that a certain user has access to the API and what they can do with it.
Authentication answers the question “can they see this data?” This usually involves requiring credentials, such as an API key or username/password.
Authorization answers the question “what objects can they modify?” This usually involves checking permissions, but is open to other implementations.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
authentication = BasicAuthentication()
authorization = DjangoAuthorization()
Authentication Options¶
Tastypie ships with the following Authentication
classes:
Authentication
¶
The no-op authentication option, the client is always allowed through. Very useful for development and read-only APIs.
BasicAuthentication
¶
This authentication scheme uses HTTP Basic Auth to check a user’s credentials.
The username is their django.contrib.auth.models.User
username (assuming
it is present) and their password should also correspond to that entry.
Warning
If you’re using Apache & mod_wsgi
, you will need to enable
WSGIPassAuthorization On
. See this post for details.
ApiKeyAuthentication
¶
As an alternative to requiring sensitive data like a password, the
ApiKeyAuthentication
allows you to collect just username & a
machine-generated api key. Tastypie ships with a special Model
just for
this purpose, so you’ll need to ensure tastypie
is in INSTALLED_APPS
.
DigestAuthentication
¶
This authentication scheme uses HTTP Digest Auth to check a user’s
credentials. The username is their django.contrib.auth.models.User
username (assuming it is present) and their password should be their
machine-generated api key. As with ApiKeyAuthentication, tastypie
should be included in INSTALLED_APPS
.
Warning
If you’re using Apache & mod_wsgi
, you will need to enable
WSGIPassAuthorization On
. See this post for details (even though it
only mentions Basic auth).
Authorization Options¶
Tastypie ships with the following Authorization
classes:
Authorization
¶
The no-op authorization option, no permissions checks are performed.
Warning
This is a potentially dangerous option, as it means ANY recognized user can modify ANY data they encounter in the API. Be careful who you trust.
ReadOnlyAuthorization
¶
This authorization class only permits reading data, regardless of what the
Resource
might think is allowed. This is the default Authorization
class and the safe option.
DjangoAuthorization
¶
The most advanced form of authorization, this checks the permission a user
has granted to them (via django.contrib.auth.models.Permission
). In
conjunction with the admin, this is a very effective means of control.
Implementing Your Own Authentication/Authorization¶
Implementing your own Authentication/Authorization
classes is a simple
process. Authentication
has two methods to override (one of which is
optional but recommended to be customized) and Authorization
has just one
required method and one optional method:
from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
class SillyAuthentication(Authentication):
def is_authenticated(self, request, **kwargs):
if 'daniel' in request.user.username:
return True
return False
# Optional but recommended
def get_identifier(self, request):
return request.user.username
class SillyAuthorization(Authorization):
def is_authorized(self, request, object=None):
if request.user.date_joined.year == 2010:
return True
else:
return False
# Optional but useful for advanced limiting, such as per user.
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
return object_list.filter(author__username=request.user.username)
return object_list.none()
Under this scheme, only users with ‘daniel’ in their username will be allowed in, and only those who joined the site in 2010 will be allowed to affect data.
If the optional apply_limits
method is included, each user that fits the
above criteria will only be able to access their own records.
Validation¶
Validation allows you to ensure that the data being submitted by the user is appropriate for storage. This can range from simple type checking on up to complex validation that compares different fields together.
If the data is valid, an empty dictionary is returned and processing continues as normal. If the data is invalid, a dictionary of error messages (keys being the field names, values being a list of error messages). This will be immediately returned to the user, serialized in the format they requested.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.validation import Validation
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
validation = Validation()
Validation Options¶
Tastypie ships with the following Validation
classes:
Validation
¶
The no-op validation option, the data submitted is always considered to be valid.
This is the default class hooked up to Resource/ModelResource
.
FormValidation
¶
A more complex form of validation, this class accepts a form_class
argument
to its constructor. You supply a Django Form
(or ModelForm
, though
save
will never get called) and Tastypie will verify the data
in the
Bundle
against the form.
Warning
Data in the bundle must line up with the fieldnames in the Form
. If they
do not, you’ll need to either munge the data or change your form.
Usage looks like:
from django import forms
class NoteForm(forms.Form):
title = forms.CharField(max_length=100)
slug = forms.CharField(max_length=50)
content = forms.CharField(required=False, widget=forms.Textarea)
is_active = forms.BooleanField()
form = FormValidation(form_class=NoteForm)
Implementing Your Own Validation¶
Implementing your own Validation
classes is a simple process. The
constructor can take whatever **kwargs
it needs (if any). The only other
method to implement is the is_valid
method:
from tastypie.validation import Validation
class AwesomeValidation(Validation):
def is_valid(self, bundle, request=None):
if not bundle.data:
return {'__all__': 'Not quite what I had in mind.'}
errors = {}
for key, value in bundle.data.items():
if not isinstance(value, basestring):
continue
if not 'awesome' in value:
errors[key] = ['NOT ENOUGH AWESOME. NEEDS MORE.']
return errors
Under this validation, every field that’s a string is checked for the word ‘awesome’. If it’s not in the string, it’s an error.
Caching¶
When adding an API to your site, it’s important to understand that most consumers of the API will not be people, but instead machines. This means that the traditional “fetch-read-click” cycle is no longer measured in minutes but in seconds or milliseconds.
As such, caching is a very important part of the deployment of your API. Tastypie ships with two classes to make working with caching easier. These caches store at the object level, reducing access time on the database.
However, it’s worth noting that these do NOT cache serialized representations. For heavy traffic, we’d encourage the use of a caching proxy, especially Varnish, as it shines under this kind of usage. It’s far faster than Django views and already neatly handles most situations.
Usage¶
Using these classes is simple. Simply provide them (or your own class) as a
Meta
option to the Resource
in question. For example:
from django.contrib.auth.models import User
from tastypie.cache import SimpleCache
from tastypie.resources import ModelResource
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
cache = SimpleCache()
Caching Options¶
Tastypie ships with the following Cache
classes:
NoCache
¶
The no-op cache option, this does no caching but serves as an api-compatible plug. Very useful for development.
SimpleCache
¶
This option does basic object caching, attempting to find the object in the
cache & writing the object to the cache. It uses Django’s current
CACHE_BACKEND
to store cached data.
Implementing Your Own Cache¶
Implementing your own Cache
class is as simple as subclassing NoCache
and overriding the get
& set
methods. For example, a json-backed
cache might look like:
import json
from django.conf import settings
from tastypie.cache import NoCache
class JSONCache(NoCache):
def _load(self):
data_file = open(settings.TASTYPIE_JSON_CACHE, 'r')
return json.load(data_file)
def _save(self, data):
data_file = open(settings.TASTYPIE_JSON_CACHE, 'w')
return json.dump(data, data_file)
def get(self, key):
data = self._load()
return data.get(key, None)
def set(self, key, value, timeout=60):
data = self._load()
data[key] = value
self._save(data)
Note that this is NOT necessarily an optimal solution, but is simply
demonstrating how one might go about implementing your own Cache
.
Serialization¶
Serialization can be one of the most contentious areas of an API. Everyone has their own requirements, their own preferred output format & the desire to have control over what is returned.
As a result, Tastypie ships with a serializer that tries to meet the basic needs of most use cases, and the flexibility to go outside of that when you need to.
The default Serializer
supports the following formats:
- json
- jsonp
- xml
- yaml
- html
Usage¶
Using this class is simple. It is the default option on all Resource
classes unless otherwise specified. The following code is a no-op, but
demonstrate how you could use your own serializer:
from django.contrib.auth.models import User
from tastypie.resources import ModelResource
from tastypie.serializers import Serializer
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
serializer = Serializer()
Implementing Your Own Serializer¶
There are several different use cases here. We’ll cover simple examples of wanting a tweaked format & adding a different format.
To tweak a format, simply override it’s to_<format>
& from_<format>
methods. So adding the server time to all output might look like so:
import time
from tastypie.serializers import Serializer
class CustomJSONSerializer(Serializer):
def to_json(self, data, options=None):
options = options or {}
data = self.to_simple(data, options)
# Add in the current time.
data['requested_time'] = time.time()
return simplejson.dumps(data, cls=json.DjangoJSONEncoder, sort_keys=True)
def from_json(self, content):
data = simplejson.loads(content)
if 'requested_time' in data:
# Log the request here...
pass
return data
In the case of adding a different format, let’s say you want to add a CSV
output option to the existing set. Your Serializer
subclass might look
like:
import csv
import StringIO
from tastypie.serializers import Serializer
class CSVSerializer(Serializer):
formats = ['json', 'jsonp', 'xml', 'yaml', 'html', 'csv']
content_types = {
'json': 'application/json',
'jsonp': 'text/javascript',
'xml': 'application/xml',
'yaml': 'text/yaml',
'html': 'text/html',
'csv': 'text/csv',
}
def to_csv(self, data, options=None):
options = options or {}
data = self.to_simple(data, options)
raw_data = StringIO.StringIO()
# Untested, so this might not work exactly right.
for item in data:
writer = csv.DictWriter(raw_data, item.keys(), extrasaction='ignore')
writer.write(item)
return raw_data
def from_csv(self, content):
raw_data = StringIO.StringIO(content)
data = []
# Untested, so this might not work exactly right.
for item in csv.DictReader(raw_data):
data.append(item)
return data
Serializer
Methods¶
A swappable class for serialization.
This handles most types of data as well as the following output formats:
* json
* jsonp
* xml
* yaml
* html
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.
get_mime_for_format
¶
-
Serializer.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.
format_datetime
¶
-
Serializer.format_datetime(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”.
format_date
¶
-
Serializer.format_date(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”.
format_time
¶
-
Serializer.format_time(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”.
serialize
¶
-
Serializer.serialize(self, bundle, format='application/json', options={}):
Given some data and a format, calls the correct method to serialize the data and returns the result.
deserialize
¶
-
Serializer.deserialize(self, content, format='application/json'):
Given some data and a format, calls the correct method to deserialize the data and returns the result.
to_simple
¶
-
Serializer.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).
to_etree
¶
-
Serializer.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.
from_etree
¶
-
Serializer.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”.
to_json
¶
-
Serializer.to_json(self, data, options=None):
Given some Python data, produces JSON output.
from_json
¶
-
Serializer.from_json(self, content):
Given some JSON data, returns a Python dictionary of the decoded data.
to_jsonp
¶
-
Serializer.to_jsonp(self, data, options=None):
Given some Python data, produces JSON output wrapped in the provided callback.
from_xml
¶
-
Serializer.from_xml(self, content):
Given some XML data, returns a Python dictionary of the decoded data.
to_yaml
¶
-
Serializer.to_yaml(self, data, options=None):
Given some Python data, produces YAML output.
from_yaml
¶
-
Serializer.from_yaml(self, content):
Given some YAML data, returns a Python dictionary of the decoded data.
to_html
¶
-
Serializer.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.
from_html
¶
-
Serializer.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.
Throttling¶
Sometimes, the client on the other end may request data too frequently or you have a business use case that dictates that the client should be limited to a certain number of requests per hour.
For this, Tastypie includes throttling as a way to limit the number of requests in a timeframe.
Usage¶
To specify a throttle, add the Throttle
class to the Meta
class on the
Resource
:
from django.contrib.auth.models import User
from tastypie.resources import ModelResource
from tastypie.throttle import BaseThrottle
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
resource_name = 'auth/user'
excludes = ['email', 'password', 'is_superuser']
# Add it here.
throttle = BaseThrottle(throttle_at=100)
Throttle Options¶
Each of the Throttle
classes accepts the following initialization
arguments:
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 thethrottle_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).
Tastypie ships with the following Throttle
classes:
BaseThrottle
¶
The no-op throttle option, this does no throttling but implements much of the common logic and serves as an api-compatible plug. Very useful for development.
CacheThrottle
¶
This uses just the cache to manage throttling. Fast but prone to cache misses and/or cache restarts.
CacheDBThrottle
¶
A write-through option that uses the cache first & foremost, but also writes through to the database to persist access times. Useful for logging client accesses & with RAM-only caches.
Implementing Your Own Throttle¶
Writing a Throttle
class is not quite as simple as the other components.
There are two important methods, should_be_throttled
& accessed
. The
should_be_throttled
method dictates whether or not the client should be
throttled. The accessed
method allows for the recording of the hit to the
API.
An example of a subclass might be:
import random
from tastypie.throttle import BaseThrottle
class RandomThrottle(BaseThrottle):
def should_be_throttled(self, identifier, **kwargs):
if random.randint(0, 10) % 2 == 0:
return True
return False
def accessed(self, identifier, **kwargs):
pass
This throttle class would pick a random number between 0 & 10. If the number is even, their request is allowed through; otherwise, their request is throttled & rejected.
Tastypie Cookbook¶
Adding Custom Values¶
You might encounter cases where you wish to include additional data in a
response which is not obtained from a field or method on your model. You can
easily extend the dehydrate()
method to
provide additional values:
class MyModelResource(Resource):
class Meta:
qs = MyModel.objects.all()
def dehydrate(self, bundle):
bundle.data['custom_field'] = "Whatever you want"
return bundle
Using Your Resource
In Regular Views¶
In addition to using your resource classes to power the API, you can also use them to write other parts of your application, such as your views. For instance, if you wanted to encode user information in the page for some Javascript’s use, you could do the following:
# views.py
from django.shortcuts import render_to_response
from myapp.api.resources import UserResource
def user_detail(request, username):
ur = UserResource()
user = ur.obj_get_detail(username=username)
# Other things get prepped to go into the context then...
return render_to_response('myapp/user_detail.html', {
# Other things here.
"user_json": ur.serialize(None, ur.full_dehydrate(obj=user), 'application/json'),
})
Using Non-PK Data For Your URLs¶
By convention, ModelResource``s usually expose the detail endpoints utilizing
the primary key of the ``Model
they represent. However, this is not a strict
requirement. Each URL can take other named URLconf parameters that can be used
for the lookup.
For example, if you want to expose User
resources by username, you can do
something like the following:
# myapp/api/resources.py
class UserResource(ModelResource):
class Meta:
queryset = User.objects.all()
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<username>[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]
The added URLconf matches before the standard URLconf included by default & matches on the username provided in the URL.
Nested Resources¶
You can also do “nested resources” (resources within another related resource)
by lightly overriding the override_urls
method & adding on a new method to
handle the children:
class ParentResource(ModelResource):
children = fields.ToManyField(ChildResource, 'children')
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w/-]*)/children%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_children'), name="api_get_children"),
]
def get_children(self, request, **kwargs):
try:
obj = self.cached_obj_get(request=request, **self.remove_api_resource_names(kwargs))
except ObjectDoesNotExist:
return HttpGone()
except MultipleObjectsReturned:
return HttpMultipleChoices("More than one resource is found at this URI.")
child_resource = ChildResource()
return child_resource.get_detail(request, parent_id=obj.pk)
Another alternative approach is to override the dispatch
method:
# myapp/api/resources.py
class EntryResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user')
class Meta:
queryset = Entry.objects.all()
resource_name = 'entry'
def dispatch(self, request_type, request, **kwargs):
username = kwargs.pop('username')
kwargs['user'] = get_object_or_404(User, username=username)
return super(EntryResource, self).dispatch(request_type, request, **kwargs)
# urls.py
from django.conf.urls.defaults import *
from myapp.api import EntryResource
entry_resource = EntryResource()
urlpatterns = patterns('',
# The normal jazz here, then...
(r'^api/(?P<username>\w+)/', include(entry_resource.urls)),
)
Adding Search Functionality¶
Another common request is being able to integrate search functionality. This
approach uses Haystack, though you could hook it up to any search technology.
We leave the CRUD methods of the resource alone, choosing to add a new endpoint
at /api/v1/notes/search/
:
from django.conf.urls.defaults import *
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from haystack.query import SearchQuerySet
from tastypie.resources import ModelResource
from tastypie.utils import trailing_slash
from notes.models import Note
class NoteResource(ModelResource):
class Meta:
queryset = Note.objects.all()
resource_name = 'notes'
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/search%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_search'), name="api_get_search"),
]
def get_search(self, request, **kwargs):
self.method_check(request, allowed=['get'])
self.is_authenticated(request)
self.throttle_check(request)
# Do the query.
sqs = SearchQuerySet().models(Note).load_all().auto_query(request.GET.get('q', ''))
paginator = Paginator(sqs, 20)
try:
page = paginator.page(int(request.GET.get('page', 1)))
except InvalidPage:
raise Http404("Sorry, no results on that page.")
objects = []
for result in page.object_list:
bundle = self.full_dehydrate(result.object)
objects.append(bundle)
object_list = {
'objects': objects,
}
self.log_throttled_access(request)
return self.create_response(request, object_list)
Creating per-user resources¶
One might want to create an API which will require every user to authenticate and every user will be working only with objects associated with him. Let’s see how to implement it for two basic operations: listing and creation of an object.
For listing we want to list only objects for which ‘user’ field matches
‘request.user’. This could be done my applying filter in apply_authorization_limits
method of your resource.
For creating we’d have to wrap obj_create
method of ModelResource
. Then the
resulting code will look something like:
# myapp/api/resources.py
class EnvironmentResource(ModelResource):
class Meta:
queryset = Environment.objects.all()
resource_name = 'environment'
list_allowed_methods = ['get', 'post']
authentication = ApiKeyAuthentication()
authorization = Authorization()
def obj_create(self, bundle, request=None, **kwargs):
return super(EnvironmentResource, self).obj_create(bundle, request, user=request.user)
def apply_authorization_limits(self, request, object_list):
return object_list.filter(user=request.user)
Debugging Tastypie¶
There are some common problems people run into when using Tastypie for the first time. Some of the common problems and things to try appear below.
“I’m getting XML output in my browser but I want JSON output!”¶
This is actually not a bug and JSON support is present in your Resource
.
This issue is that Tastypie respects the Accept
header your browser sends.
Most browsers send something like:
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Note that application/xml
comes first, which is a format that Tastypie
handles by default, hence why you receive XML.
If you use curl
from the command line, you should receive JSON by default:
curl http://localhost:8000/api/v1/
If you want JSON in the browser, simply append ?format=json
to your URL.
Tastypie always respects this override first, before it falls back to the
Accept
header.
Sites Using Tastypie¶
The following sites are a partial list of people using Tastypie. I’m always
interested in adding more sites, so please find me (daniellindsley
) via
IRC or start a mailing list thread.
LJWorld Marketplace¶
Forkinit¶
Read-only API access to recipes.
Read The Docs¶
A hosted documentation site, primarily for Python docs. General purpose read-write access.
Luzme¶
An e-book search site that lets you fetch pricing information.