Skip to content

Commit

Permalink
Merge pull request #13 from hballard/tests
Browse files Browse the repository at this point in the history
Complete subscription_transport tests
  • Loading branch information
hballard authored Jun 8, 2017
2 parents f7c095e + 19562e7 commit 4146f09
Show file tree
Hide file tree
Showing 7 changed files with 1,301 additions and 38 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
#### (Work in Progress!)
A port of apollographql subscriptions for python, using gevent websockets and redis

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.
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.

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.
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.

Initial implementation. Currently only works with Python 2.
Initial implementation. Good test coverage. Currently only works with Python 2.

## Installation
```
Expand Down Expand Up @@ -48,10 +48,10 @@ $ pip install graphql-subscriptions

#### Methods
- `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
- `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
- `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
- `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
- `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

### ApolloSubscriptionServer(subscription_manager, websocket, keep_alive=None, on_subscribe=None, on_unsubscribe=None, on_connect=None, on_disconnect=None)
### SubscriptionServer(subscription_manager, websocket, keep_alive=None, on_subscribe=None, on_unsubscribe=None, on_connect=None, on_disconnect=None)
#### Arguments
- `subscription_manager`: A subscripton manager instance (required).
- `websocket`: The websocket object passed in from your route handler (required).
Expand All @@ -78,7 +78,7 @@ from flask_sockets import Sockets
from graphql_subscriptions import (
SubscriptionManager,
RedisPubsub,
ApolloSubscriptionServer
SubscriptionServer
)

app = Flask(__name__)
Expand Down Expand Up @@ -106,7 +106,7 @@ subscription_mgr = SubscriptionManager(schema, pubsub)
# subscription app / server -- passing in subscription manager and websocket
@sockets.route('/socket')
def socket_channel(websocket):
subscription_server = ApolloSubscriptionServer(subscription_mgr, websocket)
subscription_server = SubscriptionServer(subscription_mgr, websocket)
subscription_server.handle()
return []

Expand Down
5 changes: 3 additions & 2 deletions graphql_subscriptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from subscription_manager import RedisPubsub, SubscriptionManager
from subscription_transport_ws import ApolloSubscriptionServer
from subscription_transport_ws import SubscriptionServer

__all__ = ['RedisPubsub', 'SubscriptionManager', 'SubscriptionServer']

__all__ = ['RedisPubsub', 'SubscriptionManager', 'ApolloSubscriptionServer']
30 changes: 14 additions & 16 deletions graphql_subscriptions/subscription_transport_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
GRAPHQL_SUBSCRIPTIONS = 'graphql-subscriptions'


class ApolloSubscriptionServer(WebSocketApplication):
class SubscriptionServer(WebSocketApplication):
def __init__(self,
subscription_manager,
websocket,
Expand All @@ -37,7 +37,7 @@ def __init__(self,
self.connection_subscriptions = {}
self.connection_context = {}

super(ApolloSubscriptionServer, self).__init__(websocket)
super(SubscriptionServer, self).__init__(websocket)

def timer(self, callback, period):
while True:
Expand Down Expand Up @@ -77,13 +77,11 @@ def on_message(self, msg):
if msg is None:
return

class nonlocal:
on_init_resolve = None
on_init_reject = None
non_local = {'on_init_resolve': None, 'on_init_reject': None}

def init_promise_handler(resolve, reject):
nonlocal.on_init_resolve = resolve
nonlocal.on_init_reject = reject
non_local['on_init_resolve'] = resolve
non_local['on_init_reject'] = reject

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

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

nonlocal.on_init_resolve(on_connect_promise)
non_local['on_init_resolve'](on_connect_promise)

def init_success_promise_handler(result):
if not result:
Expand All @@ -133,7 +131,8 @@ def subscription_start_promise_handler(init_result):
'callback': None,
'variables': parsed_message.get('variables'),
'context': init_result if isinstance(
init_result, dict) else {},
init_result, dict) else
parsed_message.get('context', {}),
'format_error': None,
'format_response': None
}
Expand All @@ -151,8 +150,7 @@ def subscription_start_promise_handler(init_result):
def promised_params_handler(params):
if not isinstance(params, dict):
error = 'Invalid params returned from\
OnSubscribe! Return value must\
be an dict'
OnSubscribe! Return value must be an dict'

self.send_subscription_fail(
sub_id, {'errors': [{
Expand All @@ -164,15 +162,15 @@ def params_callback(error, result):
if not error:
self.send_subscription_data(
sub_id, {'data': result.data})
elif error.errors:
self.send_subscription_data(
sub_id, {'errors': error.errors})
elif error.message:
self.send_subscription_data(
sub_id,
{'errors': [{
'message': error.message
}]})
elif error.errors:
self.send_subscription_data(
sub_id, {'errors': error.errors})
else:
self.send_subscription_data(
sub_id,
Expand Down Expand Up @@ -218,7 +216,7 @@ def error_catch_handler(e):
# not sure if this behavior is correct or
# not per promises A spec...need to
# investigate
nonlocal.on_init_resolve(Promise.resolve(True))
non_local['on_init_resolve'](Promise.resolve(True))

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

# same rationale as above
nonlocal.on_init_resolve(Promise.resolve(True))
non_local['on_init_resolve'](Promise.resolve(True))

self.connection_context['init_promise'].then(
subscription_end_promise_handler)
Expand Down
17 changes: 14 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
except (IOError, ImportError):
long_description = open('README.md').read()

tests_dep = [
'pytest', 'pytest-mock', 'fakeredis', 'graphene', 'subprocess32',
'flask', 'flask-graphql', 'flask-sockets', 'multiprocess', 'requests'
]

setup(
name='graphql-subscriptions',
version='0.1.7',
version='0.1.8',
author='Heath Ballard',
author_email='heath.ballard@gmail.com',
description=('A port of apollo-graphql subscriptions for python, using\
Expand All @@ -26,6 +31,12 @@
'Programming Language :: Python :: 2.7',
'License :: OSI Approved :: MIT License'
],
install_requires=['gevent-websocket', 'redis', 'promise', 'graphql-core'],
tests_require=['pytest', 'pytest-mock', 'fakeredis', 'graphene'],
install_requires=[
'gevent-websocket', 'redis', 'graphql-core', 'promise<=1.0.1'
],
test_suite='pytest',
tests_require=tests_dep,
extras_require={
'test': tests_dep
},
include_package_data=True)
6 changes: 6 additions & 0 deletions tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"graphql": "^0.9.6",
"subscriptions-transport-ws": "0.5.4"
}
}
13 changes: 4 additions & 9 deletions tests/test_subscription_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@


@pytest.fixture
def mock_redis(monkeypatch):
def pubsub(monkeypatch):
monkeypatch.setattr(redis, 'StrictRedis', fakeredis.FakeStrictRedis)


@pytest.fixture
def pubsub(mock_redis):
return RedisPubsub()


Expand Down Expand Up @@ -266,8 +262,7 @@ def publish_and_unsubscribe_handler(sub_id):


def test_can_subscribe_to_more_than_one_trigger(sub_mgr):
class nonlocal:
trigger_count = 0
non_local = {'trigger_count': 0}

query = 'subscription multiTrigger($filterBoolean: Boolean,\
$uga: String){testFilterMulti(filterBoolean: $filterBoolean,\
Expand All @@ -282,10 +277,10 @@ def callback(err, payload):
assert True
else:
assert payload.data.get('testFilterMulti') == 'good_filter'
nonlocal.trigger_count += 1
non_local['trigger_count'] += 1
except AssertionError as e:
sys.exit(e)
if nonlocal.trigger_count == 2:
if non_local['trigger_count'] == 2:
sub_mgr.pubsub.greenlet.kill()

def publish_and_unsubscribe_handler(sub_id):
Expand Down
Loading

0 comments on commit 4146f09

Please sign in to comment.