10000 First graphql_server integration :D · graphql-python/sanic-graphql@21a548c · GitHub
[go: up one dir, main page]

Skip to content

Commit 21a548c

Browse files
committed
First graphql_server integration :D
This commit improves the Sanic-GraphQL integration by reusing most methods from graphql-server
1 parent 7d8c6c1 commit 21a548c

File tree

3 files changed

+101
-212
lines changed

3 files changed

+101
-212
lines changed

sanic_graphql/graphqlview.py

Lines changed: 82 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
11
import json
22
import six
3+
from functools import partial
34
from cgi import parse_header
45

6+
7+
from promise import Promise
58
from sanic.response import HTTPResponse
69
from sanic.views import HTTPMethodView
710
from sanic.exceptions import SanicException
811

9-
from promise import Promise
10-
from graphql import Source, execute, parse, validate
11-
from graphql.error import format_error as format_graphql_error
12-
from graphql.error import GraphQLError
13-
from graphql.execution import ExecutionResult
1412
from graphql.type.schema import GraphQLSchema
15-
from graphql.utils.get_operation_ast import get_operation_ast
1613
from graphql.execution.executors.asyncio import AsyncioExecutor
14+
from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, encode_execution_results, json_encode
1715

1816
from .render_graphiql import render_graphiql
1917

2018

21-
class HttpError(Exception):
22-
def __init__(self, response, message=None, *args, **kwargs):
23-
self.response = response
24-
self.message = message = message or response.args[0]
25-
super(HttpError, self).__init__(message, *args, **kwargs)
26-
27-
2819
class GraphQLView(HTTPMethodView):
2920
schema = None
3021
executor = None
@@ -44,14 +35,12 @@ class GraphQLView(HTTPMethodView):
4435

4536
def __init__(self, **kwargs):
4637
super(GraphQLView, self).__init__()
47-
4838
for key, value in kwargs.items():
4939
if hasattr(self, key):
5040
setattr(self, key, value)
5141

52-
self._enable_async = self._enable_async and isinstance(kwargs.get('executor'), AsyncioExecutor)
5342

54-
assert not all((self.graphiql, self.batch)), 'Use either graphiql or batch processing'
43+
self._enable_async = self._enable_async and isinstance(kwargs.get('executor'), AsyncioExecutor)
5544
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
5645

5746
# noinspection PyUnusedLocal
@@ -70,211 +59,113 @@ def get_middleware(self, request):
7059
def get_executor(self, request):
7160
return self.executor
7261

62+
def render_graphiql(self, params, result):
63+
return render_graphiql(
64+
jinja_env=self.jinja_env,
65+
params=params,
66+
result=result,
67+
graphiql_version=self.graphiql_version,
68+
graphiql_template=self.graphiql_template,
69+
)
70+
71+
format_error = staticmethod(default_format_error)
72+
encode = staticmethod(json_encode)
73+
74+
async def await_execution_results(self, execution_results):
75+
awaited_results = []
76+
for execution_result in execution_results:
77+
if isinstance(execution_result, Promise):
78+
execution_result = await execution_result
79+
awaited_results.append(execution_result)
80+
81+
return awaited_results
82+
7383
async def dispatch_request(self, request, *args, **kwargs):
7484
try:
75-
if request.method.lower() not in ('get', 'post'):
76-
raise HttpError(SanicException('GraphQL only supports GET and POST requests.', status_code=405))
77-
85+
request_method = request.method.lower()
7886
data = self.parse_body(request)
79-
show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
8087

81-
if self.batch:
82-
responses = []
83-
for entry in data:
84-
responses.append(await self.get_response(request, entry))
88+
show_graphiql = request_method == 'get' and self.should_display_graphiql(request)
89+
catch = HttpQueryError if show_graphiql else None
90+
91+
pretty = self.pretty or show_graphiql or request.args.get('pretty')
92+
93+
execution_results, all_params = run_http_query(
94+
self.schema,
95+
request_method,
96+
data,
97+
query_data=request.args,
98+
batch_enabled=self.batch,
99+
catch=catch,
85100

86-
result = '[{}]'.format(','.join([response[0] for response in responses]))
87-
status_code = max(responses, key=lambda response: response[1])[1]
88-
else:
89-
result, status_code = await self.get_response(request, data, show_graphiql)
101+
# Execute options
102+
return_promise=self._enable_async,
103+
root_value=self.get_root_value(request),
104+
context_value=self.get_context(request),
105+
middleware=self.get_middleware(request),
106+
executor=self.get_executor(request),
107+
)
108+
awaited_execution_results = await self.await_execution_results(execution_results)
109+
result, status_code = encode_execution_results(
110+
awaited_execution_results,
111+
is_batch=isinstance(data, list),
112+
format_error=self.format_error,
113+
encode=partial(self.encode, pretty=pretty)
114+
)
90115

91116
if show_graphiql:
92-
query, variables, operation_name, id = self.get_graphql_params(request, data)
93-
return await render_graphiql(
94-
jinja_env=self.jinja_env,
95-
graphiql_version=self.graphiql_version,
96-
graphiql_template=self.graphiql_template,
97-
query=query,
98-
variables=variables,
99-
operation_name=operation_name,
117+
return await self.render_graphiql(
118+
params=all_params[0],
100119
result=result
101120
)
102121

103122
return HTTPResponse(
104-
status=status_code,
105-
body=result,
106-
content_type='application/json'
123+
result,
124+
status=status_code,
125+
content_type='application/json'
107126
)
108127

109-
except HttpError as e:
128+
except HttpQueryError as e:
110129
return HTTPResponse(
111-
self.json_encode(request, {
112-
'errors': [self.format_error(e)]
130+
self.encode({
131+
'errors': [default_format_error(e)]
113132
}),
114-
status=e.response.status_code,
115-
headers={'Allow': 'GET, POST'},
133+
status=e.status_code,
134+
headers=e.headers,
116135
content_type='application/json'
117136
)
118137

119-
async def get_response(self, request, data, show_graphiql=False):
120-
query, variables, operation_name, id = self.get_graphql_params(request, data)
121-
122-
execution_result = await self.execute_graphql_request(
123-
request,
124-
data,
125-
query,
126-
variables,
127-
operation_name,
128-
show_graphiql
1C72
129-
)
130-
131-
status_code = 200
132-
if execution_result:
133-
response = {}
134-
135-
if execution_result.errors:
136-
response['errors'] = [self.format_error(e) for e in execution_result.errors]
137-
138-
if execution_result.invalid:
139-
status_code = 400
140-
else:
141-
status_code = 200
142-
response['data'] = execution_result.data
143-
144-
if self.batch:
145-
response = {
146-
'id': id,
147-
'payload': response,
148-
'status': status_code,
149-
}
150-
151-
result = self.json_encode(request, response, show_graphiql)
152-
else:
153-
result = None
154-
155-
return result, status_code
156-
157-
def json_encode(self, request, d, show_graphiql=False):
158-
pretty = self.pretty or show_graphiql or request.args.get('pretty')
159-
if not pretty:
160-
return json.dumps(d, separators=(',', ':'))
161-
162-
return json.dumps(d, sort_keys=True,
163-
indent=2, separators=(',', ': '))
164-
165138
# noinspection PyBroadException
166139
def parse_body(self, request):
167-
content_type = self.get_content_type(request)
140+
content_type = self.get_mime_type(request)
168141
if content_type == 'application/graphql':
169-
return {'query': request.body.decode()}
142+
return {'query': request.body.decode('utf8')}
170143

171144
elif content_type == 'application/json':
172-
try:
173-
request_json = json.loads(request.body.decode('utf-8'))
174-
if (self.batch and not isinstance(request_json, list)) or (
175-
not self.batch and not isinstance(request_json, dict)):
176-
raise Exception()
177-
except:
178-
raise HttpError(SanicException('POST body sent invalid JSON.', status_code=400))
179-
return request_json
180-
181-
elif content_type == 'application/x-www-form-urlencoded':
182-
return request.form
145+
return load_json_body(request.body.decode('utf8'))
183146

184-
elif content_type == 'multipart/form-data':
147+
elif content_type == 'application/x-www-form-urlencoded' \
148+
or content_type == 'multipart/form-data':
185149
return request.form
186150

187151
return {}
188152

189-
async def execute(self, *args, **kwargs):
190-
result = execute(self.schema, return_promise=self._enable_async, *args, **kwargs)
191-
if isinstance(result, Promise):
192-
return await result
193-
else:
194-
return result
195-
196-
async def execute_graphql_request(self, request, data, query, variables, operation_name, show_graphiql=False):
197-
if not query:
198-
if show_graphiql:
199-
return None
200-
raise HttpError(SanicException('Must provide query string.', status_code=400))
201-
202-
try:
203-
source = Source(query, name='GraphQL request')
204-
ast = parse(source)
205-
validation_errors = validate(self.schema, ast)
206-
if validation_errors:
207-
return ExecutionResult(
208-
errors=validation_errors,
209-
invalid=True,
210-
)
211-
except Exception as e:
212-
return ExecutionResult(errors=[e], invalid=True)
213-
214-
if request.method.lower() == 'get':
215-
operation_ast = get_operation_ast(ast, operation_name)
216-
if operation_ast and operation_ast.operation != 'query':
217-
if show_graphiql:
218-
return None
219-
raise HttpError(SanicException(
220-
'Can only perform a {} operation from a POST request.'.format(operation_ast.operation),
221-
status_code=405,
222-
))
223-
224-
try:
225-
return await self.execute(
226-
ast,
227-
root_value=self.get_root_value(request),
228-
variable_values=variables or {},
229-
operation_name=operation_name,
230-
context_value=self.get_context(request),
231-
middleware=self.get_middleware(request),
232-
executor=self.get_executor(request)
233-
)
234-
except Exception as e:
235-
return ExecutionResult(errors=[e], invalid=True)
236-
237-
@classmethod
238-
def can_display_graphiql(cls, request, data):
239-
raw = 'raw' in request.args or 'raw' in data
240-
return not raw and cls.request_wants_html(request)
241-
242-
@classmethod
243-
def request_wants_html(cls, request):
244-
# Ugly hack
245-
accept = request.headers.get('accept', {})
246-
return 'text/html' in accept or '*/*' in accept
247-
248-
@staticmethod
249-
def get_graphql_params(request, data):
250-
query = request.args.get('query') or data.get('query')
251-
variables = request.args.get('variables') or data.get('variables')
252-
id = request.args.get('id') or data.get('id')
253-
254-
if variables and isinstance(variables, six.text_type):
255-
try:
256-
variables = json.loads(variables)
257-
except:
258-
raise HttpError(SanicException('Variables are invalid JSON.', status_code=400))
259-
260-
operation_name = request.args.get('operationName') or data.get('operationName')
261-
262-
return query, variables, operation_name, id
263-
264-
@staticmethod
265-
def format_error(error):
266-
if isinstance(error, GraphQLError):
267-
return format_graphql_error(error)
268-
269-
return {'message': six.text_type(error)}
270-
271153
@staticmethod
272-
def get_content_type(request):
154+
def get_mime_type(request):
273155
# We use mimetype here since we don't need the other
274156
# information provided by content_type
275157
if 'content-type' not in request.headers:
276-
mimetype = 'text/plain'
277-
else:
278-
mimetype, params = parse_header(request.headers['content-type'])
279-
158+
return None
159+
160+
mimetype, _ = parse_header(request.headers['content-type'])
280161
return mimetype
162+
163+
def should_display_graphiql(self, request):
164+
if not self.graphiql or 'raw' in request.args:
165+
return False
166+
167+
return self.request_wants_html(request)
168+
169+
def request_wants_html(self, request):
170+
accept = request.headers.get('accept', {})
171+
return 'text/html' in accept or '*/*' in accept

sanic_graphql/render_graphiql.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,24 @@ def simple_renderer(template, **values):
162162
return template
163163

164164

165-
async def render_graphiql(*, jinja_env=None, graphiql_version=None, graphiql_template=None, **kwargs):
165+
async def render_graphiql(jinja_env=None, graphiql_version=None, graphiql_template=None, params=None, result=None):
166166
graphiql_version = graphiql_version or GRAPHIQL_VERSION
167167
template = graphiql_template or TEMPLATE
168-
kwargs['graphiql_version'] = graphiql_version
168+
template_vars = {
169+
'graphiql_version': graphiql_version,
170+
'query': params and params.query,
171+
'variables': params and params.variables,
172+
'operation_name': params and params.operation_name,
173+
'result': result,
174+
}
169175

170176
if jinja_env:
171177
template = jinja_env.from_string(template)
172178
if jinja_env.is_async:
173-
source = await template.render_async(**kwargs)
179+
source = await template.render_async(**template_vars)
174180
else:
175-
source = template.render(**kwargs)
181+
source = template.render(**template_vars)
176182
else:
177-
source = simple_renderer(template, **kwargs)
183+
source = simple_renderer(template, **template_vars)
178184

179185
return html(source)

0 commit comments

Comments
 (0)
0