1- import json
2- import six
1+ from functools import partial
32from cgi import parse_header
43
4+
5+ from promise import Promise
56from sanic .response import HTTPResponse
67from 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
149from graphql .type .schema import GraphQLSchema
15- from graphql .utils .get_operation_ast import get_operation_ast
1610from 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
1813from .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-
2816class 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
0 commit comments