Metadata-Version: 2.1
Name: django-rest-live
Version: 0.2.2
Summary: Subscriptions for Django REST Framework over Websockets.
Home-page: https://github.com/pennlabs/django-rest-live
Author: Penn Labs
Author-email: admin@pennlabs.org
License: MIT
Project-URL: Changelog, https://github.com/pennlabs/django-rest-live/blob/master/CHANGELOG.md
Description: # Django REST Live
        
        [![CircleCI](https://circleci.com/gh/pennlabs/django-rest-live.svg?style=shield)](https://circleci.com/gh/pennlabs/django-rest-live)
        [![Coverage Status](https://codecov.io/gh/pennlabs/django-rest-live/branch/master/graph/badge.svg)](https://codecov.io/gh/pennlabs/django-rest-live)
        [![PyPi Package](https://img.shields.io/pypi/v/django-rest-live.svg)](https://pypi.org/project/django-rest-live/)
        
        `django-rest-live` adds real-time subscriptions over websockets to [Django REST Framework](https://github.com/encode/django-rest-framework)
        by leveraging websocket support provided by [Django Channels](https://github.com/django/channels).
        
        ## Contents
        * [Inspiration and Goals](#inspiration-and-goals)
        * [Dependencies](#dependencies)
        * [Installation](#installation)
        * [Usage](#usage)
            + [Basic Usage](#basic-usage)
              - [Server-Side](#server-side)
              - [Client-Side](#client-side)
            + [Advanced Usage](#advanced-usage)
              - [Subscribe to groups](#subscribe-to-groups)
              - [Permissions](#permissions)
              - [Conditional Serializer Pattern](#conditional-serializer-pattern)
        * [Limitations](#limitations)
        
        ## 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](https://www.oddbird.net/2018/12/12/channels-and-drf/).
        
        ## Dependencies
        - [Django](https://github.com/django/django/) (3.0 and up)
        - [Django Channels](https://github.com/django/channels) (2.0 and up) 
        - [Django REST Framework](https://github.com/encode/django-rest-framework/)
        - [`channels_redis`](https://github.com/django/channels_redis) for
          [channel layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html) 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](https://channels.readthedocs.io/en/latest/installation.html).
        
        1. Add `rest_live` to your `INSTALLED_APPS`
        ```python
        INSTALLED_APPS = [
            # Any other django apps
            "rest_framework",
            "channels",
            "rest_live",
        ]
        ```
            
        2. Add `rest_live.consumers.SubscriptionConsumer` to your websocket routing. Feel'
        free to choose any URL path, here we've chosen `/ws/subscribe/`. 
        ```python
        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:
        ```python
        # 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`:
        
        ```python
        # 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](https://developer.mozilla.org/en-US/docs/Web/API/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:
        
        ```json5
        {
          "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](https://docs.djangoproject.com/en/3.1/topics/db/queries/#the-pk-lookup-shortcut), 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:
        
        ```json
        {
            "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:
        
        
        ```python
        # 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:
        
        ```json5
        {
          "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:
        
        ```python
        # 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:
        
        ```python
        # 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`:
        
        ```python
        # 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](https://docs.djangoproject.com/en/3.1/topics/signals/).
        Specifically, the [`post_save`](https://docs.djangoproject.com/en/3.1/ref/signals/#post-save)
        and [`post_delete`](https://docs.djangoproject.com/en/3.1/ref/signals/#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
        
        - [x] Permissions 
        - [x] Conditional Serializers
        - [ ] Permissions helpers for DRF `Permission` classes
        - [ ] Error handling and reporting
        - [ ] Expand related fields from `field` to `field_id`
        
Platform: UNKNOWN
Classifier: Framework :: Django
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Requires-Python: >=3.6
Description-Content-Type: text/markdown
