* Seriously impressive performance.
* WebSocket support.
-* GraphQL support.
* In-process background tasks.
* Startup and shutdown events.
* Test client built on `requests`.
* [`python-multipart`][python-multipart] - Required if you want to support form parsing, with `request.form()`.
* [`itsdangerous`][itsdangerous] - Required for `SessionMiddleware` support.
* [`pyyaml`][pyyaml] - Required for `SchemaGenerator` support.
-* [`graphene`][graphene] - Required for `GraphQLApp` support.
You can install all of these with `pip3 install starlette[full]`.
[requests]: http://docs.python-requests.org/en/master/
[jinja2]: http://jinja.pocoo.org/
[python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
[itsdangerous]: https://pythonhosted.org/itsdangerous/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
+GraphQL support in Starlette was deprecated in version 0.15.0, and removed in version 0.17.0.
-!!! Warning
+Although GraphQL support is no longer built in to Starlette, you can still use GraphQL with Starlette via 3rd party libraries. These libraries all have Starlette-specific guides to help you do just that:
- GraphQL support in Starlette is **deprecated** as of version 0.15 and will
- be removed in a future release. It is also incompatible with Python 3.10+.
- Please consider using a third-party library to provide GraphQL support. This
- is usually done by mounting a GraphQL ASGI application.
- See [#619](https://github.com/encode/starlette/issues/619).
- Some example libraries are:
+- [Ariadne](https://ariadnegraphql.org/docs/starlette-integration.html)
+- [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3#example)
+- [Strawberry](https://strawberry.rocks/docs/integrations/starlette)
+- [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/usage/#starlette)
- * [Ariadne](https://ariadnegraphql.org/docs/asgi)
- * [`tartiflette-asgi`](https://tartiflette.github.io/tartiflette-asgi/)
- * [Strawberry](https://strawberry.rocks/docs/integrations/asgi)
- * [`starlette-graphene3`](https://github.com/ciscorn/starlette-graphene3)
-
-Starlette includes optional support for GraphQL, using the `graphene` library.
-
-Here's an example of integrating the support into your application.
-
-```python
-from starlette.applications import Starlette
-from starlette.routing import Route
-from starlette.graphql import GraphQLApp
-import graphene
-
-
-class Query(graphene.ObjectType):
- hello = graphene.String(name=graphene.String(default_value="stranger"))
-
- def resolve_hello(self, info, name):
- return "Hello " + name
-
-routes = [
- Route('/', GraphQLApp(schema=graphene.Schema(query=Query)))
-]
-
-app = Starlette(routes=routes)
-```
-
-If you load up the page in a browser, you'll be served the GraphiQL tool,
-which you can use to interact with your GraphQL API.
-
-
-
-
-## Accessing request information
-
-The current request is available in the context.
-
-```python
-class Query(graphene.ObjectType):
- user_agent = graphene.String()
-
- def resolve_user_agent(self, info):
- """
- Return the User-Agent of the incoming request.
- """
- request = info.context["request"]
- return request.headers.get("User-Agent", "<unknown>")
-```
-
-## Adding background tasks
-
-You can add background tasks to run once the response has been sent.
-
-```python
-class Query(graphene.ObjectType):
- user_agent = graphene.String()
-
- def resolve_user_agent(self, info):
- """
- Return the User-Agent of the incoming request.
- """
- user_agent = request.headers.get("User-Agent", "<unknown>")
- background = info.context["background"]
- background.add_task(log_user_agent, user_agent=user_agent)
- return user_agent
-
-async def log_user_agent(user_agent):
- ...
-```
-
-## Sync or Async executors
-
-If you're working with a standard ORM, then just use regular function calls for
-your "resolve" methods, and Starlette will manage running the GraphQL query within a
-separate thread.
-
-If you want to use an asynchronous ORM, then use "async resolve" methods, and
-make sure to setup Graphene's AsyncioExecutor using the `executor` argument.
-
-```python
-from graphql.execution.executors.asyncio import AsyncioExecutor
-from starlette.applications import Starlette
-from starlette.graphql import GraphQLApp
-from starlette.routing import Route
-import graphene
-
-
-class Query(graphene.ObjectType):
- hello = graphene.String(name=graphene.String(default_value="stranger"))
-
- async def resolve_hello(self, info, name):
- # We can make asynchronous network calls here.
- return "Hello " + name
-
-routes = [
- # We're using `executor_class=AsyncioExecutor` here.
- Route('/', GraphQLApp(
- schema=graphene.Schema(query=Query),
- executor_class=AsyncioExecutor
- ))
-]
-
-app = Starlette(routes=routes)
-```
[requests]: http://docs.python-requests.org/en/master/
[jinja2]: http://jinja.pocoo.org/
[python-multipart]: https://andrew-d.github.io/python-multipart/
-[graphene]: https://graphene-python.org/
[itsdangerous]: https://pythonhosted.org/itsdangerous/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
# Optionals
-graphene; python_version<'3.10'
itsdangerous
jinja2
python-multipart
filterwarnings=
# Turn warnings that aren't filtered into exceptions
error
- # Deprecated GraphQL (including https://github.com/graphql-python/graphene/issues/1055)
- ignore: GraphQLApp is deprecated and will be removed in a future release\..*:DeprecationWarning
ignore: Using or importing the ABCs from 'collections' instead of from 'collections\.abc' is deprecated.*:DeprecationWarning
ignore: The 'context' alias has been deprecated. Please use 'context_value' instead\.:DeprecationWarning
ignore: The 'variables' alias has been deprecated. Please use 'variable_values' instead\.:DeprecationWarning
[coverage:run]
source_pkgs = starlette, tests
-# GraphQLApp incompatible with and untested on Python 3.10. It's deprecated, let's just ignore
-# coverage for it until it's gone.
-omit =
- starlette/graphql.py
- tests/test_graphql.py
],
extras_require={
"full": [
- "graphene; python_version<'3.10'",
"itsdangerous",
"jinja2",
"python-multipart",
+++ /dev/null
-import json
-import typing
-import warnings
-
-from starlette import status
-from starlette.background import BackgroundTasks
-from starlette.concurrency import run_in_threadpool
-from starlette.requests import Request
-from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
-from starlette.types import Receive, Scope, Send
-
-warnings.warn(
- "GraphQLApp is deprecated and will be removed in a future release. "
- "Consider using a third-party GraphQL implementation. "
- "See https://github.com/encode/starlette/issues/619.",
- DeprecationWarning,
-)
-
-try:
- import graphene
- from graphql.error import GraphQLError, format_error as format_graphql_error
- from graphql.execution.executors.asyncio import AsyncioExecutor
-except ImportError: # pragma: nocover
- graphene = None
- AsyncioExecutor = None # type: ignore
- format_graphql_error = None # type: ignore
- GraphQLError = None # type: ignore
-
-
-class GraphQLApp:
- def __init__(
- self,
- schema: "graphene.Schema",
- executor_class: type = None,
- graphiql: bool = True,
- ) -> None:
- self.schema = schema
- self.graphiql = graphiql
- self.executor_class = executor_class
- self.is_async = executor_class is not None and issubclass(
- executor_class, AsyncioExecutor
- )
-
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
- if self.executor_class is not None:
- self.executor = self.executor_class()
-
- request = Request(scope, receive=receive)
- response = await self.handle_graphql(request)
- await response(scope, receive, send)
-
- async def handle_graphql(self, request: Request) -> Response:
- if request.method in ("GET", "HEAD"):
- if "text/html" in request.headers.get("Accept", ""):
- if not self.graphiql:
- return PlainTextResponse(
- "Not Found", status_code=status.HTTP_404_NOT_FOUND
- )
- return await self.handle_graphiql(request)
-
- data: typing.Mapping[str, typing.Any] = request.query_params
-
- elif request.method == "POST":
- content_type = request.headers.get("Content-Type", "")
-
- if "application/json" in content_type:
- data = await request.json()
- elif "application/graphql" in content_type:
- body = await request.body()
- text = body.decode()
- data = {"query": text}
- elif "query" in request.query_params:
- data = request.query_params
- else:
- return PlainTextResponse(
- "Unsupported Media Type",
- status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- )
-
- else:
- return PlainTextResponse(
- "Method Not Allowed", status_code=status.HTTP_405_METHOD_NOT_ALLOWED
- )
-
- try:
- query = data["query"]
- variables = data.get("variables")
- operation_name = data.get("operationName")
- except KeyError:
- return PlainTextResponse(
- "No GraphQL query found in the request",
- status_code=status.HTTP_400_BAD_REQUEST,
- )
-
- background = BackgroundTasks()
- context = {"request": request, "background": background}
-
- result = await self.execute(
- query, variables=variables, context=context, operation_name=operation_name
- )
- error_data = (
- [format_graphql_error(err) for err in result.errors]
- if result.errors
- else None
- )
- response_data = {"data": result.data}
- if error_data:
- response_data["errors"] = error_data
- status_code = (
- status.HTTP_400_BAD_REQUEST if result.errors else status.HTTP_200_OK
- )
-
- return JSONResponse(
- response_data, status_code=status_code, background=background
- )
-
- async def execute( # type: ignore
- self, query, variables=None, context=None, operation_name=None
- ):
- if self.is_async:
- return await self.schema.execute(
- query,
- variables=variables,
- operation_name=operation_name,
- executor=self.executor,
- return_promise=True,
- context=context,
- )
- else:
- return await run_in_threadpool(
- self.schema.execute,
- query,
- variables=variables,
- operation_name=operation_name,
- context=context,
- )
-
- async def handle_graphiql(self, request: Request) -> Response:
- text = GRAPHIQL.replace("{{REQUEST_PATH}}", json.dumps(request.url.path))
- return HTMLResponse(text)
-
-
-GRAPHIQL = """
-<!--
- * Copyright (c) Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the license found in the
- * LICENSE file in the root directory of this source tree.
--->
-<!DOCTYPE html>
-<html>
- <head>
- <style>
- body {
- height: 100%;
- margin: 0;
- width: 100%;
- overflow: hidden;
- }
- #graphiql {
- height: 100vh;
- }
- </style>
- <!--
- This GraphiQL example depends on Promise and fetch, which are available in
- modern browsers, but can be "polyfilled" for older browsers.
- GraphiQL itself depends on React DOM.
- If you do not want to rely on a CDN, you can host these files locally or
- include them directly in your favored resource bunder.
- -->
- <link href="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.css" rel="stylesheet"/>
- <script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
- <script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
- <script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
- <script src="//cdn.jsdelivr.net/npm/graphiql@0.12.0/graphiql.min.js"></script>
- </head>
- <body>
- <div id="graphiql">Loading...</div>
- <script>
- /**
- * This GraphiQL example illustrates how to use some of GraphiQL's props
- * in order to enable reading and updating the URL parameters, making
- * link sharing of queries a little bit easier.
- *
- * This is only one example of this kind of feature, GraphiQL exposes
- * various React params to enable interesting integrations.
- */
- // Parse the search string to get url parameters.
- var search = window.location.search;
- var parameters = {};
- search.substr(1).split('&').forEach(function (entry) {
- var eq = entry.indexOf('=');
- if (eq >= 0) {
- parameters[decodeURIComponent(entry.slice(0, eq))] =
- decodeURIComponent(entry.slice(eq + 1));
- }
- });
- // if variables was provided, try to format it.
- if (parameters.variables) {
- try {
- parameters.variables =
- JSON.stringify(JSON.parse(parameters.variables), null, 2);
- } catch (e) {
- // Do nothing, we want to display the invalid JSON as a string, rather
- // than present an error.
- }
- }
- // When the query and variables string is edited, update the URL bar so
- // that it can be easily shared
- function onEditQuery(newQuery) {
- parameters.query = newQuery;
- updateURL();
- }
- function onEditVariables(newVariables) {
- parameters.variables = newVariables;
- updateURL();
- }
- function onEditOperationName(newOperationName) {
- parameters.operationName = newOperationName;
- updateURL();
- }
- function updateURL() {
- var newSearch = '?' + Object.keys(parameters).filter(function (key) {
- return Boolean(parameters[key]);
- }).map(function (key) {
- return encodeURIComponent(key) + '=' +
- encodeURIComponent(parameters[key]);
- }).join('&');
- history.replaceState(null, null, newSearch);
- }
- // Defines a GraphQL fetcher using the fetch API. You're not required to
- // use fetch, and could instead implement graphQLFetcher however you like,
- // as long as it returns a Promise or Observable.
- function graphQLFetcher(graphQLParams) {
- // This example expects a GraphQL server at the path /graphql.
- // Change this to point wherever you host your GraphQL server.
- return fetch({{REQUEST_PATH}}, {
- method: 'post',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(graphQLParams),
- credentials: 'include',
- }).then(function (response) {
- return response.text();
- }).then(function (responseBody) {
- try {
- return JSON.parse(responseBody);
- } catch (error) {
- return responseBody;
- }
- });
- }
- // Render <GraphiQL /> into the body.
- // See the README in the top level of this module to learn more about
- // how you can customize GraphiQL by providing different values or
- // additional child elements.
- ReactDOM.render(
- React.createElement(GraphiQL, {
- fetcher: graphQLFetcher,
- query: parameters.query,
- variables: parameters.variables,
- operationName: parameters.operationName,
- onEditQuery: onEditQuery,
- onEditVariables: onEditVariables,
- onEditOperationName: onEditOperationName
- }),
- document.getElementById('graphiql')
- );
- </script>
- </body>
-</html>
-""" # noqa: E501
import functools
-import sys
import pytest
from starlette.testclient import TestClient
-collect_ignore = ["test_graphql.py"] if sys.version_info >= (3, 10) else []
-
@pytest.fixture
def no_trio_support(anyio_backend_name):
+++ /dev/null
-import graphene
-import pytest
-from graphql.execution.executors.asyncio import AsyncioExecutor
-
-from starlette.applications import Starlette
-from starlette.datastructures import Headers
-from starlette.graphql import GraphQLApp
-
-
-class FakeAuthMiddleware:
- def __init__(self, app) -> None:
- self.app = app
-
- async def __call__(self, scope, receive, send):
- headers = Headers(scope=scope)
- scope["user"] = "Jane" if headers.get("Authorization") == "Bearer 123" else None
- await self.app(scope, receive, send)
-
-
-class Query(graphene.ObjectType):
- hello = graphene.String(name=graphene.String(default_value="stranger"))
- whoami = graphene.String()
-
- def resolve_hello(self, info, name):
- return "Hello " + name
-
- def resolve_whoami(self, info):
- return (
- "a mystery"
- if info.context["request"]["user"] is None
- else info.context["request"]["user"]
- )
-
-
-schema = graphene.Schema(query=Query)
-
-
-@pytest.fixture
-def client(test_client_factory):
- app = GraphQLApp(schema=schema, graphiql=True)
- return test_client_factory(app)
-
-
-def test_graphql_get(client):
- response = client.get("/?query={ hello }")
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post(client):
- response = client.post("/?query={ hello }")
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_json(client):
- response = client.post("/", json={"query": "{ hello }"})
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_graphql(client):
- response = client.post(
- "/", data="{ hello }", headers={"content-type": "application/graphql"}
- )
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_post_invalid_media_type(client):
- response = client.post("/", data="{ hello }", headers={"content-type": "dummy"})
- assert response.status_code == 415
- assert response.text == "Unsupported Media Type"
-
-
-def test_graphql_put(client):
- response = client.put("/", json={"query": "{ hello }"})
- assert response.status_code == 405
- assert response.text == "Method Not Allowed"
-
-
-def test_graphql_no_query(client):
- response = client.get("/")
- assert response.status_code == 400
- assert response.text == "No GraphQL query found in the request"
-
-
-def test_graphql_invalid_field(client):
- response = client.post("/", json={"query": "{ dummy }"})
- assert response.status_code == 400
- assert response.json() == {
- "data": None,
- "errors": [
- {
- "locations": [{"column": 3, "line": 1}],
- "message": 'Cannot query field "dummy" on type "Query".',
- }
- ],
- }
-
-
-def test_graphiql_get(client):
- response = client.get("/", headers={"accept": "text/html"})
- assert response.status_code == 200
- assert "<!DOCTYPE html>" in response.text
-
-
-def test_graphiql_not_found(test_client_factory):
- app = GraphQLApp(schema=schema, graphiql=False)
- client = test_client_factory(app)
- response = client.get("/", headers={"accept": "text/html"})
- assert response.status_code == 404
- assert response.text == "Not Found"
-
-
-def test_add_graphql_route(test_client_factory):
- app = Starlette()
- app.add_route("/", GraphQLApp(schema=schema))
- client = test_client_factory(app)
- response = client.get("/?query={ hello }")
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}
-
-
-def test_graphql_context(test_client_factory):
- app = Starlette()
- app.add_middleware(FakeAuthMiddleware)
- app.add_route("/", GraphQLApp(schema=schema))
- client = test_client_factory(app)
- response = client.post(
- "/", json={"query": "{ whoami }"}, headers={"Authorization": "Bearer 123"}
- )
- assert response.status_code == 200
- assert response.json() == {"data": {"whoami": "Jane"}}
-
-
-class ASyncQuery(graphene.ObjectType):
- hello = graphene.String(name=graphene.String(default_value="stranger"))
-
- async def resolve_hello(self, info, name):
- return "Hello " + name
-
-
-async_schema = graphene.Schema(query=ASyncQuery)
-async_app = GraphQLApp(schema=async_schema, executor_class=AsyncioExecutor)
-
-
-def test_graphql_async(no_trio_support, test_client_factory):
- client = test_client_factory(async_app)
- response = client.get("/?query={ hello }")
- assert response.status_code == 200
- assert response.json() == {"data": {"hello": "Hello stranger"}}