The following tutorial explains how to setup Quart and Strawberry-GraphQL with a simple application.
Requirements: Python 3.9 or higher
References:
-
Install Poetry via https://python-poetry.org/docs/master/#installation
-
Create new Ketchup project
poetry new Ketchup
-
Add Quart to Poetry's requirements files.
poetry add Quart
At the time of writing this doc, the versions installed were:
Quart 0.15.1
-
Setup skeleton web app:
Create new file
Ketchup/ketchup/webapp.py
with the following content:from quart import Quart app = Quart(__name__) @app.route('/') async def index(): return 'Hello World' if __name__ == "__main__": app.run()
-
Test new application by running the following from inside the
Ketchup
project directory.poetry run python ketchup/webapp.py
And open up the following in your browser: http://localhost:5000
-
Add Strawberry-GraphQL to Poetry's requirements files.
poetry add Strawberry-graphql[asgi] # asgi deps are required since no native Quart support
At the time of writing this doc, the versions installed were:
Strawberry-graphql 0.77.10
-
Create a new module at
Ketchup/ketchup/gqlschema.py
import strawberry @strawberry.type class Query: @strawberry.field def upper(self, val: str) -> str: return val.upper()
-
Create new module for embedding the strawberry graphql endpoint as a standard Quart view as
Ketchup/ketchup/strawview.py
import json import logging import pathlib import traceback from typing import Any, Union import strawberry from quart import Response, abort, render_template_string, request from quart.typing import ResponseReturnValue from quart.views import View from strawberry.exceptions import MissingQueryError from strawberry.file_uploads.utils import replace_placeholders_with_files from strawberry.http import (GraphQLHTTPResponse, parse_request_data, process_result) from strawberry.schema import BaseSchema from strawberry.types import ExecutionResult logger = logging.getLogger("ketchup") def render_graphiql_page() -> str: dir_path = pathlib.Path(strawberry.__file__).absolute().parent graphiql_html_file = f"{dir_path}/static/graphiql.html" html_string = None with open(graphiql_html_file, "r") as f: html_string = f.read() return html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "false") class GraphQLView(View): methods = ["GET", "POST"] def __init__(self, schema: BaseSchema, graphiql: bool = True): self.schema = schema self.graphiql = graphiql async def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse: if result.errors: for error in result.errors: err = getattr(error, "original_error", None) or error formatted = "".join(traceback.format_exception(err.__class__, err, err.__traceback__)) logger.error(formatted) return process_result(result) async def dispatch_request(self, *args: Any, **kwargs: Any) -> Union[ResponseReturnValue, str]: if "text/html" in request.headers.get("Accept", ""): if not self.graphiql: abort(404) template = render_graphiql_page() return await render_template_string(template) content_type = str(request.headers.get("content-type", "")) if content_type.startswith("multipart/form-data"): form = await request.form operations = json.loads(form.get("operations", "{}")) files_map = json.loads(form.get("map", "{}")) data = replace_placeholders_with_files(operations, files_map, await request.files) else: data = await request.get_json() try: request_data = parse_request_data(data) except MissingQueryError: return Response("No valid query was provided for the request", 400) context = {"request": request} result = await self.schema.execute( request_data.query, variable_values=request_data.variables, context_value=context, operation_name=request_data.operation_name, root_value=None, ) response_data = await self.process_result(result) return Response( json.dumps(response_data), status=200, content_type="application/json", )
-
Modify file
Ketchup/ketchup/webapp.py
to have the following content:import asyncio from hypercorn.asyncio import serve from hypercorn.config import Config from quart import Quart from strawberry import Schema from ketchup.gqlschema import Query from ketchup.strawview import GraphQLView app = Quart("ketchup") schema = Schema(Query) app.add_url_rule("/graphql", view_func=GraphQLView.as_view("graphql_view", schema=schema)) @app.route("/") async def index(): return 'Welcome to Ketchup! Please see <a href="/graphql">Graph<em>i</em>QL</a> to interact with the GraphQL endpoint.' def hypercorn_serve(): config = Config() config.bind = ["0.0.0.0:5000"] config.use_reloader = True asyncio.run(serve(app, config, shutdown_trigger=lambda: asyncio.Future())) if __name__ == "__main__": hypercorn_serve()
-
Test new application by running the following from inside the
Ketchup
project directory.a. Run the updated web app
poetry run python -m ketchup.webapp
b. Open up the following in your browser: http://localhost:5000/graphql
c. Input the following graph query into the left side text area and hit the play button.
query { upper(val: "dude, where's my car?") }
The result should be (on the right side):
{ "data": { "upper": "DUDE, WHERE'S MY CAR?" }
-
Add pytest as a dependency.
poetry add -D pytest pytest-asyncio
-
Ensure the
Ketchup/tests/test_ketchup.py
file exists with the following content:import pytest from ketchup import __version__, webapp def test_version(): assert __version__ == "0.1.0" @pytest.mark.asyncio class TestViews: async def test_index(self): assert "Welcome" in (await webapp.index())
-
Run the tests by issuing the following from inside the
Ketchup
directory.poetry run pytest
The result should be something like:
=============================== test session starts =============================== platform linux -- Python 3.9.5, pytest-5.4.3, py-1.10.0, pluggy-0.13.1 rootdir: /home/ubuntu/dev/Ketchup plugins: anyio-3.3.1, asyncio-0.15.1 collected 2 items tests/test_ketchup.py .. [100%] ================================ 2 passed in 0.16s ================================
It is the author's advice to add the following to help with formatting all code in a standard way.
-
Add some developer dependencies:
poetry add -D black isort