Metadata-Version: 2.1
Name: djangochannelsrestframework
Version: 0.1.2
Summary: RESTful API for WebSockets using django channels.
Home-page: https://github.com/hishnash/djangochannelsrestframework
Author: Matthaus Woolard
Author-email: matthaus.woolard@gmail.com
License: MIT
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Requires-Dist: Django (>=3.*)
Requires-Dist: channels (<3.0,>=2.1.1)
Requires-Dist: djangorestframework (>=3.0)
Provides-Extra: tests
Requires-Dist: pytest (~=5.4.0) ; extra == 'tests'
Requires-Dist: pytest-django (~=3.4.1) ; extra == 'tests'
Requires-Dist: pytest-asyncio (~=0.11.0) ; extra == 'tests'
Requires-Dist: coverage (~=4.4) ; extra == 'tests'

==============================
Django Channels Rest Framework
==============================

Django Channels Rest Framework provides a DRF like interface for building channels-v2_ websocket consumers.


This project can be used alongside HyperMediaChannels_ and ChannelsMultiplexer_ to create a Hyper Media Style api over websockets. However Django Channels Rest Framework is also a free standing framework with the goal of providing an api that is familiar to DRF users.

theY4Kman_ has developed a useful Javascript client library dcrf-client_ to use with DCRF.


.. image:: https://travis-ci.org/hishnash/djangochannelsrestframework.svg?branch=master
    :target: https://travis-ci.org/hishnash/djangochannelsrestframework

Thanks to
---------


DCRF is based of a fork of `Channels Api <https://github.com/linuxlewis/channels-api>`_ and of course inspired by `Django Rest Framework <http://www.django-rest-framework.org/>`_.


Install
-------

.. code-block:: bash

  pip install djangochannelsrestframework


A Generic Api Consumer
----------------------
In DCRF you can create a ``GenericAsyncAPIConsumer`` that works much like a GenericAPIView_ in DRF: For a more indeph look into Rest Like Websocket consumers read this blog post_.


.. code-block:: python

    from . import models
    from . import serializers
    from djangochannelsrestframework import permissions
    from djangochannelsrestframework.generics import GenericAsyncAPIConsumer
    from djangochannelsrestframework.mixins import (
        ListModelMixin,
        PatchModelMixin,
        UpdateModelMixin,
        CreateModelMixin,
        DeleteModelMixin,
    )

    class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer):
        queryset = models.Test.objects.all()
        serializer_class = serializers.TestSerializer
        permission_classes = (permissions.IsAuthenticated,)


Because this class uses the ``ListModelMixin``, one has access to the ``list`` action.

One may use the same exact querysets and ``serializer_class`` utilized in their DRF Views, but must omit the DRF permissions. Permissions are to be imported from ``djangochannelsrestframework``, which provides the standard ``AllowAny`` and ``IsAuthenticated`` permissions.

To call an action from the client send a websocket message: ``{action: "list", "request_id": 42}``


There are a selection of mixins that expose the common CURD actions:

* ``ListModelMixin`` - ``list``
* ``PatchModelMixin`` - ``patch``
* ``CreateModelMixin`` - ``create``
* ``RetrieveModelMixin`` - ``retrieve``
* ``UpdateModelMixin`` - ``update``
* ``DeleteModelMixin`` - ``delete``


Observing a Model instance
--------------------------

Consumer that let you subscribe to changes on an instance:

.. code-block:: python

   class TestConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer):
       queryset = get_user_model().objects.all()
       serializer_class = UserSerializer

this exposes the ``retrieve``, ``subscribe_instance`` and ``unsubscribe_instance`` actions.

To subscribe send:

.. code-block:: python

   {
       "action": "subscribe_instance",
       "pk": 42,  # the id of the instance you are subscribing to
       "request_id": 4  # this id will be used for all resultent updates.
   }


Actions will be sent down out from the server:

.. code-block:: python

  {
    "action": "update",
    "errors": [],
    "response_status": 200,
    "request_id": 4,
    "data": {'email': '42@example.com', 'id': 42, 'username': 'thenewname'},
  }

Adding Custom actions
---------------------


.. code-block:: python

   class UserConsumer(GenericAsyncAPIConsumer):
       queryset = get_user_model().objects.all()
       serializer_class = UserSerializer

       @action()
       async def send_email(self, pk=None, to=None, **kwargs):
           user = await database_sync_to_async(self.get_object)(pk=pk)
           # ... do some stuff
           # remember to wrap all db actions in `database_sync_to_async`
           return {}, 200  # return the contenct and the response code.

       @action()  # if the method is not async it is already wrapped in `database_sync_to_async`
       def publish(self, pk=None, **kwargs):
           user = self.get_object(pk=pk)
           # ...
           return {'pk': pk}, 200

Consumers that are not bound to Models
--------------------------------------

You can also create consumers that are not at all related to any models.

.. code-block:: python

  from djangochannelsrestframework.decorators import action
  from djangochannelsrestframework.consumers import AsyncAPIConsumer

  class MyConsumer(AsyncAPIConsumer):

      @action()
      async def an_async_action(self, some=None, **kwargs):
          # do something async
          return {'response with': 'some message'}, 200

      @action()
      def a_sync_action(self, pk=None, **kwargs):
          # do something sync
          return {'response with': 'some message'}, 200

Using your normal views over a websocket connection
---------------------------------------------------

.. code-block:: python

  from djangochannelsrestframework.consumers import view_as_consumer

  application = ProtocolTypeRouter({
      "websocket": AuthMiddlewareStack(
          URLRouter([
              url(r"^front(end)/$", view_as_consumer(YourDjangoView)),
          ])
      ),
   })



Subscribing to all instances of a model
---------------------------------------

One can subscribe to all instances of a model by utilizing the ``model_observer``.

.. code-block:: python

    from djangochannelsrestframework.observer import model_observer

    @model_observer(models.Test)
    async def model_activity(self, message, observer=None, **kwargs):
        # send activity to your frontend
        await self.send_json(message)

This method will send messages to the client on all CRUD operations made through the Django ORM.

Note: These notifications do not include bulk updates, such as ``models.Test.objects.filter(name="abc").update(name="newname")``


    **WARNING**
    When using this to decorate a method to avoid the method firing multiple
    times you should ensure that if there are multiple `@model_observer`
    wrapped methods for the same model type within a single file that each
    method has a different name.


Subscribing to a `model_observer`
=================================

You can do this in a few placed, a common example is in the ``websocket_connect`` method.

.. code-block:: python

    async def websocket_connect(self, message):

        # Super Save
        await super().websocket_connect(message)

        # Initialized operation
        await self.activities_change.subscribe()


This method utilizes the previously mentioned ``model_activity`` method to subscribe to all instances of the current Consumer's model. 

One can also subscribe by creating a custom action




Subscribing to a filtered list of models
========================================

In most situations you want to filter the set of models that you subscribe to.

To do this we need to split the model updates into `groups` and then in the consumer subscribe to the groups that we want/have permission to see.


.. code-block:: python

  class MyConsumer(AsyncAPIConsumer):

    @model_observer(models.Classroom)
    async def classroom_change_handler(self, message, observer=None, **kwargs):
        # due to not being able to make DB QUERIES when selecting a group
        # maybe do an extra check here to be sure the user has permission
        # send activity to your frontend
        await self.send_json(message)

    @classroom_change_handler.groups_for_signal
    def classroom_change_handler(self, instance: models.Classroom, **kwargs):
        # this block of code is called very often *DO NOT make DB QUERIES HERE*
        yield f'-school__{instance.school_id}'
        yield f'-pk__{instance.pk}'

    @classroom_change_handler.groups_for_consumer
    def classroom_change_handler(self, school=None, classroom=None, **kwargs):
        # This is called when you subscribe/unsubscribe
        if school is not None:
            yield f'-school__{school.pk}'
        if classroom is not None:
            yield f'-pk__{classroom.pk}'

    @action()
    async def subscribe_to_classrooms_in_school(self, school_pk, **kwargs):
        # check user has permission to do this
        await self.classroom_change_handler.subscribe(school=school)

    @action()
    async def subscribe_to_classroom(self, classroom_pk, **kwargs):
        # check user has permission to do this
        await self.classroom_change_handler.subscribe(classroom=classroom)


.. _post: https://lostmoa.com/blog/DjangoChannelsRestFramework/
.. _GenericAPIView: https://www.django-rest-framework.org/api-guide/generic-views/
.. _channels-v2: https://channels.readthedocs.io/en/latest/
.. _dcrf-client: https://github.com/theY4Kman/dcrf-client
.. _theY4Kman: https://github.com/theY4Kman
.. _HyperMediaChannels: https://github.com/hishnash/hypermediachannels
.. _ChannelsMultiplexer: https://github.com/hishnash/channelsmultiplexer


