-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* use GraphQLBackend for document parsing * first version of graphql-extensions; apollo engine reporting * skip integration test * remove use of deprecated variable names * use tox * minor fixes for tests * add clarfication on extensions * remove apollo tracing from branch
- Loading branch information
Showing
9 changed files
with
394 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,8 @@ | |
build | ||
dist | ||
graphene_tornado.egg-info/ | ||
|
||
apollo.config.js | ||
schema.graphql | ||
.env | ||
.tox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
__version__ = '2.1.1' | ||
__version__ = '2.2' | ||
|
||
__all__ = [ | ||
'__version__' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
""" | ||
ExtensionStack is an adapter for GraphQLExtension that helps invoke a list of GraphQLExtension objects at runtime. | ||
""" | ||
|
||
from functools import partial | ||
|
||
from tornado.gen import coroutine, Return | ||
from typing import List | ||
|
||
from graphene_tornado.graphql_extension import GraphQLExtension | ||
|
||
|
||
class GraphQLExtensionStack(GraphQLExtension): | ||
|
||
def __init__(self, extensions=None): | ||
self.extensions = extensions or [] # type: List[GraphQLExtension] | ||
|
||
@coroutine | ||
def request_started(self, request, query_string, parsed_query, operation_name, variables, context, request_context): | ||
on_end = yield self._handle_did_start('request_started', request, query_string, parsed_query, operation_name, | ||
variables, context, request_context) | ||
raise Return(on_end) | ||
|
||
@coroutine | ||
def parsing_started(self, query_string): | ||
on_end = yield self._handle_did_start('parsing_started', query_string) | ||
raise Return(on_end) | ||
|
||
@coroutine | ||
def validation_started(self): | ||
on_end = yield self._handle_did_start('validation_started') | ||
raise Return(on_end) | ||
|
||
@coroutine | ||
def execution_started(self, schema, document, root, context, variables, operation_name): | ||
on_end = yield self._handle_did_start('execution_started', schema, document, root, context, | ||
variables, operation_name) | ||
raise Return(on_end) | ||
|
||
@coroutine | ||
def will_resolve_field(self, next, root, info, **args): | ||
ext = self.extensions[:] | ||
ext.reverse() | ||
|
||
handlers = [] | ||
for extension in self.extensions: | ||
handlers.append(extension.will_resolve_field(next, root, info, **args)) | ||
|
||
@coroutine | ||
def on_end(error=None, result=None): | ||
for handler in handlers: | ||
yield handler(error, result) | ||
|
||
raise Return(on_end) | ||
|
||
@coroutine | ||
def will_send_response(self, response, context): | ||
ref = [response, context] | ||
ext = self.extensions[:] | ||
ext.reverse() | ||
|
||
for handler in ext: | ||
result = yield handler.will_send_response(ref[0], ref[1]) | ||
if result: | ||
ref = [result, context] | ||
raise Return(ref) | ||
|
||
@coroutine | ||
def _handle_did_start(self, method, *args): | ||
end_handlers = [] | ||
for extension in self.extensions: | ||
invoker = partial(getattr(extension, method), *args) | ||
end_handler = invoker() | ||
if end_handler: | ||
end_handlers.append(end_handler) | ||
|
||
@coroutine | ||
def end(errors=None): | ||
errors = errors or [] | ||
end_handlers.reverse() | ||
for handler_future in end_handlers: | ||
handler = yield handler_future | ||
if handler: | ||
yield handler(errors) | ||
|
||
raise Return(end) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
""" | ||
GraphQLExtension is analogous to the server extensions that are provided | ||
by Apollo Server: https://github.com/apollographql/apollo-server/tree/master/packages/graphql-extensions | ||
Extensions are also middleware but have additional hooks. | ||
""" | ||
from __future__ import absolute_import, print_function | ||
|
||
import sys | ||
from abc import ABCMeta, abstractmethod | ||
|
||
from tornado.gen import coroutine | ||
from typing import NewType, List, Callable, Optional | ||
|
||
EndHandler = NewType('EndHandler', Optional[List[Callable[[List[Exception]], None]]]) | ||
|
||
|
||
class GraphQLExtension: | ||
|
||
__metaclass__ = ABCMeta | ||
|
||
@abstractmethod | ||
def request_started(self, | ||
request, # type: HTTPServerRequest | ||
query_string, # type: Optional[str], | ||
parsed_query, # type: Optional[Document] | ||
operation_name, # type: Optional[str] | ||
variables, # type: Optional[dict[str, Any]] | ||
context, # type: Any | ||
request_context # type: Any | ||
): | ||
# type: (...) -> EndHandler | ||
pass | ||
|
||
@abstractmethod | ||
def parsing_started(self, query_string): # type: (str) -> EndHandler | ||
pass | ||
|
||
@abstractmethod | ||
def validation_started(self): | ||
# type: () -> EndHandler | ||
pass | ||
|
||
@abstractmethod | ||
def execution_started(self, | ||
schema, # type: GraphQLSchema | ||
document, # type: Document | ||
root, # type: Any | ||
context, # type: Optional[Any] | ||
variables, # type: Optional[Any] | ||
operation_name # type: Optional[str] | ||
): | ||
# type: (...) -> EndHandler | ||
pass | ||
|
||
@abstractmethod | ||
def will_resolve_field(self, next, root, info, **args): | ||
# type: (...) -> EndHandler | ||
pass | ||
|
||
@abstractmethod | ||
def will_send_response(self, | ||
response, # type: Any | ||
context, # type: Any | ||
): | ||
# type: (...) -> EndHandler | ||
pass | ||
|
||
def as_middleware(self): | ||
# type: () -> Callable | ||
""" | ||
Adapter for using the stack as middleware so that the will_resolve_field function | ||
is invoked like normal middleware | ||
Returns: | ||
An adapter function | ||
""" | ||
@coroutine | ||
def middleware(next, root, info, **args): | ||
end_resolve = yield self.will_resolve_field(next, root, info, **args) | ||
errors = [] | ||
result = None | ||
try: | ||
result = next(root, info, **args) | ||
except: | ||
errors.append(sys.exc_info()[0]) | ||
finally: | ||
end_resolve(errors, result) | ||
return middleware |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from __future__ import absolute_import, print_function | ||
|
||
import hashlib | ||
|
||
from graphene import Schema | ||
from graphql import parse, execute, GraphQLError | ||
from graphql.utils.introspection_query import introspection_query | ||
from json_stable_stringify_python import stringify | ||
|
||
|
||
def generate_schema_hash(schema): | ||
# type: (Schema) -> str | ||
""" | ||
Generates a stable hash of the current schema using an introspection query. | ||
""" | ||
ast = parse(introspection_query) | ||
result = execute(schema, ast) | ||
|
||
if result and not result.data: | ||
raise GraphQLError('Unable to generate server introspection document') | ||
|
||
schema = result.data['__schema'] | ||
# It's important that we perform a deterministic stringification here | ||
# since, depending on changes in the underlying `graphql-core` execution | ||
# layer, varying orders of the properties in the introspection | ||
stringified_schema = stringify(schema).encode('utf-8') | ||
return hashlib.sha512(stringified_schema).hexdigest() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import json | ||
from functools import partial | ||
|
||
import pytest | ||
import tornado | ||
from tornado.gen import coroutine, Return | ||
|
||
from graphene_tornado.schema import schema | ||
from graphene_tornado.graphql_extension import GraphQLExtension | ||
from graphene_tornado.tests.http_helper import HttpHelper | ||
from graphene_tornado.tests.test_graphql import url_string, GRAPHQL_HEADER, response_json | ||
from graphene_tornado.tornado_graphql_handler import TornadoGraphQLHandler | ||
|
||
|
||
STARTED = [] | ||
ENDED = [] | ||
|
||
|
||
def _track_closed(name, errors=None): | ||
ENDED.append(name) | ||
|
||
|
||
class TrackingExtension(GraphQLExtension): | ||
|
||
@coroutine | ||
def request_started(self, request, query_string, parsed_query, operation_name, variables, context, request_context): | ||
phase = 'request' | ||
STARTED.append(phase) | ||
raise Return(partial(_track_closed, phase)) | ||
|
||
@coroutine | ||
def parsing_started(self, query_string): | ||
phase = 'parsing' | ||
STARTED.append(phase) | ||
raise Return(partial(_track_closed, phase)) | ||
|
||
@coroutine | ||
def validation_started(self): | ||
phase = 'validation' | ||
STARTED.append(phase) | ||
raise Return(partial(_track_closed, phase)) | ||
|
||
@coroutine | ||
def execution_started(self, schema, document, root, context, variables, operation_name): | ||
phase = 'execution' | ||
STARTED.append(phase) | ||
raise Return(partial(_track_closed, phase)) | ||
|
||
@coroutine | ||
def will_resolve_field(self, next, root, info, **args): | ||
phase = 'resolve_field' | ||
STARTED.append(phase) | ||
raise Return(partial(_track_closed, phase)) | ||
|
||
@coroutine | ||
def will_send_response(self, response, context): | ||
phase = 'response' | ||
STARTED.append(phase) | ||
|
||
|
||
class ExampleExtensionsApplication(tornado.web.Application): | ||
|
||
def __init__(self): | ||
handlers = [ | ||
(r'/graphql', TornadoGraphQLHandler, dict(graphiql=True, schema=schema, extensions=[TrackingExtension()])), | ||
(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) | ||
|
||
|
||
@pytest.fixture | ||
def app(): | ||
return ExampleExtensionsApplication() | ||
|
||
|
||
@pytest.fixture | ||
def http_helper(http_client, base_url): | ||
return HttpHelper(http_client, base_url) | ||
|
||
|
||
@pytest.mark.gen_test | ||
def test_extensions_are_called_in_order(http_helper): | ||
response = yield http_helper.get(url_string( | ||
query='query helloWho($who: String){ test(who: $who) }', | ||
variables=json.dumps({'who': "Dolly"}) | ||
), headers=GRAPHQL_HEADER) | ||
|
||
assert response.code == 200 | ||
assert response_json(response) == { | ||
'data': {'test': "Hello Dolly"} | ||
} | ||
|
||
assert ['request', 'parsing', 'validation', 'execution', 'response'] == STARTED | ||
assert ['parsing', 'validation', 'execution', 'request'] == ENDED | ||
|
Oops, something went wrong.