Graphene + Django GraphQL subscriptions over Django Channels (async WebSockets, bounded outbox, multi-operation registry).
Project description
CypartaGraphqlSubscriptionsTools
Graphene + Django GraphQL subscriptions over Django Channels (async WebSockets). Supports graphql-transport-ws and graphql-ws via the Sec-WebSocket-Protocol header.
For background on the two protocols, see GraphQL over WebSockets: subscription-transport-ws vs graphql-ws.
What you get (v4.0.1)
- Per-connection bounded outbox —
asyncio.Queue+ one sender task (slow clients cannot queue unbounded work). Configure withCYPARTA_WS_OUTBOX_MAXSIZE(default256). - Multi-operation aware — each client
subscribeuses a transportid; groups and payloads are keyed per operation (_ops/_group_ops). - Live payload shape — each event is sent as GraphQL
ExecutionResultdata shaped as{ "<responseKey>": value }, where response key is the subscription root field’s alias if present, otherwise the field name (fromgraphql.parse). - Lifecycle helpers — optional
CypartaSubscriptionModelMixin(django-lifecycle hooks) andtrigger_subscriptionfor channel layer broadcasts.
Requirements
- Python ≥ 3.9
- Django, Graphene / graphene-django, Channels, django-lifecycle (see
setup.py/requirements.txtfor pinned versions in this repo).
Install:
pip install cypartagraphqlsubscriptionstools
Or from source:
pip install -e .
1. Install and enable the app
Add the app to INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
# ...
"channels",
"CypartaGraphqlSubscriptionsTools",
]
Configure channel layers (Redis for production; in-memory is fine for local dev):
# settings.py
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
},
}
Optional:
# Max queued outbound messages per WebSocket before drops (default 256).
CYPARTA_WS_OUTBOX_MAXSIZE = 512
Point ASGI_APPLICATION at your routing module (see below).
2. Wire ASGI and WebSocket routing
Mount CypartaGraphqlSubscriptionsConsumer on a URL your GraphQL WS client will use.
Option A — reuse the package URL patterns
# your_project/routing.py
from channels.routing import URLRouter
from django.urls import path
from CypartaGraphqlSubscriptionsTools.routing import websocket_urlpatterns
# Or merge with your own patterns:
urlpatterns_websocket = [
*websocket_urlpatterns,
# path("ws/other/", OtherConsumer.as_asgi()),
]
Option B — single explicit path
from django.urls import path
from CypartaGraphqlSubscriptionsTools.consumers import CypartaGraphqlSubscriptionsConsumer
websocket_urlpatterns = [
path("graphql/", CypartaGraphqlSubscriptionsConsumer.as_asgi()),
]
ASGI entry (typical pattern):
# your_project/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
django_asgi_app = get_asgi_application()
from your_project.routing import urlpatterns_websocket # adjust import
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(URLRouter(urlpatterns_websocket)),
})
# settings.py
ASGI_APPLICATION = "your_project.asgi.application"
3. Point Graphene at your schema
The consumer runs subscriptions with graphene_settings.SCHEMA:
# settings.py
GRAPHENE = {
"SCHEMA": "your_project.schema.schema",
}
# your_project/schema.py
import graphene
from your_app.graphql.subscriptions import Subscription as AppSubscription
class Query(graphene.ObjectType):
hello = graphene.String()
def resolve_hello(self, info):
return "world"
class Subscription(AppSubscription):
pass
schema = graphene.Schema(query=Query, subscription=Subscription)
4. WebSocket protocol (what the client must do)
- Negotiate subprotocol — the handshake must include
Sec-WebSocket-Protocol: graphql-transport-wsorgraphql-ws. Unsupported values are rejected with close code1002. connection_init— send first; the server replies withconnection_ackand only then acceptssubscribe.subscribe— must include a stringid(GraphQL transport operation id). Ifsubscribearrives beforeconnection_init, the socket is closed with4401.complete— may be sent even beforeconnection_initonly when anidis present; the server tears down that operation and sends{"type": "complete", "id": "<id>"}when applicable.
Ping / keepalive: the server periodically sends ping (transport-ws) or ka (graphql-ws).
5. Writing subscription resolvers
Inside a subscription resolver, root is the CypartaGraphqlSubscriptionsConsumer instance. Join or leave channel groups with detect_register_group_status. Because the consumer is async, call it from sync Graphene code with async_to_sync:
from asgiref.sync import async_to_sync
from CypartaGraphqlSubscriptionsTools.utils import get_model_name_instance
Typical pattern:
async_to_sync(root.detect_register_group_status)(
name_list, # e.g. ["MyModelCreated"]
subscripe, # True = join groups, False = leave
requested_fields, # optional list of field names for payload filtering, or None
operation_id=None, # optional; omit during normal subscribe execution (uses active op id)
)
requested_fields — when not None, only those keys are kept under the serialized fields dict in the pushed payload (see filter_requested_fields in utils.py).
Group names — align with what you pass to trigger_subscription (see below). The mixin uses:
{ModelName}Created{ModelName}Updated.{pk}{ModelName}Deleted.{pk}
Example: model created
import graphene
from asgiref.sync import async_to_sync
from graphene_django.types import DjangoObjectType
from CypartaGraphqlSubscriptionsTools.utils import get_model_name_instance
from your_app.models import MyModel
class MyModelType(DjangoObjectType):
class Meta:
model = MyModel
class Subscription(graphene.ObjectType):
my_model_created = graphene.Field(MyModelType, subscripe=graphene.Boolean(required=True))
def resolve_my_model_created(root, info, subscripe):
requested_fields = [
s.name.value for s in info.field_nodes[0].selection_set.selections
]
model_name = get_model_name_instance(MyModelType)
return async_to_sync(root.detect_register_group_status)(
[f"{model_name}Created"],
subscripe,
requested_fields,
)
Use the same idea for Updated / Deleted with groups like f"{model_name}Updated.{id}" (match your client arguments and your trigger_subscription calls).
6. Model mixin (optional)
Subclass CypartaSubscriptionModelMixin so creates / updates / deletes emit channel events (requires django-lifecycle on the model):
# your_app/models.py
from django.db import models
from CypartaGraphqlSubscriptionsTools.mixins import CypartaSubscriptionModelMixin
class Article(CypartaSubscriptionModelMixin, models.Model):
title = models.CharField(max_length=200)
7. Publishing events from your code
Use trigger_subscription to send a message to everyone in a channel group. Values that are models.Model instances are passed through serialize_value (JSON serialize + shape with pk, fields, optional group):
from asgiref.sync import async_to_sync
from CypartaGraphqlSubscriptionsTools.events import trigger_subscription
async_to_sync(trigger_subscription)("MyModelCreated", instance)
Custom group names work as long as subscription resolvers register the same strings.
8. Optional WebSocket auth middleware
The package includes TokenAuthMiddleware (Authorization: Token <key> → sets scope["user"]). It expects Django REST framework’s Token model to be available if you use it:
# asgi.py (excerpt)
from channels.auth import AuthMiddlewareStack
from channels.routing import URLRouter
from CypartaGraphqlSubscriptionsTools.middleware import TokenAuthMiddleware
from your_project.routing import urlpatterns_websocket
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(
TokenAuthMiddleware(URLRouter(urlpatterns_websocket))
),
})
9. Upgrading from older releases
- RxPY removed — delivery uses a bounded queue + sender task.
- Adapter settings removed — no
CYPARTA_GRAPHQL_SUBSCRIPTION_ADAPTER,CYPARTA_LEGACY_SUBSCRIPTION_DATA, oradapt_channel_event. - Synthetic register/unregister “next” acks removed — wire traffic is normal GraphQL
next/datapayloads pluscompletewhen an operation ends. - Subscribe must include
id;connection_initbeforesubscribeis enforced (4401if violated). - Payload data uses
{ responseKey: ... }(alias-aware) for live subscription events.
Links
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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cypartagraphqlsubscriptionstools-4.0.1.tar.gz.
File metadata
- Download URL: cypartagraphqlsubscriptionstools-4.0.1.tar.gz
- Upload date:
- Size: 16.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9ff579cc9937b5e686c0ac0e84699cae827aff1c2d7e6a5d621fd66fb7944ae
|
|
| MD5 |
892dafd46a38ed315ac82766382ebef4
|
|
| BLAKE2b-256 |
a8969db6d432ee13eb18cecf7c8e25fea0e960389cb07079305cb0c43cca431e
|
File details
Details for the file cypartagraphqlsubscriptionstools-4.0.1-py3-none-any.whl.
File metadata
- Download URL: cypartagraphqlsubscriptionstools-4.0.1-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da079fed255a3c5816c7b5915236c8d72005eb8fde2100450f79070ad07e73ed
|
|
| MD5 |
5988a0b7f0e53e2013bb612e7beeebec
|
|
| BLAKE2b-256 |
8c5ea5780aa1e4439c651cffefa84c301ec0e0dd79cb2536d8a31f4d45f042ff
|