Subscriptions for Django REST Framework over Websockets.
Project description
Django REST Live
django-rest-live
adds real-time subscriptions over websockets to Django REST Framework
by leveraging websocket support provided by Django Channels.
Contents
Inspiration and Goals
The goal of this project is to enable realtime subscriptions without requiring any boilerplate or changing
any existing REST Framework views or serializers.
django-rest-live
took initial inspiration from this article by Kit La Touche.
Dependencies
- Django (3.0 and up)
- Django Channels (2.0 and up)
- Django REST Framework
channels_redis
for channel layer support in production.
Installation
If your project already uses REST framework, but this is the first realtime component, then make sure to install and properly configure Django Channels before continuing.
You can find details in the Channels documentation.
- Add
rest_live
to yourINSTALLED_APPS
INSTALLED_APPS = [
# Any other django apps
"rest_framework",
"channels",
"rest_live",
]
- Add
rest_live.consumers.SubscriptionConsumer
to your websocket routing. Feel' free to choose any URL path, here we've chosen/ws/subscribe/
.
from rest_live.consumers import SubscriptionConsumer
websockets = URLRouter(
[path("ws/subscribe/", SubscriptionConsumer, name="subscriptions")]
)
application = ProtocolTypeRouter({
"websocket": websockets
})
That's it! You're now ready to configure and use django-rest-live
.
Usage
These docs will use an example to-do app called todolist
with the following models and serializers:
# todolist/models.py
from django.db import models
class List(models.Model):
name = models.CharField(max_length=64)
class Task(models.Model):
text = models.CharField(max_length=140)
done = models.BooleanField(default=False)
list = models.ForeignKey("List", on_delete=models.CASCADE)
# todolist/serializers.py
from rest_framework import serializers
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ["id", "text", "done"]
class TodoListSerializer(serializers.ModelSerializer):
tasks = TaskSerializer(many=True, read_only=True)
class Meta:
model = List
fields = ["id", "name", "tasks"]
Basic Usage
An important thing to remember about the django-rest-live
package is that its sole purpose is sending updates to clients
over websocket connections. Clients should still use normal REST framework endpoints generated by ViewSets and views
to get initial data to populate a page, as well as any write-driven behavior (POST
, PATCH
, PUT
, DELETE
).
django-rest-live
gets rid of the need for periodic GET requests for updated data.
Server-Side
In order to tell django-rest-live
that you'd like to allow clients to subscribe to updates for a specific model, the package
provides the @subscribable
class decorator. This decorator is meant to be applied to ModelSerializer
subclasses,
so that the package can register both which models to allow subscriptions to as well as how those models should be
serialized when being sent to the client. To enable clients to subscribe to updates to individual to-dos, all you need
to do is apply the decorator to the TodoSerializer
:
# todolist/serializers.py
from rest_live.decorators import subscribable
...
@subscribable()
class TaskSerializer(serializers.ModelSerializer):
...
Client-Side
Subscribing to model updates from a client requires opening a WebSocket
connection to the URL you specified during setup. In our example case, that URL is /ws/subscribe/
. After the connection
is established, send a JSON message (using JSON.stringify()
) in this format:
{
"model": "todolist.Task",
"property": "id",
"value": 1
}
The model label should be in Django's standard app.modelname
format, and the pk
property is the primary key for the
model, generally the id
field.
The example message above would subscribe to updates for the todo task with an ID of 1. As mentioned above, the client should make a GET request to get the entire list, with all its tasks and their associated IDs, to figure out which IDs to subscribe to.
When the Task with primary key 1
updates, a message in this format will be sent over the websocket:
{
"model": "test_app.Todo",
"instance": {"id": 1, "text": "test", "done": true},
"action": "UPDATED"
}
Valid action
values are UPDATED
, CREATED
, and DELETED
.
Advanced Usage
Subscribe to groups
By default, subscriptions are grouped by the primary key: you send one message to the websocket to get updates for a single Task with a given primary key. But in the todo list example, you'd generally be interested in an entire list of tasks, including being notified of any tasks which have been created since the page was first loaded.
Rather than subscribe to single tasks individually, you want to subscribe to a list: an entire group of tasks.
This is where group keys come in. Pass in the group_key
you'd like to group tasks by
to the @subscribable
decorator to register subscriptions for an entire list:
# todolist/serializers.py
from rest_live.decorators import subscribable
...
@subscribable(group_key="list_id")
class TaskSerializer(serializers.ModelSerializer):
...
On the client side, subscription requests will no longer be pk
, but the list_id
of the list you'd like to get updates
from:
{
"model": "todolist.Task",
"property": "list_id",
"value": 1
}
This will subscribe you to updates for all Tasks where list_id
is 1
.
What's important to remember here is that while the field is defined as a ForeignKey
called list
on the model,
the underlying integer field in the database that links together Tasks and Lists is called list_id
, or, more generally,
<fieldname>_id
for any related fieldname on the model.
The subscribable decorator can be stacked. If you want to enable subscriptions by both list_id
for entire lists and
pk
for individual tasks, add two decorators:
# todolist/serializers.py
from rest_live.decorators import subscribable
...
@subscribable()
@subscribable(group_key="list_id")
class TaskSerializer(serializers.ModelSerializer):
...
Just note that clients which subscribe to list updates and individual pk updates will receive two messages when a task updates.
Permissions
The @subscribable
decorator also takes in a parameter called check_permission
. This is a function which takes in
a User and a model instance and determines whether or not the given user can access the given model. To make sure
users can only subscribe to lists when they are logged in, this code would suffice:
# todolist/serializers.py
from rest_live.decorators import subscribable
def has_auth(user, instance):
return user.is_authenticated
@subscribable(group_key="list_id", check_permission=has_auth)
class TaskSerializer(serializers.ModelSerializer):
...
Conditional Serializer Pattern
A common pattern in Django REST Framework is showing users different serializers based on their authentication status
by overloading get_serializer_class()
in a ViewSet
. This pattern can be mirrored in django-rest-live
using the check_permission
callback. Let's say that for our to-do app, un-authenticated users can view tasks, but
cannot see if they're completed. Users can subscribe with the proper serializer with the following serializers.py
:
# todolist/serializers.py
from rest_framework import serializers
from rest_live.decorators import subscribable
def has_auth(user, instance):
return user.is_authenticated
def has_no_auth(user, instance):
return not has_auth(user, instance)
@subscribable(group_key="list_id", check_permission=has_auth)
class AuthedTaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ["id", "text", "done"]
@subscribable(group_key="list_id", check_permission=has_no_auth)
class NoAuthTaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ["id", "text"]
Clients in both situations would send the same subscribe request, but would receive different model instances depending on their authentication status.
Limitations
This package works by listening in on model lifecycle events sent off by Django's signal dispatcher.
Specifically, the post_save
and post_delete
signals. This means that django-rest-live
can only pick up changes that Django knows about. Bulk operations, like filter().update()
, bulk_create
and bulk_delete
do not trigger Django's lifecycle signals, so updates will not be sent.
TODO
- Permissions
- Conditional Serializers
- Permissions helpers for DRF
Permission
classes - Error handling and reporting
- Expand related fields from
field
tofield_id
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_rest_live-0.2.2-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | be00d6bc5323cb5d1c9bf960a28afb4ff636e3ba2943ebc6fc6d15c6a1e34cef |
|
MD5 | 44e6af819abdde4dfd4d67d39060e93b |
|
BLAKE2b-256 | be7311b78a8a2b629756d0035dcf19c3b45d60a124822fa7ae36b1522cd5f992 |