Skip to content

Commit 4146f09

Browse files
authored
Merge pull request #13 from hballard/tests
Complete subscription_transport tests
2 parents f7c095e + 19562e7 commit 4146f09

File tree

7 files changed

+1301
-38
lines changed

7 files changed

+1301
-38
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
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 [syrusakbary/promise](https://github.com/syrusakbary/promise) python implementation to mirror the logic in the apollo-graphql libraries.
5+
This is an implementation of graphql subscriptions in Python. It uses the apollographql [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) and [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) packages as its basis. 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 [syrusakbary/promise](https://github.com/syrusakbary/promise) python implementation to mirror the logic in the apollo-graphql libraries.
66

7-
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. The api is below, but if you want more information, consult the apollo graphql libraries referenced above.
7+
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. The api is below, but if you want more information, consult the apollo graphql libraries referenced above, and specifcally as it relates to using their graphql subscriptions client.
88

9-
Initial implementation. Currently only works with Python 2.
9+
Initial implementation. Good test coverage. Currently only works with Python 2.
1010

1111
## Installation
1212
```
@@ -48,10 +48,10 @@ $ pip install graphql-subscriptions
4848

4949
#### Methods
5050
- `publish(trigger_name, payload)`: Trigger name is the subscription or pubsub channel; payload is the mutation object or message that will end up being passed to the subscription root_value; method called inside of mutation resolve function
51-
- `subscribe(query, operation_name, callback, variables, context, format_error, format_response)`: Called by ApolloSubscriptionServer upon receiving a new subscription from a websocket. Arguments are parsed by ApolloSubscriptionServer from the graphql subscription query
52-
- `unsubscribe(sub_id)`: Sub_id is the subscription ID that is being tracked by the subscription manager instance -- returned from the `subscribe()` method and called by the ApolloSubscriptionServer
51+
- `subscribe(query, operation_name, callback, variables, context, format_error, format_response)`: Called by SubscriptionServer upon receiving a new subscription from a websocket. Arguments are parsed by SubscriptionServer from the graphql subscription query
52+
- `unsubscribe(sub_id)`: Sub_id is the subscription ID that is being tracked by the subscription manager instance -- returned from the `subscribe()` method and called by the SubscriptionServer
5353

54-
### ApolloSubscriptionServer(subscription_manager, websocket, keep_alive=None, on_subscribe=None, on_unsubscribe=None, on_connect=None, on_disconnect=None)
54+
### SubscriptionServer(subscription_manager, websocket, keep_alive=None, on_subscribe=None, on_unsubscribe=None, on_connect=None, on_disconnect=None)
5555
#### Arguments
5656
- `subscription_manager`: A subscripton manager instance (required).
5757
- `websocket`: The websocket object passed in from your route handler (required).
@@ -78,7 +78,7 @@ from flask_sockets import Sockets
7878
from graphql_subscriptions import (
7979
SubscriptionManager,
8080
RedisPubsub,
81-
ApolloSubscriptionServer
81+
SubscriptionServer
8282
)
8383

8484
app = Flask(__name__)
@@ -106,7 +106,7 @@ subscription_mgr = SubscriptionManager(schema, pubsub)
106106
# subscription app / server -- passing in subscription manager and websocket
107107
@sockets.route('/socket')
108108
def socket_channel(websocket):
109-
subscription_server = ApolloSubscriptionServer(subscription_mgr, websocket)
109+
subscription_server = SubscriptionServer(subscription_mgr, websocket)
110110
subscription_server.handle()
111111
return []
112112

graphql_subscriptions/__init__.py

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

4-
__all__ = ['RedisPubsub', 'SubscriptionManager', 'ApolloSubscriptionServer']

graphql_subscriptions/subscription_transport_ws.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
GRAPHQL_SUBSCRIPTIONS = 'graphql-subscriptions'
1616

1717

18-
class ApolloSubscriptionServer(WebSocketApplication):
18+
class SubscriptionServer(WebSocketApplication):
1919
def __init__(self,
2020
subscription_manager,
2121
websocket,
@@ -37,7 +37,7 @@ def __init__(self,
3737
self.connection_subscriptions = {}
3838
self.connection_context = {}
3939

40-
super(ApolloSubscriptionServer, self).__init__(websocket)
40+
super(SubscriptionServer, self).__init__(websocket)
4141

4242
def timer(self, callback, period):
4343
while True:
@@ -77,13 +77,11 @@ def on_message(self, msg):
7777
if msg is None:
7878
return
7979

80-
class nonlocal:
81-
on_init_resolve = None
82-
on_init_reject = None
80+
non_local = {'on_init_resolve': None, 'on_init_reject': None}
8381

8482
def init_promise_handler(resolve, reject):
85-
nonlocal.on_init_resolve = resolve
86-
nonlocal.on_init_reject = reject
83+
non_local['on_init_resolve'] = resolve
84+
non_local['on_init_reject'] = reject
8785

8886
self.connection_context['init_promise'] = Promise(init_promise_handler)
8987

@@ -107,7 +105,7 @@ def on_message_return_handler(message):
107105
self.on_connect(
108106
parsed_message.get('payload'), self.ws))
109107

110-
nonlocal.on_init_resolve(on_connect_promise)
108+
non_local['on_init_resolve'](on_connect_promise)
111109

112110
def init_success_promise_handler(result):
113111
if not result:
@@ -133,7 +131,8 @@ def subscription_start_promise_handler(init_result):
133131
'callback': None,
134132
'variables': parsed_message.get('variables'),
135133
'context': init_result if isinstance(
136-
init_result, dict) else {},
134+
init_result, dict) else
135+
parsed_message.get('context', {}),
137136
'format_error': None,
138137
'format_response': None
139138
}
@@ -151,8 +150,7 @@ def subscription_start_promise_handler(init_result):
151150
def promised_params_handler(params):
152151
if not isinstance(params, dict):
153152
error = 'Invalid params returned from\
154-
OnSubscribe! Return value must\
155-
be an dict'
153+
OnSubscribe! Return value must be an dict'
156154

157155
self.send_subscription_fail(
158156
sub_id, {'errors': [{
@@ -164,15 +162,15 @@ def params_callback(error, result):
164162
if not error:
165163
self.send_subscription_data(
166164
sub_id, {'data': result.data})
167-
elif error.errors:
168-
self.send_subscription_data(
169-
sub_id, {'errors': error.errors})
170165
elif error.message:
171166
self.send_subscription_data(
172167
sub_id,
173168
{'errors': [{
174169
'message': error.message
175170
}]})
171+
elif error.errors:
172+
self.send_subscription_data(
173+
sub_id, {'errors': error.errors})
176174
else:
177175
self.send_subscription_data(
178176
sub_id,
@@ -218,7 +216,7 @@ def error_catch_handler(e):
218216
# not sure if this behavior is correct or
219217
# not per promises A spec...need to
220218
# investigate
221-
nonlocal.on_init_resolve(Promise.resolve(True))
219+
non_local['on_init_resolve'](Promise.resolve(True))
222220

223221
self.connection_context['init_promise'].then(
224222
subscription_start_promise_handler)
@@ -231,7 +229,7 @@ def subscription_end_promise_handler(result):
231229
del self.connection_subscriptions[sub_id]
232230

233231
# same rationale as above
234-
nonlocal.on_init_resolve(Promise.resolve(True))
232+
non_local['on_init_resolve'](Promise.resolve(True))
235233

236234
self.connection_context['init_promise'].then(
237235
subscription_end_promise_handler)

setup.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
except (IOError, ImportError):
77
long_description = open('README.md').read()
88

9+
tests_dep = [
10+
'pytest', 'pytest-mock', 'fakeredis', 'graphene', 'subprocess32',
11+
'flask', 'flask-graphql', 'flask-sockets', 'multiprocess', 'requests'
12+
]
13+
914
setup(
1015
name='graphql-subscriptions',
11-
version='0.1.7',
16+
version='0.1.8',
1217
author='Heath Ballard',
1318
author_email='[email protected]',
1419
description=('A port of apollo-graphql subscriptions for python, using\
@@ -26,6 +31,12 @@
2631
'Programming Language :: Python :: 2.7',
2732
'License :: OSI Approved :: MIT License'
2833
],
29-
install_requires=['gevent-websocket', 'redis', 'promise', 'graphql-core'],
30-
tests_require=['pytest', 'pytest-mock', 'fakeredis', 'graphene'],
34+
install_requires=[
35+
'gevent-websocket', 'redis', 'graphql-core', 'promise<=1.0.1'
36+
],
37+
test_suite='pytest',
38+
tests_require=tests_dep,
39+
extras_require={
40+
'test': tests_dep
41+
},
3142
include_package_data=True)

tests/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"graphql": "^0.9.6",
4+
"subscriptions-transport-ws": "0.5.4"
5+
}
6+
}

tests/test_subscription_manager.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@
1313

1414

1515
@pytest.fixture
16-
def mock_redis(monkeypatch):
16+
def pubsub(monkeypatch):
1717
monkeypatch.setattr(redis, 'StrictRedis', fakeredis.FakeStrictRedis)
18-
19-
20-
@pytest.fixture
21-
def pubsub(mock_redis):
2218
return RedisPubsub()
2319

2420

@@ -266,8 +262,7 @@ def publish_and_unsubscribe_handler(sub_id):
266262

267263

268264
def test_can_subscribe_to_more_than_one_trigger(sub_mgr):
269-
class nonlocal:
270-
trigger_count = 0
265+
non_local = {'trigger_count': 0}
271266

272267
query = 'subscription multiTrigger($filterBoolean: Boolean,\
273268
$uga: String){testFilterMulti(filterBoolean: $filterBoolean,\
@@ -282,10 +277,10 @@ def callback(err, payload):
282277
assert True
283278
else:
284279
assert payload.data.get('testFilterMulti') == 'good_filter'
285-
nonlocal.trigger_count += 1
280+
non_local['trigger_count'] += 1
286281
except AssertionError as e:
287282
sys.exit(e)
288-
if nonlocal.trigger_count == 2:
283+
if non_local['trigger_count'] == 2:
289284
sub_mgr.pubsub.greenlet.kill()
290285

291286
def publish_and_unsubscribe_handler(sub_id):

0 commit comments

Comments
 (0)