Skip to content

Migration to graphql-core-v3 #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: use graphql and graphql-sync functions
  • Loading branch information
KingDarBoja committed Apr 12, 2020
commit 99c40ec15db4fff77ef5ff40ad6fbe6412aba67e
127 changes: 55 additions & 72 deletions graphql_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
for building GraphQL servers or integrations into existing web frameworks using
[GraphQL-Core](https://github.com/graphql-python/graphql-core).
"""


import json
from collections import namedtuple
from collections.abc import MutableMapping
from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, Union
from typing import Any, Callable, Dict, List, Optional, Type, Union

from graphql import ExecutionResult, GraphQLError, GraphQLSchema, execute
from graphql import ExecutionResult, GraphQLError, GraphQLSchema, OperationType
from graphql import format_error as format_error_default
from graphql import get_operation_ast, parse, validate, validate_schema
from graphql import get_operation_ast, parse
from graphql.graphql import graphql, graphql_sync
from graphql.pyutils import AwaitableOrValue

from .error import HttpQueryError

Expand Down Expand Up @@ -50,6 +50,7 @@ def run_http_query(
query_data: Optional[Dict] = None,
batch_enabled: bool = False,
catch: bool = False,
run_sync: bool = True,
**execute_options,
) -> GraphQLResponse:
"""Execute GraphQL coming from an HTTP query against a given schema.
Expand Down Expand Up @@ -105,11 +106,12 @@ def run_http_query(
get_graphql_params(entry, extra_data) for entry in data
]

results = [
get_response(schema, params, catch_exc, allow_only_query, **execute_options)
results: List[Optional[AwaitableOrValue[ExecutionResult]]] = [
get_response(
schema, params, catch_exc, allow_only_query, run_sync, **execute_options
)
for params in all_params
]

return GraphQLResponse(results, all_params)


Expand Down Expand Up @@ -212,82 +214,63 @@ def load_json_variables(variables: Optional[Union[str, Dict]]) -> Optional[Dict]
return variables # type: ignore


def execute_graphql_request(
schema: GraphQLSchema,
params: GraphQLParams,
allow_only_query: bool = False,
**kwargs,
) -> Union[Awaitable[ExecutionResult], ExecutionResult]:
"""Execute a GraphQL request and return an ExecutionResult.

You need to pass the GraphQL schema and the GraphQLParams that you can get
with the get_graphql_params() function. If you only want to allow GraphQL query
operations, then set allow_only_query=True. You can also specify a custom
GraphQLBackend instance that shall be used by GraphQL-Core instead of the
default one. All other keyword arguments are passed on to the GraphQL-Core
function for executing GraphQL queries.
"""
if not params.query:
raise HttpQueryError(400, "Must provide query string.")

# Validate the schema and return a list of errors if it
# does not satisfy the Type System.
schema_validation_errors = validate_schema(schema)
if schema_validation_errors:
return ExecutionResult(data=None, errors=schema_validation_errors)

# Parse the query and return ExecutionResult with errors found.
# Any Exception is parsed as GraphQLError.
try:
document = parse(params.query)
except GraphQLError as e:
return ExecutionResult(data=None, errors=[e])
except Exception as e:
e = GraphQLError(str(e), original_error=e)
return ExecutionResult(data=None, errors=[e])

if allow_only_query:
operation_ast = get_operation_ast(document, params.operation_name)
if operation_ast:
operation = operation_ast.operation.value
if operation != "query":
raise HttpQueryError(
405,
f"Can only perform a {operation} operation from a POST request.",
headers={"Allow": "POST"},
)

validation_errors = validate(schema, document)
if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)

return execute(
schema,
document,
variable_values=params.variables,
operation_name=params.operation_name,
**kwargs,
)


def get_response(
schema: GraphQLSchema,
params: GraphQLParams,
catch_exc: Type[BaseException],
allow_only_query: bool = False,
run_sync: bool = True,
**kwargs,
) -> Optional[Union[Awaitable[ExecutionResult], ExecutionResult]]:
) -> Optional[AwaitableOrValue[ExecutionResult]]:
"""Get an individual execution result as response, with option to catch errors.

This does the same as execute_graphql_request() except that you can catch errors
that belong to an exception class that you need to pass as a parameter.
This does the same as graphql_impl() except that you can either
throw an error on the ExecutionResult if allow_only_query is set to True
or catch errors that belong to an exception class that you need to pass
as a parameter.
"""

if not params.query:
raise HttpQueryError(400, "Must provide query string.")

# noinspection PyBroadException
try:
execution_result = execute_graphql_request(
schema, params, allow_only_query, **kwargs
)
# Parse document to trigger a new HttpQueryError if allow_only_query is True
try:
document = parse(params.query)
except GraphQLError as e:
return ExecutionResult(data=None, errors=[e])
except Exception as e:
e = GraphQLError(str(e), original_error=e)
return ExecutionResult(data=None, errors=[e])

if allow_only_query:
operation_ast = get_operation_ast(document, params.operation_name)
if operation_ast:
operation = operation_ast.operation.value
if operation != OperationType.QUERY.value:
raise HttpQueryError(
405,
f"Can only perform a {operation} operation from a POST request.", # noqa
headers={"Allow": "POST"},
)

if run_sync:
execution_result = graphql_sync(
schema=schema,
source=params.query,
variable_values=params.variables,
operation_name=params.operation_name,
**kwargs,
)
else:
execution_result = graphql( # type: ignore
schema=schema,
source=params.query,
variable_values=params.variables,
operation_name=params.operation_name,
**kwargs,
)
except catch_exc:
return None

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use_parentheses=True

[tool:pytest]
norecursedirs = venv .venv .tox .git .cache .mypy_cache .pytest_cache
markers = asyncio

[bdist_wheel]
universal=1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
tests_requires = [
"pytest>=5.3,<5.4",
"pytest-cov>=2.8,<3",
"pytest-asyncio>=0.10,<1"
]

dev_requires = [
Expand Down
74 changes: 74 additions & 0 deletions tests/test_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import asyncio

from graphql.type.definition import (
GraphQLField,
GraphQLNonNull,
GraphQLObjectType,
)
from graphql.type.scalars import GraphQLString
from graphql.type.schema import GraphQLSchema
from promise import Promise
from pytest import mark

from graphql_server import GraphQLParams, run_http_query

from .utils import as_dicts


def resolve_error_sync(_obj, _info):
raise ValueError("error sync")


async def resolve_error_async(_obj, _info):
await asyncio.sleep(0.001)
raise ValueError("error async")


def resolve_field_sync(_obj, _info):
return "sync"


async def resolve_field_async(_obj, info):
await asyncio.sleep(0.001)
return "async"


NonNullString = GraphQLNonNull(GraphQLString)

QueryRootType = GraphQLObjectType(
name="QueryRoot",
fields={
"errorSync": GraphQLField(NonNullString, resolve=resolve_error_sync),
"errorAsync": GraphQLField(NonNullString, resolve=resolve_error_async),
"fieldSync": GraphQLField(NonNullString, resolve=resolve_field_sync),
"fieldAsync": GraphQLField(NonNullString, resolve=resolve_field_async),
},
)

schema = GraphQLSchema(QueryRootType)


@mark.asyncio
def test_get_responses_using_asyncio_executor():
query = "{fieldSync fieldAsync}"

loop = asyncio.get_event_loop()

async def get_results():
result_promises, params = run_http_query(
schema, "get", {}, dict(query=query), run_sync=False
)
results = await Promise.all(result_promises)
return results, params

try:
results, params = loop.run_until_complete(get_results())
finally:
loop.close()

expected_results = [
{"data": {"fieldSync": "sync", "fieldAsync": "async"}, "errors": None}
]

assert as_dicts(results) == expected_results
assert params == [GraphQLParams(query=query, variables=None, operation_name=None)]
1 change: 0 additions & 1 deletion tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,6 @@ def test_handles_errors_caused_by_a_lack_of_query():

def test_handles_errors_caused_by_invalid_query_type():
results, params = run_http_query(schema, "get", dict(query=42))

assert results == [(None, [{"message": "Must provide Source. Received: 42."}])]


Expand Down