From b622a6bb7ab14a56b293207eb82e6b79269e5bc6 Mon Sep 17 00:00:00 2001 From: Taoufik Date: Sat, 26 Oct 2024 03:49:18 +0200 Subject: [PATCH] GraphQL extension (2024) --- responder/ext/graphql/__init__.py | 72 ++++++++++++++ responder/ext/graphql/templates.py | 146 +++++++++++++++++++++++++++++ setup.py | 2 +- tests/test_graphql.py | 34 +++++++ 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 responder/ext/graphql/__init__.py create mode 100644 responder/ext/graphql/templates.py create mode 100644 tests/test_graphql.py diff --git a/responder/ext/graphql/__init__.py b/responder/ext/graphql/__init__.py new file mode 100644 index 00000000..a0e67e23 --- /dev/null +++ b/responder/ext/graphql/__init__.py @@ -0,0 +1,72 @@ +import json +from functools import partial + +from graphql_server import default_format_error, encode_execution_results, json_encode + +from .templates import GRAPHIQL + + +class GraphQLView: + def __init__(self, *, api, schema): + self.api = api + self.schema = schema + + @staticmethod + async def _resolve_graphql_query(req): + # TODO: Get variables and operation_name from form data, params, request text? + + if "json" in req.mimetype: + json_media = await req.media("json") + return ( + json_media["query"], + json_media.get("variables"), + json_media.get("operationName"), + ) + + # Support query/q in form data. + # Form data is awaiting https://github.com/encode/starlette/pull/102 + """ + if "query" in req.media("form"): + return req.media("form")["query"], None, None + if "q" in req.media("form"): + return req.media("form")["q"], None, None + """ + + # Support query/q in params. + if "query" in req.params: + return req.params["query"], None, None + if "q" in req.params: + return req.params["q"], None, None + + # Otherwise, the request text is used (typical). + # TODO: Make some assertions about content-type here. + return req.text, None, None + + async def graphql_response(self, req, resp, schema): + show_graphiql = req.method == "get" and req.accepts("text/html") + + if show_graphiql: + resp.content = self.api.templates.render_string( + GRAPHIQL, endpoint=req.url.path + ) + return None + + query, variables, operation_name = await self._resolve_graphql_query(req) + context = {"request": req, "response": resp} + result = schema.execute( + query, variables=variables, operation_name=operation_name, context=context + ) + result, status_code = encode_execution_results( + [result], + is_batch=False, + format_error=default_format_error, + encode=partial(json_encode, pretty=False), + ) + resp.media = json.loads(result) + return (query, result, status_code) + + async def on_request(self, req, resp): + await self.graphql_response(req, resp, self.schema) + + async def __call__(self, req, resp): + await self.on_request(req, resp) diff --git a/responder/ext/graphql/templates.py b/responder/ext/graphql/templates.py new file mode 100644 index 00000000..c8dea03c --- /dev/null +++ b/responder/ext/graphql/templates.py @@ -0,0 +1,146 @@ +# ruff: noqa: E501 +GRAPHIQL = """ +{% set GRAPHIQL_VERSION = '0.12.0' %} + + + + + + + + + + + + + + + +
Loading...
+ + + +""".strip() diff --git a/setup.py b/setup.py index c22d3b5d..1f0048b3 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ def run(self): "ruff; python_version>='3.7'", "validate-pyproject", ], - "graphql": ["graphene"], + "graphql": ["graphene<3", "graphql-server-core>=1.2,<2"], "release": ["build", "twine"], "test": ["pytest", "pytest-cov", "pytest-mock", "flask"], }, diff --git a/tests/test_graphql.py b/tests/test_graphql.py new file mode 100644 index 00000000..312aa05f --- /dev/null +++ b/tests/test_graphql.py @@ -0,0 +1,34 @@ +import graphene +import pytest + +from responder.ext.graphql import GraphQLView + + +@pytest.fixture +def schema(): + class Query(graphene.ObjectType): + hello = graphene.String(name=graphene.String(default_value="stranger")) + + def resolve_hello(self, info, name): + return f"Hello {name}" + + return graphene.Schema(query=Query) + + +def test_graphql_schema_query_querying(api, schema): + api.add_route("/", GraphQLView(schema=schema, api=api)) + r = api.requests.get("http://;/?q={ hello }", headers={"Accept": "json"}) + assert r.json() == {"data": {"hello": "Hello stranger"}} + + +def test_graphql_schema_json_query(api, schema): + api.add_route("/", GraphQLView(schema=schema, api=api)) + r = api.requests.post("http://;/", json={"query": "{ hello }"}) + assert r.status_code < 300 + + +def test_graphiql(api, schema): + api.add_route("/", GraphQLView(schema=schema, api=api)) + r = api.requests.get("http://;/", headers={"Accept": "text/html"}) + assert r.status_code < 300 + assert "GraphiQL" in r.text