Skip to content

Commit 685753a

Browse files
authored
Merge pull request #2 from graphql-python/features/graphql-server
Features/graphql server
2 parents 7d8c6c1 + d915b3f commit 685753a

5 files changed

Lines changed: 102 additions & 217 deletions

File tree

sanic_graphql/graphqlview.py

Lines changed: 70 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
1-
import json
2-
import six
1+
from functools import partial
32
from cgi import parse_header
43

4+
5+
from promise import Promise
56
from sanic.response import HTTPResponse
67
from sanic.views import HTTPMethodView
7-
from sanic.exceptions import SanicException
88

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
149
from graphql.type.schema import GraphQLSchema
15-
from graphql.utils.get_operation_ast import get_operation_ast
1610
from graphql.execution.executors.asyncio import AsyncioExecutor
11+
from graphql_server import run_http_query, HttpQueryError, default_format_error, load_json_body, encode_execution_results, json_encode
1712

1813
from .render_graphiql import render_graphiql
1914

2015

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-
2816
class GraphQLView(HTTPMethodView):
2917
schema = None
3018
executor = None
@@ -44,14 +32,11 @@ class GraphQLView(HTTPMethodView):
4432

4533
def __init__(self, **kwargs):
4634
super(GraphQLView, self).__init__()
47-
4835
for key, value in kwargs.items():
4936
if hasattr(self, key):
5037
setattr(self, key, value)
5138

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

5742
# noinspection PyUnusedLocal
@@ -70,211 +55,103 @@ def get_middleware(self, request):
7055
def get_executor(self, request):
7156
return self.executor
7257

58+
def render_graphiql(self, params, result):
59+
return render_graphiql(
60+
jinja_env=self.jinja_env,
61+
params=params,
62+
result=result,
63+
graphiql_version=self.graphiql_version,
64+
graphiql_template=self.graphiql_template,
65+
)
66+
67+
format_error = staticmethod(default_format_error)
68+
encode = staticmethod(json_encode)
69+
7370
async def dispatch_request(self, request, *args, **kwargs):
7471
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-
72+
request_method = request.method.lower()
7873
data = self.parse_body(request)
79-
show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
8074

81-
if self.batch:
82-
responses = []
83-
for entry in data:
84-
responses.append(await self.get_response(request, entry))
75+
show_graphiql = request_method == 'get' and self.should_display_graphiql(request)
76+
catch = HttpQueryError if show_graphiql else None
77+
78+
pretty = self.pretty or show_graphiql or request.args.get('pretty')
8579

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)
80+
execution_results, all_params = run_http_query(
81+
self.schema,
82+
request_method,
83+
data,
84+
query_data=request.args,
85+
batch_enabled=self.batch,
86+
catch=catch,
87+
88+
# Execute options
89+
return_promise=self._enable_async,
90+
root_value=self.get_root_value(request),
91+
context_value=self.get_context(request),
92+
middleware=self.get_middleware(request),
93+
executor=self.get_executor(request),
94+
)
95+
awaited_execution_results = await Promise.all(execution_results)
96+
result, status_code = encode_execution_results(
97+
awaited_execution_results,
98+
is_batch=isinstance(data, list),
99+
format_error=self.format_error,
100+
encode=partial(self.encode, pretty=pretty)
101+
)
90102

91103
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,
104+
return await self.render_graphiql(
105+
params=all_params[0],
100106
result=result
101107
)
102108

103109
return HTTPResponse(
104-
status=status_code,
105-
body=result,
106-
content_type='application/json'
110+
result,
111+
status=status_code,
112+
content_type='application/json'
107113
)
108114

109-
except HttpError as e:
115+
except HttpQueryError as e:
110116
return HTTPResponse(
111-
self.json_encode(request, {
112-
'errors': [self.format_error(e)]
117+
self.encode({
118+
'errors': [default_format_error(e)]
113119
}),
114-
status=e.response.status_code,
115-
headers={'Allow': 'GET, POST'},
120+
status=e.status_code,
121+
headers=e.headers,
116122
content_type='application/json'
117123
)
118124

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
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-
165125
# noinspection PyBroadException
166126
def parse_body(self, request):
167-
content_type = self.get_content_type(request)
127+
content_type = self.get_mime_type(request)
168128
if content_type == 'application/graphql':
169-
return {'query': request.body.decode()}
129+
return {'query': request.body.decode('utf8')}
170130

171131
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
132+
return load_json_body(request.body.decode('utf8'))
183133

184-
elif content_type == 'multipart/form-data':
134+
elif content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'):
185135
return request.form
186136

187137
return {}
188138

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-
264139
@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-
271-
@staticmethod
272-
def get_content_type(request):
140+
def get_mime_type(request):
273141
# We use mimetype here since we don't need the other
274142
# information provided by content_type
275143
if 'content-type' not in request.headers:
276-
mimetype = 'text/plain'
277-
else:
278-
mimetype, params = parse_header(request.headers['content-type'])
144+
return None
279145

146+
mimetype, _ = parse_header(request.headers['content-type'])
280147
return mimetype
148+
149+
def should_display_graphiql(self, request):
150+
if not self.graphiql or 'raw' in request.args:
151+
return False
152+
153+
return self.request_wants_html(request)
154+
155+
def request_wants_html(self, request):
156+
accept = request.headers.get('accept', {})
157+
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)

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from setuptools import setup, find_packages
22

3-
required_packages = ['graphql-core>=1.0', 'sanic>=0.4.0', 'pytest-runner']
3+
required_packages = [
4+
'graphql-core>=1.0',
5+
'graphql-server-core>=1.0.dev',
6+
'sanic>=0.4.0',
7+
'pytest-runner'
8+
]
49

510
setup(
611
name='Sanic-GraphQL',

0 commit comments

Comments
 (0)