strawberry-graphql 0.168.2__py3-none-any.whl → 0.170.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strawberry/aiohttp/handlers/__init__.py +1 -2
- strawberry/aiohttp/views.py +109 -47
- strawberry/asgi/__init__.py +118 -29
- strawberry/asgi/handlers/__init__.py +1 -2
- strawberry/chalice/views.py +81 -158
- strawberry/cli/debug_server.py +2 -1
- strawberry/django/views.py +138 -200
- strawberry/fastapi/router.py +101 -164
- strawberry/file_uploads/utils.py +2 -2
- strawberry/flask/views.py +117 -178
- strawberry/http/async_base_view.py +215 -0
- strawberry/http/base.py +63 -0
- strawberry/http/exceptions.py +4 -0
- strawberry/http/sync_base_view.py +210 -0
- strawberry/http/temporal_response.py +3 -1
- strawberry/http/types.py +13 -0
- strawberry/http/typevars.py +7 -0
- strawberry/sanic/utils.py +9 -2
- strawberry/sanic/views.py +86 -136
- strawberry/schema/name_converter.py +4 -1
- strawberry/schema/schema_converter.py +6 -1
- strawberry/starlite/controller.py +119 -177
- strawberry/types/graphql.py +5 -2
- {strawberry_graphql-0.168.2.dist-info → strawberry_graphql-0.170.0.dist-info}/METADATA +1 -1
- {strawberry_graphql-0.168.2.dist-info → strawberry_graphql-0.170.0.dist-info}/RECORD +28 -24
- strawberry/aiohttp/handlers/http_handler.py +0 -163
- strawberry/asgi/handlers/http_handler.py +0 -214
- {strawberry_graphql-0.168.2.dist-info → strawberry_graphql-0.170.0.dist-info}/LICENSE +0 -0
- {strawberry_graphql-0.168.2.dist-info → strawberry_graphql-0.170.0.dist-info}/WHEEL +0 -0
- {strawberry_graphql-0.168.2.dist-info → strawberry_graphql-0.170.0.dist-info}/entry_points.txt +0 -0
strawberry/flask/views.py
CHANGED
@@ -1,33 +1,59 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
from typing import TYPE_CHECKING, Dict
|
3
|
+
from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Union, cast
|
5
4
|
|
6
|
-
from flask import Response, render_template_string, request
|
5
|
+
from flask import Request, Response, render_template_string, request
|
6
|
+
from flask.views import View
|
7
|
+
from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
|
8
|
+
from strawberry.http.exceptions import HTTPException
|
9
|
+
from strawberry.http.sync_base_view import (
|
10
|
+
SyncBaseHTTPView,
|
11
|
+
SyncHTTPRequestAdapter,
|
12
|
+
)
|
13
|
+
from strawberry.http.types import FormData, HTTPMethod, QueryParams
|
14
|
+
from strawberry.http.typevars import Context, RootValue
|
15
|
+
from strawberry.utils.graphiql import get_graphiql_html
|
7
16
|
|
8
17
|
if TYPE_CHECKING:
|
9
18
|
from flask.typing import ResponseReturnValue
|
10
19
|
from strawberry.http import GraphQLHTTPResponse
|
11
20
|
from strawberry.schema.base import BaseSchema
|
12
|
-
from strawberry.types import ExecutionResult
|
13
21
|
|
14
|
-
from flask.views import View
|
15
|
-
from strawberry.exceptions import MissingQueryError
|
16
|
-
from strawberry.file_uploads.utils import replace_placeholders_with_files
|
17
|
-
from strawberry.flask.graphiql import should_render_graphiql
|
18
|
-
from strawberry.http import (
|
19
|
-
parse_query_params,
|
20
|
-
parse_request_data,
|
21
|
-
process_result,
|
22
|
-
)
|
23
|
-
from strawberry.schema.exceptions import InvalidOperationTypeError
|
24
|
-
from strawberry.types.graphql import OperationType
|
25
|
-
from strawberry.utils.graphiql import get_graphiql_html
|
26
22
|
|
23
|
+
class FlaskHTTPRequestAdapter(SyncHTTPRequestAdapter):
|
24
|
+
def __init__(self, request: Request):
|
25
|
+
self.request = request
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
@property
|
28
|
+
def query_params(self) -> Mapping[str, Union[str, Optional[List[str]]]]:
|
29
|
+
return self.request.args.to_dict()
|
30
|
+
|
31
|
+
@property
|
32
|
+
def body(self) -> Union[str, bytes]:
|
33
|
+
return self.request.data.decode()
|
34
|
+
|
35
|
+
@property
|
36
|
+
def method(self) -> HTTPMethod:
|
37
|
+
return cast(HTTPMethod, self.request.method.upper())
|
30
38
|
|
39
|
+
@property
|
40
|
+
def headers(self) -> Mapping[str, str]:
|
41
|
+
return self.request.headers
|
42
|
+
|
43
|
+
@property
|
44
|
+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
|
45
|
+
return self.request.form
|
46
|
+
|
47
|
+
@property
|
48
|
+
def files(self) -> Mapping[str, Any]:
|
49
|
+
return self.request.files
|
50
|
+
|
51
|
+
@property
|
52
|
+
def content_type(self) -> Optional[str]:
|
53
|
+
return self.request.content_type
|
54
|
+
|
55
|
+
|
56
|
+
class BaseGraphQLView:
|
31
57
|
def __init__(
|
32
58
|
self,
|
33
59
|
schema: BaseSchema,
|
@@ -38,187 +64,100 @@ class BaseGraphQLView(View):
|
|
38
64
|
self.graphiql = graphiql
|
39
65
|
self.allow_queries_via_get = allow_queries_via_get
|
40
66
|
|
41
|
-
def
|
42
|
-
|
67
|
+
def render_graphiql(self, request: Request) -> Response:
|
68
|
+
template = get_graphiql_html(False)
|
43
69
|
|
44
|
-
|
45
|
-
return json.dumps(response_data)
|
70
|
+
return render_template_string(template) # type: ignore
|
46
71
|
|
72
|
+
def create_response(
|
73
|
+
self, response_data: GraphQLHTTPResponse, sub_response: Response
|
74
|
+
) -> Response:
|
75
|
+
sub_response.set_data(self.encode_json(response_data)) # type: ignore
|
47
76
|
|
48
|
-
|
49
|
-
def get_root_value(self) -> object:
|
50
|
-
return None
|
77
|
+
return sub_response
|
51
78
|
|
52
|
-
def get_context(self, response: Response) -> Dict[str, object]:
|
53
|
-
return {"request": request, "response": response}
|
54
79
|
|
55
|
-
|
56
|
-
|
80
|
+
class GraphQLView(
|
81
|
+
BaseGraphQLView,
|
82
|
+
SyncBaseHTTPView[Request, Response, Response, Context, RootValue],
|
83
|
+
View,
|
84
|
+
):
|
85
|
+
methods = ["GET", "POST"]
|
86
|
+
allow_queries_via_get: bool = True
|
87
|
+
request_adapter_class = FlaskHTTPRequestAdapter
|
57
88
|
|
58
|
-
def
|
59
|
-
|
60
|
-
content_type = request.content_type or ""
|
89
|
+
def get_context(self, request: Request, response: Response) -> Context:
|
90
|
+
return {"request": request, "response": response} # type: ignore
|
61
91
|
|
62
|
-
|
63
|
-
|
64
|
-
"Unsupported method, must be of request type POST or GET", 405
|
65
|
-
)
|
92
|
+
def get_root_value(self, request: Request) -> Optional[RootValue]:
|
93
|
+
return None
|
66
94
|
|
67
|
-
|
68
|
-
|
69
|
-
data = json.loads(request.data)
|
70
|
-
except json.JSONDecodeError:
|
71
|
-
return Response(
|
72
|
-
status=400, response="Unable to parse request body as JSON"
|
73
|
-
)
|
74
|
-
elif content_type.startswith("multipart/form-data"):
|
75
|
-
try:
|
76
|
-
operations = json.loads(request.form.get("operations", "{}"))
|
77
|
-
files_map = json.loads(request.form.get("map", "{}"))
|
78
|
-
except json.JSONDecodeError:
|
79
|
-
return Response(
|
80
|
-
status=400, response="Unable to parse request body as JSON"
|
81
|
-
)
|
82
|
-
|
83
|
-
try:
|
84
|
-
data = replace_placeholders_with_files(
|
85
|
-
operations, files_map, request.files
|
86
|
-
)
|
87
|
-
except KeyError:
|
88
|
-
return Response(status=400, response="File(s) missing in form data")
|
89
|
-
elif method == "GET" and request.args:
|
90
|
-
try:
|
91
|
-
data = parse_query_params(request.args.to_dict())
|
92
|
-
except json.JSONDecodeError:
|
93
|
-
return Response(
|
94
|
-
status=400, response="Unable to parse request body as JSON"
|
95
|
-
)
|
96
|
-
elif method == "GET" and should_render_graphiql(self.graphiql, request):
|
97
|
-
template = get_graphiql_html(False)
|
98
|
-
|
99
|
-
return self.render_template(template=template)
|
100
|
-
elif method == "GET":
|
101
|
-
return Response(status=404)
|
102
|
-
else:
|
103
|
-
return Response("Unsupported Media Type", 415)
|
104
|
-
|
105
|
-
request_data = parse_request_data(data)
|
106
|
-
|
107
|
-
response = Response(status=200, content_type="application/json")
|
108
|
-
context = self.get_context(response)
|
109
|
-
|
110
|
-
allowed_operation_types = OperationType.from_http(method)
|
111
|
-
|
112
|
-
if not self.allow_queries_via_get and method == "GET":
|
113
|
-
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
95
|
+
def get_sub_response(self, request: Request) -> Response:
|
96
|
+
return Response(status=200, content_type="application/json")
|
114
97
|
|
98
|
+
def dispatch_request(self) -> ResponseReturnValue:
|
115
99
|
try:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
root_value=self.get_root_value(),
|
122
|
-
allowed_operation_types=allowed_operation_types,
|
100
|
+
return self.run(request=request)
|
101
|
+
except HTTPException as e:
|
102
|
+
return Response(
|
103
|
+
response=e.reason,
|
104
|
+
status=e.status_code,
|
123
105
|
)
|
124
|
-
except InvalidOperationTypeError as e:
|
125
|
-
return Response(e.as_http_error_reason(method), 400)
|
126
|
-
except MissingQueryError:
|
127
|
-
return Response("No GraphQL query found in the request", 400)
|
128
106
|
|
129
|
-
response_data = self.process_result(result)
|
130
|
-
response.set_data(self.encode_json(response_data))
|
131
107
|
|
132
|
-
|
108
|
+
class AsyncFlaskHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
109
|
+
def __init__(self, request: Request):
|
110
|
+
self.request = request
|
133
111
|
|
112
|
+
@property
|
113
|
+
def query_params(self) -> QueryParams:
|
114
|
+
return self.request.args.to_dict()
|
134
115
|
|
135
|
-
|
136
|
-
|
116
|
+
@property
|
117
|
+
def method(self) -> HTTPMethod:
|
118
|
+
return cast(HTTPMethod, self.request.method.upper())
|
137
119
|
|
138
|
-
|
139
|
-
|
120
|
+
@property
|
121
|
+
def content_type(self) -> Optional[str]:
|
122
|
+
return self.request.content_type
|
140
123
|
|
141
|
-
|
142
|
-
|
124
|
+
@property
|
125
|
+
def headers(self) -> Mapping[str, str]:
|
126
|
+
return self.request.headers
|
143
127
|
|
144
|
-
async def
|
145
|
-
return
|
128
|
+
async def get_body(self) -> str:
|
129
|
+
return self.request.data.decode()
|
146
130
|
|
147
|
-
async def
|
148
|
-
|
149
|
-
|
131
|
+
async def get_form_data(self) -> FormData:
|
132
|
+
return FormData(
|
133
|
+
files=self.request.files,
|
134
|
+
form=self.request.form,
|
135
|
+
)
|
150
136
|
|
151
|
-
if request.method not in {"POST", "GET"}:
|
152
|
-
return Response(
|
153
|
-
"Unsupported method, must be of request type POST or GET", 405
|
154
|
-
)
|
155
137
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
try:
|
165
|
-
operations = json.loads(request.form.get("operations", "{}"))
|
166
|
-
files_map = json.loads(request.form.get("map", "{}"))
|
167
|
-
except json.JSONDecodeError:
|
168
|
-
return Response(
|
169
|
-
status=400, response="Unable to parse request body as JSON"
|
170
|
-
)
|
171
|
-
|
172
|
-
try:
|
173
|
-
data = replace_placeholders_with_files(
|
174
|
-
operations, files_map, request.files
|
175
|
-
)
|
176
|
-
except KeyError:
|
177
|
-
return Response(status=400, response="File(s) missing in form data")
|
178
|
-
elif method == "GET" and request.args:
|
179
|
-
try:
|
180
|
-
data = parse_query_params(request.args.to_dict())
|
181
|
-
except json.JSONDecodeError:
|
182
|
-
return Response(
|
183
|
-
status=400, response="Unable to parse request body as JSON"
|
184
|
-
)
|
185
|
-
|
186
|
-
elif method == "GET" and should_render_graphiql(self.graphiql, request):
|
187
|
-
template = get_graphiql_html(False)
|
188
|
-
|
189
|
-
return self.render_template(template=template)
|
190
|
-
elif method == "GET":
|
191
|
-
return Response(status=404)
|
192
|
-
else:
|
193
|
-
return Response("Unsupported Media Type", 415)
|
194
|
-
|
195
|
-
request_data = parse_request_data(data)
|
196
|
-
|
197
|
-
response = Response(status=200, content_type="application/json")
|
198
|
-
context = await self.get_context(response)
|
199
|
-
|
200
|
-
allowed_operation_types = OperationType.from_http(method)
|
201
|
-
|
202
|
-
if not self.allow_queries_via_get and method == "GET":
|
203
|
-
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
204
|
-
|
205
|
-
root_value = await self.get_root_value()
|
138
|
+
class AsyncGraphQLView(
|
139
|
+
BaseGraphQLView,
|
140
|
+
AsyncBaseHTTPView[Request, Response, Response, Context, RootValue],
|
141
|
+
View,
|
142
|
+
):
|
143
|
+
methods = ["GET", "POST"]
|
144
|
+
allow_queries_via_get: bool = True
|
145
|
+
request_adapter_class = AsyncFlaskHTTPRequestAdapter
|
206
146
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
operation_name=request_data.operation_name,
|
213
|
-
root_value=root_value,
|
214
|
-
allowed_operation_types=allowed_operation_types,
|
215
|
-
)
|
216
|
-
except InvalidOperationTypeError as e:
|
217
|
-
return Response(e.as_http_error_reason(method), 400)
|
218
|
-
except MissingQueryError:
|
219
|
-
return Response("No GraphQL query found in the request", 400)
|
147
|
+
async def get_context(self, request: Request, response: Response) -> Context:
|
148
|
+
return {"request": request, "response": response} # type: ignore
|
149
|
+
|
150
|
+
async def get_root_value(self, request: Request) -> Optional[RootValue]:
|
151
|
+
return None
|
220
152
|
|
221
|
-
|
222
|
-
|
153
|
+
async def get_sub_response(self, request: Request) -> Response:
|
154
|
+
return Response(status=200, content_type="application/json")
|
223
155
|
|
224
|
-
|
156
|
+
async def dispatch_request(self) -> ResponseReturnValue: # type: ignore
|
157
|
+
try:
|
158
|
+
return await self.run(request=request)
|
159
|
+
except HTTPException as e:
|
160
|
+
return Response(
|
161
|
+
response=e.reason,
|
162
|
+
status=e.status_code,
|
163
|
+
)
|
@@ -0,0 +1,215 @@
|
|
1
|
+
import abc
|
2
|
+
import json
|
3
|
+
from typing import (
|
4
|
+
Callable,
|
5
|
+
Dict,
|
6
|
+
Generic,
|
7
|
+
Mapping,
|
8
|
+
Optional,
|
9
|
+
Union,
|
10
|
+
)
|
11
|
+
|
12
|
+
from strawberry import UNSET
|
13
|
+
from strawberry.exceptions import MissingQueryError
|
14
|
+
from strawberry.file_uploads.utils import replace_placeholders_with_files
|
15
|
+
from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData, process_result
|
16
|
+
from strawberry.schema.base import BaseSchema
|
17
|
+
from strawberry.schema.exceptions import InvalidOperationTypeError
|
18
|
+
from strawberry.types import ExecutionResult
|
19
|
+
from strawberry.types.graphql import OperationType
|
20
|
+
|
21
|
+
from .base import BaseView
|
22
|
+
from .exceptions import HTTPException
|
23
|
+
from .types import FormData, HTTPMethod, QueryParams
|
24
|
+
from .typevars import Context, Request, Response, RootValue, SubResponse
|
25
|
+
|
26
|
+
|
27
|
+
class AsyncHTTPRequestAdapter(abc.ABC):
|
28
|
+
@property
|
29
|
+
@abc.abstractmethod
|
30
|
+
def query_params(self) -> QueryParams:
|
31
|
+
...
|
32
|
+
|
33
|
+
@property
|
34
|
+
@abc.abstractmethod
|
35
|
+
def method(self) -> HTTPMethod:
|
36
|
+
...
|
37
|
+
|
38
|
+
@property
|
39
|
+
@abc.abstractmethod
|
40
|
+
def headers(self) -> Mapping[str, str]:
|
41
|
+
...
|
42
|
+
|
43
|
+
@property
|
44
|
+
@abc.abstractmethod
|
45
|
+
def content_type(self) -> Optional[str]:
|
46
|
+
...
|
47
|
+
|
48
|
+
@abc.abstractmethod
|
49
|
+
async def get_body(self) -> Union[str, bytes]:
|
50
|
+
...
|
51
|
+
|
52
|
+
@abc.abstractmethod
|
53
|
+
async def get_form_data(self) -> FormData:
|
54
|
+
...
|
55
|
+
|
56
|
+
|
57
|
+
class AsyncBaseHTTPView(
|
58
|
+
abc.ABC,
|
59
|
+
BaseView[Request],
|
60
|
+
Generic[Request, Response, SubResponse, Context, RootValue],
|
61
|
+
):
|
62
|
+
schema: BaseSchema
|
63
|
+
graphiql: bool
|
64
|
+
request_adapter_class: Callable[[Request], AsyncHTTPRequestAdapter]
|
65
|
+
|
66
|
+
@property
|
67
|
+
@abc.abstractmethod
|
68
|
+
def allow_queries_via_get(self) -> bool:
|
69
|
+
...
|
70
|
+
|
71
|
+
@abc.abstractmethod
|
72
|
+
async def get_sub_response(self, request: Request) -> SubResponse:
|
73
|
+
...
|
74
|
+
|
75
|
+
@abc.abstractmethod
|
76
|
+
async def get_context(self, request: Request, response: SubResponse) -> Context:
|
77
|
+
...
|
78
|
+
|
79
|
+
@abc.abstractmethod
|
80
|
+
async def get_root_value(self, request: Request) -> Optional[RootValue]:
|
81
|
+
...
|
82
|
+
|
83
|
+
@abc.abstractmethod
|
84
|
+
def render_graphiql(self, request: Request) -> Response:
|
85
|
+
# TODO: this could be non abstract
|
86
|
+
# maybe add a get template function?
|
87
|
+
...
|
88
|
+
|
89
|
+
@abc.abstractmethod
|
90
|
+
def create_response(
|
91
|
+
self, response_data: GraphQLHTTPResponse, sub_response: SubResponse
|
92
|
+
) -> Response:
|
93
|
+
...
|
94
|
+
|
95
|
+
async def execute_operation(
|
96
|
+
self, request: Request, context: Context, root_value: Optional[RootValue]
|
97
|
+
) -> ExecutionResult:
|
98
|
+
request_adapter = self.request_adapter_class(request)
|
99
|
+
|
100
|
+
try:
|
101
|
+
request_data = await self.parse_http_body(request_adapter)
|
102
|
+
except json.decoder.JSONDecodeError as e:
|
103
|
+
raise HTTPException(400, "Unable to parse request body as JSON") from e
|
104
|
+
# DO this only when doing files
|
105
|
+
except KeyError as e:
|
106
|
+
raise HTTPException(400, "File(s) missing in form data") from e
|
107
|
+
|
108
|
+
allowed_operation_types = OperationType.from_http(request_adapter.method)
|
109
|
+
|
110
|
+
if not self.allow_queries_via_get and request_adapter.method == "GET":
|
111
|
+
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
112
|
+
|
113
|
+
assert self.schema
|
114
|
+
|
115
|
+
return await self.schema.execute(
|
116
|
+
request_data.query,
|
117
|
+
root_value=root_value,
|
118
|
+
variable_values=request_data.variables,
|
119
|
+
context_value=context,
|
120
|
+
operation_name=request_data.operation_name,
|
121
|
+
allowed_operation_types=allowed_operation_types,
|
122
|
+
)
|
123
|
+
|
124
|
+
async def parse_multipart(self, request: AsyncHTTPRequestAdapter) -> Dict[str, str]:
|
125
|
+
try:
|
126
|
+
form_data = await request.get_form_data()
|
127
|
+
except ValueError as e:
|
128
|
+
raise HTTPException(400, "Unable to parse the multipart body") from e
|
129
|
+
|
130
|
+
operations = form_data["form"].get("operations", "{}")
|
131
|
+
files_map = form_data["form"].get("map", "{}")
|
132
|
+
|
133
|
+
if isinstance(operations, (bytes, str)):
|
134
|
+
operations = self.parse_json(operations)
|
135
|
+
|
136
|
+
if isinstance(files_map, (bytes, str)):
|
137
|
+
files_map = self.parse_json(files_map)
|
138
|
+
|
139
|
+
try:
|
140
|
+
return replace_placeholders_with_files(
|
141
|
+
operations, files_map, form_data["files"]
|
142
|
+
)
|
143
|
+
except KeyError as e:
|
144
|
+
raise HTTPException(400, "File(s) missing in form data") from e
|
145
|
+
|
146
|
+
async def run(
|
147
|
+
self,
|
148
|
+
request: Request,
|
149
|
+
context: Optional[Context] = UNSET,
|
150
|
+
root_value: Optional[RootValue] = UNSET,
|
151
|
+
) -> Response:
|
152
|
+
request_adapter = self.request_adapter_class(request)
|
153
|
+
|
154
|
+
if not self.is_request_allowed(request_adapter):
|
155
|
+
raise HTTPException(405, "GraphQL only supports GET and POST requests.")
|
156
|
+
|
157
|
+
if self.should_render_graphiql(request_adapter):
|
158
|
+
if self.graphiql:
|
159
|
+
return self.render_graphiql(request)
|
160
|
+
else:
|
161
|
+
raise HTTPException(404, "Not Found")
|
162
|
+
|
163
|
+
sub_response = await self.get_sub_response(request)
|
164
|
+
context = (
|
165
|
+
await self.get_context(request, response=sub_response)
|
166
|
+
if context is UNSET
|
167
|
+
else context
|
168
|
+
)
|
169
|
+
root_value = (
|
170
|
+
await self.get_root_value(request) if root_value is UNSET else root_value
|
171
|
+
)
|
172
|
+
|
173
|
+
assert context
|
174
|
+
|
175
|
+
try:
|
176
|
+
result = await self.execute_operation(
|
177
|
+
request=request, context=context, root_value=root_value
|
178
|
+
)
|
179
|
+
except InvalidOperationTypeError as e:
|
180
|
+
raise HTTPException(
|
181
|
+
400, e.as_http_error_reason(request_adapter.method)
|
182
|
+
) from e
|
183
|
+
except MissingQueryError as e:
|
184
|
+
raise HTTPException(400, "No GraphQL query found in the request") from e
|
185
|
+
|
186
|
+
response_data = await self.process_result(request=request, result=result)
|
187
|
+
|
188
|
+
return self.create_response(
|
189
|
+
response_data=response_data, sub_response=sub_response
|
190
|
+
)
|
191
|
+
|
192
|
+
async def parse_http_body(
|
193
|
+
self, request: AsyncHTTPRequestAdapter
|
194
|
+
) -> GraphQLRequestData:
|
195
|
+
content_type = request.content_type or ""
|
196
|
+
|
197
|
+
if "application/json" in content_type:
|
198
|
+
data = self.parse_json(await request.get_body())
|
199
|
+
elif content_type.startswith("multipart/form-data"):
|
200
|
+
data = await self.parse_multipart(request)
|
201
|
+
elif request.method == "GET":
|
202
|
+
data = self.parse_query_params(request.query_params)
|
203
|
+
else:
|
204
|
+
raise HTTPException(400, "Unsupported content type")
|
205
|
+
|
206
|
+
return GraphQLRequestData(
|
207
|
+
query=data.get("query"),
|
208
|
+
variables=data.get("variables"), # type: ignore
|
209
|
+
operation_name=data.get("operationName"),
|
210
|
+
)
|
211
|
+
|
212
|
+
async def process_result(
|
213
|
+
self, request: Request, result: ExecutionResult
|
214
|
+
) -> GraphQLHTTPResponse:
|
215
|
+
return process_result(result)
|
strawberry/http/base.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Any, Dict, Generic, List, Mapping, Optional, Union
|
3
|
+
from typing_extensions import Protocol
|
4
|
+
|
5
|
+
from strawberry.http import GraphQLHTTPResponse
|
6
|
+
from strawberry.http.types import HTTPMethod
|
7
|
+
|
8
|
+
from .exceptions import HTTPException
|
9
|
+
from .typevars import Request
|
10
|
+
|
11
|
+
|
12
|
+
class BaseRequestProtocol(Protocol):
|
13
|
+
@property
|
14
|
+
def query_params(self) -> Mapping[str, Optional[Union[str, List[str]]]]:
|
15
|
+
...
|
16
|
+
|
17
|
+
@property
|
18
|
+
def method(self) -> HTTPMethod:
|
19
|
+
...
|
20
|
+
|
21
|
+
@property
|
22
|
+
def headers(self) -> Mapping[str, str]:
|
23
|
+
...
|
24
|
+
|
25
|
+
|
26
|
+
class BaseView(Generic[Request]):
|
27
|
+
def should_render_graphiql(self, request: BaseRequestProtocol) -> bool:
|
28
|
+
return (
|
29
|
+
request.method == "GET"
|
30
|
+
and request.query_params.get("query") is None
|
31
|
+
and any(
|
32
|
+
supported_header in request.headers.get("accept", "")
|
33
|
+
for supported_header in ("text/html", "*/*")
|
34
|
+
)
|
35
|
+
)
|
36
|
+
|
37
|
+
def is_request_allowed(self, request: BaseRequestProtocol) -> bool:
|
38
|
+
return request.method in ("GET", "POST")
|
39
|
+
|
40
|
+
def parse_json(self, data: Union[str, bytes]) -> Dict[str, str]:
|
41
|
+
try:
|
42
|
+
return json.loads(data)
|
43
|
+
except json.JSONDecodeError as e:
|
44
|
+
raise HTTPException(400, "Unable to parse request body as JSON") from e
|
45
|
+
|
46
|
+
def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
|
47
|
+
return json.dumps(response_data)
|
48
|
+
|
49
|
+
def parse_query_params(
|
50
|
+
self, params: Mapping[str, Optional[Union[str, List[str]]]]
|
51
|
+
) -> Dict[str, Any]:
|
52
|
+
params = dict(params)
|
53
|
+
|
54
|
+
if "variables" in params:
|
55
|
+
variables = params["variables"]
|
56
|
+
|
57
|
+
if isinstance(variables, list):
|
58
|
+
variables = variables[0]
|
59
|
+
|
60
|
+
if variables:
|
61
|
+
params["variables"] = json.loads(variables)
|
62
|
+
|
63
|
+
return params
|