Django REST Framework client is a Python client for REST APIs serving relational data. At the moment it supports JSON responses produced by Django REST Framework's ModelSerializer
. It provides a subset of Django ORM API.
Define a client model:
class Customer(restframeworkclient.Model):
class Meta:
resource = 'customers'
base_url = 'http://example.org/v1'
(NOTE: You can also define REST_FRAMEWORK_CLIENT = {'DEFAULT_BASE_URL': 'http://example.org/v1'}
in your Django settings to provide a default value for all models so you don't need to specify Meta.base_url
for each model separately.)
Then use it just as you would use django.db.models.Model
:
customer = Customer.objects.get(pk=29481739)
This will make a GET
request to http://example.org/v1/customers/29481739/
. The server might reply with the following response:
{
"id": 133562,
"name": "John Smith",
"created_at": "2016-08-24T00:34:26Z"
}
The returned object field values are available as customer.id
, customer.name
and customer.created_at
.
Additionally customer.pk
is an alias for customer.id
by default. To change the default:
class Customer(restframeworkclient.Model):
class Meta:
resource = 'customers'
primary_key = 'pk' # 'id' is the default value
JSON doesn't support datetime objects natively so in order to get the native python datetime.datetime
objects you need to declare the fields explicitly using restframeworkclient.fields.DateTimeField
:
class Customer(restframeworkclient.Model):
created_at = restframeworkclient.fields.DateTimeField()
class Meta:
resource = 'customers'
There are also restframeworkclient.fields.DateField
and restframeworkclient.fields.TimeField
for datetime.date
and datetime.time
respectively.
You can subclass restframeworkclient.fields.Field for implementing custom field types. For an example, see restframeworkclient.fields.FileField
which is designed to simlulate Django's FileField
, see https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.FileField.
When iterating over Customer.objects.all()
the server might reply with the following response:
{
"count": 10,
"next": "http://example.org/v1/customers/?limit=2&offset=2",
"previous": null,
"results": [
{
"id": 133562,
"name": "John Smith",
"created_at": "2016-08-24T00:34:26Z"
},
{
"id": 133563,
"name": "John Carpenter",
"created_at": "2016-08-25T00:34:26Z"
},
]
}
The keys count
(the total amount of records), next
(next page URL), previous
(previous page URL) and results
must be present in the response. Django REST Framework does this natively when using its ModelSerializer
.
After iterating over the first page results the next
page URL will be used to get the next results as needed. If the iterator is to be consumed fully then iteration stops when the server responds with null
as the next page URL.
It is assumed that the server supports offset
and limit
GET parameters --for example Django REST Framework is used with LimitOffsetPagination
enabled (see http://www.django-rest-framework.org/api-guide/pagination/#limitoffsetpagination) as restframeworkclient will use offset
and limit
GET parameters where appropriate, for example:
Customer.objects.first()
will make a GET
request to http://example.org/v1/customers/?limit=1
and
list(Customer.objects.all()[10:30])
will make a GET
request to http://example.org/v1/customers/?offset=10&limit=20
.
You can use restframeworkclient.Reference
to refer to another client model similarly as you would when using Django's ForeignKey
.
class UserAccount(restframeworkclient.Model):
customer = Reference(Customer, related_name='user', one_to_one=True)
class Meta:
resource = 'ssusers'
The first parameter can be either a direct reference to a class (e.g. Customer
) or a string containing the importable class reference (e.g. 'Customer'
). The use of strings here can help avoid circular dependencies.
Getting another referenced model instance is easy
user = UserAccount.objects.get(pk=1)
The server might reply with the following response:
{
"id": 1,
"customer": 133562
}
When accessing user.customer
a GET
request to http://example.org/v1/customers/133562/
will be made and a Customer
instance will be returned:
>>> customer = user.customer
>>> customer
Customer(id=133562, ...)
Use the _id
sufffix after the field name (e.g. user.customer_id
) to get the raw reference value (133562
)
Use related_name
as a field name to get the other instance in the other direction just as you would do in Django's OneToOneField
or ForeignKey
:
>>> customer.user
UserAccount(id=1, ...)
This will make a GET
request to http://example.org/v1/ssusers/?customer=133562&limit=1
.
You can specify for each client model its own Meta.base_url
so there can be several REST APIs referencing each other seamlessly on the client-side.
Consider these two client models:
class Contract(restframeworkclient.Model):
class Meta:
resource = 'contracts'
class Device(restframeworkclient.Model):
contract = Reference(Contract, related_name='devices')
class Meta:
resource = 'devices'
contract = Contract.objects.get(pk=1)
Getting the devices of a given contract:
contract.devices.all()
which will make a GET
request to http://example.org/v1/devices/?contract=28537
.
You can use more filters at the same time:
contract.devices.filter(is_active=True)
which will make a GET
request to http://example.org/v1/devices/?contract=28537&is_active=True
.
Like with Django contenttypes framework (see https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/) restframeworkclient also supports generic relations with which different objects can refer to other objects regardless of the model.
When using Django REST Framework's HyperlinkedModelSerializer
the related objects are referred by hyperlinks so support for generic relations comes natively as the URL contains all the necessary information where to fetch the related objects.
When using Django REST Framework's ModelSerializer
on the other hand there is only the primary key so for a generic relation an additional field must be dedicated for storing the information of the resource where the target object can be found.
Let's consider the following two client models:
class Memo(restframeworkclient.Model):
content_object = GenericRelationField('content_type', 'object_id')
class Meta:
resource = 'memos'
class Customer(restframeworkclient.Model):
class Meta:
resource = 'customers'
content_type = 'customer'
When doing:
memo = Memo.objects.get(pk=1)
the server might respond with:
{
"id": 1,
"target_id": 10,
"content_type": "customer"
}
Accessing the name of the GenericRelationField
will then fetch the related object from the client model that has Meta.content_type
identical with the value of the first field passed to GenericRelationField
(in this case content_type
):
>>> memo.content_object
Customer(id=10)
which will make a GET request to http://example.org/v1/customers/15643/
To be able to get all the objects related to a object via a generic relation use ReverseReference
, for example:
class Customer(restframeworkclient.Model):
memos = ReverseReference('Memo', field_name='object_id', filters={'model': 'Customer'})
class Meta:
resource = 'customers'
You can then get all memos related to a given customer via:
customer.memos.all()
which will make a GET
request to http://example.org/v1/memos/?object_id=133562&model=Customer&limit=1
Calling
Customer.objects.filter(first_name='John')
will make a GET
request to http://example.org/v1/customers/?first_name=John
.
Chaining multiple filters is also possible:
Customer.objects.filter(first_name='John').filter(last_name='Smith')
which is equivalent to
Customer.objects.filter(first_name='John', last_name='Smith')
both would make a GET
request to http://example.org/v1/customers/?first_name=John&last_name=Smith
.
get()
works similarly to Django's get()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.get
When specifying just the pk
parameter, e.g. Customer.objects.get(pk=1)
) the GET
query sent to the server will be https://example.org/v1/customers/1/
instead of the universal form https://example.org/v1/customers/?id=1
. Parameter pk
is being rewritten as Meta.primary_key
(id
by default). When specifying more than one parameter, the universal form is used.
get_or_create()
works similarly to Django's get_or_create()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.get\_or\_create
create()
works similarly to Django's create()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.create
Calling
Customer.objects.exclude(something=1)
is equivalent to calling
Customer.objects.filter(exclude__something=1)
Unlike Django, exclude
doesn't accept more than one parameter at the same time.
exists()
works similarly to Django's exists()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.exists
Calling
contract.devices.exists()
makes a GET
request to http://example.org/v1/devices/?contract=28537&limit=1
first()
works similarly to Django's first()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.first
contract.devices.first()
makes a GET
request to http://example.org/v1/devices/?contract=28537&limit=1
and returns the client model instance or None
if server returned empty results.
all()
works similarly to Django's all()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.all
none()
works similarly to Django's none()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.none
Calling none()
will return empty results and won't make any requests to the server.
Customer.objects.none()
count()
returns the total number of objects as reported by the server in the reply. Calling:
Customer.objects.filter(first_name='John').count()
makes a GET
query to http://example.org/v1/customers/?first_name=John&limit=1
and returns the number 10
extracted from the count
key from the JSON response:
{
"count": 10,
"next": "...",
"previous": null,
"results": [
{
...
}
]
}
Note that Django REST Framework returns the total number of results regardless of paging parameters so doing calls like Customer.objects.all()[10:30].count()
will return the same value as Customer.objects.all().count()
.
last()
works similarly to Django's last()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.last
contract.devices.last()
makes a GET
request to http://example.org/v1/devices/?contract=28537&ordering=-id&limit=1
order_by()
works similarly to Django's order_by()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.order\_by
contract.devices.order_by('-created_at', 'termination_time').last()
makes a GET
request to http://example.org/v1/devices/?ordering=created_at%2C-termination_time&limit=1&contract=28537
earliest()
works similarly to Django's earliest()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.earliest
latest()
works similarly to Django's latest()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.latest
contract.devices.latest('created_at')
makes a GET
request to https://example.org/v1/devices/?ordering=-created_at&limit=1&contract=28537
Calling
Customer.objects.all().select_related('field1', 'field2')
is identical to calling
Customer.objects.all().filter(select_related='field1,field2')
as both produce the same GET
request. The restframeworkclient.Reference
will automatically turn nested objects into object instances without making additional requests regardless of calling select_related()
or not. If the server is setup to return nested objects based on the value of the select_related
GET
parameter then it will behave similarly to the Django's select_related()
, see https://docs.djangoproject.com/en/dev/ref/models/querysets/#select-related.
Call prefetch_related()
when filtering objects that have ReverseReference
fields which will be accessed multiple times for multiple objects returned. Accessing ReverseReference
with its name passed into prefetch_related()
will fetch all related objects at once. For example:
for customer in Customer.objects.all().prefetch_related('devices'):
customer.devices.filter(is_active=True)
First call to customer.devices
will make a cumulative GET
query with all of the customer ids that the query Customer.objects.all()
returns (let's say 1,2,3): http://example.org/v1/devices/?customer__in=1&customer__in=2&customer__in=3&is_active=True
instead of doing these 3 queries separately:
http://example.org/v1/devices/?customer1&is_active=True
http://example.org/v1/devices/?customer2&is_active=True
http://example.org/v1/devices/?customer3&is_active=True
To enable the built-in per-request response caching of the restframeworkclient
put 'restframeworkclient.middleware.RESTFrameworkClientCacheMiddleware'
into your Django's MIDDLEWARE_CLASSES
. Only GET
requests will be cached. New web application requests will invalidate the cache. Non-GET
requests in the same web application request will invalidate the cache. There is one cache per thread.
To change a field value do:
customer = Customer.objects.get(pk=1)
customer.first_name = 'Joe'
customer.save()
which will make a PATCH
request to http://example.org/v1/customers/1/ with the body {"first_name": "Joe"}
. As opposed to Django not all fields are saved, only the changed ones.
Call delete()
on an client model instance to request deletion on the server, e.g.
Customer.objects.get(pk=1).delete()
will make a DELETE
request to http://example.org/v1/customers/1/
Call refresh_from_db()
to re-fetch an object from the server.
customer = Customer.objects.get(pk=1)
customer.first_name = 'Joe'
customer.refresh_from_db()
will make a GET
request to http://example.org/v1/customers/1/ and local changes to customer.first_name
will be lost.
Although the main point of restframeworkclient is working with relational data there is also support for invoking custom server-side logic returning arbitrary data given a specific model instance. Example:
class Customer(restframeworkclient.Model):
fetch_invoices = Method('fetch_invoices', method='POST')
invoice_payers = Method('invoice_payers', method='GET', as_property=True)
class Meta:
resource = 'customers'
Calling
customer.fetch_invoices(param='value')
makes a POST
request to http://example.org/v1/customers/133562/fetch_invoices/?param=value
returning what the server returns (must be JSON).
customer.invoice_payers
on the other hand makes a GET
request to http://example.org/v1/customers/133562/invoice_payers/
. The parameter as_property=True
makes invoice_payers
an object property instead of a callable method.
You can pass unwrapping_key='result'
to Method()
to extract a single value from the response (e.g. returning True
from JSON response {'result': true}
).
You can pass static=True
to Method()
to enable such functionality so you can do:
class Customer(restframeworkclient.Model):
fetch_invoices = Method('fetch_invoices', method='POST', static=True)
# ...
Then calling:
Customer.fetch_invoices(param='value')
which will make a POST
requst to http://example.org/v1/customers/fetch_invoices/?param=value
.
If the server returns a list of objects you can use MethodReturningCollection
to have them wrapped into instances of some restframeworkclient.Model
instead of working with them as a plain python dict
s. Example:
class Customer(restframeworkclient.Model):
active_devices = MethodReturningCollection('active_devices', model='Device')
class Meta:
resource = 'customers'
class Device(restframeworkclient.Model):
pass
Calling customer.active_contracts()
will make a GET
request to http://example.org/v1/customers/133562/active_devices
and if the server responds with:
[
{"id": 1, "color": "black"},
{"id": 2, "color": "white"}
]
The call will turn this response into a list of Contract
instances. No paging is supported here.
When using Django REST Framework the usage of ReverseReference
should be preferred to MethodReturningCollection
as ReverseReference
can be used together with paging and other Django REST Framework filters simultaneously:
class Customer(restframeworkclient.Model):
active_devices = ReverseReference('Device', field_name='active_devices_of_customer')
class Meta:
resource = 'customers'
class Device(restframeworkclient.Model):
class Meta:
resource = 'devices'
Calling customer.active_contracts.filter(param='value')
will make a GET
request to http://example.org/v1/devices/?active_devices_of_customer=133562¶m=value
. The server response must be in the paginated form (see Handling paginated results in this document).
The default configuration of restframeworkclient should be good for most use cases but some settings can be customized.
When REST_FRAMEWORK_CLIENT['USE_LOCAL_REST_FRAMEWORK']
is enabled restframeworkclient won't attempt to connect to the REST APIs using the Model.Meta.base_url
or REST_FRAMEWORK_CLIENT['DEFAULT_BASE_URL']
. Instead, it will use the REST APIs running inside the same Django application that the restframeworkclient is running in. It will use rest_framework.test.APIClient
of the Django REST Framework to avoid HTTP overhead. In order for this to work REST_FRAMEWORK_CLIENT['BASE_URLS']
must be set to a dict where keys should be equal to the URL prefixes under which are included urlconfs of each of the REST APIs respectively and values should contain the original base URLs of the REST APIs.
Example of a Django settings:
REST_FRAMEWORK_CLIENT = {
'USE_LOCAL_REST_FRAMEWORK': True, # default False
'BASE_URLS': {'example-org-v1': 'http://example.org/v1'},
}
Example of urls.py
additional patterns:
urlpatterns += patterns('', url('^example-org-v1/', include('path.to.example-org-v1.urls')))
Beware that this way both the server and the client share the same Django settings.
- Work was sponsored by Qvantel (http://qvantel.com).
- Author and package maintainer: Martin Riesz (https://github.com/matmas/).
Copyright (c) 2017, Qvantel
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the Qvantel nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL QVANTEL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.