Skip to content

Commit 06e7c65

Browse files
authored
Merge pull request #4 from colanconnon/django-channels
basic django channels example working
2 parents f6ffd5b + 9546900 commit 06e7c65

File tree

13 files changed

+587
-4
lines changed

13 files changed

+587
-4
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ target/
6363
.DS_Store
6464

6565
.mypy_cache/
66-
.vscode/
66+
.vscode/
67+
68+
*.sqlite3

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,78 @@ schema = graphene.Schema(query=Query, subscription=Subscription)
128128
```
129129

130130
You can see a full example here: https://github.com/graphql-python/graphql-ws/tree/master/examples/flask_gevent
131+
132+
133+
### Django Channels
134+
135+
136+
First `pip install channels` and it to your django apps
137+
138+
Then add the following to your settings.py
139+
140+
```python
141+
CHANNELS_WS_PROTOCOLS = ["graphql-ws", ]
142+
CHANNEL_LAYERS = {
143+
"default": {
144+
"BACKEND": "asgiref.inmemory.ChannelLayer",
145+
"ROUTING": "django_subscriptions.urls.channel_routing",
146+
},
147+
148+
}
149+
```
150+
151+
Setup your graphql schema
152+
153+
```python
154+
import graphene
155+
from rx import Observable
156+
157+
158+
class Query(graphene.ObjectType):
159+
hello = graphene.String()
160+
161+
def resolve_hello(self, info, **kwargs):
162+
return 'world'
163+
164+
class Subscription(graphene.ObjectType):
165+
166+
count_seconds = graphene.Int(up_to=graphene.Int())
167+
168+
169+
def resolve_count_seconds(
170+
root,
171+
info,
172+
up_to=5
173+
):
174+
return Observable.interval(1000)\
175+
.map(lambda i: "{0}".format(i))\
176+
.take_while(lambda i: int(i) <= up_to)
177+
178+
179+
180+
schema = graphene.Schema(
181+
query=Query,
182+
subscription=Subscription
183+
)
184+
185+
186+
````
187+
188+
Setup your schema in settings.py
189+
190+
```python
191+
GRAPHENE = {
192+
'SCHEMA': 'path.to.schema'
193+
}
194+
```
195+
196+
and finally add the channel routes
197+
198+
```python
199+
from channels.routing import route_class
200+
from graphql_ws.django_channels import GraphQLSubscriptionConsumer
201+
202+
channel_routing = [
203+
route_class(GraphQLSubscriptionConsumer, path=r"^/subscriptions"),
204+
]
205+
```

examples/django_subscriptions/django_subscriptions/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import os
2+
from channels.asgi import get_channel_layer
3+
4+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_subscriptions.settings")
5+
6+
channel_layer = get_channel_layer()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import graphene
2+
from rx import Observable
3+
4+
5+
class Query(graphene.ObjectType):
6+
hello = graphene.String()
7+
8+
def resolve_hello(self, info, **kwargs):
9+
return 'world'
10+
11+
class Subscription(graphene.ObjectType):
12+
13+
count_seconds = graphene.Int(up_to=graphene.Int())
14+
15+
16+
def resolve_count_seconds(root, info, up_to=5):
17+
return Observable.interval(1000)\
18+
.map(lambda i: "{0}".format(i))\
19+
.take_while(lambda i: int(i) <= up_to)
20+
21+
22+
23+
schema = graphene.Schema(query=Query, subscription=Subscription)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Django settings for django_subscriptions project.
3+
4+
Generated by 'django-admin startproject' using Django 1.11.6.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/1.11/topics/settings/
8+
9+
For the full list of settings and their values, see
10+
https://docs.djangoproject.com/en/1.11/ref/settings/
11+
"""
12+
13+
import os
14+
15+
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17+
18+
19+
# Quick-start development settings - unsuitable for production
20+
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
21+
22+
# SECURITY WARNING: keep the secret key used in production secret!
23+
SECRET_KEY = 'fa#kz8m$l6)4(np9+-j_-z!voa090mah!s9^4jp=kj!^nwdq^c'
24+
25+
# SECURITY WARNING: don't run with debug turned on in production!
26+
DEBUG = True
27+
28+
ALLOWED_HOSTS = []
29+
30+
31+
# Application definition
32+
33+
INSTALLED_APPS = [
34+
'django.contrib.admin',
35+
'django.contrib.auth',
36+
'django.contrib.contenttypes',
37+
'django.contrib.sessions',
38+
'django.contrib.messages',
39+
'django.contrib.staticfiles',
40+
'channels',
41+
]
42+
43+
MIDDLEWARE = [
44+
'django.middleware.security.SecurityMiddleware',
45+
'django.contrib.sessions.middleware.SessionMiddleware',
46+
'django.middleware.common.CommonMiddleware',
47+
'django.middleware.csrf.CsrfViewMiddleware',
48+
'django.contrib.auth.middleware.AuthenticationMiddleware',
49+
'django.contrib.messages.middleware.MessageMiddleware',
50+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
51+
]
52+
53+
ROOT_URLCONF = 'django_subscriptions.urls'
54+
55+
TEMPLATES = [
56+
{
57+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
58+
'DIRS': [],
59+
'APP_DIRS': True,
60+
'OPTIONS': {
61+
'context_processors': [
62+
'django.template.context_processors.debug',
63+
'django.template.context_processors.request',
64+
'django.contrib.auth.context_processors.auth',
65+
'django.contrib.messages.context_processors.messages',
66+
],
67+
},
68+
},
69+
]
70+
71+
WSGI_APPLICATION = 'django_subscriptions.wsgi.application'
72+
73+
74+
# Database
75+
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
76+
77+
DATABASES = {
78+
'default': {
79+
'ENGINE': 'django.db.backends.sqlite3',
80+
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
81+
}
82+
}
83+
84+
85+
# Password validation
86+
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
87+
88+
AUTH_PASSWORD_VALIDATORS = [
89+
{
90+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
91+
},
92+
{
93+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
94+
},
95+
{
96+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
97+
},
98+
{
99+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
100+
},
101+
]
102+
103+
104+
# Internationalization
105+
# https://docs.djangoproject.com/en/1.11/topics/i18n/
106+
107+
LANGUAGE_CODE = 'en-us'
108+
109+
TIME_ZONE = 'UTC'
110+
111+
USE_I18N = True
112+
113+
USE_L10N = True
114+
115+
USE_TZ = True
116+
117+
118+
# Static files (CSS, JavaScript, Images)
119+
# https://docs.djangoproject.com/en/1.11/howto/static-files/
120+
121+
STATIC_URL = '/static/'
122+
CHANNELS_WS_PROTOCOLS = ["graphql-ws", ]
123+
CHANNEL_LAYERS = {
124+
"default": {
125+
"BACKEND": "asgi_redis.RedisChannelLayer",
126+
"CONFIG": {
127+
"hosts": [("localhost", 6379)],
128+
},
129+
"ROUTING": "django_subscriptions.urls.channel_routing",
130+
},
131+
132+
}
133+
134+
135+
GRAPHENE = {
136+
'SCHEMA': 'django_subscriptions.schema.schema'
137+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
2+
from string import Template
3+
4+
5+
def render_graphiql():
6+
return Template('''
7+
<!DOCTYPE html>
8+
<html>
9+
<head>
10+
<meta charset="utf-8" />
11+
<title>GraphiQL</title>
12+
<meta name="robots" content="noindex" />
13+
<style>
14+
html, body {
15+
height: 100%;
16+
margin: 0;
17+
overflow: hidden;
18+
width: 100%;
19+
}
20+
</style>
21+
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.10/graphiql.css" rel="stylesheet" />
22+
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
23+
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
24+
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
25+
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.10/graphiql.js"></script>
26+
<script src="//unpkg.com/subscriptions-transport-ws@${SUBSCRIPTIONS_TRANSPORT_VERSION}/browser/client.js"></script>
27+
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
28+
</head>
29+
<body>
30+
<script>
31+
// Collect the URL parameters
32+
var parameters = {};
33+
window.location.search.substr(1).split('&').forEach(function (entry) {
34+
var eq = entry.indexOf('=');
35+
if (eq >= 0) {
36+
parameters[decodeURIComponent(entry.slice(0, eq))] =
37+
decodeURIComponent(entry.slice(eq + 1));
38+
}
39+
});
40+
// Produce a Location query string from a parameter object.
41+
function locationQuery(params, location) {
42+
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
43+
return encodeURIComponent(key) + '=' +
44+
encodeURIComponent(params[key]);
45+
}).join('&');
46+
}
47+
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
48+
var graphqlParamNames = {
49+
query: true,
50+
variables: true,
51+
operationName: true
52+
};
53+
var otherParams = {};
54+
for (var k in parameters) {
55+
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
56+
otherParams[k] = parameters[k];
57+
}
58+
}
59+
var fetcher;
60+
if (true) {
61+
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('${subscriptionsEndpoint}', {
62+
reconnect: true
63+
});
64+
fetcher = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);
65+
} else {
66+
fetcher = graphQLFetcher;
67+
}
68+
// We don't use safe-serialize for location, because it's not client input.
69+
var fetchURL = locationQuery(otherParams, '${endpointURL}');
70+
// Defines a GraphQL fetcher using the fetch API.
71+
function graphQLFetcher(graphQLParams) {
72+
return fetch(fetchURL, {
73+
method: 'post',
74+
headers: {
75+
'Accept': 'application/json',
76+
'Content-Type': 'application/json',
77+
},
78+
body: JSON.stringify(graphQLParams),
79+
credentials: 'include',
80+
}).then(function (response) {
81+
return response.text();
82+
}).then(function (responseBody) {
83+
try {
84+
return JSON.parse(responseBody);
85+
} catch (error) {
86+
return responseBody;
87+
}
88+
});
89+
}
90+
// When the query and variables string is edited, update the URL bar so
91+
// that it can be easily shared.
92+
function onEditQuery(newQuery) {
93+
parameters.query = newQuery;
94+
updateURL();
95+
}
96+
function onEditVariables(newVariables) {
97+
parameters.variables = newVariables;
98+
updateURL();
99+
}
100+
function onEditOperationName(newOperationName) {
101+
parameters.operationName = newOperationName;
102+
updateURL();
103+
}
104+
function updateURL() {
105+
history.replaceState(null, null, locationQuery(parameters) + window.location.hash);
106+
}
107+
// Render <GraphiQL /> into the body.
108+
ReactDOM.render(
109+
React.createElement(GraphiQL, {
110+
fetcher: fetcher,
111+
onEditQuery: onEditQuery,
112+
onEditVariables: onEditVariables,
113+
onEditOperationName: onEditOperationName,
114+
}),
115+
document.body
116+
);
117+
</script>
118+
</body>
119+
</html>''').substitute(
120+
GRAPHIQL_VERSION='0.11.10',
121+
SUBSCRIPTIONS_TRANSPORT_VERSION='0.7.0',
122+
subscriptionsEndpoint='ws://localhost:8000/subscriptions',
123+
# subscriptionsEndpoint='ws://localhost:5000/',
124+
endpointURL='/graphql',
125+
)

0 commit comments

Comments
 (0)