Skip to content

Commit

Permalink
GraphQL: Re-add extension and dependencies (2024)
Browse files Browse the repository at this point in the history
  • Loading branch information
taoufik07 authored and amotl committed Oct 31, 2024
1 parent 3aa21ee commit 696cfe0
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 3 deletions.
7 changes: 5 additions & 2 deletions docs/source/tour.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ Here, you can spawn off a background thread to run any function, out-of-request:

GraphQL
-------
responder supports GraphQL::
Responder supports GraphQL, a query language for APIs that enables clients to
request exactly the data they need::

pip install 'responder[graphql]'

For more information about GraphQL, visit https://graphql.org/.

Serve a GraphQL API::

import graphene
from responder.ext.graphql import GraphQLView

class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
Expand All @@ -50,7 +53,7 @@ Serve a GraphQL API::
return f"Hello {name}"

schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
view = GraphQLView(api=api, schema=schema)

api.add_route("/graph", view)

Expand Down
79 changes: 79 additions & 0 deletions responder/ext/graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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, resp):
# TODO: Get variables and operation_name from form data, params, request text?

if "json" in req.mimetype:
json_media = await req.media("json")
if "query" not in json_media:
resp.status_code = 400
resp.media = {"errors": ["'query' key is required in the JSON payload"]}
return None, None, None
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):
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, resp)
if query is None:
return None

context = {"request": req, "response": resp}
result = self.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)

async def __call__(self, req, resp):
await self.on_request(req, resp)
146 changes: 146 additions & 0 deletions responder/ext/graphql/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# ruff: noqa: E501
GRAPHIQL = """
{% set GRAPHIQL_VERSION = '0.12.0' %}
<!--
* 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@{{ GRAPHIQL_VERSION }}/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@{{ GRAPHIQL_VERSION }}/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('{{ endpoint }}', {
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>
""".strip()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def run(self):
"sphinxext.opengraph",
],
"full": ["responder[graphql,openapi]"],
"graphql": ["graphene"],
"graphql": ["graphene<3", "graphql-server-core>=1.2,<2"],
"openapi": ["apispec>=1.0.0"],
"release": ["build", "twine"],
"test": ["flask", "mypy", "pytest", "pytest-cov", "pytest-mock"],
Expand Down
38 changes: 38 additions & 0 deletions tests/test_graphql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ruff: noqa: E402
import pytest

graphene = pytest.importorskip("graphene")

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.status_code == 200
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
assert r.json() == {"data": {"hello": "Hello stranger"}}


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

0 comments on commit 696cfe0

Please sign in to comment.