Metadata-Version: 2.0
Name: graphql-subscriptions
Version: 0.1.5
Summary: A port of apollo-graphql subscriptions for python, using                 gevent websockets, promises, and redis
Home-page: https://github.com/hballard/graphql-python-subscriptions
Author: Heath Ballard
Author-email: heath.ballard@gmail.com
License: MIT
Keywords: graphql websockets concurrent subscriptions
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Classifier: Environment :: Web Environment
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: License :: OSI Approved :: MIT License
Requires-Dist: gevent-websocket
Requires-Dist: graphql-core
Requires-Dist: promise
Requires-Dist: redis

graphql-python-subscriptions
============================

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

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.

Initial implementation. Currently only works with Python 2. No tests
yet.

Installation
------------

::

    $ pip install graphql-subscriptions

API
---

RedisPubsub(host='localhost', port=6379, \*args, \*\*kwargs) #### Arguments - ``host``: Redis server instance url or IP (optional) - ``port``: Redis server port (optional) - ``args, kwargs``: Any additional position and keyword args will be passed to Redis-py constructor (optional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Methods
^^^^^^^

-  ``publish(trigger_name, message)``: Trigger name is a subscription or
   pubsub channel; message is the mutation object or message that will
   end up being passed to the subscription as the root\_value; this
   method should be called inside of mutation resolve function
-  ``subscribe(trigger_name, on_message_handler, options)``: Trigger
   name is a subscription or pubsub channel; on\_message\_handler is the
   callback that will be triggered on each mutation; this method is
   called by the subscription manager
-  ``unsubscribe(sub_id)``: Sub\_id is the subscription ID that is being
   tracked by the pubsub instance -- it is returned from the
   ``subscribe`` method and called by the subscription manager
-  ``wait_and_get_message()``: Called by the ``subscribe`` method during
   the first subscription for the server; run in a separate greenlet and
   calls Redis ``get_message()`` method to constantly poll for new
   messages on pubsub channels
-  ``handle_message(message)``: Called by the pubsub when a message is
   received on a subscribed channel; will check all existing pubsub
   subscriptons and then calls ``on_message_handler()`` for all matches

SubscriptionManager(schema, pubsub, setup\_funcs={})
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Arguments
^^^^^^^^^

-  ``schema``: Graphql schema instance (required)
-  ``pubsub``: Any pubsub instance with publish, subscribe, and
   unsubscribe methods (in this case an instance of the RedisPubsub
   class) (required)
-  ``setup_funcs``: Dictionary of setup functions that map from
   subscription name to a map of pubsub channel names and their filter
   functions; kwargs parameters are:
   ``query, operation_name, callback, variables, context, format_error, format_response, args, subscription_name``
   (optional)

example: \`\`\`python def new\_user(\*\*kwargs): args =
kwargs.get('args') return { 'new\_user\_channel': { 'filter': lambda
user, context: user.active == args.active } }

setup\_funcs = {'new\_user': new\_user} \`\`\`

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

ApolloSubscriptionServer(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).
-  ``keep_alive``: The time period, in seconds, that the server will
   send keep alive messages to the client. (optional)
-  ``on_subscribe(message, subscription_parameters, websocket)``:
   Function to create custom params that will be used when resolving
   this subscription (optional)
-  ``on_unsubscribe(websocket)``: Function that is called when a client
   unsubscribes (optional)
-  ``on_connect(message_payload, websocket)``: Function that will be
   called when a client connects to the socket, called with the
   message\_payload from the client, if the return value is an object,
   its elements will be added to the context. Return false or throw an
   exception to reject the connection. May return a Promise. (optional)
-  ``on_disconnect(websocket)``: Function that called when a client
   disconnects (optional)

Methods
^^^^^^^

-  ``on_open()``: Called when the socket first opens; checks for correct
   subscription protocol and initializes keep alive messages
-  ``on_close(reason)``: Called when socket is closed; unsubscribes from
   subscriptions and deletes subscription objects
-  ``on_message(message)``: provides main control flow for all messaging
   exchanged on the socket between server and client; parses initial
   message, checks for exceptions, responds to client and subscribes /
   unsubscribes socket to mutation channels, via pubsub
-  ``unsubscribe(sub_id)``: Unsubscribes socket from subscriptions
   specified by client
-  ``timer()``: Timer for sending keep alive messages to client; run in
   separate greenlet per socket
-  ``send_init_result(result), send_keep_alive(), send_subscription_data(sub_id, payload), send_subscription_fail(sub_id, payload), send_subscription_success(sub_id)``:
   convenience methods for sending different messages and payloads to
   client

Example Usage
-------------

Server (using Flask and Flask-Sockets):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: python

    from flask import Flask
    from flask_sockets import Sockets
    from graphql_subscriptions import (
        SubscriptionManager,
        RedisPubsub,
        ApolloSubscriptionServer
    )

    app = Flask(__name__)

    # using Flask Sockets here, but could use gevent-websocket directly
    # to create a websocket app and attach it to flask app object
    sockets = Sockets(app)

    # instantiate pubsub -- this will be used to "publish" mutations
    # and also to pass it into your subscription manager
    pubsub = RedisPubsub()

    # create schema using graphene or another python graphql library
    # not showing models or schema design here for brevity
    schema = graphene.Schema(
        query=Query,
        mutation=Mutation,
        subscription=Subscription
    )

    # instantiate subscription manager object -- passing in schema and pubsub
    subscription_mgr = SubscriptionManager(schema, pubsub)

    # using Flask Sockets here -- on each new connection instantiate a
    # subscription app / server -- passing in subscription manager and websocket
    @sockets.route('/socket')
    def socket_channel(websocket):
        subscription_server = ApolloSubscriptionServer(subscription_mgr, websocket)
        subscription_server.handle()
        return []

    if __name__ == "__main__":

        # using a gevent webserver so multiple connections can be
        # maintained concurrently -- gevent websocket spawns a new
        # greenlet for each request and forwards the request to flask
        # app or socket app, depending on request type
        from geventwebsocket import WebSocketServer

        server = WebSocketServer(('', 5000), app)
        print '  Serving at host 0.0.0.0:5000...\n'
        server.serve_forever()

Of course on the server you have to "publish" each time you have a
mutation (in this case to a redis channel). That would look something
like this (using graphene / sql-alchemy):

.. code:: python

    class AddUser(graphene.ClientIDMutation):

        class Input:
            username = graphene.String(required=True)
            email = graphene.String()

        ok = graphene.Boolean()
        user = graphene.Field(lambda: User)

        @classmethod
        def mutate_and_get_payload(cls, args, context, info):
            _input = args.copy()
            del _input['clientMutationId']
            new_user = UserModel(**_input)
            db.session.add(new_user)
            db.session.commit()
            ok = True
            # publish result of mutation to pubsub; check to see if there are any
            # active subscriptions first; this implementation uses cPickle to serialize,
            # so you could send regular python object; here I'm converting to a dict before
            # publishing
            if pubsub.subscriptions:
                pubsub.publish('users', new_user.as_dict())
            return AddUser(ok=ok, user=new_user)

    class Subscription(graphene.ObjectType):
        users = graphene_sqlalchemy.SQLAlchemyConnectionField(
            User,
            active=graphene.Boolean()
        )

        # mutation oject that was published will be passed as
        # root_value of subscription
        def resolve_users(self, args, context, info):
            query = User.get_query(context)
            return query.filter_by(id=info.root_value.get('id'))

Client (using Apollo Client library):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

First create create network interface and and client instances and then
wrap them in a subscription client instance

.. code:: js

    import ReactDOM from 'react-dom'
    import { ApolloProvider } from 'react-apollo'
    import ApolloClient, { createNetworkInterface } from 'apollo-client'
    import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws'

    import ChatApp from './screens/ChatApp'

    const networkInterface = createNetworkInterface({
      uri: 'http://localhost:5000/graphql'
    })

    const wsClient = new SubscriptionClient(`ws://localhost:5000/socket`, {
      reconnect: true
    })

    const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
      networkInterface,
      wsClient,
    )

    const client = new ApolloClient({
      dataIdFromObject: o => o.id,
      networkInterface: networkInterfaceWithSubscriptions
    })

    ReactDOM.render(
      <ApolloProvider client={client}>
        <ChatApp />
      </ApolloProvider>,
      document.getElementById('root')
    )

Build a simple component and then call subscribeToMore method on the
returned data object from the inital graphql query

.. code:: js


    import React from 'react'
    import { graphql } from 'react-apollo'
    import gql from 'graphql-tag'
    import ListBox from '../components/ListBox'

    const SUBSCRIPTION_QUERY = gql`
      subscription newUsers {
        users(active: true) {
          edges {
            node {
              id
              username
            }
          }
        }
      }
    `

    const LIST_BOX_QUERY = gql`
      query AllUsers {
        users(active: true) {
          edges {
            node {
              id
              username
            }
          }
        }
      }
    `

    class ChatListBox extends React.Component {

      componentWillReceiveProps(newProps) {
        if (!newProps.data.loading) {
          if (this.subscription) {
            return
          }
          this.subscription = newProps.data.subscribeToMore({
            document: SUBSCRIPTION_QUERY,
            updateQuery: (previousResult, {subscriptionData}) => {
              const newUser = subscriptionData.data.users.edges
              const newResult = {
                users: {
                  edges: [
                    ...previousResult.users.edges,
                    ...newUser
                  ]
                }
              }
              return newResult
            },
            onError: (err) => console.error(err)
          })
        }
      }

      render() {
        return <ListBox data={this.props.data} />
      }
    }

    const ChatListBoxWithData = graphql(LIST_BOX_QUERY)(ChatListBox)

    export default ChatListBoxWithData



