Skip to content

Improve tests and docstrings #23

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 1 commit into from
May 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@

/build/
/dist/

docs
26 changes: 14 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
language: python
sudo: false
matrix:
include:
- python: pypy
env: TOX_ENV=pypy
- python: '2.7'
env: TOX_ENV=py27
- python: '3.4'
env: TOX_ENV=py34
- python: '3.5'
env: TOX_ENV=py35
- python: '3.6'
env: TOX_ENV=py36
- python: '3.7'
env: TOX_ENV=py37,import-order,flake8,mypy
env: TOX_ENV=black,flake8,mypy,py37
dist: xenial
sudo: true # required workaround for https://github.com/travis-ci/travis-ci/issues/9815
- python: '3.6'
env: TOX_ENV=py36
- python: '3.5'
env: TOX_ENV=py35
- python: '3.4'
env: TOX_ENV=py34
- python: '2.7'
env: TOX_ENV=py27
- python: 'pypy3.5'
env: TOX_ENV=pypy3
- python: 'pypy'
env: TOX_ENV=pypy
cache:
directories:
- $HOME/.cache/pip
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ for building GraphQL servers or integrations into existing web frameworks using

## Documentation

The `graphql_server` package provides these three public helper functions:
The `graphql_server` package provides these public helper functions:

* `run_http_query`
* `encode_execution_results`
* `laod_json_body`
* `json_encode`
* `json_encode_pretty`

All functions in the package are annotated with type hints and docstrings,
and you can build HTML documentation from these using `bin/build_docs`.
Expand Down
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ Django `graphene-django <https://github.com/graphql-python/graphene-
Documentation
-------------

The ``graphql_server`` package provides these three public helper
functions:
The ``graphql_server`` package provides these public helper functions:

- ``run_http_query``
- ``encode_execution_results``
- ``laod_json_body``
- ``json_encode``
- ``json_encode_pretty``

All functions in the package are annotated with type hints and
docstrings, and you can build HTML documentation from these using
Expand Down
111 changes: 70 additions & 41 deletions graphql_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,49 @@


import json
from collections import MutableMapping, namedtuple
from collections import namedtuple

import six
from graphql import get_default_backend
from graphql.error import format_error as default_format_error
from graphql.execution import ExecutionResult
from graphql.type import GraphQLSchema

from .error import HttpQueryError

try: # pragma: no cover (Python >= 3.3)
from collections.abc import MutableMapping
except ImportError: # pragma: no cover (Python < 3.3)
# noinspection PyUnresolvedReferences,PyProtectedMember
from collections import MutableMapping

# Necessary for static type checking
# noinspection PyUnreachableCode
if False: # flake8: noqa
from typing import List, Dict, Optional, Tuple, Any, Union, Callable, Type
from graphql import GraphQLSchema, GraphQLBackend
from typing import Any, Callable, Dict, List, Optional, Type, Union
from graphql import GraphQLBackend


__all__ = [
"run_http_query",
"encode_execution_results",
"load_json_body",
"HttpQueryError"]
"json_encode",
"json_encode_pretty",
"HttpQueryError",
"RequestParams",
"ServerResults",
"ServerResponse",
]


# The public data structures

RequestParams = namedtuple("RequestParams", "query variables operation_name")

ServerResults = namedtuple("ServerResults", "results params")

ServerResponse = namedtuple("ServerResponse", "body status_code")


# The public helper functions
Expand All @@ -41,7 +64,7 @@ def run_http_query(
query_data=None, # type: Optional[Dict]
batch_enabled=False, # type: bool
catch=False, # type: bool
**execute_options # type: Dict
**execute_options # type: Any
):
"""Execute GraphQL coming from an HTTP query against a given schema.

Expand All @@ -55,9 +78,11 @@ def run_http_query(
All other keyword arguments are passed on to the GraphQL-Core function for
executing GraphQL queries.

This functions returns a tuple with the list of ExecutionResults as first item
Returns a ServerResults tuple with the list of ExecutionResults as first item
and the list of parameters that have been used for execution as second item.
"""
if not isinstance(schema, GraphQLSchema):
raise TypeError("Expected a GraphQL schema, but received {!r}.".format(schema))
if request_method not in ("get", "post"):
raise HttpQueryError(
405,
Expand All @@ -78,7 +103,7 @@ def run_http_query(
if not is_batch:
if not isinstance(data, (dict, MutableMapping)):
raise HttpQueryError(
400, "GraphQL params should be a dict. Received {}.".format(data)
400, "GraphQL params should be a dict. Received {!r}.".format(data)
)
data = [data]
elif not batch_enabled:
Expand All @@ -94,12 +119,12 @@ def run_http_query(

all_params = [get_graphql_params(entry, extra_data) for entry in data]

responses = [
results = [
get_response(schema, params, catch_exc, allow_only_query, **execute_options)
for params in all_params
]

return responses, all_params
return ServerResults(results, all_params)


def encode_execution_results(
Expand All @@ -108,7 +133,7 @@ def encode_execution_results(
is_batch=False, # type: bool
encode=None, # type: Callable[[Dict], Any]
):
# type: (...) -> Tuple[Any, int]
# type: (...) -> ServerResponse
"""Serialize the ExecutionResults.

This function takes the ExecutionResults that are returned by run_http_query()
Expand All @@ -117,18 +142,21 @@ def encode_execution_results(
the first one will be used. You can also pass a custom function that formats the
errors in the ExecutionResults, expecting a dictionary as result and another custom
function that is used to serialize the output.

Returns a ServerResponse tuple with the serialized response as the first item and
a status code of 200 or 400 in case any result was invalid as the second item.
"""
responses = [
results = [
format_execution_result(execution_result, format_error or default_format_error)
for execution_result in execution_results
]
result, status_codes = zip(*responses)
result, status_codes = zip(*results)
status_code = max(status_codes)

if not is_batch:
result = result[0]

return (encode or json_encode)(result), status_code
return ServerResponse((encode or json_encode)(result), status_code)


def load_json_body(data):
Expand All @@ -144,32 +172,46 @@ def load_json_body(data):
raise HttpQueryError(400, "POST body sent invalid JSON.")


# Some more private helpers
def json_encode(data):
# type: (Union[Dict,List]) -> str
"""Serialize the given data(a dictionary or a list) using JSON.

The given data (a dictionary or a list) will be serialized using JSON
and returned as a string that will be nicely formatted if you set pretty=True.
"""
return json.dumps(data, separators=(",", ":"))


GraphQLParams = namedtuple("GraphQLParams", "query variables operation_name")
def json_encode_pretty(data):
# type: (Union[Dict,List]) -> str
"""Serialize the given data using JSON with nice formatting."""
return json.dumps(data, indent=2, separators=(",", ": "))


# Some more private helpers

GraphQLResponse = namedtuple("GraphQLResponse", "result status_code")
FormattedResult = namedtuple("FormattedResult", "result status_code")


class _NoException(Exception):
"""Private exception used when we don't want to catch any real exception."""


def get_graphql_params(data, query_data):
# type: (Dict, Dict) -> GraphQLParams
# type: (Dict, Dict) -> RequestParams
"""Fetch GraphQL query, variables and operation name parameters from given data.

You need to pass both the data from the HTTP request body and the HTTP query string.
Params from the request body will take precedence over those from the query string.

You will get a GraphQLParams object with these parameters as attributes in return.
You will get a RequestParams tuple with these parameters in return.
"""
query = data.get("query") or query_data.get("query")
variables = data.get("variables") or query_data.get("variables")
# document_id = data.get('documentId')
operation_name = data.get("operationName") or query_data.get("operationName")

return GraphQLParams(query, load_json_variables(variables), operation_name)
return RequestParams(query, load_json_variables(variables), operation_name)


def load_json_variables(variables):
Expand All @@ -190,12 +232,12 @@ def load_json_variables(variables):

def execute_graphql_request(
schema, # type: GraphQLSchema
params, # type: GraphQLParams
params, # type: RequestParams
allow_only_query=False, # type: bool
backend=None, # type: GraphQLBackend
**kwargs # type: Dict
**kwargs # type: Any
):
"""Execute a GraphQL request and return a 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
Expand Down Expand Up @@ -235,18 +277,18 @@ def execute_graphql_request(

def get_response(
schema, # type: GraphQLSchema
params, # type: GraphQLParams
params, # type: RequestParams
catch_exc, # type: Type[BaseException]
allow_only_query=False, # type: bool
**kwargs # type: Dict
**kwargs # type: Any
):
# type: (...) -> Optional[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.
"""

# noinspection PyBroadException
try:
execution_result = execute_graphql_request(
schema, params, allow_only_query, **kwargs
Expand All @@ -261,10 +303,10 @@ def format_execution_result(
execution_result, # type: Optional[ExecutionResult]
format_error, # type: Optional[Callable[[Exception], Dict]]
):
# type: (...) -> GraphQLResponse
# type: (...) -> FormattedResult
"""Format an execution result into a GraphQLResponse.

This converts the given execution result into a GraphQLResponse that contains
This converts the given execution result into a FormattedResult that contains
the ExecutionResult converted to a dictionary and an appropriate status code.
"""
status_code = 200
Expand All @@ -276,17 +318,4 @@ def format_execution_result(
else:
response = None

return GraphQLResponse(response, status_code)


def json_encode(data, pretty=False):
# type: (Union[Dict,List], bool) -> str
"""Serialize the given data using JSON.

The given data (a dictionary or a list) will be serialized using JSON
and returned as a string that will be nicely formatted if you set pretty=True.
"""
if not pretty:
return json.dumps(data, separators=(",", ":"))

return json.dumps(data, indent=2, separators=(",", ": "))
return FormattedResult(response, status_code)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages

required_packages = ["graphql-core>=2.1", "promise"]
required_packages = ["graphql-core>=2.1,<3", "promise"]

setup(
name="graphql-server-core",
Expand Down Expand Up @@ -30,7 +30,7 @@
keywords="api graphql protocol rest",
packages=find_packages(exclude=["tests"]),
install_requires=required_packages,
tests_require=["pytest>=2.7.3"],
tests_require=["pytest>=3.0"],
include_package_data=True,
zip_safe=False,
platforms="any",
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""GraphQL-Server-Core Tests"""
Loading