Skip to content

Commit 92615eb

Browse files
committed
feat: Quart Server Integration
1 parent e39398a commit 92615eb

File tree

12 files changed

+1078
-34
lines changed

12 files changed

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

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.13.1"
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)
@@ -83,6 +88,7 @@
8388
"sanic": install_sanic_requires,
8489
"webob": install_webob_requires,
8590
"aiohttp": install_aiohttp_requires,
91+
"quart": install_quart_requires,
8692
},
8793
include_package_data=True,
8894
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"},
560551
),
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)