8000 feat: Quart Server Integration (#70) · graphql-python/graphql-server@5b7f5de · GitHub
[go: up one dir, main page]

Skip to content

Commit 5b7f5de

Browse files
authored
feat: Quart Server Integration (#70)
* feat: Quart Server Integration * chore: change quart version constraint * tests: check py version for test_request_context * tests: refactor graphiqlview test suite * tests: properly match py36 quart API * fix: manually get accept mime types for py36
1 parent f89d93c commit 5b7f5de

File tree

12 files changed

+1115
-34
lines changed

12 files changed

+1115
-34
lines changed

graphql_server/aiohttp/graphqlview.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def get_context(self, request):
7575
def get_middleware(self):
7676
return self.middleware
7777

78-
# This method can be static
79-
async def parse_body(self, request):
78+
@staticmethod
79+
async def parse_body(request):
8080
content_type = request.content_type
8181
# request.text() is the aiohttp equivalent to
8282
# request.body.decode("utf8")

graphql_server/flask/graphqlview.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ def dispatch_request(self):
139139
content_type="application/json",
140140
)
141141

142-
# Flask
143-
def parse_body(self):
142+
@staticmethod
143+
def parse_body():
144144
# We use mimetype here since we don't need the other
145145
# information provided by content_type
146146
content_type = request.mimetype
@@ -164,7 +164,8 @@ def should_display_graphiql(self):
164164

165165
return self.request_wants_html()
166166

167-
def request_wants_html(self):
167+
@staticmethod
168+
def request_wants_html():
168169
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
169170
return (
170171
best == "text/html"

graphql_server/quart/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .graphqlview import GraphQLView
2+
3+
__all__ = ["GraphQLView"]

graphql_server/quart/graphqlview.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import copy
2+
import sys
3+
from collections.abc import MutableMapping
4+
from functools import partial
5+
from typing import List
6+
7+
from graphql import ExecutionResult
8+
from graphql.error import GraphQLError
9+
from graphql.type.schema import GraphQLSchema
10+
from quart import Response, render_template_string, request
11+
from quart.views import View
12+
13+
from graphql_server import (
14+
GraphQLParams,
15+
HttpQueryError,
16+
encode_execution_results,
17+
format_error_default,
18+
json_encode,
19+
load_json_body,
20+
run_http_query,
21+
)
22+
from graphql_server.render_graphiql import (
23+
GraphiQLConfig,
24+
GraphiQLData,
25+
GraphiQLOptions,
26+
render_graphiql_sync,
27+
)
28+
29+
30+
class GraphQLView(View):
31+
schema = None
32+
root_value = None
33+
context = None
34+
pretty = False
35+
graphiql = False
36+
graphiql_version = None
37+
graphiql_template = None
38+
graphiql_html_title = None
39+
middleware = None
40+
batch = False
41+
enable_async = False
42+
subscriptions = None
43+
headers = None
44+
default_query = None
45+
header_editor_enabled = None
46+
should_persist_headers = None
47+
48+
methods = ["GET", "POST", "PUT", "DELETE"]
49+
50+
format_error = staticmethod(format_error_default)
51+
encode = staticmethod(json_encode)
52+
53+
def __init__(self, **kwargs):
54+
super(GraphQLView, self).__init__()
55+
for key, value in kwargs.items():
56+
if hasattr(self, key):
57+
setattr(self, key, value)
58+
59+
assert isinstance(
60+
self.schema, GraphQLSchema
61+
), "A Schema is required to be provided to GraphQLView."
62+
63+
def get_root_value(self):
64+
return self.root_value
65+
66+
def get_context(self):
67+
context = (
68+
copy.copy(self.context)
69+
if self.context and isinstance(self.context, MutableMapping)
70+
else {}
71+
)
72+
if isinstance(context, MutableMapping) and "request" not in context:
73+
context.update({"request": request})
74+
return context
75+
76+
def get_middleware(self):
77+
return self.middleware
78+
79+
async def dispatch_request(self):
80+
try:
81+
request_method = request.method.lower()
82+
data = await self.parse_body()
83+
84+
show_graphiql = request_method == "get" and self.should_display_graphiql()
85+
catch = show_graphiql
86+
87+
pretty = self.pretty or show_graphiql or request.args.get("pretty")
88+
all_params: List[GraphQLParams]
89+
execution_results, all_params = run_http_query(
90+
self.schema,
91+
request_method,
92+
data,
93+
query_data=request.args,
94+
batch_enabled=self.batch,
95+
catch=catch,
96+
# Execute options
97+
run_sync=not self.enable_async,
98+
root_value=self.get_root_value(),
99+
context_value=self.get_context(),
100+
middleware=self.get_middleware(),
101+
)
102+
exec_res = (
103+
[
104+
ex if ex is None or isinstance(ex, ExecutionResult) else await ex
105+
for ex in execution_results
106+
]
107+
if self.enable_async
108+
else execution_results
109+
)
110+
result, status_code = encode_execution_results(
111+
exec_res,
112+
is_batch=isinstance(data, list),
113+
format_error=self.format_error,
114+
encode=partial(self.encode, pretty=pretty), # noqa
115+
)
116+
117+
if show_graphiql:
118+
graphiql_data = GraphiQLData(
119+
result=result,
120+
query=getattr(all_params[0], "query"),
121+
variables=getattr(all_params[0], "variables"),
122+
operation_name=getattr(all_params[0], "operation_name"),
123+
subscription_url=self.subscriptions,
124+
headers=self.headers,
125+
)
126+
graphiql_config = GraphiQLConfig(
127+
graphiql_version=self.graphiql_version,
128+
graphiql_template=self.graphiql_template,
129+
graphiql_html_title=self.graphiql_html_title,
130+
jinja_env=None,
131+
)
132+
graphiql_options = GraphiQLOptions(
133+
default_query=self.default_query,
134+
header_editor_enabled=self.header_editor_enabled,
135+
should_persist_headers=self.should_persist_headers,
136+
)
137+
source = render_graphiql_sync(
138+
data=graphiql_data, config=graphiql_config, options=graphiql_options
139+
)
140+
return await render_template_string(source)
141+
142+
return Response(result, status=status_code, content_type="application/json")
143+
144+
except HttpQueryError as e:
145+
parsed_error = GraphQLError(e.message)
146+
return Response(
147+
self.encode(dict(errors=[self.format_error(parsed_error)])),
148+
status=e.status_code,
149+
headers=e.headers,
150+
content_type="application/json",
151+
)
152+
153+
@staticmethod
154+
async def parse_body():
155+
# We use mimetype here since we don't need the other
156+
# information provided by content_type
157+
content_type = request.mimetype
158+
if content_type == "application/graphql":
159+
refined_data = await request.get_data(raw=False)
160+
return {"query": refined_data}
161+
162+
elif content_type == "application/json":
163+
refined_data = await request.get_data(raw=False)
164+
return load_json_body(refined_data)
165+
166+
elif content_type == "application/x-www-form-urlencoded":
167+
return await request.form
168+
169+
# TODO: Fix this check
170+
elif content_type == "multipart/form-data":
171+
return await request.files
172+
173+
return {}
174+
175+
def should_display_graphiql(self):
176+
if not self.graphiql or "raw" in request.args:
177+
return False
178+
179+
return self.request_wants_html()
180+
181+
@staticmethod
182+
def request_wants_html():
183+
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
184+
185+
# Needed as this was introduced at Quart 0.8.0: https://gitlab.com/pgjones/quart/-/issues/189
186+
def _quality(accept, key: str) -> float:
187+
for option in accept.options:
188+
if accept._values_match(key, option.value):
189+
return option.quality
190+
return 0.0
191+
192+
if sys.version_info >= (3, 7):
193+
return (
194+
best == "text/html"
195+
and request.accept_mimetypes[best]
196+
> request.accept_mimetypes["application/json"]
197+
)
198+
else:
199+
return best == "text/html" and _quality(
200+
request.accept_mimetypes, best
201+
) > _quality(request.accept_mimetypes, "application/json")

setup.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,17 @@
3838
"aiohttp>=3.5.0,<4",
3939
]
4040

41+
install_quart_requires = [
42+
"quart>=0.6.15"
43+
]
44+
4145
install_all_requires = \
4246
install_requires + \
4347
install_flask_requires + \
4448
install_sanic_requires + \
4549
install_webob_requires + \
46-
install_aiohttp_requires
50+
install_aiohttp_requires + \
51+
install_quart_requires
4752

4853
with open("graphql_server/version.py") as version_file:
4954
version = search('version = "(.*)"', version_file.read()).group(1)
@@ -84,6 +89,7 @@
8489
"sanic": install_sanic_requires,
8590
"webob": install_webob_requires,
8691
"aiohttp": install_aiohttp_requires,
92+
"quart": install_quart_requires,
8793
},
8894
include_package_data=True,
8995
zip_safe=False,

tests/flask/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66

77
def create_app(path="/graphql", **kwargs):
8-
app = Flask(__name__)
9-
app.debug = True
10-
app.add_url_rule(
8+
server = Flask(__name__)
9+
server.debug = True
10+
server.add_url_rule(
1111
path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs)
1212
)
13-
return app
13+
return server
1414

1515

1616
if __name__ == "__main__":

tests/flask/test_graphqlview.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
@pytest.fixture
12-
def app(request):
12+
def app():
1313
# import app factory pattern
1414
app = create_app()
1515

@@ -269,7 +269,7 @@ def test_supports_post_url_encoded_query_with_string_variables(app, client):
269269
assert response_json(response) == {"data": {"test": "Hello Dolly"}}
270270

271271

272-
def test_supports_post_json_quey_with_get_variable_values(app, client):
272+
def test_supports_post_json_query_with_get_variable_values(app, client):
273273
response = client.post(
274274
url_string(app, variables=json.dumps({"who": "Dolly"})),
275275
data=json_dump_kwarg(query="query helloWho($who: String){ test(who: $who) }",),
@@ -533,49 +533,34 @@ def test_post_multipart_data(app, client):
533533
def test_batch_allows_post_with_json_encoding(app, client):
534534
response = client.post(
535535
url_string(app),
536-
data=json_dump_kwarg_list(
537-
# id=1,
538-
query="{test}"
539-
),
536+
data=json_dump_kwarg_list(query="{test}"),
540537
content_type="application/json",
541538
)
542539

543540
assert response.status_code == 200
544-
assert response_json(response) == [
545-
{
546-
# 'id': 1,
547-
"data": {"test": "Hello World"}
548-
}
549-
]
541+
assert response_json(response) == [{"data": {"test": "Hello World"}}]
550542

551543

552544
@pytest.mark.parametrize("app", [create_app(batch=True)])
553545
def test_batch_supports_post_json_query_with_json_variables(app, client):
554546
response = client.post(
555547
url_string(app),
556548
data=json_dump_kwarg_list(
557-
# id=1,
558549
query="query helloWho($who: String){ test(who: $who) }",
559550
variables={"who": "Dolly"},
560 10000 551
),
561552
content_type="application/json",
562553
)
563554

564555
assert response.status_code == 200
565-
assert response_json(response) == [
566-
{
567-
# 'id': 1,
568-
"data": {"test": "Hello Dolly"}
569-
}
570-
]
556+
assert response_json(response) == [{"data": {"test": "Hello Dolly"}}]
571557

572558

573559
@pytest.mark.parametrize("app", [create_app(batch=True)])
574560
def test_batch_allows_post_with_operation_name(app, client):
575561
response = client.post(
576562
url_string(app),
577563
data=json_dump_kwarg_list(
578-
# id=1,
579564
query="""
580565
query helloYou { test(who: "You"), ...shared }
581566
query helloWorld { test(who: "World"), ...shared }
@@ -591,8 +576,5 @@ def test_batch_allows_post_with_operation_name(app, client):
591576

592577
assert response.status_code == 200
593578
assert response_json(response) == [
594-
{
595-
# 'id': 1,
596-
"data": {"test": "Hello World", "shared": "Hello Everyone"}
597-
}
579+
{"data": {"test": "Hello World", "shared": "Hello Everyone"}}
598580
]

tests/quart/__init__.py

Whitespace-only changes.

tests/quart/app.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from quart import Quart
2+
3+
from graphql_server.quart import GraphQLView
4+
from tests.quart.schema import Schema
5+
6+
7+
def create_app(path="/graphql", **kwargs):
8+
server = Quart(__name__)
9+
server.debug = True
10+
server.add_url_rule(
11+
path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs)
12+
)
13+
return server
14+
15+
16+
if __name__ == "__main__":
17+
app = create_app(graphiql=True)
18+
app.run()

0 commit comments

Comments
 (0)
0