diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6a34bba..271642c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Build wheel and source tarball run: | pip install wheel python setup.py sdist - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@v1.1.0 + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90ba2a1..454ab1b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31616ec..7e58bb5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,22 +8,22 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, windows-latest] exclude: - - os: windows-latest - python-version: "3.6" - os: windows-latest python-version: "3.7" - os: windows-latest python-version: "3.8" - os: windows-latest - python-version: "3.10" + python-version: "3.9" + - os: windows-latest + python-version: "3.11" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -39,11 +39,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - name: Install test dependencies run: | python -m pip install --upgrade pip diff --git a/docs/aiohttp.md b/docs/aiohttp.md index d65bcb8..b301855 100644 --- a/docs/aiohttp.md +++ b/docs/aiohttp.md @@ -47,19 +47,19 @@ gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Req ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. - * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses -`Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `max_age`: Sets the response header Access-Control-Max-Age for preflight requests. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. diff --git a/docs/flask.md b/docs/flask.md index f3a36e7..dfe0aa7 100644 --- a/docs/flask.md +++ b/docs/flask.md @@ -39,26 +39,21 @@ if __name__ == '__main__': This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. -### Special Note for Graphene v3 - -If you are using the `Schema` type of [Graphene](https://github.com/graphql-python/graphene) library, be sure to use the `graphql_schema` attribute to pass as schema on the `GraphQLView` view. Otherwise, the `GraphQLSchema` from `graphql-core` is the way to go. - -More info at [Graphene v3 release notes](https://github.com/graphql-python/graphene/wiki/v3-release-notes#graphene-schema-no-longer-subclasses-graphqlschema-type) and [GraphQL-core 3 usage](https://github.com/graphql-python/graphql-core#usage). - - ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws. @@ -79,4 +74,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/docs/sanic.md b/docs/sanic.md index e922598..102e38d 100644 --- a/docs/sanic.md +++ b/docs/sanic.md @@ -39,19 +39,19 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. - * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses -`Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses `Template.render_async` instead of `Template.render`. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `max_age`: Sets the response header Access-Control-Max-Age for preflight requests. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. @@ -72,4 +72,4 @@ class UserRootValue(GraphQLView): ``` ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/docs/webob.md b/docs/webob.md index 41c0ad1..2f88a31 100644 --- a/docs/webob.md +++ b/docs/webob.md @@ -38,17 +38,19 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. ### Supported options for GraphQLView - * `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request. + * `schema`: The GraphQL schema object that you want the view to execute when it gets a valid request. Accepts either an object of type `GraphQLSchema` from `graphql-core` or `Schema` from `graphene`. For Graphene v3, passing either `schema: graphene.Schema` or `schema.graphql_schema` is allowed. * `context`: A value to pass as the `context_value` to graphql `execute` function. By default is set to `dict` with request object at key `request`. * `root_value`: The `root_value` you want to provide to graphql `execute`. * `pretty`: Whether or not you want the response to be pretty printed JSON. * `graphiql`: If `True`, may present [GraphiQL](https://github.com/graphql/graphiql) when loaded directly from a browser (a useful tool for debugging and exploration). - * `graphiql_version`: The graphiql version to load. Defaults to **"1.0.3"**. + * `graphiql_version`: The graphiql version to load. Defaults to **"2.2.0"**. * `graphiql_template`: Inject a Jinja template string to customize GraphiQL. * `graphiql_html_title`: The graphiql title to display. Defaults to **"GraphiQL"**. + * `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If environment is not set, fallbacks to simple regex-based renderer. * `batch`: Set the GraphQL view as batch (for using in [Apollo-Client](http://dev.apollodata.com/core/network.html#query-batching) or [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)) * `middleware`: A list of graphql [middlewares](http://docs.graphene-python.org/en/latest/execution/middleware/). - * `validation_rules`: A list of graphql validation rules. + * `validation_rules`: A list of graphql validation rules. + * `execution_context_class`: Specifies a custom execution context class. * `encode`: the encoder to use for responses (sensibly defaults to `graphql_server.json_encode`). * `format_error`: the error formatter to use for responses (sensibly defaults to `graphql_server.default_format_error`. * `enable_async`: whether `async` mode will be enabled. @@ -59,4 +61,4 @@ This will add `/graphql` endpoint to your app and enable the GraphiQL IDE. * `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**. ## Contributing -See [CONTRIBUTING.md](../CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](../CONTRIBUTING.md) diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py index ee54cdb..f8456de 100644 --- a/graphql_server/__init__.py +++ b/graphql_server/__init__.py @@ -9,17 +9,7 @@ import json from collections import namedtuple from collections.abc import MutableMapping -from typing import ( - Any, - Callable, - Collection, - Dict, - List, - Optional, - Type, - Union, - cast, -) +from typing import Any, Callable, Collection, Dict, List, Optional, Type, Union, cast from graphql.error import GraphQLError from graphql.execution import ExecutionResult, execute @@ -345,3 +335,17 @@ def format_execution_result( response = {"data": execution_result.data} return FormattedResult(response, status_code) + + +def _check_jinja(jinja_env: Any) -> None: + try: + from jinja2 import Environment + except ImportError: # pragma: no cover + raise RuntimeError( + "Attempt to set 'jinja_env' to a value other than None while Jinja2 is not installed.\n" + "Please install Jinja2 to render GraphiQL with Jinja2.\n" + "Otherwise set 'jinja_env' to None to use the simple regex renderer." + ) + + if not isinstance(jinja_env, Environment): # pragma: no cover + raise TypeError("'jinja_env' has to be of type jinja2.Environment.") diff --git a/graphql_server/aiohttp/graphqlview.py b/graphql_server/aiohttp/graphqlview.py index d98becd..4087946 100644 --- a/graphql_server/aiohttp/graphqlview.py +++ b/graphql_server/aiohttp/graphqlview.py @@ -1,15 +1,18 @@ +import asyncio import copy from collections.abc import MutableMapping from functools import partial from typing import List from aiohttp import web -from graphql import ExecutionResult, GraphQLError, specified_rules +from graphql import GraphQLError, specified_rules +from graphql.pyutils import is_awaitable from graphql.type.schema import GraphQLSchema from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -22,6 +25,7 @@ GraphiQLOptions, render_graphiql_async, ) +from graphql_server.utils import wrap_in_async class GraphQLView: @@ -35,6 +39,7 @@ class GraphQLView: graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False jinja_env = None max_age = 86400 @@ -62,13 +67,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self, request): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -83,6 +91,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + @staticmethod async def parse_body(request): content_type = request.content_type @@ -158,13 +169,18 @@ async def __call__(self, request): context_value=self.get_context(request), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) exec_res = ( - [ - ex if ex is None or isinstance(ex, ExecutionResult) else await ex - for ex in execution_results - ] + await asyncio.gather( + *( + ex + if ex is not None and is_awaitable(ex) + else wrap_in_async(lambda: ex)() + for ex in execution_results + ) + ) if self.enable_async else execution_results ) diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py index 063a67a..7440f82 100644 --- a/graphql_server/flask/graphqlview.py +++ b/graphql_server/flask/graphqlview.py @@ -12,6 +12,7 @@ from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -37,7 +38,9 @@ class GraphQLView(View): graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False + jinja_env = None subscriptions = None headers = None default_query = None @@ -61,13 +64,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -82,6 +88,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + def dispatch_request(self): try: request_method = request.method.lower() @@ -105,6 +114,7 @@ def dispatch_request(self): context_value=self.get_context(), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) result, status_code = encode_execution_results( execution_results, @@ -126,7 +136,7 @@ def dispatch_request(self): graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, graphiql_html_title=self.graphiql_html_title, - jinja_env=None, + jinja_env=self.jinja_env, ) graphiql_options = GraphiQLOptions( default_query=self.default_query, diff --git a/graphql_server/quart/graphqlview.py b/graphql_server/quart/graphqlview.py index ff737ec..7dd479f 100644 --- a/graphql_server/quart/graphqlview.py +++ b/graphql_server/quart/graphqlview.py @@ -1,11 +1,12 @@ +import asyncio import copy -import sys from collections.abc import MutableMapping from functools import partial from typing import List -from graphql import ExecutionResult, specified_rules +from graphql import specified_rules from graphql.error import GraphQLError +from graphql.pyutils import is_awaitable from graphql.type.schema import GraphQLSchema from quart import Response, render_template_string, request from quart.views import View @@ -13,6 +14,7 @@ from graphql_server import ( GraphQLParams, HttpQueryError, + _check_jinja, encode_execution_results, format_error_default, json_encode, @@ -25,6 +27,7 @@ GraphiQLOptions, render_graphiql_sync, ) +from graphql_server.utils import wrap_in_async class GraphQLView(View): @@ -38,7 +41,9 @@ class GraphQLView(View): graphiql_html_title = None middleware = None validation_rules = None + execution_context_class = None batch = False + jinja_env = None enable_async = False subscriptions = None headers = None @@ -63,13 +68,16 @@ def __init__(self, **kwargs): if not isinstance(self.schema, GraphQLSchema): raise TypeError("A Schema is required to be provided to GraphQLView.") + if self.jinja_env is not None: + _check_jinja(self.jinja_env) + def get_root_value(self): return self.root_value def get_context(self): context = ( copy.copy(self.context) - if self.context and isinstance(self.context, MutableMapping) + if self.context is not None and isinstance(self.context, MutableMapping) else {} ) if isinstance(context, MutableMapping) and "request" not in context: @@ -84,6 +92,9 @@ def get_validation_rules(self): return specified_rules return self.validation_rules + def get_execution_context_class(self): + return self.execution_context_class + async def dispatch_request(self): try: request_method = request.method.lower() @@ -107,12 +118,17 @@ async def dispatch_request(self): context_value=self.get_context(), middleware=self.get_middleware(), validation_rules=self.get_validation_rules(), + execution_context_class=self.get_execution_context_class(), ) exec_res = ( - [ - ex if ex is None or isinstance(ex, ExecutionResult) else await ex - for ex in execution_results - ] + await asyncio.gather( + *( + ex + if ex is not None and is_awaitable(ex) + else wrap_in_async(lambda: ex)() + for ex in execution_results + ) + ) if self.enable_async else execution_results ) @@ -136,7 +152,7 @@ async def dispatch_request(self): graphiql_version=self.graphiql_version, graphiql_template=self.graphiql_template, graphiql_html_title=self.graphiql_html_title, - jinja_env=None, + jinja_env=self.jinja_env, ) graphiql_options = GraphiQLOptions( default_query=self.default_query, @@ -165,11 +181,11 @@ async def parse_body(): # information provided by content_type content_type = request.mimetype if content_type == "application/graphql": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return {"query": refined_data} elif content_type == "application/json": - refined_data = await request.get_data(raw=False) + refined_data = await request.get_data(as_text=True) return load_json_body(refined_data) elif content_type == "application/x-www-form-urlencoded": @@ -191,20 +207,8 @@ def should_display_graphiql(self): def request_wants_html(): best = request.accept_mimetypes.best_match(["application/json", "text/html"]) - # Needed as this was introduced at Quart 0.8.0: https://gitlab.com/pgjones/quart/-/issues/189 - def _quality(accept, key: str) -> float: - for option in accept.options: - if accept._values_match(key, option.value): - return option.quality - return 0.0 - - if sys.version_info >= (3, 7): - return ( - best == "text/html" - and request.accept_mimetypes[best] - > request.accept_mimetypes["application/json"] - ) - else: - return best == "text/html" and _quality( - request.accept_mimetypes, best - ) > _quality(request.accept_mimetypes, "application/json") + return ( + best == "text/html" + and request.accept_mimetypes[best] + > request.accept_mimetypes["application/json"] + ) diff --git a/graphql_server/render_graphiql.py b/graphql_server/render_graphiql.py index c942300..0da06b9 100644 --- a/graphql_server/render_graphiql.py +++ b/graphql_server/render_graphiql.py @@ -1,13 +1,19 @@ -"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js] and -(subscriptions-transport-ws)[https://github.com/apollographql/subscriptions-transport-ws]""" +"""Based on (express-graphql)[https://github.com/graphql/express-graphql/blob/main/src/renderGraphiQL.ts] and +(graphql-ws)[https://github.com/enisdenjo/graphql-ws]""" import json import re from typing import Any, Dict, Optional, Tuple -from jinja2 import Environment +# This Environment import is only for type checking purpose, +# and only relevant if rendering GraphiQL with Jinja +try: + from jinja2 import Environment +except ImportError: # pragma: no cover + pass + from typing_extensions import TypedDict -GRAPHIQL_VERSION = "1.0.3" +GRAPHIQL_VERSION = "2.2.0" GRAPHIQL_TEMPLATE = """
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: