11graphql-python-subscriptions
22============================
33
4+ (Work in Progress!)
5+ ^^^^^^^^^^^^^^^^^^^
6+
47A port of apollographql subscriptions for python, using gevent
58websockets 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
1626Very initial implementation. Currently only works with Python 2. No
1727tests 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+
0 commit comments