mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
GraphQL: Re-add extension and dependencies (2024)
This commit is contained in:
@@ -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"))
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -133,7 +133,7 @@ setup(
|
||||
"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"],
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user