1
1
import json
2
2
import six
3
+ from functools import partial
3
4
from cgi import parse_header
4
5
6
+
7
+ from promise import Promise
5
8
from sanic .response import HTTPResponse
6
9
from sanic .views import HTTPMethodView
7
10
from sanic .exceptions import SanicException
8
11
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
14
12
from graphql .type .schema import GraphQLSchema
15
- from graphql .utils .get_operation_ast import get_operation_ast
16
13
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
17
15
18
16
from .render_graphiql import render_graphiql
19
17
20
18
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
-
28
19
class GraphQLView (HTTPMethodView ):
29
20
schema = None
30
21
executor = None
@@ -44,14 +35,12 @@ class GraphQLView(HTTPMethodView):
44
35
45
36
def __init__ (self , ** kwargs ):
46
37
super (GraphQLView , self ).__init__ ()
47
-
48
38
for key , value in kwargs .items ():
49
39
if hasattr (self , key ):
50
40
setattr (self , key , value )
51
41
52
- self ._enable_async = self ._enable_async and isinstance (kwargs .get ('executor' ), AsyncioExecutor )
53
42
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 )
55
44
assert isinstance (self .schema , GraphQLSchema ), 'A Schema is required to be provided to GraphQLView.'
56
45
57
46
# noinspection PyUnusedLocal
@@ -70,211 +59,113 @@ def get_middleware(self, request):
70
59
def get_executor (self , request ):
71
60
return self .executor
72
61
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
+
73
83
async def dispatch_request (self , request , * args , ** kwargs ):
74
84
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 ()
78
86
data = self .parse_body (request )
79
- show_graphiql = self .graphiql and self .can_display_graphiql (request , data )
80
87
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 ,
85
100
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
+ )
90
115
91
116
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 ],
100
119
result = result
101
120
)
102
121
103
122
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'
107
126
)
108
127
109
- except HttpError as e :
128
+ except HttpQueryError as e :
110
129
return HTTPResponse (
111
- self .json_encode ( request , {
112
- 'errors' : [self . format_error (e )]
130
+ self .encode ( {
131
+ 'errors' : [default_format_error (e )]
113
132
}),
114
- status = e .response . status_code ,
115
- headers = { 'Allow' : 'GET, POST' } ,
133
+ status = e .status_code ,
134
+ headers = e . headers ,
116
135
content_type = 'application/json'
117
136
)
118
137
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
-
165
138
# noinspection PyBroadException
166
139
def parse_body (self , request ):
167
- content_type = self .get_content_type (request )
140
+ content_type = self .get_mime_type (request )
168
141
if content_type == 'application/graphql' :
169
- return {'query' : request .body .decode ()}
142
+ return {'query' : request .body .decode ('utf8' )}
170
143
171
144
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' ))
183
146
184
- elif content_type == 'multipart/form-data' :
147
+ elif content_type == 'application/x-www-form-urlencoded' \
148
+ or content_type == 'multipart/form-data' :
185
149
return request .form
186
150
187
151
return {}
188
152
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
-
271
153
@staticmethod
272
- def get_content_type (request ):
154
+ def get_mime_type (request ):
273
155
# We use mimetype here since we don't need the other
274
156
# information provided by content_type
275
157
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' ])
280
161
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