{"id":13625383,"url":"https://github.com/NilCoalescing/djangochannelsrestframework","last_synced_at":"2025-04-16T06:32:30.787Z","repository":{"id":39612607,"uuid":"132431168","full_name":"NilCoalescing/djangochannelsrestframework","owner":"NilCoalescing","description":"A Rest-framework for websockets using Django channels-v4","archived":false,"fork":false,"pushed_at":"2024-07-08T16:32:24.000Z","size":362,"stargazers_count":617,"open_issues_count":24,"forks_count":86,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-02T02:17:31.470Z","etag":null,"topics":["channels","django","django-rest-framework"],"latest_commit_sha":null,"homepage":"https://djangochannelsrestframework.readthedocs.io/en/latest/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NilCoalescing.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["NilCoalescing"]}},"created_at":"2018-05-07T08:34:16.000Z","updated_at":"2024-10-31T09:44:02.000Z","dependencies_parsed_at":"2024-05-14T00:40:55.288Z","dependency_job_id":"c0d9f00c-3e7b-49aa-befd-db837493aa4d","html_url":"https://github.com/NilCoalescing/djangochannelsrestframework","commit_stats":{"total_commits":255,"total_committers":22,"mean_commits":"11.590909090909092","dds":0.6274509803921569,"last_synced_commit":"583e843c43137b13517f63dfbdf1d17513d83700"},"previous_names":["lostmoa/djangochannelsrestframework"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NilCoalescing%2Fdjangochannelsrestframework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NilCoalescing%2Fdjangochannelsrestframework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NilCoalescing%2Fdjangochannelsrestframework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NilCoalescing%2Fdjangochannelsrestframework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NilCoalescing","download_url":"https://codeload.github.com/NilCoalescing/djangochannelsrestframework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223700404,"owners_count":17188316,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["channels","django","django-rest-framework"],"created_at":"2024-08-01T21:01:54.852Z","updated_at":"2024-11-08T14:31:13.610Z","avatar_url":"https://github.com/NilCoalescing.png","language":"Python","funding_links":["https://github.com/sponsors/NilCoalescing"],"categories":["Python"],"sub_categories":[],"readme":"==============================\nDjango Channels Rest Framework\n==============================\n\nDjango Channels Rest Framework provides a DRF like interface for building channels-v4_ websocket consumers.\n\n\nThis 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.\n\ntheY4Kman_ has developed a useful Javascript client library dcrf-client_ to use with DCRF.\n\nThanks to\n---------\n\n\nDCRF is based of a fork of `Channels Api \u003chttps://github.com/linuxlewis/channels-api\u003e`_ and of course inspired by `Django Rest Framework \u003chttp://www.django-rest-framework.org/\u003e`_.\n\nDocumentation\n-------------\nReadTheDocs_\n\n\nInstall\n-------\n\n.. code-block:: bash\n  \n  pip install djangochannelsrestframework\n\n\n.. warning ::\n\n            In your application definition when you declare your consumers it is very important to use the ``.as_asgi()`` class method (e.g. ``MyConsumer.as_asgi()``). You **should not** have any instances of ``MyConsumer()`` in your code base.\n\n\n\nA Generic Api Consumer\n----------------------\nIn DCRF you can create a ``GenericAsyncAPIConsumer`` that works much like a GenericAPIView_ in DRF: For a more indepth look into Rest-Like Websocket consumers read this blog post_.\n\n\n.. code-block:: python\n\n    from . import models\n    from . import serializers\n    from djangochannelsrestframework import permissions\n    from djangochannelsrestframework.generics import GenericAsyncAPIConsumer\n    from djangochannelsrestframework.mixins import (\n        ListModelMixin,\n        PatchModelMixin,\n        UpdateModelMixin,\n        CreateModelMixin,\n        DeleteModelMixin,\n    )\n\n    class LiveConsumer(ListModelMixin, GenericAsyncAPIConsumer):\n        queryset = models.Test.objects.all()\n        serializer_class = serializers.TestSerializer\n        permission_classes = (permissions.IsAuthenticated,)\n\n\nBecause this class uses the ``ListModelMixin``, one has access to the ``list`` action.\n\nOne 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.\n\nTo call an action from the client send a websocket message: ``{action: \"list\", \"request_id\": 42}``\n\n\nThere are a selection of mixins that expose the common CRUD actions:\n\n* ``ListModelMixin`` - ``list``\n* ``PatchModelMixin`` - ``patch``\n* ``CreateModelMixin`` - ``create``\n* ``RetrieveModelMixin`` - ``retrieve``\n* ``UpdateModelMixin`` - ``update``\n* ``DeleteModelMixin`` - ``delete``\n\n\nObserving a Model instance\n--------------------------\n\nConsumer that let you subscribe to changes on an instance:\n\n.. code-block:: python\n\n   class TestConsumer(ObserverModelInstanceMixin, GenericAsyncAPIConsumer):\n       queryset = get_user_model().objects.all()\n       serializer_class = UserSerializer\n\nthis exposes the ``retrieve``, ``subscribe_instance`` and ``unsubscribe_instance`` actions.\n\nTo subscribe send:\n\n.. code-block:: python\n\n   {\n       \"action\": \"subscribe_instance\",\n       \"pk\": 42,  # the id of the instance you are subscribing to\n       \"request_id\": 4  # this id will be used for all result updates.\n   }\n\n\nActions will be sent down out from the server:\n\n.. code-block:: python\n\n  {\n    \"action\": \"update\",\n    \"errors\": [],\n    \"response_status\": 200,\n    \"request_id\": 4,\n    \"data\": {'email': '42@example.com', 'id': 42, 'username': 'thenewname'},\n  }\n\nAdding Custom actions\n---------------------\n\n\n.. code-block:: python\n\n   class UserConsumer(GenericAsyncAPIConsumer):\n       queryset = get_user_model().objects.all()\n       serializer_class = UserSerializer\n\n       @action()\n       async def send_email(self, pk=None, to=None, **kwargs):\n           user = await database_sync_to_async(self.get_object)(pk=pk)\n           # ... do some stuff\n           # remember to wrap all db actions in `database_sync_to_async`\n           return {}, 200  # return the context and the response code.\n\n       @action()  # if the method is not async it is already wrapped in `database_sync_to_async`\n       def publish(self, pk=None, **kwargs):\n           user = self.get_object(pk=pk)\n           # ...\n           return {'pk': pk}, 200\n\nConsumers that are not bound to Models\n--------------------------------------\n\nYou can also create consumers that are not at all related to any models.\n\n.. code-block:: python\n\n  from djangochannelsrestframework.decorators import action\n  from djangochannelsrestframework.consumers import AsyncAPIConsumer\n\n\n  class MyConsumer(AsyncAPIConsumer):\n\n      @action()\n      async def an_async_action(self, some=None, **kwargs):\n          # do something async\n          return {'response with': 'some message'}, 200\n      \n      @action()\n      def a_sync_action(self, pk=None, **kwargs):\n          # do something sync\n          return {'response with': 'some message'}, 200\n\nUsing your normal views over a websocket connection\n---------------------------------------------------\n\n.. code-block:: python\n  \n  from djangochannelsrestframework.consumers import view_as_consumer\n\n  application = ProtocolTypeRouter({\n      \"websocket\": AuthMiddlewareStack(\n          URLRouter([\n              url(r\"^front(end)/$\", view_as_consumer(YourDjangoView)),\n          ])\n      ),\n   })\n\n\n\nIn this situation if your view needs to read the `GET` query string values you can provides these using the `query` option.\nAnd if the view method reads parameters from the URL you can provides these with the `parameters`.\n\nSending the following over your WS connection will result in a GET request being evaluated on your View.\n\n.. code-block:: javascript\n\n    {\n      action: \"retrieve\",\n      query: {\"user_id\": 42}\n      parameters: {\"project_id\": 92}\n    }\n\n\n\nSubscribing to a signal.\n------------------------\n\nOne can subscribe to a custom ``Signal`` utilizing the ``observer`` decorator.\n\nHere we have a custom signal that will be triggered when a user join a chat.\n\n.. code-block:: python\n\n    # signals.py\n    from django.dispatch.dispatcher import Signal\n\n    joined_chat_signal = Signal()\n\nNow we will create the consumer with two actions, one for subscribing to our custom signal for specific chat, and another one \nfor manually trigger the signal.\n\n.. code-block:: python\n\n    # consumers.py\n    from djangochannelsrestframework.consumers import AsyncAPIConsumer\n    from djangochannelsrestframework.decorators import action\n    from djangochannelsrestframework.observer import observer\n    from rest_framework import status\n    from .signals import joined_chat_signal\n    from .serializers import UserSerializer\n\n\n    class TestConsumer(AsyncAPIConsumer):\n\n        @action()\n        def join_chat(self, chat_id, **kwargs):\n            serializer = UserSerializer(instance=self.scope['user'])\n            joined_chat_signal.send(sender='join_chat', data=serializer.data, **kwargs)\n            return {}, status.HTTP_204_NO_CONTENT\n\n        @observer(signal=joined_chat_signal)\n        async def joined_chat_handler(self, data, observer=None, action=None, subscribing_request_ids=[], **kwargs):\n            for request_id in subscribing_request_ids:\n                await self.reply(action='joined_chat', data=data, status=status.HTTP_200_OK, request_id=request_id)\n\n        @joined_chat_handler.serializer\n        def join_chat_handler(self, sender, data, **kwargs): # the data comes from the signal.send and will be available in the observer\n            return data\n\n        @joined_chat_handler.groups_for_signal\n        def joined_chat_handler(self, instance, **kwargs):\n            yield f'chat__{instance}'\n\n        @joined_chat_handler.groups_for_consumer\n        def joined_chat_handler(self, chat, **kwargs):\n            if chat:\n                yield f'chat__{chat}'\n\n        @action()\n        async def subscribe_joined(self, chat_id, request_id, **kwargs):\n            await self.joined_chat_handler.subscribe(chat_id, request_id=request_id)\n\n\nSubscribing to all instances of a model\n---------------------------------------\n\nOne can subscribe to all instances of a model by utilizing the ``model_observer``.\n\n.. code-block:: python\n\n    from djangochannelsrestframework.observer import model_observer\n\n    @model_observer(models.Test)\n    async def model_activity(self, message, observer=None, action=None, **kwargs):\n        # send activity to your frontend\n        await self.send_json(message)\n\nThis method will send messages to the client on all CRUD operations made through the Django ORM. The `action` arg here it will take values such as `create`, `delete` and `update` you should consider passing this to your frontend client.\n\nNote: These notifications do not include bulk updates, such as ``models.Test.objects.filter(name=\"abc\").update(name=\"newname\")``\n\n\n    **WARNING**\n    When using this to decorate a method to avoid the method firing multiple\n    times you should ensure that if there are multiple `@model_observer`\n    wrapped methods for the same model type within a single file that each\n    method has a different name.\n\n\nSubscribing to a `model_observer`\n=================================\n\nYou can do this in a few placed, a common example is in the ``websocket_connect`` method.\n\n.. code-block:: python\n\n    async def websocket_connect(self, message):\n\n        # Super Save\n        await super().websocket_connect(message)\n\n        # Initialized operation\n        await self.activities_change.subscribe()\n\n\nThis method utilizes the previously mentioned ``model_activity`` method to subscribe to all instances of the current Consumer's model.\n\nOne can also subscribe by creating a custom action\n\nAnother way is override ``AsyncAPIConsumer.accept(self, **kwargs)``\n\n.. code-block:: python\n\n    class ModelConsumerObserver(AsyncAPIConsumer):\n\n        async def accept(self, **kwargs):\n            await super().accept(** kwargs)\n            await self.model_change.subscribe()\n\n        @model_observer(models.Test)\n        async def model_change(self, message, action=None, **kwargs):\n            \"\"\"\n            This method is evaluated once for every user that subscribed,\n            here you have access to info about the user by reading `self.scope`\n\n            However it is best to avoid doing DB quires here since if you have lots of\n            subscribers to a given instance you will end up with a LOT of database traffic.\n            \"\"\"\n            await self.send_json(message)\n        \n        # If you want the data serialized instead of pk\n        @model_change.serializer\n        def model_serialize(self, instance, action, **kwargs):\n            \"\"\"\n            This block is evaluated before the data is sent over the channel layer\n            this means you are unable to access information\n            such as the user that it will be sent to.\n\n            If you need the user info when serializing then you can do the serialization\n            in the above method.\n            \"\"\"\n            return TestSerializer(instance).data\n\n.. note::\n\n    New Feature!\n    Now you can rewrite this as:\n    \n.. code-block:: python\n\n    class ModelConsumerObserver(AsyncAPIConsumer):\n\n        async def accept(self, **kwargs):\n            await super().accept(** kwargs)\n            await self.model_change.subscribe()\n\n        @model_observer(models.Test, serializer_class=TestSerializer)\n        async def model_change(self, message, action=None, **kwargs):\n            # in this case since we subscribe int he `accept` method\n            # we do not expect to have any `subscribing_request_ids` to loop over.\n            await self.reply(data=message, action=action)\n\n\n\nSubscribing to a filtered list of models\n========================================\n\nIn most situations you want to filter the set of models that you subscribe to.\n\nTo 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.\n\n\n.. code-block:: python\n\n  class MyConsumer(AsyncAPIConsumer):\n    # This class MUST subclass `AsyncAPIConsumer` to use `@model_observer`\n\n    @model_observer(models.Classroom)\n    async def classroom_change_handler(\n        self,\n        message,\n        observer=None,\n        action=None,\n        subscribing_request_ids=[],\n        **kwargs\n    ):\n        # due to not being able to make DB QUERIES when selecting a group\n        # maybe do an extra check here to be sure the user has permission\n        # send activity to your frontend\n        for request_id in subscribing_request_ids:\n            # we can send a separate message for each subscribing request\n            # this lets ws clients rout these messages.\n            await self.send_json(dict(body=message, action=action, request_id=request_id))\n        # note if we do not pass `request_id` to the `subscribe` method\n        # then `subscribing_request_ids` will be and empty list.\n\n    @classroom_change_handler.groups_for_signal\n    def classroom_change_handler(self, instance: models.Classroom, **kwargs):\n        # this block of code is called very often *DO NOT make DB QUERIES HERE*\n        yield f'-school__{instance.school_id}'\n        yield f'-pk__{instance.pk}'\n\n    @classroom_change_handler.groups_for_consumer\n    def classroom_change_handler(self, school=None, classroom=None, **kwargs):\n        # This is called when you subscribe/unsubscribe\n        if school is not None:\n            yield f'-school__{school.pk}'\n        if classroom is not None:\n            yield f'-pk__{classroom.pk}'\n\n    @action()\n    async def subscribe_to_classrooms_in_school(self, school_pk, request_id, **kwargs):\n        # check user has permission to do this\n        await self.classroom_change_handler.subscribe(school=school, request_id=request_id)\n\n    @action()\n    async def subscribe_to_classroom(self, classroom_pk, request_id, **kwargs):\n        # check user has permission to do this\n        await self.classroom_change_handler.subscribe(classroom=classroom, request_id=request_id)\n\n\n.. _ReadTheDocs: https://djangochannelsrestframework.readthedocs.io/en/latest/\n.. _post: https://lostmoa.com/blog/DjangoChannelsRestFramework/\n.. _GenericAPIView: https://www.django-rest-framework.org/api-guide/generic-views/\n.. _channels-v4: https://channels.readthedocs.io/en/latest/\n.. _dcrf-client: https://github.com/theY4Kman/dcrf-client\n.. _theY4Kman: https://github.com/theY4Kman\n.. _HyperMediaChannels: https://github.com/hishnash/hypermediachannels\n.. _ChannelsMultiplexer: https://github.com/hishnash/channelsmultiplexer\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNilCoalescing%2Fdjangochannelsrestframework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FNilCoalescing%2Fdjangochannelsrestframework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNilCoalescing%2Fdjangochannelsrestframework/lists"}