Skip to content

Commit e8474a0

Browse files
authored
Merge pull request #10 from hballard/dev
Changes to top level imports and updated README
2 parents 74ee1ee + 91b3a37 commit e8474a0

4 files changed

Lines changed: 324 additions & 17 deletions

File tree

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#### (Work in Progress!)
33
A port of apollographql subscriptions for python, using gevent websockets and redis
44

5-
This is a implementation of apollographql [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) and [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) in Python. It currently implements a pubsub using [redis-py](https://github.com/andymccurdy/redis-py) and uses [gevent-websockets](https://bitbucket.org/noppo/gevent-websocket) for concurrency. It also makes heavy use of
5+
This is a implementation of apollographql [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) and [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) in Python. It currently implements a pubsub using [redis-py](https://github.com/andymccurdy/redis-py) and uses [gevent-websockets](https://bitbucket.org/noppo/gevent-websocket) for concurrency. It also makes heavy use of
66
[syrusakbary/promise](https://github.com/syrusakbary/promise) python implementation to mirror the logic in the apollo-graphql libraries.
77

88
Meant to be used in conjunction with [graphql-python](https://github.com/graphql-python) / [graphene](http://graphene-python.org/) server and [apollo-client](http://dev.apollodata.com/) for graphql.
@@ -25,10 +25,10 @@ $ pip install graphql-subscriptions
2525
#### Methods
2626
- `publish(trigger_name, message)`: Trigger name is a subscription
2727
or pubsub channel; message is the mutation object or message that will end
28-
up being passed to the subscription root_value; this method will be called inside of
28+
up being passed to the subscription root_value; this method will be called inside of
2929
mutation resolve function
3030
- `subscribe(trigger_name, on_message_handler, options)`: Trigger name
31-
is a subscription or pubsub channel; on_message_handler is the callback
31+
is a subscription or pubsub channel; on_message_handler is the callback
3232
that will be triggered on each mutation; this method is called by the subscription
3333
manager
3434
- `unsubscribe(sub_id)`: Sub_id is the subscription ID that is being
@@ -37,7 +37,7 @@ $ pip install graphql-subscriptions
3737
- `wait_and_get_message()`: Called by the subscribe method during the first
3838
subscription for server; run in a separate greenlet and calls Redis `get_message()`
3939
method to constantly poll for new messages on pubsub channels
40-
- `handle_message(message)`: Called by pubsub when a message is
40+
- `handle_message(message)`: Called by pubsub when a message is
4141
received on a subscribed channel; will check all existing pubsub subscriptons and
4242
then calls `on_message_handler()` for all matches
4343

@@ -95,8 +95,11 @@ $ pip install graphql-subscriptions
9595
from flask import Flask
9696
from flask_sqlalchemy import SQLAlchemy
9797
from flask_sockets import Sockets
98-
from graphql_subscriptions import SubscriptionManager, RedisPubsub
99-
from graphql_subscriptions import ApolloSubscriptionServer
98+
from graphql_subscriptions import (
99+
SubscriptionManager,
100+
RedisPubsub,
101+
ApolloSubscriptionServer
102+
)
100103

101104
app = Flask(__name__)
102105

@@ -118,7 +121,7 @@ schema = graphene.Schema(
118121
# instantiate subscription manager object--passing in schema and pubsub
119122
subscription_mgr = SubscriptionManager(schema, pubsub)
120123

121-
# using Flask Sockets here, on each new connection instantiate a
124+
# using Flask Sockets here, on each new connection instantiate a
122125
# subscription app / server--passing in subscription manger and websocket
123126
@sockets.route('/socket')
124127
def socket_channel(websocket):
@@ -128,8 +131,8 @@ def socket_channel(websocket):
128131

129132
if __name__ == "__main__":
130133

131-
# using gevent webserver here so multiple connections can be
132-
# maintained concurrently -- gevent websocket spawns a new
134+
# using gevent webserver here so multiple connections can be
135+
# maintained concurrently -- gevent websocket spawns a new
133136
# greenlet for each request and forwards to flask app or socket app
134137
# depending on request type
135138
from geventwebsocket import WebSocketServer

README.rst

Lines changed: 307 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
graphql-python-subscriptions
22
============================
33

4+
(Work in Progress!)
5+
^^^^^^^^^^^^^^^^^^^
6+
47
A port of apollographql subscriptions for python, using gevent
58
websockets and redis
69

7-
This is a implementation of apollographql subscriptions-transport-ws and
8-
graphql-subscriptions in Python. It currently implements a pubsub using
9-
redis.py and uses gevent for concurrency. It also makes heavy use of
10-
syrusakbary/promise python implementation to mirror the logic in the
11-
apollo-graphql libraries.
10+
This is a implementation of apollographql
11+
`subscriptions-transport-ws <https://github.com/apollographql/subscriptions-transport-ws>`__
12+
and
13+
`graphql-subscriptions <https://github.com/apollographql/graphql-subscriptions>`__
14+
in Python. It currently implements a pubsub using
15+
`redis-py <https://github.com/andymccurdy/redis-py>`__ and uses
16+
`gevent-websockets <https://bitbucket.org/noppo/gevent-websocket>`__ for
17+
concurrency. It also makes heavy use of
18+
`syrusakbary/promise <https://github.com/syrusakbary/promise>`__ python
19+
implementation to mirror the logic in the apollo-graphql libraries.
1220

13-
Meant to be used in conjunction with graphql-python / graphene server
14-
and apollo-graphql client.
21+
Meant to be used in conjunction with
22+
`graphql-python <https://github.com/graphql-python>`__ /
23+
`graphene <http://graphene-python.org/>`__ server and
24+
`apollo-client <http://dev.apollodata.com/>`__ for graphql.
1525

1626
Very initial implementation. Currently only works with Python 2. No
1727
tests yet.
@@ -22,3 +32,293 @@ Installation
2232
::
2333

2434
$ pip install graphql-subscriptions
35+
36+
API
37+
---
38+
39+
RedisPubsub(host='localhost', port=6379, \*args, \*\*kwargs) #### Arguments - ``host``: Redis server instance url or IP - ``port``: Redis server port - ``args, kwargs``: additional position and keyword args will be passed to Redis-py constructor
40+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
41+
42+
Methods
43+
^^^^^^^
44+
45+
- ``publish(trigger_name, message)``: Trigger name is a subscription or
46+
pubsub channel; message is the mutation object or message that will
47+
end up being passed to the subscription root\_value; this method will
48+
be called inside of mutation resolve function
49+
- ``subscribe(trigger_name, on_message_handler, options)``: Trigger
50+
name is a subscription or pubsub channel; on\_message\_handler is the
51+
callback that will be triggered on each mutation; this method is
52+
called by the subscription manager
53+
- ``unsubscribe(sub_id)``: Sub\_id is the subscription ID that is being
54+
tracked by the pubsub instance -- it is returned from the
55+
``subscribe`` method and called by the subscription manager
56+
- ``wait_and_get_message()``: Called by the subscribe method during the
57+
first subscription for server; run in a separate greenlet and calls
58+
Redis ``get_message()`` method to constantly poll for new messages on
59+
pubsub channels
60+
- ``handle_message(message)``: Called by pubsub when a message is
61+
received on a subscribed channel; will check all existing pubsub
62+
subscriptons and then calls ``on_message_handler()`` for all matches
63+
64+
SubscriptionManager(schema, pubsub, setup\_funcs={})
65+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
66+
67+
Arguments
68+
^^^^^^^^^
69+
70+
- ``schema``: graphql schema instance
71+
- ``pubsub``: any pubsub instance with publish, subscribe, and
72+
unsubscribe methods (in this case an instance of the RedisPubsub
73+
class)
74+
- ``setup_funcs``: dictionary of setup functions that map from
75+
subscription name to a map of pubsub channel names and their filter
76+
functions; kwargs parameters are:
77+
``query, operation_name, callback, variables, context, format_error, format_response, args, subscription_name``
78+
79+
example: \`\`\`python def new\_user(\*\*kwargs): args =
80+
kwargs.get('args') return { 'new\_user\_channel': { 'filter': lambda
81+
user, context: user.active == args.active } }
82+
83+
setup\_funcs = {'new\_user': new\_user} \`\`\`
84+
85+
Methods
86+
^^^^^^^
87+
88+
- ``publish(trigger_name, payload)``: Trigger name is the subscription
89+
or pubsub channel; payload is the mutation object or message that
90+
will end up being passed to the subscription root\_value; method
91+
called inside of mutation resolve function
92+
- ``subscribe(query, operation_name, callback, variables, context, format_error, format_response)``:
93+
Called by ApolloSubscriptionServer upon receiving a new subscription
94+
from a websocket. Arguments are parsed by ApolloSubscriptionServer
95+
from graphql subscription query
96+
- ``unsubscribe(sub_id)``: Sub\_id is the subscription ID that is being
97+
tracked by the subscription manager instance -- returned from the
98+
``subscribe()`` method and called by the ApolloSubscriptionServer
99+
100+
ApolloSubscriptionServer(subscription\_manager, websocket, keep\_alive=None, on\_subscribe=None, on\_unsubscribe=None, on\_connect=None, on\_disconnect=None)
101+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
102+
103+
Arguments
104+
^^^^^^^^^
105+
106+
- ``subscription_manager``: TODO
107+
- ``websocket``: TODO
108+
- ``keep_alive``: TODO
109+
- ``on_subscribe, on_unsubscribe, on_connect, on_disconnet``: TODO
110+
111+
Methods
112+
^^^^^^^
113+
114+
- TODO
115+
116+
Example Usage
117+
-------------
118+
119+
Server (using Flask and Flask-Sockets):
120+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
121+
122+
.. code:: python
123+
124+
from flask import Flask
125+
from flask_sqlalchemy import SQLAlchemy
126+
from flask_sockets import Sockets
127+
from graphql_subscriptions import (
128+
SubscriptionManager,
129+
RedisPubsub,
130+
ApolloSubscriptionServer
131+
)
132+
133+
app = Flask(__name__)
134+
135+
# using Flask Sockets here, but could use gevent-websocket directly
136+
# to create a websocket app
137+
sockets = Sockets(app)
138+
139+
# instantiate pubsub
140+
pubsub = RedisPubsub()
141+
142+
# create schema using graphene or another python graphql library
143+
# not showing models or schema design here for brevity
144+
schema = graphene.Schema(
145+
query=Query,
146+
mutation=Mutation,
147+
subscription=Subscription
148+
)
149+
150+
# instantiate subscription manager object--passing in schema and pubsub
151+
subscription_mgr = SubscriptionManager(schema, pubsub)
152+
153+
# using Flask Sockets here, on each new connection instantiate a
154+
# subscription app / server--passing in subscription manger and websocket
155+
@sockets.route('/socket')
156+
def socket_channel(websocket):
157+
subscription_server = ApolloSubscriptionServer(subscription_mgr, websocket)
158+
subscription_server.handle()
159+
return []
160+
161+
if __name__ == "__main__":
162+
163+
# using gevent webserver here so multiple connections can be
164+
# maintained concurrently -- gevent websocket spawns a new
165+
# greenlet for each request and forwards to flask app or socket app
166+
# depending on request type
167+
from geventwebsocket import WebSocketServer
168+
169+
server = WebSocketServer(('', 5000), app)
170+
print ' Serving at host 0.0.0.0:5000...\n'
171+
server.serve_forever()
172+
173+
Of course on the server you have to "publish" each time you have a
174+
mutation (in this case to a redis channel). That would look something
175+
like this (using graphene / sql-alchemy):
176+
177+
.. code:: python
178+
179+
class AddUser(graphene.ClientIDMutation):
180+
181+
class Input:
182+
username = graphene.String(required=True)
183+
email = graphene.String()
184+
185+
ok = graphene.Boolean()
186+
user = graphene.Field(lambda: User)
187+
188+
@classmethod
189+
def mutate_and_get_payload(cls, args, context, info):
190+
_input = args.copy()
191+
del _input['clientMutationId']
192+
new_user = UserModel(**_input)
193+
db.session.add(new_user)
194+
db.session.commit()
195+
ok = True
196+
# publish result of mutation to pubsub
197+
if pubsub.subscriptions:
198+
pubsub.publish('users', new_user.as_dict())
199+
return AddUser(ok=ok, user=new_user)
200+
201+
class Subscription(graphene.ObjectType):
202+
users = graphene_sqlalchemy.SQLAlchemyConnectionField(
203+
User,
204+
active=graphene.Boolean()
205+
)
206+
207+
# mutation oject that was published will be passed as
208+
# root_value of subscription
209+
def resolve_users(self, args, context, info):
210+
query = User.get_query(context)
211+
return query.filter_by(id=info.root_value.get('id'))
212+
213+
Client (using Apollo Client library):
214+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
215+
216+
First create create network interface and and client instances and then
217+
wrap them in a subscription client instance
218+
219+
.. code:: js
220+
221+
import ReactDOM from 'react-dom'
222+
import { ApolloProvider } from 'react-apollo'
223+
import ApolloClient, { createNetworkInterface } from 'apollo-client'
224+
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws'
225+
226+
import ChatApp from './screens/ChatApp'
227+
228+
const networkInterface = createNetworkInterface({
229+
uri: 'http://localhost:5000/graphql'
230+
})
231+
232+
const wsClient = new SubscriptionClient(`ws://localhost:5000/socket`, {
233+
reconnect: true
234+
})
235+
236+
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
237+
networkInterface,
238+
wsClient,
239+
)
240+
241+
const client = new ApolloClient({
242+
dataIdFromObject: o => o.id,
243+
networkInterface: networkInterfaceWithSubscriptions
244+
})
245+
246+
ReactDOM.render(
247+
<ApolloProvider client={client}>
248+
<ChatApp />
249+
</ApolloProvider>,
250+
document.getElementById('root')
251+
)
252+
253+
Build a simple component and then call subscribeToMore method on the
254+
returned data object from the inital graphql query
255+
256+
.. code:: js
257+
258+
259+
import React from 'react'
260+
import { graphql } from 'react-apollo'
261+
import gql from 'graphql-tag'
262+
import ListBox from '../components/ListBox'
263+
264+
const SUBSCRIPTION_QUERY = gql`
265+
subscription newUsers {
266+
users(active: true) {
267+
edges {
268+
node {
269+
id
270+
username
271+
}
272+
}
273+
}
274+
}
275+
`
276+
277+
const LIST_BOX_QUERY = gql`
278+
query AllUsers {
279+
users(active: true) {
280+
edges {
281+
node {
282+
id
283+
username
284+
}
285+
}
286+
}
287+
}
288+
`
289+
290+
class ChatListBox extends React.Component {
291+
292+
componentWillReceiveProps(newProps) {
293+
if (!newProps.data.loading) {
294+
if (this.subscription) {
295+
return
296+
}
297+
this.subscription = newProps.data.subscribeToMore({
298+
document: SUBSCRIPTION_QUERY,
299+
updateQuery: (previousResult, {subscriptionData}) => {
300+
const newUser = subscriptionData.data.users.edges
301+
const newResult = {
302+
users: {
303+
edges: [
304+
...previousResult.users.edges,
305+
...newUser
306+
]
307+
}
308+
}
309+
return newResult
310+
},
311+
onError: (err) => console.error(err)
312+
})
313+
}
314+
}
315+
316+
render() {
317+
return <ListBox data={this.props.data} />
318+
}
319+
}
320+
321+
const ChatListBoxWithData = graphql(LIST_BOX_QUERY)(ChatListBox)
322+
323+
export default ChatListBoxWithData
324+

graphql_subscriptions/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from subscription_manager import RedisPubsub, SubscriptionManager
2+
from subscription_transport_ws import ApolloSubscriptionServer
3+
4+
__all__ = ['RedisPubsub', 'SubscriptionManager', 'ApolloSubscriptionServer']

0 commit comments

Comments
 (0)