11import json
22import six
3+ from functools import partial
34from cgi import parse_header
45
6+
7+ from promise import Promise
58from sanic .response import HTTPResponse
69from sanic .views import HTTPMethodView
710from 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
1412from graphql .type .schema import GraphQLSchema
15- from graphql .utils .get_operation_ast import get_operation_ast
1613from 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
1816from .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-
2819class 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
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
0 commit comments