diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..454da04
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,26 @@
+name: 🚀 Deploy to PyPI
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - 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@release/v1
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..58601c5
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,22 @@
+name: Lint
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox
+ - name: Run lint and static type checks
+ run: tox
+ env:
+ TOXENV: pre-commit,mypy
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..ceecb95
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,50 @@
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ max-parallel: 4
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
+ os: [ubuntu-latest, windows-latest]
+ exclude:
+ - os: windows-latest
+ python-version: "3.8"
+ - os: windows-latest
+ python-version: "3.9"
+ - os: windows-latest
+ python-version: "3.11"
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
+
+ coverage:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: "3.11"
+ - name: Install test dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install .[test]
+ - name: Test with coverage
+ run: pytest --cov=graphql_server --cov-report=xml tests
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v1
diff --git a/.gitignore b/.gitignore
index 608847c..bfac963 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,19 +1,207 @@
-*.pyc
-*.pyo
+# Created by https://www.gitignore.io/api/python,intellij+all,visualstudiocode
+# Edit at https://www.gitignore.io/?templates=python,intellij+all,visualstudiocode
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignores the whole .idea folder and all .iml files
+# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
+
+.idea/
+
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
*.egg
-*.egg-info
+MANIFEST
-.cache
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.venv/
.coverage
-.idea
-.mypy_cache
-.pytest_cache
-.tox
-.venv
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# pyenv
+.python-version
+
+# Pycharm venv
+venv/
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+### VisualStudioCode ###
.vscode
-/build/
-/dist/
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
-docs
+# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..bb933c2
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,27 @@
+default_language_version:
+ python: python3.11
+exclude: LICENSE
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: check-merge-conflict
+ - id: check-json
+ - id: check-yaml
+ - id: debug-statements
+ - id: end-of-file-fixer
+ exclude: ^docs/.*$
+ - id: pretty-format-json
+ args:
+ - --autofix
+ - id: trailing-whitespace
+- repo: https://github.com/mgedmin/check-manifest
+ rev: "0.49"
+ hooks:
+ - id: check-manifest
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.1.2
+ hooks:
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix, --show-fixes]
+ - id: ruff-format
diff --git a/.ruff.toml b/.ruff.toml
new file mode 100644
index 0000000..228088e
--- /dev/null
+++ b/.ruff.toml
@@ -0,0 +1,34 @@
+select = [
+ "E", # pycodestyle
+ "W", # pycodestyle
+ "F", # pyflake
+ "I", # isort
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "UP", # pyupgrade
+]
+
+ignore = [
+ "E501", # line-too-long
+ "B904", # check for raise statements in exception handlers that lack a from clause
+]
+
+exclude = [
+ "**/docs",
+]
+
+target-version = "py38"
+
+[per-file-ignores]
+# Ignore unused imports (F401) in these files
+"__init__.py" = ["F401"]
+
+[isort]
+known-first-party = ["graphql_server"]
+combine-as-imports = true
+
+[pyupgrade]
+# this keeps annotation syntaxes like Union[X, Y] instead of X | Y
+# to not break Python 3.8
+# https://beta.ruff.rs/docs/settings/#pyupgrade-keep-runtime-typing
+keep-runtime-typing = true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 7789878..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-language: python
-sudo: false
-python:
- - 2.7
- - 3.5
- - 3.6
- - 3.7
- - 3.8
- - 3.9-dev
- - pypy
- - pypy3
-matrix:
- include:
- - python: 3.6
- env: TOXENV=flake8,black,import-order,mypy,manifest
-cache: pip
-install: pip install tox-travis codecov
-script: tox
-after_success: codecov
-deploy:
- provider: pypi
- on:
- branch: master
- tags: true
- python: 3.7
- skip_existing: true
- user: __token__
- password:
- secure: WcZf7AVMDzheXWUxNhZF/TUcyvyCdHZGyhHTakjBhUs8I8khSvlMPofaXTdN1Qn3WbHPK+IXeIPh/2NX0Le3Cdzp08Q/Tgrf9EZ4y02UrZxwSxtsUmjCVd8GaCsQnhR5t5cgrtw33OAf0O22rUnMXsFtw7xMIuCNTgFiYclNbHzYbvnJAEcY3qE8RBbP8zF5Brx+Bl49SjfVR3dJ7CBkjgC9scZjSBAo/yc64d506W59LOjfvXEiDtGUH2gxZNwNiteZtI3frMYqLRjS563SwEFlG36B8g0hBOj6FVpU+YXeImYXw3XFqC6dCvcwn1dAf/vUZ4IDiDIVf5KvFcyDx0ZwZlMSzqlkLVpSDGqPU+7Mx15NW00Yk2+Zs2ZWFMK+g5WtSehhrAWR6El3d0MRlDXKgt9QbCRyh8b2jPV/vQZN2FOBOg9V9a6IszOy/W1J81q39cLOroBhQF4mDFYTAQ5QpBVUyauAfB49QzXsmSWy2uOTsbgo+oAc+OGJ6q9vXCzNqHxhUvtDT9HIq4w5ixw9wqtpSf6n+l2F2RFl5SzHIR7Dt0m9Eg2Ig5NqSGlymz46ZcxpRjd4wVXALD4M8usqy35jGTeEXsqSTO98n3jwKTj/7Xi6GOZuBlwW+SGAjXQ0vzlWD3AEv0Jnh+4AH5UqWwBeD1skw8gtbjM4dos=
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c573f21..79d237b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-Thanks for helping to make graphql-server-core awesome!
+Thanks for helping to make graphql-server awesome!
We welcome all kinds of contributions:
@@ -12,7 +12,7 @@ We welcome all kinds of contributions:
## Getting started
-If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/graphql-server-core/issues) and [pull requests](https://github.com/graphql-python/graphql-server-core/pulls) in progress - someone could already be working on something similar and you can help out.
+If you have a specific contribution in mind, be sure to check the [issues](https://github.com/graphql-python/graphql-server/issues) and [pull requests](https://github.com/graphql-python/graphql-server/pulls) in progress - someone could already be working on something similar and you can help out.
## Project setup
@@ -22,7 +22,7 @@ If you have a specific contribution in mind, be sure to check the [issues](https
After cloning this repo, create a virtualenv:
```console
-virtualenv graphql-server-core-dev
+virtualenv graphql-server-dev
```
Activate the virtualenv and install dependencies by running:
@@ -31,8 +31,7 @@ Activate the virtualenv and install dependencies by running:
python pip install -e ".[test]"
```
-If you are using Linux or MacOS, you can make use of Makefile command
-`make dev-setup`, which is a shortcut for the above python command.
+If you are using Linux or MacOS, you can make use of Makefile command `make dev-setup`, which is a shortcut for the above python command.
### Development on Conda
@@ -57,11 +56,10 @@ And you ready to start development!
After developing, the full test suite can be evaluated by running:
```sh
-pytest tests --cov=graphql-server-core -vv
+pytest tests --cov=graphql-server -vv
```
-If you are using Linux or MacOS, you can make use of Makefile command
-`make tests`, which is a shortcut for the above python command.
+If you are using Linux or MacOS, you can make use of Makefile command `make tests`, which is a shortcut for the above python command.
You can also test on several python environments by using tox.
@@ -73,8 +71,7 @@ Install tox:
pip install tox
```
-Run `tox` on your virtualenv (do not forget to activate it!)
-and that's it!
+Run `tox` on your virtualenv (do not forget to activate it!) and that's it!
### Running tox on Conda
@@ -89,5 +86,4 @@ This install tox underneath so no need to install it before.
Then uncomment the `requires = tox-conda` line on `tox.ini` file.
-Run `tox` and you will see all the environments being created
-and all passing tests. :rocket:
+Run `tox` and you will see all the environments being created and all passing tests. :rocket:
diff --git a/MANIFEST.in b/MANIFEST.in
index 12b4ad7..67b0e2f 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,6 +6,10 @@ include CONTRIBUTING.md
include codecov.yml
include tox.ini
+include .pre-commit-config.yaml
+include .ruff.toml
+
+recursive-include docs *.md *.svg
graft tests
prune bin
diff --git a/README.md b/README.md
index fdb3d40..3e4588d 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,45 @@
-# GraphQL-Server-Core
+
-[](https://badge.fury.io/py/graphql-server-core)
-[](https://travis-ci.org/graphql-python/graphql-server-core)
-[](https://codecov.io/gh/graphql-python/graphql-server-core)
+[](https://badge.fury.io/py/graphql-server)
+[](https://codecov.io/gh/graphql-python/graphql-server)
-GraphQL-Server-Core is a base library that serves as a helper
+GraphQL-Server is a base library that serves as a helper
for building GraphQL servers or integrations into existing web frameworks using
[GraphQL-Core](https://github.com/graphql-python/graphql-core).
-## Existing integrations built with GraphQL-Server-Core
+## Integrations built with GraphQL-Server
-| Server integration | Package |
-|---|---|
-| Flask | [flask-graphql](https://github.com/graphql-python/flask-graphql/) |
-| Sanic |[sanic-graphql](https://github.com/graphql-python/sanic-graphql/) |
-| AIOHTTP | [aiohttp-graphql](https://github.com/graphql-python/aiohttp-graphql) |
-| WebOb (Pyramid, TurboGears) | [webob-graphql](https://github.com/graphql-python/webob-graphql/) |
-| WSGI | [wsgi-graphql](https://github.com/moritzmhmk/wsgi-graphql) |
-| Responder | [responder.ext.graphql](https://github.com/kennethreitz/responder/blob/master/responder/ext/graphql.py) |
+| Server integration | Docs |
+| --------------------------- | --------------------------------------------------------------------------------------- |
+| Flask | [flask](https://github.com/graphql-python/graphql-server/blob/master/docs/flask.md) |
+| Sanic | [sanic](https://github.com/graphql-python/graphql-server/blob/master/docs/sanic.md) |
+| AIOHTTP | [aiohttp](https://github.com/graphql-python/graphql-server/blob/master/docs/aiohttp.md) |
+| WebOb (Pyramid, TurboGears) | [webob](https://github.com/graphql-python/graphql-server/blob/master/docs/webob.md) |
+
+## Other integrations built with GraphQL-Server
+
+| Server integration | Package |
+| ------------------ | ------------------------------------------------------------------------------------------------------- |
+| WSGI | [wsgi-graphql](https://github.com/moritzmhmk/wsgi-graphql) |
+| Responder | [responder.ext.graphql](https://github.com/kennethreitz/responder/blob/master/responder/ext/graphql.py) |
## Other integrations using GraphQL-Core or Graphene
-| Server integration | Package |
-|---|---|
-| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
+| Server integration | Package |
+| ------------------ | --------------------------------------------------------------------- |
+| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
## Documentation
The `graphql_server` package provides these public helper functions:
- * `run_http_query`
- * `encode_execution_results`
- * `load_json_body`
- * `json_encode`
- * `json_encode_pretty`
+- `run_http_query`
+- `encode_execution_results`
+- `load_json_body`
+- `json_encode`
+- `json_encode_pretty`
+
+**NOTE:** the `json_encode_pretty` is kept as backward compatibility change as it uses `json_encode` with `pretty` parameter set to `True`.
All functions in the package are annotated with type hints and docstrings,
and you can build HTML documentation from these using `bin/build_docs`.
@@ -44,4 +50,5 @@ blueprint to build your own integration or GraphQL server implementations.
Please let us know when you have built something new, so we can list it here.
## Contributing
-See [CONTRIBUTING.md](CONTRIBUTING.md)
+
+See [CONTRIBUTING.md](https://github.com/graphql-python/graphql-server/blob/master/CONTRIBUTING.md)
diff --git a/codecov.yml b/codecov.yml
index c393a12..c155caa 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -7,4 +7,4 @@ coverage:
status:
project:
default:
- target: auto
\ No newline at end of file
+ target: auto
diff --git a/docs/_static/graphql-server-logo.svg b/docs/_static/graphql-server-logo.svg
new file mode 100644
index 0000000..7cf6592
--- /dev/null
+++ b/docs/_static/graphql-server-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/aiohttp.md b/docs/aiohttp.md
new file mode 100644
index 0000000..b301855
--- /dev/null
+++ b/docs/aiohttp.md
@@ -0,0 +1,74 @@
+# aiohttp-Graphql
+
+Adds GraphQL support to your aiohttp application.
+
+## Installation
+
+To install the integration with aiohttp, run the below command on your terminal.
+
+`pip install graphql-server[aiohttp]`
+
+## Usage
+
+Use the `GraphQLView` view from `graphql_server.aiohttp`
+
+```python
+from aiohttp import web
+from graphql_server.aiohttp import GraphQLView
+
+from schema import schema
+
+app = web.Application()
+
+GraphQLView.attach(app, schema=schema, graphiql=True)
+
+# Optional, for adding batch query support (used in Apollo-Client)
+GraphQLView.attach(app, schema=schema, batch=True, route_path="/graphql/batch")
+
+if __name__ == '__main__':
+ web.run_app(app)
+```
+
+This will add `/graphql` endpoint to your app (customizable by passing `route_path='/mypath'` to `GraphQLView.attach`) and enable the GraphiQL IDE.
+
+Note: `GraphQLView.attach` is just a convenience function, and the same functionality can be achieved with
+
+```python
+gql_view = GraphQLView(schema=schema, graphiql=True)
+app.router.add_route('*', '/graphql', gql_view, name='graphql')
+```
+
+It's worth noting that the the "view function" of `GraphQLView` is contained in `GraphQLView.__call__`. So, when you create an instance, that instance is callable with the request object as the sole positional argument. To illustrate:
+
+```python
+gql_view = GraphQLView(schema=Schema, **kwargs)
+gql_view(request) # <-- the instance is callable and expects a `aiohttp.web.Request` object.
+```
+
+### Supported options for GraphQLView
+
+ * `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 **"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.
+ * `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`.
+ * `enable_async`: whether `async` mode will be enabled.
+ * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws.
+ * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used.
+ * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query.
+* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**.
+* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**.
+
+## Contributing
+See [CONTRIBUTING.md](../CONTRIBUTING.md)
diff --git a/docs/flask.md b/docs/flask.md
new file mode 100644
index 0000000..dfe0aa7
--- /dev/null
+++ b/docs/flask.md
@@ -0,0 +1,77 @@
+# Flask-GraphQL
+
+Adds GraphQL support to your Flask application.
+
+## Installation
+
+To install the integration with Flask, run the below command on your terminal.
+
+`pip install graphql-server[flask]`
+
+## Usage
+
+Use the `GraphQLView` view from `graphql_server.flask`.
+
+```python
+from flask import Flask
+from graphql_server.flask import GraphQLView
+
+from schema import schema
+
+app = Flask(__name__)
+
+app.add_url_rule('/graphql', view_func=GraphQLView.as_view(
+ 'graphql',
+ schema=schema,
+ graphiql=True,
+))
+
+# Optional, for adding batch query support (used in Apollo-Client)
+app.add_url_rule('/graphql/batch', view_func=GraphQLView.as_view(
+ 'graphql',
+ schema=schema,
+ batch=True
+))
+
+if __name__ == '__main__':
+ app.run()
+```
+
+This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
+
+### Supported options for GraphQLView
+
+ * `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 **"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.
+ * `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.
+ * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used.
+ * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query.
+* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**.
+* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**.
+
+
+You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value
+per request.
+
+```python
+class UserRootValue(GraphQLView):
+ def get_root_value(self, request):
+ return request.user
+
+```
+
+## Contributing
+See [CONTRIBUTING.md](../CONTRIBUTING.md)
diff --git a/docs/sanic.md b/docs/sanic.md
new file mode 100644
index 0000000..102e38d
--- /dev/null
+++ b/docs/sanic.md
@@ -0,0 +1,75 @@
+# Sanic-GraphQL
+
+Adds GraphQL support to your Sanic application.
+
+## Installation
+
+To install the integration with Sanic, run the below command on your terminal.
+
+`pip install graphql-server[sanic]`
+
+## Usage
+
+Use the `GraphQLView` view from `graphql_server.sanic`
+
+```python
+from graphql_server.sanic import GraphQLView
+from sanic import Sanic
+
+from schema import schema
+
+app = Sanic(name="Sanic Graphql App")
+
+app.add_route(
+ GraphQLView.as_view(schema=schema, graphiql=True),
+ '/graphql'
+)
+
+# Optional, for adding batch query support (used in Apollo-Client)
+app.add_route(
+ GraphQLView.as_view(schema=schema, batch=True),
+ '/graphql/batch'
+)
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=8000)
+```
+
+This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
+
+### Supported options for GraphQLView
+
+ * `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 **"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.
+ * `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`.
+ * `enable_async`: whether `async` mode will be enabled.
+ * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws.
+ * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used.
+ * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query.
+* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**.
+* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**.
+
+
+You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value per request.
+
+```python
+class UserRootValue(GraphQLView):
+ def get_root_value(self, request):
+ return request.user
+```
+
+## Contributing
+See [CONTRIBUTING.md](../CONTRIBUTING.md)
diff --git a/docs/webob.md b/docs/webob.md
new file mode 100644
index 0000000..2f88a31
--- /dev/null
+++ b/docs/webob.md
@@ -0,0 +1,64 @@
+# WebOb-GraphQL
+
+Adds GraphQL support to your WebOb (Pyramid, Pylons, ...) application.
+
+## Installation
+
+To install the integration with WebOb, run the below command on your terminal.
+
+`pip install graphql-server[webob]`
+
+## Usage
+
+Use the `GraphQLView` view from `graphql_server.webob`
+
+### Pyramid
+
+```python
+from wsgiref.simple_server import make_server
+from pyramid.config import Configurator
+
+from graphql_server.webob import GraphQLView
+
+from schema import schema
+
+def graphql_view(request):
+ return GraphQLView(request=request, schema=schema, graphiql=True).dispatch_request(request)
+
+if __name__ == '__main__':
+ with Configurator() as config:
+ config.add_route('graphql', '/graphql')
+ config.add_view(graphql_view, route_name='graphql')
+ app = config.make_wsgi_app()
+ server = make_server('0.0.0.0', 6543, app)
+ server.serve_forever()
+```
+
+This will add `/graphql` endpoint to your app and enable the GraphiQL IDE.
+
+### Supported options for GraphQLView
+
+ * `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 **"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.
+ * `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.
+ * `subscriptions`: The GraphiQL socket endpoint for using subscriptions in graphql-ws.
+ * `headers`: An optional GraphQL string to use as the initial displayed request headers, if not provided, the stored headers will be used.
+ * `default_query`: An optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If not provided, GraphiQL will use its own default query.
+* `header_editor_enabled`: An optional boolean which enables the header editor when true. Defaults to **false**.
+* `should_persist_headers`: An optional boolean which enables to persist headers to storage when true. Defaults to **false**.
+
+## Contributing
+See [CONTRIBUTING.md](../CONTRIBUTING.md)
diff --git a/graphql_server/__init__.py b/graphql_server/__init__.py
index cb802ee..f8456de 100644
--- a/graphql_server/__init__.py
+++ b/graphql_server/__init__.py
@@ -1,76 +1,74 @@
"""
-GraphQL-Server-Core
+GraphQL-Server
===================
-GraphQL-Server-Core is a base library that serves as a helper
+GraphQL-Server is a base library that serves as a helper
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, Callable, Collection, Dict, List, Optional, Type, Union, cast
-import six
-
-from promise import promisify, is_thenable, Promise
-
-from graphql import get_default_backend
-from graphql.error import format_error as default_format_error
-from graphql.execution import ExecutionResult
-from graphql.execution.executors.sync import SyncExecutor
-from graphql.type import GraphQLSchema
+from graphql.error import GraphQLError
+from graphql.execution import ExecutionResult, execute
+from graphql.language import OperationType, parse
+from graphql.pyutils import AwaitableOrValue
+from graphql.type import GraphQLSchema, validate_schema
+from graphql.utilities import get_operation_ast
+from graphql.validation import ASTValidationRule, validate
from .error import HttpQueryError
+from .version import version, version_info
-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: # pragma: no cover
- # flake8: noqa
- from typing import Any, Callable, Dict, List, Optional, Type, Union
- from graphql import GraphQLBackend
+# The GraphQL-Server 3 version info.
+__version__ = version
+__version_info__ = version_info
__all__ = [
+ "version",
+ "version_info",
"run_http_query",
"encode_execution_results",
"load_json_body",
"json_encode",
"json_encode_pretty",
"HttpQueryError",
- "RequestParams",
- "ServerResults",
+ "GraphQLParams",
+ "GraphQLResponse",
"ServerResponse",
+ "format_execution_result",
+ "format_error_default",
]
# The public data structures
-RequestParams = namedtuple("RequestParams", "query variables operation_name")
-
-ServerResults = namedtuple("ServerResults", "results params")
-
+GraphQLParams = namedtuple("GraphQLParams", "query variables operation_name")
+GraphQLResponse = namedtuple("GraphQLResponse", "results params")
ServerResponse = namedtuple("ServerResponse", "body status_code")
# The public helper functions
+def format_error_default(error: GraphQLError) -> Dict:
+ """The default function for converting GraphQLError to a dictionary."""
+ return cast(Dict, error.formatted)
+
+
def run_http_query(
- schema, # type: GraphQLSchema
- request_method, # type: str
- data, # type: Union[Dict, List[Dict]]
- query_data=None, # type: Optional[Dict]
- batch_enabled=False, # type: bool
- catch=False, # type: bool
- **execute_options # type: Any
-):
+ schema: GraphQLSchema,
+ request_method: str,
+ data: Union[Dict, List[Dict]],
+ 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.
You need to pass the schema (that is supposed to be already validated),
@@ -87,7 +85,7 @@ def run_http_query(
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))
+ raise TypeError(f"Expected a GraphQL schema, but received {schema!r}.")
if request_method not in ("get", "post"):
raise HttpQueryError(
405,
@@ -95,9 +93,7 @@ def run_http_query(
headers={"Allow": "GET, POST"},
)
if catch:
- catch_exc = (
- HttpQueryError
- ) # type: Union[Type[HttpQueryError], Type[_NoException]]
+ catch_exc: Union[Type[HttpQueryError], Type[_NoException]] = HttpQueryError
else:
catch_exc = _NoException
is_batch = isinstance(data, list)
@@ -108,7 +104,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 {!r}.".format(data)
+ 400, f"GraphQL params should be a dict. Received {data!r}."
)
data = [data]
elif not batch_enabled:
@@ -117,50 +113,45 @@ def run_http_query(
if not data:
raise HttpQueryError(400, "Received an empty list in the batch request.")
- extra_data = {} # type: Dict[str, Any]
+ extra_data: Dict[str, Any] = {}
# If is a batch request, we don't consume the data from the query
if not is_batch:
extra_data = query_data or {}
- all_params = [get_graphql_params(entry, extra_data) for entry in data]
+ all_params: List[GraphQLParams] = [
+ get_graphql_params(entry, extra_data) for entry in data
+ ]
+
+ 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)
- if execute_options.get("return_promise"):
- results = [
- get_response(schema, params, catch_exc, allow_only_query, **execute_options)
- for params in all_params
- ]
- else:
- executor = execute_options.get("executor")
- response_executor = executor if executor else SyncExecutor()
-
- response_promises = [
- response_executor.execute(
- get_response,
- schema,
- params,
- catch_exc,
- allow_only_query,
- **execute_options
- )
- for params in all_params
- ]
- response_executor.wait_until_finished()
- results = [
- result.get() if is_thenable(result) else result
- for result in response_promises
- ]
+def json_encode(data: Union[Dict, List], pretty: bool = False) -> 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.
+ """
+ if not pretty:
+ return json.dumps(data, separators=(",", ":"))
+ return json.dumps(data, indent=2, separators=(",", ": "))
- return ServerResults(results, all_params)
+
+def json_encode_pretty(data: Union[Dict, List]) -> str:
+ return json_encode(data, True)
def encode_execution_results(
- execution_results, # type: List[Optional[ExecutionResult]]
- format_error=None, # type: Callable[[Exception], Dict]
- is_batch=False, # type: bool
- encode=None, # type: Callable[[Dict], Any]
-):
- # type: (...) -> ServerResponse
+ execution_results: List[Optional[ExecutionResult]],
+ format_error: Callable[[GraphQLError], Dict] = format_error_default,
+ is_batch: bool = False,
+ encode: Callable[[Dict], Any] = json_encode,
+) -> ServerResponse:
"""Serialize the ExecutionResults.
This function takes the ExecutionResults that are returned by run_http_query()
@@ -174,7 +165,7 @@ def encode_execution_results(
a status code of 200 or 400 in case any result was invalid as the second item.
"""
results = [
- format_execution_result(execution_result, format_error or default_format_error)
+ format_execution_result(execution_result, format_error)
for execution_result in execution_results
]
result, status_codes = zip(*results)
@@ -183,7 +174,7 @@ def encode_execution_results(
if not is_batch:
result = result[0]
- return ServerResponse((encode or json_encode)(result), status_code)
+ return ServerResponse(encode(result), status_code)
def load_json_body(data):
@@ -199,24 +190,6 @@ def load_json_body(data):
raise HttpQueryError(400, "POST body sent invalid JSON.")
-def json_encode(data, pretty=False):
- # type: (Union[Dict,List],Optional[bool]) -> 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.
- """
- if pretty:
- return json_encode_pretty(data)
- return json.dumps(data, separators=(",", ":"))
-
-
-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
FormattedResult = namedtuple("FormattedResult", "result status_code")
@@ -226,8 +199,7 @@ 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) -> RequestParams
+def get_graphql_params(data: Dict, query_data: Dict) -> GraphQLParams:
"""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.
@@ -240,18 +212,17 @@ def get_graphql_params(data, query_data):
# document_id = data.get('documentId')
operation_name = data.get("operationName") or query_data.get("operationName")
- return RequestParams(query, load_json_variables(variables), operation_name)
+ return GraphQLParams(query, load_json_variables(variables), operation_name)
-def load_json_variables(variables):
- # type: (Optional[Union[str, Dict]]) -> Optional[Dict]
+def load_json_variables(variables: Optional[Union[str, Dict]]) -> Optional[Dict]:
"""Return the given GraphQL variables as a dictionary.
The function returns the given GraphQL variables, making sure they are
deserialized from JSON to a dictionary first if necessary. In case of
invalid JSON input, an HttpQueryError will be raised.
"""
- if variables and isinstance(variables, six.string_types):
+ if variables and isinstance(variables, str):
try:
return json.loads(variables)
except Exception:
@@ -259,82 +230,78 @@ def load_json_variables(variables):
return variables # type: ignore
-def execute_graphql_request(
- schema, # type: GraphQLSchema
- params, # type: RequestParams
- allow_only_query=False, # type: bool
- backend=None, # type: GraphQLBackend
- **kwargs # type: Any
-):
- # type: (...) -> 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.")
-
- try:
- if not backend:
- backend = get_default_backend()
- document = backend.document_from_string(schema, params.query)
- except Exception as e:
- return ExecutionResult(errors=[e], invalid=True)
-
- if allow_only_query:
- operation_type = document.get_operation_type(params.operation_name)
- if operation_type and operation_type != "query":
- raise HttpQueryError(
- 405,
- "Can only perform a {} operation from a POST request.".format(
- operation_type
- ),
- headers={"Allow": "POST"},
- )
-
- try:
- return document.execute(
- operation_name=params.operation_name,
- variable_values=params.variables,
- **kwargs
- )
- except Exception as e:
- return ExecutionResult(errors=[e], invalid=True)
-
-
-@promisify
-def execute_graphql_request_as_promise(*args, **kwargs):
- return execute_graphql_request(*args, **kwargs)
+def assume_not_awaitable(_value: Any) -> bool:
+ """Replacement for isawaitable if everything is assumed to be synchronous."""
+ return False
def get_response(
- schema, # type: GraphQLSchema
- params, # type: RequestParams
- catch_exc, # type: Type[BaseException]
- allow_only_query=False, # type: bool
- **kwargs # type: Any
-):
- # type: (...) -> Optional[Union[ExecutionResult, Promise[ExecutionResult]]]
+ schema: GraphQLSchema,
+ params: GraphQLParams,
+ catch_exc: Type[BaseException],
+ allow_only_query: bool = False,
+ run_sync: bool = True,
+ validation_rules: Optional[Collection[Type[ASTValidationRule]]] = None,
+ max_errors: Optional[int] = None,
+ **kwargs,
+) -> 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 will validate the schema (if the schema is used for the first time),
+ parse the query, check if this is a query if allow_only_query is set to True,
+ validate the query (optionally with additional validation rules and limiting
+ the number of errors), execute the request (asynchronously if run_sync is not
+ set to True), and return the ExecutionResult. You can also catch all errors that
+ belong to an exception class specified by catch_exc.
"""
- # Note: PyCharm will display a error due to the triple dot being used on Callable.
- execute = (
- execute_graphql_request
- ) # type: Callable[..., Union[Promise[ExecutionResult], ExecutionResult]]
- if kwargs.get("return_promise", False):
- execute = execute_graphql_request_as_promise
-
# noinspection PyBroadException
try:
- execution_result = execute(schema, params, allow_only_query, **kwargs)
+ if not params.query:
+ raise HttpQueryError(400, "Must provide query string.")
+
+ # Sanity check query
+ if not isinstance(params.query, str):
+ raise HttpQueryError(400, "Unexpected query type.")
+
+ schema_validation_errors = validate_schema(schema)
+ if schema_validation_errors:
+ return ExecutionResult(data=None, errors=schema_validation_errors)
+
+ 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.",
+ headers={"Allow": "POST"},
+ )
+
+ validation_errors = validate(
+ schema, document, rules=validation_rules, max_errors=max_errors
+ )
+ if validation_errors:
+ return ExecutionResult(data=None, errors=validation_errors)
+
+ execution_result = execute(
+ schema,
+ document,
+ variable_values=params.variables,
+ operation_name=params.operation_name,
+ is_awaitable=assume_not_awaitable if run_sync else None,
+ **kwargs,
+ )
+
except catch_exc:
return None
@@ -342,21 +309,43 @@ def get_response(
def format_execution_result(
- execution_result, # type: Optional[ExecutionResult]
- format_error, # type: Optional[Callable[[Exception], Dict]]
-):
- # type: (...) -> FormattedResult
+ execution_result: Optional[ExecutionResult],
+ format_error: Optional[Callable[[GraphQLError], Dict]] = format_error_default,
+) -> FormattedResult:
"""Format an execution result into a GraphQLResponse.
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
+ response: Optional[Dict[str, Any]] = None
- response = None
if execution_result:
- if execution_result.invalid:
- status_code = 400
- response = execution_result.to_dict(format_error=format_error)
+ if execution_result.errors:
+ fe = [format_error(e) for e in execution_result.errors] # type: ignore
+ response = {"errors": fe}
+
+ if execution_result.errors and any(
+ not getattr(e, "path", None) for e in execution_result.errors
+ ):
+ status_code = 400
+ else:
+ response["data"] = execution_result.data
+ else:
+ 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/__init__.py b/graphql_server/aiohttp/__init__.py
new file mode 100644
index 0000000..8f5beaf
--- /dev/null
+++ b/graphql_server/aiohttp/__init__.py
@@ -0,0 +1,3 @@
+from .graphqlview import GraphQLView
+
+__all__ = ["GraphQLView"]
diff --git a/graphql_server/aiohttp/graphqlview.py b/graphql_server/aiohttp/graphqlview.py
new file mode 100644
index 0000000..ea23037
--- /dev/null
+++ b/graphql_server/aiohttp/graphqlview.py
@@ -0,0 +1,271 @@
+import asyncio
+import copy
+from collections.abc import MutableMapping
+from functools import partial
+from typing import List
+
+from aiohttp import web
+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,
+ load_json_body,
+ run_http_query,
+)
+from graphql_server.render_graphiql import (
+ GraphiQLConfig,
+ GraphiQLData,
+ GraphiQLOptions,
+ render_graphiql_async,
+)
+from graphql_server.utils import wrap_in_async
+
+
+class GraphQLView:
+ schema = None
+ root_value = None
+ context = None
+ pretty = False
+ graphiql = False
+ graphiql_version = None
+ graphiql_template = None
+ graphiql_html_title = None
+ middleware = None
+ validation_rules = None
+ execution_context_class = None
+ batch = False
+ jinja_env = None
+ max_age = 86400
+ enable_async = False
+ subscriptions = None
+ headers = None
+ default_query = None
+ header_editor_enabled = None
+ should_persist_headers = None
+
+ accepted_methods = ["GET", "POST", "PUT", "DELETE"]
+
+ format_error = staticmethod(format_error_default)
+ encode = staticmethod(json_encode)
+
+ def __init__(self, **kwargs):
+ super().__init__()
+ for key, value in kwargs.items():
+ if hasattr(self, key):
+ setattr(self, key, value)
+
+ if not isinstance(self.schema, GraphQLSchema):
+ # maybe the GraphQL schema is wrapped in a Graphene schema
+ self.schema = getattr(self.schema, "graphql_schema", None)
+ 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 is not None and isinstance(self.context, MutableMapping)
+ else {}
+ )
+ if isinstance(context, MutableMapping) and "request" not in context:
+ context.update({"request": request})
+ return context
+
+ def get_middleware(self):
+ return self.middleware
+
+ def get_validation_rules(self):
+ if self.validation_rules is None:
+ 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
+ # request.text() is the aiohttp equivalent to
+ # request.body.decode("utf8")
+ if content_type == "application/graphql":
+ r_text = await request.text()
+ return {"query": r_text}
+
+ if content_type == "application/json":
+ text = await request.text()
+ return load_json_body(text)
+
+ if content_type in (
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ ):
+ # TODO: seems like a multidict would be more appropriate
+ # than casting it and de-duping variables. Alas, it's what
+ # graphql-python wants.
+ return dict(await request.post())
+
+ return {}
+
+ # TODO:
+ # use this method to replace flask and sanic
+ # checks as this is equivalent to `should_display_graphiql` and
+ # `request_wants_html` methods.
+ def is_graphiql(self, request):
+ return all(
+ [
+ self.graphiql,
+ request.method.lower() == "get",
+ "raw" not in request.query,
+ any(
+ [
+ "text/html" in request.headers.get("accept", {}),
+ "*/*" in request.headers.get("accept", {}),
+ ]
+ ),
+ ]
+ )
+
+ # TODO: Same stuff as above method.
+ def is_pretty(self, request):
+ return any(
+ [self.pretty, self.is_graphiql(request), request.query.get("pretty")]
+ )
+
+ async def __call__(self, request):
+ try:
+ data = await self.parse_body(request)
+ request_method = request.method.lower()
+ is_graphiql = self.is_graphiql(request)
+ is_pretty = self.is_pretty(request)
+
+ # TODO: way better than if-else so better
+ # implement this too on flask and sanic
+ if request_method == "options":
+ return self.process_preflight(request)
+
+ all_params: List[GraphQLParams]
+ execution_results, all_params = run_http_query(
+ self.schema,
+ request_method,
+ data,
+ query_data=request.query,
+ batch_enabled=self.batch,
+ catch=is_graphiql,
+ # Execute options
+ run_sync=not self.enable_async,
+ root_value=self.get_root_value(),
+ 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 = (
+ await asyncio.gather(
+ *(
+ ex
+ if ex is not None and is_awaitable(ex)
+ else wrap_in_async(lambda x: x)(ex)
+ for ex in execution_results
+ )
+ )
+ if self.enable_async
+ else execution_results
+ )
+ result, status_code = encode_execution_results(
+ exec_res,
+ is_batch=isinstance(data, list),
+ format_error=self.format_error,
+ encode=partial(self.encode, pretty=is_pretty),
+ )
+
+ if is_graphiql:
+ graphiql_data = GraphiQLData(
+ result=result,
+ query=all_params[0].query,
+ variables=all_params[0].variables,
+ operation_name=all_params[0].operation_name,
+ subscription_url=self.subscriptions,
+ headers=self.headers,
+ )
+ graphiql_config = GraphiQLConfig(
+ graphiql_version=self.graphiql_version,
+ graphiql_template=self.graphiql_template,
+ graphiql_html_title=self.graphiql_html_title,
+ jinja_env=self.jinja_env,
+ )
+ graphiql_options = GraphiQLOptions(
+ default_query=self.default_query,
+ header_editor_enabled=self.header_editor_enabled,
+ should_persist_headers=self.should_persist_headers,
+ )
+ source = await render_graphiql_async(
+ data=graphiql_data, config=graphiql_config, options=graphiql_options
+ )
+ return web.Response(text=source, content_type="text/html")
+
+ return web.Response(
+ text=result,
+ status=status_code,
+ content_type="application/json",
+ )
+
+ except HttpQueryError as err:
+ parsed_error = GraphQLError(err.message)
+ return web.Response(
+ body=self.encode({"errors": [self.format_error(parsed_error)]}),
+ status=err.status_code,
+ headers=err.headers,
+ content_type="application/json",
+ )
+
+ def process_preflight(self, request):
+ """
+ Preflight request support for apollo-client
+ https://www.w3.org/TR/cors/#resource-preflight-requests
+ """
+ headers = request.headers
+ origin = headers.get("Origin", "")
+ method = headers.get("Access-Control-Request-Method", "").upper()
+
+ if method and method in self.accepted_methods:
+ return web.Response(
+ status=200,
+ headers={
+ "Access-Control-Allow-Origin": origin,
+ "Access-Control-Allow-Methods": ", ".join(self.accepted_methods),
+ "Access-Control-Max-Age": str(self.max_age),
+ },
+ )
+ return web.Response(status=400)
+
+ @classmethod
+ def attach(cls, app, *, route_path="/graphql", route_name="graphql", **kwargs):
+ view = cls(**kwargs)
+ app.router.add_route("*", route_path, _asyncify(view), name=route_name)
+
+
+def _asyncify(handler):
+ """Return an async version of the given handler.
+
+ This is mainly here because ``aiohttp`` can't infer the async definition of
+ :py:meth:`.GraphQLView.__call__` and raises a :py:class:`DeprecationWarning`
+ in tests. Wrapping it into an async function avoids the noisy warning.
+ """
+
+ async def _dispatch(request):
+ return await handler(request)
+
+ return _dispatch
diff --git a/graphql_server/error.py b/graphql_server/error.py
index b0ca74a..497f121 100644
--- a/graphql_server/error.py
+++ b/graphql_server/error.py
@@ -16,7 +16,7 @@ def __init__(self, status_code, message=None, is_graphql_error=False, headers=No
self.message = message
self.is_graphql_error = is_graphql_error
self.headers = headers
- super(HttpQueryError, self).__init__(message)
+ super().__init__(message)
def __eq__(self, other):
"""Check whether this HTTP query error is equal to another one."""
diff --git a/graphql_server/flask/__init__.py b/graphql_server/flask/__init__.py
new file mode 100644
index 0000000..8f5beaf
--- /dev/null
+++ b/graphql_server/flask/__init__.py
@@ -0,0 +1,3 @@
+from .graphqlview import GraphQLView
+
+__all__ = ["GraphQLView"]
diff --git a/graphql_server/flask/graphqlview.py b/graphql_server/flask/graphqlview.py
new file mode 100644
index 0000000..d59124d
--- /dev/null
+++ b/graphql_server/flask/graphqlview.py
@@ -0,0 +1,194 @@
+import copy
+from collections.abc import MutableMapping
+from functools import partial
+from typing import List
+
+from flask import Response, render_template_string, request
+from flask.views import View
+from graphql import specified_rules
+from graphql.error import GraphQLError
+from graphql.type.schema import GraphQLSchema
+
+from graphql_server import (
+ GraphQLParams,
+ HttpQueryError,
+ _check_jinja,
+ encode_execution_results,
+ format_error_default,
+ json_encode,
+ load_json_body,
+ run_http_query,
+)
+from graphql_server.render_graphiql import (
+ GraphiQLConfig,
+ GraphiQLData,
+ GraphiQLOptions,
+ render_graphiql_sync,
+)
+
+
+class GraphQLView(View):
+ schema = None
+ root_value = None
+ context = None
+ pretty = False
+ graphiql = False
+ graphiql_version = None
+ graphiql_template = None
+ 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
+ header_editor_enabled = None
+ should_persist_headers = None
+
+ methods = ["GET", "POST", "PUT", "DELETE"]
+
+ format_error = staticmethod(format_error_default)
+ encode = staticmethod(json_encode)
+
+ def __init__(self, **kwargs):
+ super().__init__()
+ for key, value in kwargs.items():
+ if hasattr(self, key):
+ setattr(self, key, value)
+
+ if not isinstance(self.schema, GraphQLSchema):
+ # maybe the GraphQL schema is wrapped in a Graphene schema
+ self.schema = getattr(self.schema, "graphql_schema", None)
+ 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 is not None and isinstance(self.context, MutableMapping)
+ else {}
+ )
+ if isinstance(context, MutableMapping) and "request" not in context:
+ context.update({"request": request})
+ return context
+
+ def get_middleware(self):
+ return self.middleware
+
+ def get_validation_rules(self):
+ if self.validation_rules is None:
+ 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()
+ data = self.parse_body()
+
+ show_graphiql = request_method == "get" and self.should_display_graphiql()
+ catch = show_graphiql
+
+ pretty = self.pretty or show_graphiql or request.args.get("pretty")
+
+ all_params: List[GraphQLParams]
+ execution_results, all_params = run_http_query(
+ self.schema,
+ request_method,
+ data,
+ query_data=request.args,
+ batch_enabled=self.batch,
+ catch=catch,
+ # Execute options
+ root_value=self.get_root_value(),
+ 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,
+ is_batch=isinstance(data, list),
+ format_error=self.format_error,
+ encode=partial(self.encode, pretty=pretty),
+ )
+
+ if show_graphiql:
+ graphiql_data = GraphiQLData(
+ result=result,
+ query=all_params[0].query,
+ variables=all_params[0].variables,
+ operation_name=all_params[0].operation_name,
+ subscription_url=self.subscriptions,
+ headers=self.headers,
+ )
+ graphiql_config = GraphiQLConfig(
+ graphiql_version=self.graphiql_version,
+ graphiql_template=self.graphiql_template,
+ graphiql_html_title=self.graphiql_html_title,
+ jinja_env=self.jinja_env,
+ )
+ graphiql_options = GraphiQLOptions(
+ default_query=self.default_query,
+ header_editor_enabled=self.header_editor_enabled,
+ should_persist_headers=self.should_persist_headers,
+ )
+ source = render_graphiql_sync(
+ data=graphiql_data, config=graphiql_config, options=graphiql_options
+ )
+ return render_template_string(source)
+
+ return Response(result, status=status_code, content_type="application/json")
+
+ except HttpQueryError as e:
+ parsed_error = GraphQLError(e.message)
+ return Response(
+ self.encode({"errors": [self.format_error(parsed_error)]}),
+ status=e.status_code,
+ headers=e.headers,
+ content_type="application/json",
+ )
+
+ @staticmethod
+ def parse_body():
+ # We use mimetype here since we don't need the other
+ # information provided by content_type
+ content_type = request.mimetype
+ if content_type == "application/graphql":
+ return {"query": request.data.decode("utf8")}
+
+ elif content_type == "application/json":
+ return load_json_body(request.data.decode("utf8"))
+
+ elif content_type in (
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ ):
+ return request.form
+
+ return {}
+
+ def should_display_graphiql(self):
+ if not self.graphiql or "raw" in request.args:
+ return False
+
+ return self.request_wants_html()
+
+ @staticmethod
+ def request_wants_html():
+ best = request.accept_mimetypes.best_match(["application/json", "text/html"])
+ return (
+ best == "text/html"
+ and request.accept_mimetypes[best]
+ > request.accept_mimetypes["application/json"]
+ )
diff --git a/graphql_server/quart/__init__.py b/graphql_server/quart/__init__.py
new file mode 100644
index 0000000..8f5beaf
--- /dev/null
+++ b/graphql_server/quart/__init__.py
@@ -0,0 +1,3 @@
+from .graphqlview import GraphQLView
+
+__all__ = ["GraphQLView"]
diff --git a/graphql_server/quart/graphqlview.py b/graphql_server/quart/graphqlview.py
new file mode 100644
index 0000000..8885e5e
--- /dev/null
+++ b/graphql_server/quart/graphqlview.py
@@ -0,0 +1,214 @@
+import asyncio
+import copy
+from collections.abc import MutableMapping
+from functools import partial
+from typing import List
+
+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
+
+from graphql_server import (
+ GraphQLParams,
+ HttpQueryError,
+ _check_jinja,
+ encode_execution_results,
+ format_error_default,
+ json_encode,
+ load_json_body,
+ run_http_query,
+)
+from graphql_server.render_graphiql import (
+ GraphiQLConfig,
+ GraphiQLData,
+ GraphiQLOptions,
+ render_graphiql_sync,
+)
+from graphql_server.utils import wrap_in_async
+
+
+class GraphQLView(View):
+ schema = None
+ root_value = None
+ context = None
+ pretty = False
+ graphiql = False
+ graphiql_version = None
+ graphiql_template = None
+ 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
+ default_query = None
+ header_editor_enabled = None
+ should_persist_headers = None
+
+ methods = ["GET", "POST", "PUT", "DELETE"]
+
+ format_error = staticmethod(format_error_default)
+ encode = staticmethod(json_encode)
+
+ def __init__(self, **kwargs):
+ super().__init__()
+ for key, value in kwargs.items():
+ if hasattr(self, key):
+ setattr(self, key, value)
+
+ if not isinstance(self.schema, GraphQLSchema):
+ # maybe the GraphQL schema is wrapped in a Graphene schema
+ self.schema = getattr(self.schema, "graphql_schema", None)
+ 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 is not None and isinstance(self.context, MutableMapping)
+ else {}
+ )
+ if isinstance(context, MutableMapping) and "request" not in context:
+ context.update({"request": request})
+ return context
+
+ def get_middleware(self):
+ return self.middleware
+
+ def get_validation_rules(self):
+ if self.validation_rules is None:
+ 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()
+ data = await self.parse_body()
+
+ show_graphiql = request_method == "get" and self.should_display_graphiql()
+ catch = show_graphiql
+
+ pretty = self.pretty or show_graphiql or request.args.get("pretty")
+ all_params: List[GraphQLParams]
+ execution_results, all_params = run_http_query(
+ self.schema,
+ request_method,
+ data,
+ query_data=request.args,
+ batch_enabled=self.batch,
+ catch=catch,
+ # Execute options
+ run_sync=not self.enable_async,
+ root_value=self.get_root_value(),
+ 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 = (
+ await asyncio.gather(
+ *(
+ ex
+ if ex is not None and is_awaitable(ex)
+ else wrap_in_async(lambda x: x)(ex)
+ for ex in execution_results
+ )
+ )
+ if self.enable_async
+ else execution_results
+ )
+ result, status_code = encode_execution_results(
+ exec_res,
+ is_batch=isinstance(data, list),
+ format_error=self.format_error,
+ encode=partial(self.encode, pretty=pretty),
+ )
+
+ if show_graphiql:
+ graphiql_data = GraphiQLData(
+ result=result,
+ query=all_params[0].query,
+ variables=all_params[0].variables,
+ operation_name=all_params[0].operation_name,
+ subscription_url=self.subscriptions,
+ headers=self.headers,
+ )
+ graphiql_config = GraphiQLConfig(
+ graphiql_version=self.graphiql_version,
+ graphiql_template=self.graphiql_template,
+ graphiql_html_title=self.graphiql_html_title,
+ jinja_env=self.jinja_env,
+ )
+ graphiql_options = GraphiQLOptions(
+ default_query=self.default_query,
+ header_editor_enabled=self.header_editor_enabled,
+ should_persist_headers=self.should_persist_headers,
+ )
+ source = render_graphiql_sync(
+ data=graphiql_data, config=graphiql_config, options=graphiql_options
+ )
+ return await render_template_string(source)
+
+ return Response(result, status=status_code, content_type="application/json")
+
+ except HttpQueryError as e:
+ parsed_error = GraphQLError(e.message)
+ return Response(
+ self.encode({"errors": [self.format_error(parsed_error)]}),
+ status=e.status_code,
+ headers=e.headers,
+ content_type="application/json",
+ )
+
+ @staticmethod
+ async def parse_body():
+ # We use mimetype here since we don't need the other
+ # information provided by content_type
+ content_type = request.mimetype
+ if content_type == "application/graphql":
+ refined_data = await request.get_data(as_text=True)
+ return {"query": refined_data}
+
+ elif content_type == "application/json":
+ refined_data = await request.get_data(as_text=True)
+ return load_json_body(refined_data)
+
+ elif content_type == "application/x-www-form-urlencoded":
+ return await request.form
+
+ # TODO: Fix this check
+ elif content_type == "multipart/form-data":
+ return await request.files
+
+ return {}
+
+ def should_display_graphiql(self):
+ if not self.graphiql or "raw" in request.args:
+ return False
+
+ return self.request_wants_html()
+
+ @staticmethod
+ def request_wants_html():
+ best = request.accept_mimetypes.best_match(["application/json", "text/html"])
+
+ 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
new file mode 100644
index 0000000..0da06b9
--- /dev/null
+++ b/graphql_server/render_graphiql.py
@@ -0,0 +1,311 @@
+"""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
+
+# 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 = "2.2.0"
+
+GRAPHIQL_TEMPLATE = """
+
+
+