{"id":13725231,"url":"https://github.com/datadvance/DjangoChannelsGraphqlWs","last_synced_at":"2025-05-07T19:33:22.822Z","repository":{"id":32742174,"uuid":"138874616","full_name":"datadvance/DjangoChannelsGraphqlWs","owner":"datadvance","description":"Django Channels based WebSocket GraphQL server with Graphene-like subscriptions","archived":false,"fork":false,"pushed_at":"2024-07-19T19:34:49.000Z","size":949,"stargazers_count":281,"open_issues_count":33,"forks_count":85,"subscribers_count":15,"default_branch":"master","last_synced_at":"2024-11-11T21:38:44.178Z","etag":null,"topics":["django","django-channels","graphene","graphql","graphql-server","graphql-subscriptions","graphql-websocket-server","python","websockets"],"latest_commit_sha":null,"homepage":null,"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/datadvance.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-06-27T11:50:46.000Z","updated_at":"2024-10-31T15:07:25.000Z","dependencies_parsed_at":"2024-09-18T19:01:18.486Z","dependency_job_id":null,"html_url":"https://github.com/datadvance/DjangoChannelsGraphqlWs","commit_stats":{"total_commits":382,"total_committers":16,"mean_commits":23.875,"dds":0.418848167539267,"last_synced_commit":"1ade107f61a1c39bdfd5896d186416fe642a5ec3"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datadvance%2FDjangoChannelsGraphqlWs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datadvance%2FDjangoChannelsGraphqlWs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datadvance%2FDjangoChannelsGraphqlWs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/datadvance%2FDjangoChannelsGraphqlWs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/datadvance","download_url":"https://codeload.github.com/datadvance/DjangoChannelsGraphqlWs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224645267,"owners_count":17346106,"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":["django","django-channels","graphene","graphql","graphql-server","graphql-subscriptions","graphql-websocket-server","python","websockets"],"created_at":"2024-08-03T01:02:16.440Z","updated_at":"2024-11-14T15:30:57.309Z","avatar_url":"https://github.com/datadvance.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"\u003c!--\nCopyright (C) DATADVANCE, 2010-2023\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n--\u003e\n\n# Django Channels based WebSocket GraphQL server with Graphene-like subscriptions\n\n[![PyPI](https://img.shields.io/pypi/v/django-channels-graphql-ws.svg)](https://pypi.org/project/django-channels-graphql-ws/)\n[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-channels-graphql-ws.svg)](https://pypi.org/project/django-channels-graphql-ws/)\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/django-channels-graphql-ws)](https://pypi.org/project/django-channels-graphql-ws/)\n[![GitHub Release Date](https://img.shields.io/github/release-date/datadvance/DjangoChannelsGraphqlWs)](https://github.com/datadvance/DjangoChannelsGraphqlWs/releases)\n[![Travis CI Build Status](https://travis-ci.com/datadvance/DjangoChannelsJobmanager.svg?branch=master)](https://travis-ci.com/datadvance/DjangoChannelsJobmanager)\n[![GitHub Actions Tests](https://github.com/datadvance/DjangoChannelsGraphqlWs/workflows/Tests/badge.svg)](https://github.com/datadvance/DjangoChannelsGraphqlWs/actions?query=workflow%3ATests)\n[![Code style](https://img.shields.io/badge/code%20style-black-black.svg)](https://github.com/ambv/black)\n[![PyPI - License](https://img.shields.io/pypi/l/django-channels-graphql-ws.svg)](https://github.com/datadvance/DjangoChannelsGraphqlWs/blob/master/LICENSE)\n\n- [Django Channels based WebSocket GraphQL server with Graphene-like subscriptions](#django-channels-based-websocket-graphql-server-with-graphene-like-subscriptions)\n  - [Features](#features)\n  - [Installation](#installation)\n  - [Getting started](#getting-started)\n  - [Example](#example)\n  - [Details](#details)\n    - [Automatic Django model serialization](#automatic-django-model-serialization)\n    - [Execution](#execution)\n    - [Context and scope](#context-and-scope)\n    - [Authentication](#authentication)\n    - [The Python client](#the-python-client)\n    - [The GraphiQL client](#the-graphiql-client)\n    - [Testing](#testing)\n    - [Subscription activation confirmation](#subscription-activation-confirmation)\n    - [GraphQL middleware](#graphql-middleware)\n  - [Alternatives](#alternatives)\n  - [Development](#development)\n    - [Bootstrap](#bootstrap)\n    - [Where to start reading the code](#where-to-start-reading-the-code)\n    - [Running tests](#running-tests)\n    - [Making release](#making-release)\n  - [Contributing](#contributing)\n  - [Acknowledgements](#acknowledgements)\n\n\n## Features\n\n- WebSocket-based GraphQL server implemented on the\n  [Django Channels v3](https://github.com/django/channels).\n- WebSocket protocol is compatible with\n  [Apollo GraphQL](https://github.com/apollographql) client.\n- [Graphene](https://github.com/graphql-python/graphene)-like\n  subscriptions.\n- All GraphQL requests are processed concurrently (in parallel).\n- Subscription notifications delivered in the order they were issued.\n- Optional subscription activation message can be sent to a client. This\n  is useful to avoid race conditions on the client side. Consider the\n  case when client subscribes to some subscription and immediately\n  invokes a mutations which triggers this subscription. In such case the\n  subscription notification can be lost, cause these subscription and\n  mutation requests are processed concurrently. To avoid this client\n  shall wait for the subscription activation message before sending such\n  mutation request.\n- Customizable notification strategies:\n    - A subscription can be put to one or many subscription groups. This\n      allows to granularly notify only selected clients, or, looking\n      from the client's perspective - to subscribe to some selected\n      source of events. For example, imaginary subscription\n      \"OnNewMessage\" may accept argument \"user\" so subscription will\n      only trigger on new messages from the selected user.\n    - Notification can be suppressed in the subscription resolver method\n      `publish`. For example, this is useful to avoid sending\n      self-notifications.\n- All GraphQL \"resolvers\" run in the main eventloop. Asynchronous\n  \"resolvers\" able to execute blocking calls with `asyncio.to_thread` or\n  `channels.db.database_sync_to_async` wrappers.\n- Resolvers (including subscription's `subscribe` \u0026 `publish`) can be\n  represented both as synchronous or asynchronous (`async def`) methods.\n- Subscription notifications can be sent from both synchronous and\n  asynchronous contexts. Just call `MySubscription.broadcast()` or\n  `await MySubscription.broadcast()` depending on the context.\n- Clients for the GraphQL WebSocket server:\n    - AIOHTTP-based client.\n    - Client for unit test based on the Channels testing communicator.\n- Requires Python 3.8 and newer. Tests run on 3.8, 3.9, 3.10.\n- Works on Linux, macOS, and Windows.\n\n\n## Installation\n\n```shell\npip install django-channels-graphql-ws\n```\n\n\n## Getting started\n\nCreate a GraphQL schema using Graphene. Note the `MySubscription` class.\n\n```python\nimport channels_graphql_ws\nimport graphene\n\nclass MySubscription(channels_graphql_ws.Subscription):\n    \"\"\"Simple GraphQL subscription.\"\"\"\n\n    # Leave only latest 64 messages in the server queue.\n    notification_queue_limit = 64\n\n    # Subscription payload.\n    event = graphene.String()\n\n    class Arguments:\n        \"\"\"That is how subscription arguments are defined.\"\"\"\n        arg1 = graphene.String()\n        arg2 = graphene.String()\n\n    @staticmethod\n    def subscribe(root, info, arg1, arg2):\n        \"\"\"Called when user subscribes.\"\"\"\n\n        # Return the list of subscription group names.\n        return [\"group42\"]\n\n    @staticmethod\n    def publish(payload, info, arg1, arg2):\n        \"\"\"Called to notify the client.\"\"\"\n\n        # Here `payload` contains the `payload` from the `broadcast()`\n        # invocation (see below). You can return `None` if you wish to\n        # suppress the notification to a particular client. For example,\n        # this allows to avoid notifications for the actions made by\n        # this particular client.\n\n        return MySubscription(event=\"Something has happened!\")\n\nclass Query(graphene.ObjectType):\n    \"\"\"Root GraphQL query.\"\"\"\n    # Graphene requires at least one field to be present. Check\n    # Graphene docs to see how to define queries.\n    value = graphene.String()\n    async def resolve_value(self):\n        return \"test\"\n\nclass Mutation(graphene.ObjectType):\n    \"\"\"Root GraphQL mutation.\"\"\"\n    # Check Graphene docs to see how to define mutations.\n    pass\n\nclass Subscription(graphene.ObjectType):\n    \"\"\"Root GraphQL subscription.\"\"\"\n    my_subscription = MySubscription.Field()\n\ngraphql_schema = graphene.Schema(\n    query=Query,\n    mutation=Mutation,\n    subscription=Subscription,\n)\n```\n\nMake your own WebSocket consumer subclass and set the schema it serves:\n\n```python\nclass MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):\n    \"\"\"Channels WebSocket consumer which provides GraphQL API.\"\"\"\n    schema = graphql_schema\n\n    # Uncomment to send ping message every 42 seconds.\n    # send_ping_every = 42\n\n    # Uncomment to process requests sequentially (useful for tests).\n    # strict_ordering = True\n\n    async def on_connect(self, payload):\n        \"\"\"New client connection handler.\"\"\"\n        # You can `raise` from here to reject the connection.\n        print(\"New client connected!\")\n```\n\nSetup Django Channels routing:\n\n```python\napplication = channels.routing.ProtocolTypeRouter({\n    \"websocket\": channels.routing.URLRouter([\n        django.urls.path(\"graphql/\", MyGraphqlWsConsumer.as_asgi()),\n    ])\n})\n```\n\nNotify\u003csup\u003e[﹡](#redis-layer)\u003c/sup\u003e clients when some event happens using\nthe `broadcast()` or `broadcast_sync()` method from the OS thread where\nthere is no running event loop:\n\n```python\nMySubscription.broadcast(\n    # Subscription group to notify clients in.\n    group=\"group42\",\n    # Dict delivered to the `publish` method.\n    payload={},\n)\n```\n\nNotify\u003csup\u003e[﹡](#redis-layer)\u003c/sup\u003e clients in a coroutine function\nwith async `broadcast()` or `broadcast_async()` method:\n\n```python\nawait MySubscription.broadcast(\n    # Subscription group to notify clients in.\n    group=\"group42\",\n    # Dict delivered to the `publish` method.\n    payload={},\n)\n```\n\n\u003ca name=\"redis-layer\"\u003e﹡)\u003c/a\u003e In case you are testing your client code\nby notifying it from the Django Shell, you have to setup a\n[channel layer](https://channels.readthedocs.io/en/latest/topics/channel_layers.html#configuration)\nin order for the two instance of your application. The same applies in\nproduction with workers.\n\nYou should prefer async resolvers and async middleware over sync ones.\nAsync versions will result in faster code execution. To do DB operations\nyou can use\n[Django 4 asynchronous queries](https://docs.djangoproject.com/en/4.1/topics/async/).\n\n\n## Example\n\nYou can find simple usage example in the [example](example/) directory.\n\nRun:\n```shell\ncd example/\n# Initialize database.\n./manage.py migrate\n# Create \"user\" with password \"user\".\n./manage.py createsuperuser\n# Run development server.\n./manage.py runserver\n```\n\nPlay with the API though the GraphiQL browser at http://127.0.0.1:8000.\n\nYou can start with the following GraphQL requests:\n```graphql\n\n# Check there are no messages.\nquery read { history(chatroom: \"kittens\") { chatroom text sender }}\n\n# Send a message from your session.\nmutation send { sendChatMessage(chatroom: \"kittens\", text: \"Hi all!\"){ ok }}\n\n# Check there is a message.\nquery read { history(chatroom: \"kittens\") { text sender } }\n\n# Open another browser or a new incognito window (to have another\n# session cookie) subscribe to make it wait for events.\nsubscription s { onNewChatMessage(chatroom: \"kittens\") { text sender }}\n\n# Send another message from the original window and see how subscription\n# triggers in the other one.\nmutation send { sendChatMessage(chatroom: \"kittens\", text: \"Something ;-)!\"){ ok }}\n```\n\n\n## Details\n\nThe `channels_graphql_ws` module provides the following key classes:\n\n- `GraphqlWsConsumer`: Django Channels WebSocket consumer which\n    maintains WebSocket connection with the client.\n- `Subscription`: Subclass this to define GraphQL subscription. Very\n    similar to defining mutations with Graphene. (The class itself is a\n    \"creative\" copy of the Graphene `Mutation` class.)\n- `GraphqlWsClient`: A client for the GraphQL backend. Executes strings\n    with queries and receives subscription notifications.\n- `GraphqlWsTransport`: WebSocket transport interface for the client.\n- `GraphqlWsTransportAiohttp`: WebSocket transport implemented on the\n    [AIOHTTP](https://github.com/aio-libs/aiohttp) library.\n\nFor details check the [source code](channels_graphql_ws/) which is\nthoroughly commented. The docstrings of classes are especially useful.\n\nSince the WebSocket handling is based on the Django Channels and\nsubscriptions are implemented in the Graphene-like style it is\nrecommended to have a look the documentation of these great projects:\n\n- [Django Channels](http://channels.readthedocs.io)\n- [Graphene](http://graphene-python.org/)\n\nThe implemented WebSocket-based protocol was taken from the library\n[graphql-ws](https://github.com/enisdenjo/graphql-ws)\nwhich is used by the [Apollo GraphQL](https://github.com/apollographql).\nCheck the\n[protocol description](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)\nfor details.\n\nNOTE: Prior to 1.0.0rc7 the library used another protocol:\n[subscription-transport-ws](https://github.com/apollographql/subscriptions-transport-ws)\n(see [the protocol description](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md)).\nIn fact [Apollo GraphQL](https://github.com/apollographql) has been\nbased on this protocol for years, but eventually has switched to a new\none, so we did this as well.\n\n\n### Automatic Django model serialization\n\nThe `Subscription.broadcast` uses Channels groups to deliver a message\nto the `Subscription`'s `publish` method.\n[ASGI specification](https://github.com/django/asgiref/blob/master/specs/asgi.rst#events)\nclearly states what can be sent over a channel, and Django models are\nnot in the list. Since it is common to notify clients about Django\nmodels changes we manually serialize the `payload` using\n[MessagePack](https://github.com/msgpack/msgpack-python)\nand hack the process to automatically serialize Django models following\nthe the Django's guide\n[Serializing Django objects](https://docs.djangoproject.com/en/dev/topics/serialization/).\n\n\n### Execution\n\n- Different requests from different WebSocket client are processed\n  asynchronously.\n- By default different requests (WebSocket messages) from a single\n  client are processed concurrently in an event loop. So there is no\n  guarantee that requests will be processed in the same order the client\n  sent these requests. Actually, with HTTP we have this behavior for\n  decades.\n- It is possible to serialize message processing by setting\n  `strict_ordering` to `True`. But note, this disables parallel requests\n  execution - in other words, the server will not start processing a new\n  request from the client until it finishes the current one. See\n  comments in the class `GraphqlWsConsumer`. This mode is here primarily\n  for testing.\n- All subscription notifications are delivered in the order they were\n  issued.\n- Each request (WebSocket message) processing starts in the main thread.\n  The request's parsing and validation is offloaded into the thread\n  pool. Resolver calls made from the main thread. And for each resolver\n  it checks whether the resolver is awaitable and `await` it if so.\n\n\n### Context and scope\n\nThe context object (`info.context` in resolvers) is a `SimpleNamespace`\ninstance useful to transfer extra data between GraphQL resolvers. The\nlifetime of `info.context` corresponds to the lifetime of GraphQL\nrequest, so it does not persist content between different\nqueries/mutations/subscriptions. It also contains some useful extras:\n- `graphql_operation_id`: The GraphQL operation id came from the client.\n- `graphql_operation_name`: The name of GraphQL operation.\n- `channels_scope`:\n  [Channels scope](https://channels.readthedocs.io/en/latest/topics/consumers.html#scope).\n  Contrary to the `info.context`, the Channels scope corresponds to the\n  WebSocket connection not to the GraphQL operation/request.\n- `channel_name`: WebSocket channel name.\n\n\n### Authentication\n\nTo enable authentication it is typically enough to wrap your ASGI\napplication into the  `channels.auth.AuthMiddlewareStack`:\n\n```python\napplication = channels.routing.ProtocolTypeRouter({\n    \"websocket\": channels.auth.AuthMiddlewareStack(\n        channels.routing.URLRouter([\n            django.urls.path(\"graphql/\", MyGraphqlWsConsumer),\n        ])\n    ),\n})\n```\n\nThis gives you a Django user `info.context.channels_scope[\"user\"]` in\nall the resolvers. To authenticate user you can create a `Login`\nmutation like the following:\n\n```python\nclass Login(graphene.Mutation, name=\"LoginPayload\"):\n    \"\"\"Login mutation.\"\"\"\n\n    ok = graphene.Boolean(required=True)\n\n    class Arguments:\n        \"\"\"Login request arguments.\"\"\"\n\n        username = graphene.String(required=True)\n        password = graphene.String(required=True)\n\n    def mutate(self, info, username, password):\n        \"\"\"Login request.\"\"\"\n\n        # Ask Django to authenticate user.\n        user = django.contrib.auth.authenticate(username=username, password=password)\n        if user is None:\n            return Login(ok=False)\n\n        # Use Channels to login, in other words to put proper data to\n        # the session stored in the scope.\n        asgiref.sync.async_to_sync(channels.auth.login)(info.context.channels_scope, user)\n        # Save the session,cause `channels.auth.login` does not do this.\n        info.context.session.save()\n\n        return Login(ok=True)\n```\n\nThe authentication is based on the Channels authentication mechanisms.\nCheck\n[the Channels documentation](https://channels.readthedocs.io/en/latest/topics/authentication.html).\nAlso take a look at the example in the [example](example/) directory.\n\n\n### The Python client\n\nThere is the `GraphqlWsClient` which implements GraphQL client working\nover the WebSockets. The client needs a transport instance which\ncommunicates with the server. Transport is an implementation of the\n`GraphqlWsTransport` interface (class must be derived from it). There is\nthe `GraphqlWsTransportAiohttp` which implements the transport on the\n[AIOHTTP](https://github.com/aio-libs/aiohttp) library. Here is an\nexample:\n\n```python\ntransport = channels_graphql_ws.GraphqlWsTransportAiohttp(\n    \"ws://backend.endpoint/graphql/\", cookies={\"sessionid\": session_id}\n)\nclient = channels_graphql_ws.GraphqlWsClient(transport)\nawait client.connect_and_init()\nresult = await client.execute(\"query { users { id login email name } }\")\nusers = result[\"data\"]\nawait client.finalize()\n```\n\nSee the `GraphqlWsClient` class docstring for the details.\n\n\n### The GraphiQL client\n\nThe GraphiQL provided by Graphene doesn't connect to your GraphQL\nendpoint via WebSocket; instead you should use a modified GraphiQL\ntemplate under `graphene/graphiql.html` which will take precedence over\nthe one of Graphene. One such modified GraphiQL is provided in the\n[example](example/) directory.\n\n\n### Testing\n\nTo test GraphQL WebSocket API read the\n[appropriate page in the Channels documentation](https://channels.readthedocs.io/en/latest/topics/testing.html).\n\nIn order to simplify unit testing there is a `GraphqlWsTransport`\nimplementation based on the Django Channels testing communicator:\n`channels_graphql_ws.testing.GraphqlWsTransport`. Check its docstring\nand take a look at the [tests](/tests) to see how to use it.\n\n\n### Subscription activation confirmation\n\nThe original Apollo's protocol does not allow client to know when a\nsubscription activates. This inevitably leads to the race conditions on\nthe client side. Sometimes it is not that crucial, but there are cases\nwhen this leads to serious issues.\n[Here is the discussion](https://github.com/apollographql/subscriptions-transport-ws/issues/451)\nin the\n[`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws)\ntracker.\n\nTo solve this problem, there is the `GraphqlWsConsumer` setting\n`confirm_subscriptions` which when set to `True` will make the consumer\nissue an additional `next` message which confirms the subscription\nactivation. Please note, you have to modify the client's code to make it\nconsume this message, otherwise it will be mistakenly considered as the\nfirst subscription notification.\n\nTo customize the confirmation message itself set the `GraphqlWsConsumer`\nsetting `subscription_confirmation_message`. It must be a dictionary\nwith two keys `\"data\"` and `\"errors\"`. By default it is set to\n`{\"data\": None, \"errors\": None}`.\n\n\n### GraphQL middleware\n\nIt is possible to inject middleware into the GraphQL operation\nprocessing. For that define `middleware` setting of your\n`GraphqlWsConsumer` subclass, like this:\n\n```python\nasync def threadpool_for_sync_resolvers(next_middleware, root, info, *args, **kwds):\n    \"\"\"Offload synchronous resolvers to the threadpool.\n\n    This middleware should always be the last in the middlewares calls\n    stack and the closest to the real resolver. If this middleware is\n    not the last it will check the next middleware to call instead of\n    real resolver. The first middleware in the middlewares list will be\n    the closest to the resolver.\n    \"\"\"\n    # Invoke next middleware.\n    if asyncio.iscoroutinefunction(next_middleware):\n        result = await next_middleware(root, info, *args, **kwds)\n    else:\n        result = await asyncio.to_thread(next_middleware, root, info, *args, **kwds)\n    return result\n\nclass MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):\n    ...\n    middleware = [threadpool_for_sync_resolvers]\n```\n\nIt is recommended to write asynchronous middlewares. But synchronous\nmiddlewares are also supported:\n\n```python\ndef my_middleware(next_middleware, root, info, *args, **kwds):\n    \"\"\"My custom GraphQL middleware.\"\"\"\n    # Invoke next middleware.\n    return next_middleware(root, info, *args, **kwds)\n\nclass MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):\n    ...\n    middleware = [my_middleware]\n```\n\nFor more information about GraphQL middleware please take a look at the\n[relevant section in the Graphene documentation](https://docs.graphene-python.org/en/latest/execution/middleware/#middleware).\n\n\n## Alternatives\n\nThere is a [Tomáš Ehrlich](https://gist.github.com/tricoder42)\nGitHubGist\n[GraphQL Subscription with django-channels](https://gist.github.com/tricoder42/af3d0337c1b33d82c1b32d12bd0265ec)\nwhich this implementation was initially based on.\n\nThere is a promising\n[GraphQL WS](https://github.com/graphql-python/graphql-ws)\nlibrary by the Graphene authors. In particular\n[this pull request](https://github.com/graphql-python/graphql-ws/pull/9)\ngives a hope that there will be native Graphene implementation of the\nWebSocket transport with subscriptions one day.\n\n\n## Development\n\n\n### Bootstrap\n\n_A reminder of how to setup an environment for the development._\n\n1. Install PyEnv to be able to work with many Python versions at once\n   [PyEnv→Installation](https://github.com/pyenv/pyenv#installation).\n2. Install Python versions needed. The command should be executed in the\n   project's directory:\n   ```shell\n   $ pyenv local | xargs -L1 pyenv install\n   ```\n3. Check that pyenv works correctly. The command:\n   ```shell\n   $ pyenv versions\n   ```\n   should show python versions enlisted in\n   [.python-version](.python-version).  If everything is set up\n   correctly pyenv will switch version of python when you enter and\n   leave the project's directory. Inside the directory `pyenv which\n   python` should show you a python installed in pyenv, outside the dir\n   it should be the system python.\n4. Install Poetry (https://python-poetry.org/docs/#installation).\n   ```shell\n   $ pipx install poetry\n   ```\n5. Create local virtualenv in `.venv`, install all project dependencies\n   (from `pyproject.toml`) except the project itself.\n   ```shell\n   $ poetry install --no-root\n   ```\n6. Activate virtualenv\n   There are options:\n   - With Poetry:\n     ```shell\n     $ poetry shell\n     ```\n   - Manually:\n     ```shell\n     $ source .venv/bin/activate\n     ```\n   - With VS Code: Choose `.venv` with \"Python: Select interpreter\" and\n     reopen the terminal.\n\n7. Upgrade Pip:\n   ```shell\n   $ pip install --upgrade pip\n   ```\n\nUse:\n\n[![Code style: black](\n    https://img.shields.io/badge/code%20style-black-000000.svg\n)](\n    https://github.com/ambv/black\n)\n\n\n### Where to start reading the code\n\nThe code is inherently complex because it glues two rather different\nlibraries/frameworks Channels and Graphene. You might need some time to\ndive into. Here are some quick insights to help you to get on track.\n\nThe main classes are `GraphqlWsConsumer` and `Subscription`. The former\none is a Channels consumer which instantiates each time a WebSocket\nconnection establishes. User (of the library) subclasses it and tunes\nsettings in the successor class. The latter is from the Graphene world.\nBoth classes are tightly coupled. When client subscribes an instance of\n`GraphqlWsConsumer` subclass holding the WebSocket connection passes to\nthe `Subscription`.\n\nTo better dive in it is useful to understand in general terms how\nregular request are handled. When server receives JSON from the client,\nthe `GraphqlWsConsumer.receive_json` method is called by Channels\nroutines. Then the request passes to the `_on_gql_subscribe` method\nwhich handles GraphQL message \"SUBSCRIBE\". Most magic happens there.\n\n\n### Running tests\n\n_A reminder of how to run tests._\n\n- Run all tests on all supported Python versions:\n   ```shell\n   $ tox\n   ```\n- Run all tests on a single Python version, e.g on Python 3.8:\n   ```shell\n   $ tox -e py38\n   ```\n- Example of running a single test:\n   ```shell\n   $ tox -e py310 -- tests/test_basic.py::test_main_usecase\n   ```\n- Running on currently active Python directly with Pytest:\n   ```shell\n   $ poetry run pytest\n   ```\n\n\n### Making release\n\n_A reminder of how to make and publish a new release._\n\n1. Merge all changes to the master branch and switch to it.\n2. Update version: `poetry version minor`.\n3. Update [CHANGELOG.md](./CHANGELOG.md).\n4. Update [README.md](./README.md) (if needed).\n5. Commit changes made above.\n6. Git tag: `git tag vX.X.X \u0026\u0026 git push --tags`.\n7. Publish release to PyPI: `poetry publish --build`.\n8. Update\n   [release notes](https://github.com/datadvance/DjangoChannelsGraphqlWs/releases)\n   on GitHub.\n\n\n## Contributing\n\nThis project is developed and maintained by DATADVANCE LLC. Please\nsubmit an issue if you have any questions or want to suggest an\nimprovement.\n\n\n## Acknowledgements\n\nThis work is supported by the Russian Foundation for Basic Research\n(project No. 15-29-07043).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatadvance%2FDjangoChannelsGraphqlWs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatadvance%2FDjangoChannelsGraphqlWs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatadvance%2FDjangoChannelsGraphqlWs/lists"}