Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Hauser committed Sep 24, 2017
0 parents commit 45ec699
Show file tree
Hide file tree
Showing 18 changed files with 1,400 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = */tests/*
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.eggs
.idea

*.pyc

build
dist
graphene_tornado.egg-info/
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# graphene-tornado

A project for running [Graphene](http://graphene-python.org/) on top of [Tornado](http://www.tornadoweb.org/) in Python 2.7. The codebase is a port of [graphene-django](https://github.com/graphql-python/graphene-django).

# Examples

See the [example](examples/example.py) application for an example on how to create GraphQL handlers in a Tornado project.
Empty file added README.rst
Empty file.
Empty file added examples/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions examples/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import tornado.web
from tornado.ioloop import IOLoop

from graphene_tornado.schema import schema
from graphene_tornado.tornado_graphql_handler import TornadoGraphQLHandler


class ExampleApplication(tornado.web.Application):

def __init__(self):
handlers = [
(r'/graphql', TornadoGraphQLHandler, dict(graphiql=True, schema=schema)),
(r'/graphql/batch', TornadoGraphQLHandler, dict(graphiql=True, schema=schema, batch=True)),
(r'/graphql/graphiql', TornadoGraphQLHandler, dict(graphiql=True, schema=schema))
]
tornado.web.Application.__init__(self, handlers)

if __name__ == '__main__':
app = ExampleApplication()
app.listen(5000)
IOLoop.instance().start()
5 changes: 5 additions & 0 deletions graphene_tornado/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__version__ = '2.0.dev2017083101'

__all__ = [
'__version__'
]
193 changes: 193 additions & 0 deletions graphene_tornado/render_graphiql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import json

from jinja2 import Environment, Undefined
from markupsafe import Markup

GRAPHIQL_VERSION = '0.10.2'

TEMPLATE = '''<!--
The request to this GraphQL server provided the header "Accept: text/html"
and as a result has been presented GraphiQL - an in-browser IDE for
exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser.
-->
<!DOCTYPE html>
<html>
<head>
<title>{{graphiql_html_title|default("GraphiQL", true)}}</title>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
<meta name="referrer" content="no-referrer">
<link href="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
<script src="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.min.js"></script>
</head>
<body>
<script>
// Collect the URL parameters
var parameters = {};
window.location.search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// Produce a Location query string from a parameter object.
function locationQuery(params) {
return '?' + Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(params[key]);
}).join('&');
}
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
var graphqlParamNames = {
query: true,
variables: true,
operationName: true
};
var otherParams = {};
for (var k in parameters) {
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
otherParams[k] = parameters[k];
}
}
var fetchURL = locationQuery(otherParams);
// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams) {
return fetch(fetchURL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared.
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
history.replaceState(null, null, locationQuery(parameters));
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: {{ query|tojson }},
response: {{ result|tojson }},
variables: {{ variables|tojson }},
operationName: {{ operation_name|tojson }},
}),
document.body
);
</script>
</body>
</html>'''

_slash_escape = '\\/' not in json.dumps('/')
jinja_options = {
'extensions': ['jinja2.ext.autoescape', 'jinja2.ext.with_']
}


# TODO Memoize
def create_jinja_environment():
options = dict(jinja_options)
options['autoescape'] = True
rv = Environment()
rv.filters['tojson'] = tojson_filter
return rv


def tojson_filter(obj, **kwargs):
if isinstance(obj, Undefined):
return str(obj)
return Markup(htmlsafe_dumps(obj, **kwargs))


def htmlsafe_dumps(obj, **kwargs):
"""Works exactly like :func:`dumps` but is safe for use in ``<script>``
tags. It accepts the same arguments and returns a JSON string. Note that
this is available in templates through the ``|tojson`` filter which will
also mark the result as safe. Due to how this function escapes certain
characters this is safe even if used outside of ``<script>`` tags.
The following characters are escaped in strings:
- ``<``
- ``>``
- ``&``
- ``'``
This makes it safe to embed such strings in any place in HTML with the
notable exception of double quoted attributes. In that case single
quote your attributes or HTML escape it in addition.
.. versionchanged:: 0.10
This function's return value is now always safe for HTML usage, even
if outside of script tags or if used in XHTML. This rule does not
hold true when using this function in HTML attributes that are double
quoted. Always single quote attributes if you use the ``|tojson``
filter. Alternatively use ``|tojson|forceescape``.
"""
rv = json.dumps(obj, **kwargs) \
.replace(u'<', u'\\u003c') \
.replace(u'>', u'\\u003e') \
.replace(u'&', u'\\u0026') \
.replace(u"'", u'\\u0027')
if not _slash_escape:
rv = rv.replace('\\/', '/')
return rv


def render_graphiql(query, variables, operation_name, result, graphiql_version=None,
graphiql_template=None, graphiql_html_title=None):
graphiql_version = graphiql_version or GRAPHIQL_VERSION
template = graphiql_template or TEMPLATE

jinja = create_jinja_environment()
context = dict(
graphiql_version=graphiql_version,
graphiql_html_title=graphiql_html_title,
result=result,
query=query,
variables=variables,
operation_name=operation_name
)

tmpl = jinja.from_string(template)
return tmpl.render(context)

31 changes: 31 additions & 0 deletions graphene_tornado/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import absolute_import, division, print_function

import graphene
from graphene import ObjectType, Schema


class QueryRoot(ObjectType):

thrower = graphene.String(required=True)
request = graphene.String(required=True)
test = graphene.String(who=graphene.String())

def resolve_thrower(self, info):
raise Exception("Throws!")

def resolve_request(self, info):
return info.context.arguments['q'][0]

def resolve_test(self, info, who=None):
return 'Hello %s' % (who or 'World')


class MutationRoot(ObjectType):
write_test = graphene.Field(QueryRoot)

def resolve_write_test(self, info):
return QueryRoot()


schema = Schema(query=QueryRoot, mutation=MutationRoot)

Empty file.
48 changes: 48 additions & 0 deletions graphene_tornado/tests/base_test_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
import urllib

from tornado.testing import AsyncHTTPTestCase

from examples.example import ExampleApplication


class BaseTestCase(AsyncHTTPTestCase):

def get_app(self):
self.app = ExampleApplication()
return self.app

def setUp(self):
pass
super(BaseTestCase, self).setUp()

def get(self, url, **kwargs):
return self.http_client.fetch(self.get_url(url), **kwargs)

def delete(self, url, **kwargs):
kwargs['method'] = kwargs.get('method', 'DELETE')
return self.get(url, **kwargs)

def post(self, url, post_data, **kwargs):
kwargs['method'] = kwargs.get('method', 'POST')
kwargs['body'] = urllib.urlencode(post_data)
return self.get(url, **kwargs)

def post_body(self, url, **kwargs):
kwargs['method'] = kwargs.get('method', 'POST')
return self.http_client.fetch(self.get_url(url), **kwargs)

def post_json(self, url, post_data, **kwargs):
kwargs['method'] = kwargs.get('method', 'POST')
kwargs['body'] = json.dumps(post_data)
kwargs['headers'] = kwargs.get('headers', {})
kwargs['headers']['Content-Type'] = "application/json"
return self.get(url, **kwargs)

def put(self, url, put_data, **kwargs):
kwargs['method'] = kwargs.get('method', 'PUT')
return self.post(url, put_data, **kwargs)

def get_url(self, path):
return '%s://localhost:%s%s' % (self.get_protocol(),
self.get_http_port(), path)
55 changes: 55 additions & 0 deletions graphene_tornado/tests/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType
from graphql.type.scalars import GraphQLString
from graphql.type.schema import GraphQLSchema
from tornado.gen import coroutine, Return


@coroutine
def resolve_raises(*_):
raise Exception("Throws!")


@coroutine
def resolve1(obj, args, context, info):
raise Return(context.args.get('q'))


@coroutine
def resolve2(obj, args, context, info):
raise Return(context)


@coroutine
def resolve3(obj, args, context, info):
raise Return('Hello %s' % (args.get('who') or 'World'))


QueryRootType = GraphQLObjectType(
name='QueryRoot',
fields={
'thrower': GraphQLField(GraphQLNonNull(GraphQLString), resolver=resolve_raises),
'request': GraphQLField(GraphQLNonNull(GraphQLString),
resolver=resolve1),
'context': GraphQLField(GraphQLNonNull(GraphQLString),
resolver=resolve2),
'test': GraphQLField(
type=GraphQLString,
args={
'who': GraphQLArgument(GraphQLString)
},
resolver=resolve3
)
}
)

MutationRootType = GraphQLObjectType(
name='MutationRoot',
fields={
'writeTest': GraphQLField(
type=QueryRootType,
resolver=lambda *_: QueryRootType
)
}
)

Schema = GraphQLSchema(QueryRootType, MutationRootType)
Loading

0 comments on commit 45ec699

Please sign in to comment.