Skip to content

New module to support Django Channels v2 #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 85 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
abd73b9
Django Channels v2
SmileyChris Jul 26, 2018
86762d3
Make the django app more uniquely named
SmileyChris Jul 26, 2018
d365c72
Split the channels 2 app into different modules
SmileyChris Jul 26, 2018
c9cda69
Update and simplify the channels2 example
SmileyChris Jul 26, 2018
2ce92ac
Support async generator responses in django channels
SmileyChris Jul 28, 2018
d46f6bd
Ensure graphql_ws submodules are installed
SmileyChris Aug 12, 2018
5829ff2
Ensure the graphiql template is added in the distribution
SmileyChris Aug 12, 2018
bc16ee5
Remove some unused observer code
SmileyChris Aug 21, 2018
5ddcb1b
Execute iterable operations as a separate task
SmileyChris Aug 21, 2018
dc478be
Add session_application helper to django routing
SmileyChris Aug 27, 2018
f0a2727
Behave correctly by cancelling async tasks
SmileyChris Aug 27, 2018
fe91dbb
Use lower level asyncio.wait, abstract the on_complete command
SmileyChris Aug 30, 2018
187ee78
Optimize unsubscribing to ops
SmileyChris Aug 31, 2018
99bc3a8
Cleaner iterable op running
SmileyChris Aug 31, 2018
7879f32
Simplify the django async futures
SmileyChris Sep 4, 2018
1eb443e
When closing a channels subscription, correctly await for the unsubsc…
SmileyChris Nov 20, 2018
d8d83ca
Merge branch 'master' into channels2
SmileyChris Nov 20, 2018
1eb83a5
Await pending promises in the payload
SmileyChris Jan 9, 2019
a99c1cf
Update pipfile.lock versions for django channels2 example
SmileyChris Mar 29, 2019
2e1ddfd
Get rid of rx.Observable django channels example, async is simpler
SmileyChris Jul 21, 2019
8d6fe93
Use common middleware in channels2 example
SmileyChris Jul 22, 2019
7e969a2
Change the graphql-core requirement to avoid 3.0.0a0 install in pip
SmileyChris Jul 23, 2019
d7a05bd
Merge remote-tracking branch 'upstream/master' into channels2
SmileyChris Jan 6, 2020
b1b15e0
Safer django socket sending
SmileyChris Jan 6, 2020
d7689b7
Add a django connection_context.closed prop
SmileyChris Jan 6, 2020
faea9cf
Django Channels connection closed fix
SmileyChris Jan 7, 2020
184367b
Fix a property collision in django connectioncontext
SmileyChris Jan 7, 2020
f805f3e
Ensure the Django subscription consumer cleans up any dangling tasks …
SmileyChris May 4, 2020
c00066b
Split base classes into sync and async
SmileyChris May 17, 2020
b6951ed
Fix tests to match deduplication changes
SmileyChris May 17, 2020
4fe4736
Add some base tests
SmileyChris Jun 1, 2020
f7ef09f
Add base async tests
SmileyChris Jun 6, 2020
944d949
Add django_channels tests
SmileyChris Jun 6, 2020
e4b3d9f
Remove a redundant check for an internal detail
SmileyChris May 19, 2020
7b21f0f
Move execute back to base
SmileyChris May 19, 2020
75cad35
Move operation unsubscription to BaseSubscriptionServer
SmileyChris Jun 25, 2020
e904b15
Black the example code
SmileyChris Jun 25, 2020
9499ae9
Black the modules in graphql_ws root
SmileyChris Jun 25, 2020
738f447
Skip flake8 false positives and remove unneeded import
SmileyChris Jun 28, 2020
f641e58
Update contributing doc
SmileyChris Jun 28, 2020
c007f58
Correctly test a bad graphql parameter
SmileyChris Jun 28, 2020
bb4f1be
Make removing an operation from context fail silently
SmileyChris Jun 28, 2020
a4eef79
Make async methods send an error if an operation raises an exception
SmileyChris Jun 28, 2020
3e670e6
Send completion messages when the sync observer completes / errors out.
SmileyChris Jun 28, 2020
650db34
Cody tidy
SmileyChris Jun 28, 2020
a8c2f33
Abstract ensuring async task is a future
SmileyChris Jun 29, 2020
8d32f4b
Tidy up django_channels (1) backend and example
SmileyChris Jun 29, 2020
7797c29
Update readme
SmileyChris Jun 29, 2020
cb5126a
Merge branch 'deduplicate' into channels2
SmileyChris Jun 29, 2020
d85df83
Use new abstracted base code for django channels 2
SmileyChris Jun 29, 2020
5ed4f1d
Fix a readme typo
SmileyChris Jun 30, 2020
a3197d0
Recursively resolve Promises, fix async tests
SmileyChris Jul 1, 2020
d879480
Merge commit 'deduplicate' into channels2
SmileyChris Jul 1, 2020
b6da149
Simplify django consumer now promises are resolved in the base
SmileyChris Jul 1, 2020
84d5d17
Ignore cancellederror when closing connections
SmileyChris Jul 29, 2020
6f72dec
Merge branch 'deduplicate' into channels2
SmileyChris Jul 29, 2020
a9e63be
Add the required receive method to ChannelsConnectionContext
SmileyChris Jul 29, 2020
7bfc590
Fix async processing messages
SmileyChris Jul 29, 2020
6284da1
Merge branch 'deduplicate' into HEAD
SmileyChris Jul 29, 2020
0cbcdef
Simpler receive_json
SmileyChris Jul 29, 2020
583f3f0
Fix async unsubscribe
SmileyChris Jul 29, 2020
9204c91
Merge branch 'deduplicate' into channels2
SmileyChris Jul 29, 2020
de8ced3
Move unsubscribe logic to the connection context
SmileyChris Jul 29, 2020
9bec86e
Remove a redundant async method
SmileyChris Jul 29, 2020
a1d2ebc
Only send messages for operations that exist
SmileyChris Jul 30, 2020
94d8740
Iterators are considered awaitable with the new method, so check only…
SmileyChris Jul 30, 2020
19871cf
Merge branch 'deduplicate' into channels2
SmileyChris Jul 30, 2020
218c7fc
Add request context directly to the payload rather than a request_con…
SmileyChris Jul 30, 2020
9a2aca3
Merge branch 'deduplicate' into channels2
SmileyChris Jul 30, 2020
ae0b0c7
Correctly unsubscribe after on_start operation is complete
SmileyChris Nov 24, 2020
81537ea
Merge branch 'deduplicate' into channels2
SmileyChris Nov 24, 2020
a964800
Fix tests
SmileyChris Nov 24, 2020
b81df97
Merge remote-tracking branch 'origin/deduplicate' into channels2
SmileyChris Nov 26, 2020
cdbdda1
Async unsubscription needs to wait around for the future to cancel
SmileyChris Mar 29, 2021
26fc75c
Merge branch 'deduplicate' into channels2
SmileyChris Mar 29, 2021
4554636
Allow collection of tests even if aiohttp isn't installed
SmileyChris Mar 29, 2021
56f46a1
Make the python 2 async observer send graphql error for exceptions ex…
SmileyChris Mar 29, 2021
5abd858
asyncio.wait receiving coroutines is deprecated, create tasks explicitly
SmileyChris Mar 29, 2021
d2d55a1
Tidy up a test warning
SmileyChris Mar 29, 2021
f7cb773
Rename TestServer to avoid it being collected by pytest
SmileyChris Mar 29, 2021
80890c3
Update test matrix
SmileyChris Mar 29, 2021
9ced609
Update travis python versions
SmileyChris Mar 29, 2021
3adfaa9
Try using a newer travis dist to fix cryptography building issues
SmileyChris Mar 29, 2021
703e407
Use python 3.6 friendly asyncio method
SmileyChris Mar 29, 2021
efcd905
Merge branch 'deduplicate' into channels2
SmileyChris Mar 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ deploy:
tags: true
install: pip install -U tox-travis
language: python
dist: focal
python:
- 3.9
- 3.8
- 3.7
- 3.6
- 3.5
- 2.7
script: tox
15 changes: 2 additions & 13 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Ready to contribute? Here's how to set up `graphql_ws` for local development.

$ mkvirtualenv graphql_ws
$ cd graphql_ws/
$ python setup.py develop
$ pip install -e .[dev]

4. Create a branch for local development::

Expand All @@ -79,11 +79,8 @@ Ready to contribute? Here's how to set up `graphql_ws` for local development.
5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox::

$ flake8 graphql_ws tests
$ python setup.py test or py.test
$ tox

To get flake8 and tox, just pip install them into your virtualenv.

6. Commit your changes and push your branch to GitHub::

$ git add .
Expand All @@ -101,14 +98,6 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check
3. The pull request should work for Python 2.7, 3.5, 3.6, 3.7 and 3.8. Check
https://travis-ci.org/graphql-python/graphql_ws/pull_requests
and make sure that the tests pass for all supported Python versions.

Tips
----

To run a subset of tests::

$ py.test tests.test_graphql_ws

3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ include LICENSE
include README.rst
include tox.ini

graft graphql_ws/django/templates
graft examples
prune examples/django_channels2/.cache
graft tests
global-exclude __pycache__
global-exclude *.py[co]
Expand Down
225 changes: 137 additions & 88 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
==========
GraphQL WS
==========

Websocket server for GraphQL subscriptions.
Websocket backend for GraphQL subscriptions.

Supports the following application servers:

Python 3 application servers, using asyncio:

* `aiohttp`_
* `websockets compatible servers`_ such as Sanic
(via `websockets <https://github.com/aaugustin/websockets/>`__ library)
* `Django v2+`_

Currently supports:
Python 2 application servers:

* `Gevent compatible servers`_ such as Flask
* `Django v1.x`_
(via `channels v1.x <https://channels.readthedocs.io/en/1.x/inshort.html>`__)

* `aiohttp <https://github.com/graphql-python/graphql-ws#aiohttp>`__
* `Gevent <https://github.com/graphql-python/graphql-ws#gevent>`__
* Sanic (uses `websockets <https://github.com/aaugustin/websockets/>`__
library)

Installation instructions
=========================
Expand All @@ -19,21 +29,54 @@ For instaling graphql-ws, just run this command in your shell

pip install graphql-ws


Examples
--------
========

Python 3 servers
----------------

Create a subscribable schema like this:

.. code:: python

import asyncio
import graphene


class Query(graphene.ObjectType):
hello = graphene.String()

@staticmethod
def resolve_hello(obj, info, **kwargs):
return "world"


class Subscription(graphene.ObjectType):
count_seconds = graphene.Float(up_to=graphene.Int())

async def resolve_count_seconds(root, info, up_to):
for i in range(up_to):
yield i
await asyncio.sleep(1.)
yield up_to


schema = graphene.Schema(query=Query, subscription=Subscription)

aiohttp
~~~~~~~

For setting up, just plug into your aiohttp server.
Then just plug into your aiohttp server.

.. code:: python

from graphql_ws.aiohttp import AiohttpSubscriptionServer

from .schema import schema

subscription_server = AiohttpSubscriptionServer(schema)


async def subscriptions(request):
ws = web.WebSocketResponse(protocols=('graphql-ws',))
await ws.prepare(request)
Expand All @@ -47,16 +90,20 @@ For setting up, just plug into your aiohttp server.

web.run_app(app, port=8000)

Sanic
~~~~~
You can see a full example here:
https://github.com/graphql-python/graphql-ws/tree/master/examples/aiohttp


websockets compatible servers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Works with any framework that uses the websockets library for it’s
websocket implementation. For this example, plug in your Sanic server.
Works with any framework that uses the websockets library for its websocket
implementation. For this example, plug in your Sanic server.

.. code:: python

from graphql_ws.websockets_lib import WsLibSubscriptionServer

from . import schema

app = Sanic(__name__)

Expand All @@ -70,49 +117,61 @@ websocket implementation. For this example, plug in your Sanic server.

app.run(host="0.0.0.0", port=8000)

And then, plug into a subscribable schema:

.. code:: python
Django v2+
~~~~~~~~~~

import asyncio
import graphene

Django Channels 2
~~~~~~~~~~~~~~~~~

class Query(graphene.ObjectType):
base = graphene.String()
Set up with Django Channels just takes three steps:

1. Install the apps
2. Set up your schema
3. Configure the channels router application

class Subscription(graphene.ObjectType):
count_seconds = graphene.Float(up_to=graphene.Int())
First ``pip install channels`` and it to your ``INSTALLED_APPS``. If you
want graphiQL, install the ``graphql_ws.django`` app before
``graphene_django`` to serve a graphiQL template that will work with
websockets:

async def resolve_count_seconds(root, info, up_to):
for i in range(up_to):
yield i
await asyncio.sleep(1.)
yield up_to
.. code:: python

INSTALLED_APPS = [
"channels",
"graphql_ws.django",
"graphene_django",
# ...
]

schema = graphene.Schema(query=Query, subscription=Subscription)
Point to your schema in Django settings:

You can see a full example here:
https://github.com/graphql-python/graphql-ws/tree/master/examples/aiohttp
.. code:: python

Gevent
~~~~~~
GRAPHENE = {
'SCHEMA': 'yourproject.schema.schema'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this tripped me up: you should mention in the doc that it is required for the channel work to function where it is technically not needed for graphene to work.

I had to add to my existing project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is under the django channels configuration section, so it would imply that it's the setup required for channels, wouldn't it? Maybe I'm not understanding - please elaborate

Copy link

@slorg1 slorg1 Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a setup I found on the Django-graphene website: https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#update-settings

It is "new"ish and, so far, works without it unless you activate the subscriptions (not sure why).

I point it out for anyone like me who has a working project setup before the django-graphene update that works as-is: subscriptions will not work and the error is not that clear. To be clear, I am using the latest stable version of the django-graphene though. mystery mystery

It is a suggestion because I chased my tail for ~20mins looking at an opaque error ;-)

}

For setting up, just plug into your Gevent server.
Finally, you can set up channels routing yourself (maybe using
``graphql_ws.django.routing.websocket_urlpatterns`` in your
``URLRouter``), or you can just use one of the preset channels
applications:

.. code:: python

subscription_server = GeventSubscriptionServer(schema)
app.app_protocol = lambda environ_path_info: 'graphql-ws'
ASGI_APPLICATION = 'graphql_ws.django.routing.application'
# or
ASGI_APPLICATION = 'graphql_ws.django.routing.auth_application'

@sockets.route('/subscriptions')
def echo_socket(ws):
subscription_server.handle(ws)
return []
Run ``./manage.py runserver`` and go to
`http://localhost:8000/graphql`__ to test!

And then, plug into a subscribable schema:

Python 2 servers
-----------------

Create a subscribable schema like this:

.. code:: python

Expand All @@ -121,7 +180,11 @@ And then, plug into a subscribable schema:


class Query(graphene.ObjectType):
base = graphene.String()
hello = graphene.String()

@staticmethod
def resolve_hello(obj, info, **kwargs):
return "world"


class Subscription(graphene.ObjectType):
Expand All @@ -135,71 +198,54 @@ And then, plug into a subscribable schema:

schema = graphene.Schema(query=Query, subscription=Subscription)

You can see a full example here:
https://github.com/graphql-python/graphql-ws/tree/master/examples/flask_gevent

Django Channels
~~~~~~~~~~~~~~~
Gevent compatible servers
~~~~~~~~~~~~~~~~~~~~~~~~~

First ``pip install channels`` and it to your django apps

Then add the following to your settings.py
Then just plug into your Gevent server, for example, Flask:

.. code:: python

CHANNELS_WS_PROTOCOLS = ["graphql-ws", ]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "django_subscriptions.urls.channel_routing",
},

}

Setup your graphql schema
from flask_sockets import Sockets
from graphql_ws.gevent import GeventSubscriptionServer
from schema import schema

.. code:: python

import graphene
from rx import Observable


class Query(graphene.ObjectType):
hello = graphene.String()
subscription_server = GeventSubscriptionServer(schema)
app.app_protocol = lambda environ_path_info: 'graphql-ws'

def resolve_hello(self, info, **kwargs):
return 'world'

class Subscription(graphene.ObjectType):
@sockets.route('/subscriptions')
def echo_socket(ws):
subscription_server.handle(ws)
return []

count_seconds = graphene.Int(up_to=graphene.Int())
You can see a full example here:
https://github.com/graphql-python/graphql-ws/tree/master/examples/flask_gevent

Django v1.x
~~~~~~~~~~~

def resolve_count_seconds(
root,
info,
up_to=5
):
return Observable.interval(1000)\
.map(lambda i: "{0}".format(i))\
.take_while(lambda i: int(i) <= up_to)

For Django v1.x and Django Channels v1.x, setup your schema in ``settings.py``

.. code:: python

schema = graphene.Schema(
query=Query,
subscription=Subscription
)
GRAPHENE = {
'SCHEMA': 'yourproject.schema.schema'
}

Setup your schema in settings.py
Then ``pip install "channels<1"`` and it to your django apps, adding the
following to your ``settings.py``

.. code:: python

GRAPHENE = {
'SCHEMA': 'path.to.schema'
CHANNELS_WS_PROTOCOLS = ["graphql-ws", ]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "django_subscriptions.urls.channel_routing",
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the example is out-dated. They retired this. I used:

    "default": {"BACKEND": "channels.layers.InMemoryChannelLayer",},
}

instead, and that works now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the outdated "old django" section anyway, is that backend configuration really retired in channels v1?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I found asgiref retired their in-memory channel layer. So, if you get asgiref, it no longer has the class listed in the doc :-/

about the ROUTING part, idk, I only tested channels V2 and having that key with V2 was crashing (actively rejected, not ignored).

}

and finally add the channel routes
And finally add the channel routes

.. code:: python

Expand All @@ -209,3 +255,6 @@ and finally add the channel routes
channel_routing = [
route_class(GraphQLSubscriptionConsumer, path=r"^/subscriptions"),
]

You can see a full example here:
https://github.com/graphql-python/graphql-ws/tree/master/examples/django_subscriptions
Loading