Django-Conduit¶
Easy and powerful REST APIs for Django.
Why Use Django-Conduit?¶
- Easy to read, easy to debug, easy to extend
- Smart and efficient Related Resources
See the full list of features.
Table of Contents¶
Filtering and Ordering¶
During a get list view, it is useful to be able to filter or rearrange the results. Django-Conduit provides a few helpful properties and hooks to to filter your resources.
Server Side Filters to Limit Access¶
The default_filters
dict on a ModelResource’s Meta class will apply the listed queryset filters before fetching results. The keys in default_filters
ought to be a valid Queryset filter method for the specified model. Here is an example that only returns Foo objects that have a name starting with the the word ‘lamp’:
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
default_filters = {
'name__startswith': 'lamp'
}
The default filters will eventually be applied to the queryset during the apply_filters
method, resulting in something like this:
filtered_instances = Foo.objects.filter(name__startswith='lamp')
Client Side Filtering with Get Params¶
API consumers often need to be able to filter against certain resource fields using GET parameters. Filtering is enabled by specifying the allowed_filters
array. The array takes a series of Queryset filter keywords:
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
allowed_filters = [
'name__icontains',
'created__lte',
'created__gte',
'bar__name'
]
In the above example, API consumers will be allowed to get Foo objects by searching for strings in the Foo name, or by finding Foos created before or after a given datetime.
Note
Each Queryset filter has to be specified using the entire filter name. While verbose, this allows custom or related field parameters such as bar__name
to be easily specified.
Ordering Results¶
If you want to specify the default order for objected, returned, you can simply specify the order_by
string using the default_ordering
Meta field:
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
default_ordering='-created'
The value of default_ordering
should be the same one you would use when performing order_by on a queryset. The above example will result in the following operation:
Foo.objects.order_by('-created')
To allow API consumers to order the results, the allowed_ordering
field is an array of valid ordering keys:
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
allowed_ordering = [
'created',
'-created'
]
Note how the forward and reverse string both have to be specified. This is to provide precise control over client ordering values.
How Filters & Ordering are Applied¶
Filtering and ordering happens inside two steps in the default conduit pipeline. The first happens inside process_filters
. To determine order, first the method looks for an order_by GET parameter. If none are specified, it defaults to the default_ordering
attribute. If the order_by parameter is not a valid value, the client receives a 400.
The filters start with the default_filters
dictionary. This dictionary is then updated from filters specified in the GET parameters, provided they are specified in allowed_filters
.
After the order_by and filters are determined, their values are sent forward in the kwargs dictionary where they are picked up again in pre_get_list
. This is the method that first applies the kwargs['order_by']
value, and then applies the values inside kwargs['filters']
. It stores the ordered and filtered queryset inside of kwargs['objs']
. The objects are then subject to authorization limits and paginated inside get_list
before the final set of objects is determined.
Default Behavior¶
By default, conduit will serialize your model’s related object fields by their raw value. A ForeignKey field will produce the primary key of your related object. A ManyToMany field will produce a list of primary keys.
An example resource Foo has one FK and one M2M field:
class Foo(models.Model):
name = models.CharField(max_length=255)
bar = models.ForeignKey(Bar)
bazzes = models.ManyToManyField(Baz)
Will produce a detail response looking like this:
{
"name": "My Foo",
"bar": 45,
"bazzes": [5, 87, 200],
"resource_uri": "/api/v1/foo/1/"
}
When updating a ForeignKey field, conduit will set the model’s [field]_id to the integer you send it. Be careful not to set it to a nonexistent related model, since there are not constraint checks done when saved to the database.
Similarly, when updated a ManyToMany field and give it a nonexistent primary key, the add will silently fail and the invalid primary key will not enter the ManyToMany list.
Important
Updating raw primary keys will not produce errors for invalid keys.
Access, Authorization & Permissions¶
Django-Conduit provides several ‘out of the box’ ways to control access to your resources.
Note
See the Filtering & Ordering` guide to limit retrievable objected based on static values.
Allowed Methods¶
One quick way to prevent create or update access to a resource is to limit the allowed http methods:
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
model = Foo
allowed_methods = ['get']
The above example will prevent sending put, post, or delete requests to the Foo Resource. Currently only ‘get’, ‘put’, ‘post’, and ‘delete’ are valid values.
Authorization Hooks¶
For granular permissions on individual objects, Django-Conduit provides a list of ready made hooks for you to implement your permission checks. The hook methods follow a naming pattern of auth_[method]_[list/detail]
. The method being the http method, and list/detail whether it is an action on a list url (api/v1/foo
) or a detail url (api/v1/foo/1
)
It is entirely up to you how you want to handle permissions, but here are a couple suggestions.
Filter objects a user can retrieve based on ownership. Ownership is determined by the user specified in a owner field on the model:
@match(match=['get', 'list']) def auth_get_list(self, request, *args, **kwargs): objs = kwargs['objs'] objs = objs.filter(owner=request.user) return (request, args, kwargs)
Disable update access if a user does not own an object:
@match(match=['put', 'detail']) def auth_put_detail(self, request, *args, **kwargs): # single obj is still found in 'objs' kwarg obj = kwargs['objs'][0] if request.user != obj.owner: # HttpInterrupt will immediately end processing # the request. Get it by: # from conduit.exceptions import HttpInterrupt response = HttpResponse('', status=403) raise HttpInterrupt(response) return (request, args, kwargs)
Note
It is important that you include the @match
wrapper so that the check is only executed on the right requests. More about Method Subscriptions
Here is a full list of the authorization hook methods:
'auth_get_detail',
'auth_get_list',
'auth_put_detail',
'auth_put_list',
'auth_post_detail',
'auth_post_list',
'auth_delete_detail',
'auth_delete_list'
Some of these already have checks, such as auth_put_list
, which will automatically raise an HttpInterrupt. This is because sending a PUT to a list endpoint is not a valid request.
Forms & Validation¶
With django-conduit you can use Django’s ModelForms to easily validate your resources. You can specify a form to be used by assigning the form_class
on the resource’s Meta class:
from example.forms import FooForm
class FooResource(ModelResource):
class Meta(ModelResource.Meta):
form_class = FooForm
The form validation happens during POST and PUT requests. Any data sent in the request that does not correspond to the model’s field names will be discarded for validation.
If errors are found during validation, they are serialized into JSON and immediately return a 400 Http response. If an error occurs while validating a related field, the JSON error is specified as having occured within that related field.
Conduit Overview¶
What is a Conduit?¶
Conduits are views that send requests through a simple list of functions to produce a response. This process is often called a pipeline (hence the name conduit). Here is an example:
conduit = (
'deserialize_json',
'run_form_validation',
'response'
)
Each of the items in the conduit
tuple reference a method. Each method is called in succession. This is very similar to how Django’s MIDDLEWARE_CLASSES
work. A conduit pipeline is specified in a Conduit
view like this:
class FormView(Conduit):
"""
Simple view for processing form input
"""
form_class = MyForm
class Meta:
conduit = (
'deserialized_json_data',
'validate_form',
'process_data',
'response'
)
Conduit Methods¶
All functions in a conduit pipeline take the same four parameters as input.
- self
- The Conduit view instance
- request
- The Django request object
- *args
- Capture variable number of arguments
- **kwargs
- Capture variable number of keyword arguments
The methods also return these same values, though they may be modified in place. The only response that is different is the last, which must return a response, most likely an HttpResponse
.
Warning
The last method in a conduit must return a response, such as HttpResponse
Inheriting & Extending¶
To inherit the conduit tuple from another Conduit
view, your metaclass must do the inheriting. We can use a different form with the above view by inheriting its methods and conduit, while overriding its form_class:
class OtherFormView(FormView):
"""
Process a different form
"""
form_class = OtherForm
class Meta(FormView.Meta):
pass
If you want to add or remove a step from another conduit, you must specify the new pipeline in its entirety. Here is a simple but not recommended example that extends our view from above by adding a publish_to_redis
method:
class PublishFormView(FormView):
"""
Process a form and publish event to redis
"""
form_class = OtherForm
class Meta:
conduit = (
'deserialized_json_data',
'validate_form',
'process_data',
'publish_to_redis',
'response'
)
In this example, we didn’t inherit the meta class since we were overriding conduit anyway.
Warning
Class inheritance is NOT the recommended way to customize your Conduit views.
While inheriting views, including multiple inheritance, is very familiar to Django developers, there is another more flexible way to extend your Conduit views. The methods in the conduit can reference any namespaced function, as long as they take the correct 4 input parameters.
Using namespaced methods, the recommended way to create the above view would look like this:
class PublishFormView(Conduit):
"""
Process a form and publish event to redis
"""
form_class = OtherForm
class Meta:
conduit = (
'myapp.views.FormView.deserialized_json_data',
'myapp.views.FormView.validate_form',
'myapp.views.FormView.process_data',
'publish_to_redis',
'myapp.views.FormView.response'
)
The advantage here over multiple inheritance is that the source of the methods is made explicit. This makes debugging much easier if a little inconvenient.
About¶
Django-Conduit is meant to accomodate the most common API patterns such as dealing with related resources and permissions access. It was designed to be very easy to read, debug, and extend, and ultimately make your code more readable as well.
The conduit based views are aimed to respect and take advantage of the request-response cycle. This includes ideas such as transaction support, minimizing database queries, but most importantly a pipeline of events which can be easily intercepted, extended, and introspected.
We believe Django-Conduit is the fastest way to develop a REST API that works like you would expect or want it to.
Why not Django-REST-Framework?¶
DRF was built around Django’s Class Based Views and as a result produces highly cryptic yet verbose view classes. Interacting with CBVs often involves using special class methods that hopefully hook into the right part of the request-response cycle. Class based inheritance makes a very frustrating experience when developing complex views.
Why not Django-Tastypie?¶
Django-Conduit is heavily inspired by Tastypie. It uses a similar declarative syntax for resources, as well as a similar syntax for related fields and metaclasses. Conduit was partly built to simplify and streamline a lot of Tastypie’s internal logic. While built rather differently, Conduit aims to emulate some of Tastypie’s best features.
Getting Started¶
Django-Conduit will automatically create your starting api based on your existing models.
Install via PyPI:
pip install django-conduit
Add the following to your INSTALLED_APPS:
INSTALLED_APPS = ( ... 'conduit', # 'api', )
Generate your API app by running the following:
./manage.py create_api [name_of_your_app] --folder=api
Uncomment ‘api’ in your INSTALLED_APPS
Point your main URLconf (normally project_name/urls.py) to your new ‘api’ app:
urlpatterns = patterns('', ... url(r'^api/', include('api.urls')), ... )
Visit
localhost:8000/api/v1/[model_name]
to fetch one of your new resources!
All your new resources will be defined in api/views.py, and they will be registered with your Api object in api/urls.py.
Topics¶
- Filtering & Ordering
- Related Resources
- Access & Authorization
- Custom Fields
- Forms & Validation
- Conduit Views
- ModelResource
- Customizing Resources`