strawberry-graphql 0.277.1__py3-none-any.whl → 0.278.1__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/views.py +5 -49
- strawberry/asgi/__init__.py +5 -38
- strawberry/chalice/views.py +7 -40
- strawberry/channels/handlers/http_handler.py +34 -14
- strawberry/channels/handlers/ws_handler.py +3 -1
- strawberry/django/views.py +7 -74
- strawberry/fastapi/router.py +11 -5
- strawberry/flask/views.py +7 -75
- strawberry/http/async_base_view.py +113 -62
- strawberry/http/base.py +19 -1
- strawberry/http/exceptions.py +5 -7
- strawberry/http/sync_base_view.py +110 -62
- strawberry/litestar/controller.py +4 -40
- strawberry/quart/views.py +6 -34
- strawberry/sanic/views.py +8 -44
- strawberry/schema/config.py +6 -1
- {strawberry_graphql-0.277.1.dist-info → strawberry_graphql-0.278.1.dist-info}/METADATA +2 -1
- {strawberry_graphql-0.277.1.dist-info → strawberry_graphql-0.278.1.dist-info}/RECORD +21 -21
- {strawberry_graphql-0.277.1.dist-info → strawberry_graphql-0.278.1.dist-info}/LICENSE +0 -0
- {strawberry_graphql-0.277.1.dist-info → strawberry_graphql-0.278.1.dist-info}/WHEEL +0 -0
- {strawberry_graphql-0.277.1.dist-info → strawberry_graphql-0.278.1.dist-info}/entry_points.txt +0 -0
@@ -8,14 +8,16 @@ from typing import (
|
|
8
8
|
Any,
|
9
9
|
Callable,
|
10
10
|
Generic,
|
11
|
+
Literal,
|
11
12
|
Optional,
|
12
13
|
Union,
|
13
14
|
cast,
|
14
15
|
overload,
|
15
16
|
)
|
16
|
-
from typing_extensions import
|
17
|
+
from typing_extensions import TypeGuard
|
17
18
|
|
18
19
|
from graphql import GraphQLError
|
20
|
+
from lia import AsyncHTTPRequestAdapter, HTTPException
|
19
21
|
|
20
22
|
from strawberry.exceptions import MissingQueryError
|
21
23
|
from strawberry.file_uploads.utils import replace_placeholders_with_files
|
@@ -43,9 +45,7 @@ from strawberry.types.graphql import OperationType
|
|
43
45
|
from strawberry.types.unset import UNSET, UnsetType
|
44
46
|
|
45
47
|
from .base import BaseView
|
46
|
-
from .exceptions import HTTPException
|
47
48
|
from .parse_content_type import parse_content_type
|
48
|
-
from .types import FormData, HTTPMethod, QueryParams
|
49
49
|
from .typevars import (
|
50
50
|
Context,
|
51
51
|
Request,
|
@@ -57,30 +57,6 @@ from .typevars import (
|
|
57
57
|
)
|
58
58
|
|
59
59
|
|
60
|
-
class AsyncHTTPRequestAdapter(abc.ABC):
|
61
|
-
@property
|
62
|
-
@abc.abstractmethod
|
63
|
-
def query_params(self) -> QueryParams: ...
|
64
|
-
|
65
|
-
@property
|
66
|
-
@abc.abstractmethod
|
67
|
-
def method(self) -> HTTPMethod: ...
|
68
|
-
|
69
|
-
@property
|
70
|
-
@abc.abstractmethod
|
71
|
-
def headers(self) -> Mapping[str, str]: ...
|
72
|
-
|
73
|
-
@property
|
74
|
-
@abc.abstractmethod
|
75
|
-
def content_type(self) -> Optional[str]: ...
|
76
|
-
|
77
|
-
@abc.abstractmethod
|
78
|
-
async def get_body(self) -> Union[str, bytes]: ...
|
79
|
-
|
80
|
-
@abc.abstractmethod
|
81
|
-
async def get_form_data(self) -> FormData: ...
|
82
|
-
|
83
|
-
|
84
60
|
class AsyncWebSocketAdapter(abc.ABC):
|
85
61
|
def __init__(self, view: "AsyncBaseHTTPView") -> None:
|
86
62
|
self.view = view
|
@@ -153,7 +129,9 @@ class AsyncBaseHTTPView(
|
|
153
129
|
|
154
130
|
@abc.abstractmethod
|
155
131
|
def create_response(
|
156
|
-
self,
|
132
|
+
self,
|
133
|
+
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
|
134
|
+
sub_response: SubResponse,
|
157
135
|
) -> Response: ...
|
158
136
|
|
159
137
|
@abc.abstractmethod
|
@@ -184,8 +162,12 @@ class AsyncBaseHTTPView(
|
|
184
162
|
) -> WebSocketResponse: ...
|
185
163
|
|
186
164
|
async def execute_operation(
|
187
|
-
self,
|
188
|
-
|
165
|
+
self,
|
166
|
+
request: Request,
|
167
|
+
context: Context,
|
168
|
+
root_value: Optional[RootValue],
|
169
|
+
sub_response: SubResponse,
|
170
|
+
) -> Union[ExecutionResult, list[ExecutionResult], SubscriptionExecutionResult]:
|
189
171
|
request_adapter = self.request_adapter_class(request)
|
190
172
|
|
191
173
|
try:
|
@@ -201,6 +183,22 @@ class AsyncBaseHTTPView(
|
|
201
183
|
if not self.allow_queries_via_get and request_adapter.method == "GET":
|
202
184
|
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
203
185
|
|
186
|
+
if isinstance(request_data, list):
|
187
|
+
# batch GraphQL requests
|
188
|
+
return await asyncio.gather(
|
189
|
+
*[
|
190
|
+
self.execute_single(
|
191
|
+
request=request,
|
192
|
+
request_adapter=request_adapter,
|
193
|
+
sub_response=sub_response,
|
194
|
+
context=context,
|
195
|
+
root_value=root_value,
|
196
|
+
request_data=data,
|
197
|
+
)
|
198
|
+
for data in request_data
|
199
|
+
]
|
200
|
+
)
|
201
|
+
|
204
202
|
if request_data.protocol == "multipart-subscription":
|
205
203
|
return await self.schema.subscribe(
|
206
204
|
request_data.query, # type: ignore
|
@@ -211,24 +209,59 @@ class AsyncBaseHTTPView(
|
|
211
209
|
operation_extensions=request_data.extensions,
|
212
210
|
)
|
213
211
|
|
214
|
-
return await self.
|
215
|
-
|
212
|
+
return await self.execute_single(
|
213
|
+
request=request,
|
214
|
+
request_adapter=request_adapter,
|
215
|
+
sub_response=sub_response,
|
216
|
+
context=context,
|
216
217
|
root_value=root_value,
|
217
|
-
|
218
|
-
context_value=context,
|
219
|
-
operation_name=request_data.operation_name,
|
220
|
-
allowed_operation_types=allowed_operation_types,
|
221
|
-
operation_extensions=request_data.extensions,
|
218
|
+
request_data=request_data,
|
222
219
|
)
|
223
220
|
|
221
|
+
async def execute_single(
|
222
|
+
self,
|
223
|
+
request: Request,
|
224
|
+
request_adapter: AsyncHTTPRequestAdapter,
|
225
|
+
sub_response: SubResponse,
|
226
|
+
context: Context,
|
227
|
+
root_value: Optional[RootValue],
|
228
|
+
request_data: GraphQLRequestData,
|
229
|
+
) -> ExecutionResult:
|
230
|
+
allowed_operation_types = OperationType.from_http(request_adapter.method)
|
231
|
+
|
232
|
+
if not self.allow_queries_via_get and request_adapter.method == "GET":
|
233
|
+
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
234
|
+
|
235
|
+
try:
|
236
|
+
result = await self.schema.execute(
|
237
|
+
request_data.query,
|
238
|
+
root_value=root_value,
|
239
|
+
variable_values=request_data.variables,
|
240
|
+
context_value=context,
|
241
|
+
operation_name=request_data.operation_name,
|
242
|
+
allowed_operation_types=allowed_operation_types,
|
243
|
+
operation_extensions=request_data.extensions,
|
244
|
+
)
|
245
|
+
except CannotGetOperationTypeError as e:
|
246
|
+
raise HTTPException(400, e.as_http_error_reason()) from e
|
247
|
+
except InvalidOperationTypeError as e:
|
248
|
+
raise HTTPException(
|
249
|
+
400, e.as_http_error_reason(request_adapter.method)
|
250
|
+
) from e
|
251
|
+
except MissingQueryError as e:
|
252
|
+
raise HTTPException(400, "No GraphQL query found in the request") from e
|
253
|
+
|
254
|
+
return result
|
255
|
+
|
224
256
|
async def parse_multipart(self, request: AsyncHTTPRequestAdapter) -> dict[str, str]:
|
225
257
|
try:
|
226
258
|
form_data = await request.get_form_data()
|
227
259
|
except ValueError as e:
|
228
260
|
raise HTTPException(400, "Unable to parse the multipart body") from e
|
229
261
|
|
230
|
-
operations = form_data
|
231
|
-
files_map = form_data
|
262
|
+
operations = form_data.form.get("operations", "{}")
|
263
|
+
files_map = form_data.form.get("map", "{}")
|
264
|
+
files = form_data.files
|
232
265
|
|
233
266
|
if isinstance(operations, (bytes, str)):
|
234
267
|
operations = self.parse_json(operations)
|
@@ -237,9 +270,7 @@ class AsyncBaseHTTPView(
|
|
237
270
|
files_map = self.parse_json(files_map)
|
238
271
|
|
239
272
|
try:
|
240
|
-
return replace_placeholders_with_files(
|
241
|
-
operations, files_map, form_data["files"]
|
242
|
-
)
|
273
|
+
return replace_placeholders_with_files(operations, files_map, files)
|
243
274
|
except KeyError as e:
|
244
275
|
raise HTTPException(400, "File(s) missing in form data") from e
|
245
276
|
|
@@ -330,18 +361,12 @@ class AsyncBaseHTTPView(
|
|
330
361
|
return await self.render_graphql_ide(request)
|
331
362
|
raise HTTPException(404, "Not Found")
|
332
363
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
except InvalidOperationTypeError as e:
|
340
|
-
raise HTTPException(
|
341
|
-
400, e.as_http_error_reason(request_adapter.method)
|
342
|
-
) from e
|
343
|
-
except MissingQueryError as e:
|
344
|
-
raise HTTPException(400, "No GraphQL query found in the request") from e
|
364
|
+
result = await self.execute_operation(
|
365
|
+
request=request,
|
366
|
+
context=context,
|
367
|
+
root_value=root_value,
|
368
|
+
sub_response=sub_response,
|
369
|
+
)
|
345
370
|
|
346
371
|
if isinstance(result, SubscriptionExecutionResult):
|
347
372
|
stream = self._get_stream(request, result)
|
@@ -425,10 +450,22 @@ class AsyncBaseHTTPView(
|
|
425
450
|
},
|
426
451
|
)
|
427
452
|
|
428
|
-
response_data
|
453
|
+
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]]
|
454
|
+
|
455
|
+
if isinstance(result, list):
|
456
|
+
response_data = []
|
457
|
+
for execution_result in result:
|
458
|
+
processed_result = await self.process_result(
|
459
|
+
request=request, result=execution_result
|
460
|
+
)
|
461
|
+
if execution_result.errors:
|
462
|
+
self._handle_errors(execution_result.errors, processed_result)
|
463
|
+
response_data.append(processed_result)
|
464
|
+
else:
|
465
|
+
response_data = await self.process_result(request=request, result=result)
|
429
466
|
|
430
|
-
|
431
|
-
|
467
|
+
if result.errors:
|
468
|
+
self._handle_errors(result.errors, response_data)
|
432
469
|
|
433
470
|
return self.create_response(
|
434
471
|
response_data=response_data, sub_response=sub_response
|
@@ -584,15 +621,16 @@ class AsyncBaseHTTPView(
|
|
584
621
|
|
585
622
|
async def parse_http_body(
|
586
623
|
self, request: AsyncHTTPRequestAdapter
|
587
|
-
) -> GraphQLRequestData:
|
624
|
+
) -> Union[GraphQLRequestData, list[GraphQLRequestData]]:
|
588
625
|
headers = {key.lower(): value for key, value in request.headers.items()}
|
589
626
|
content_type, _ = parse_content_type(request.content_type or "")
|
590
627
|
accept = headers.get("accept", "")
|
591
628
|
|
592
|
-
protocol: Literal["http", "multipart-subscription"] =
|
593
|
-
|
594
|
-
|
595
|
-
|
629
|
+
protocol: Literal["http", "multipart-subscription"] = (
|
630
|
+
"multipart-subscription"
|
631
|
+
if self._is_multipart_subscriptions(*parse_content_type(accept))
|
632
|
+
else "http"
|
633
|
+
)
|
596
634
|
|
597
635
|
if request.method == "GET":
|
598
636
|
data = self.parse_query_params(request.query_params)
|
@@ -603,6 +641,19 @@ class AsyncBaseHTTPView(
|
|
603
641
|
else:
|
604
642
|
raise HTTPException(400, "Unsupported content type")
|
605
643
|
|
644
|
+
if isinstance(data, list):
|
645
|
+
self._validate_batch_request(data, protocol=protocol)
|
646
|
+
return [
|
647
|
+
GraphQLRequestData(
|
648
|
+
query=item.get("query"),
|
649
|
+
variables=item.get("variables"),
|
650
|
+
operation_name=item.get("operationName"),
|
651
|
+
extensions=item.get("extensions"),
|
652
|
+
protocol=protocol,
|
653
|
+
)
|
654
|
+
for item in data
|
655
|
+
]
|
656
|
+
|
606
657
|
query = data.get("query")
|
607
658
|
if not isinstance(query, (str, type(None))):
|
608
659
|
raise HTTPException(
|
strawberry/http/base.py
CHANGED
@@ -3,10 +3,13 @@ from collections.abc import Mapping
|
|
3
3
|
from typing import Any, Generic, Optional, Union
|
4
4
|
from typing_extensions import Protocol
|
5
5
|
|
6
|
+
from lia import HTTPException
|
7
|
+
|
8
|
+
from strawberry.http import GraphQLRequestData
|
6
9
|
from strawberry.http.ides import GraphQL_IDE, get_graphql_ide_html
|
7
10
|
from strawberry.http.types import HTTPMethod, QueryParams
|
11
|
+
from strawberry.schema.base import BaseSchema
|
8
12
|
|
9
|
-
from .exceptions import HTTPException
|
10
13
|
from .typevars import Request
|
11
14
|
|
12
15
|
|
@@ -24,6 +27,7 @@ class BaseRequestProtocol(Protocol):
|
|
24
27
|
class BaseView(Generic[Request]):
|
25
28
|
graphql_ide: Optional[GraphQL_IDE]
|
26
29
|
multipart_uploads_enabled: bool = False
|
30
|
+
schema: BaseSchema
|
27
31
|
|
28
32
|
def should_render_graphql_ide(self, request: BaseRequestProtocol) -> bool:
|
29
33
|
return (
|
@@ -82,5 +86,19 @@ class BaseView(Generic[Request]):
|
|
82
86
|
|
83
87
|
return params.get("subscriptionspec", "").startswith("1.0")
|
84
88
|
|
89
|
+
def _validate_batch_request(
|
90
|
+
self, request_data: list[GraphQLRequestData], protocol: str
|
91
|
+
) -> None:
|
92
|
+
if self.schema.config.batching_config is None:
|
93
|
+
raise HTTPException(400, "Batching is not enabled")
|
94
|
+
|
95
|
+
if protocol == "multipart-subscription":
|
96
|
+
raise HTTPException(
|
97
|
+
400, "Batching is not supported for multipart subscriptions"
|
98
|
+
)
|
99
|
+
|
100
|
+
if len(request_data) > self.schema.config.batching_config["max_operations"]:
|
101
|
+
raise HTTPException(400, "Too many operations")
|
102
|
+
|
85
103
|
|
86
104
|
__all__ = ["BaseView"]
|
strawberry/http/exceptions.py
CHANGED
@@ -1,9 +1,3 @@
|
|
1
|
-
class HTTPException(Exception):
|
2
|
-
def __init__(self, status_code: int, reason: str) -> None:
|
3
|
-
self.status_code = status_code
|
4
|
-
self.reason = reason
|
5
|
-
|
6
|
-
|
7
1
|
class NonTextMessageReceived(Exception):
|
8
2
|
pass
|
9
3
|
|
@@ -16,4 +10,8 @@ class WebSocketDisconnected(Exception):
|
|
16
10
|
pass
|
17
11
|
|
18
12
|
|
19
|
-
__all__ = [
|
13
|
+
__all__ = [
|
14
|
+
"NonJsonMessageReceived",
|
15
|
+
"NonTextMessageReceived",
|
16
|
+
"WebSocketDisconnected",
|
17
|
+
]
|
@@ -1,15 +1,15 @@
|
|
1
1
|
import abc
|
2
2
|
import json
|
3
|
-
from collections.abc import Mapping
|
4
3
|
from typing import (
|
5
|
-
Any,
|
6
4
|
Callable,
|
7
5
|
Generic,
|
6
|
+
Literal,
|
8
7
|
Optional,
|
9
8
|
Union,
|
10
9
|
)
|
11
10
|
|
12
11
|
from graphql import GraphQLError
|
12
|
+
from lia import HTTPException, SyncHTTPRequestAdapter
|
13
13
|
|
14
14
|
from strawberry.exceptions import MissingQueryError
|
15
15
|
from strawberry.file_uploads.utils import replace_placeholders_with_files
|
@@ -29,42 +29,10 @@ from strawberry.types.graphql import OperationType
|
|
29
29
|
from strawberry.types.unset import UNSET
|
30
30
|
|
31
31
|
from .base import BaseView
|
32
|
-
from .exceptions import HTTPException
|
33
32
|
from .parse_content_type import parse_content_type
|
34
|
-
from .types import HTTPMethod, QueryParams
|
35
33
|
from .typevars import Context, Request, Response, RootValue, SubResponse
|
36
34
|
|
37
35
|
|
38
|
-
class SyncHTTPRequestAdapter(abc.ABC):
|
39
|
-
@property
|
40
|
-
@abc.abstractmethod
|
41
|
-
def query_params(self) -> QueryParams: ...
|
42
|
-
|
43
|
-
@property
|
44
|
-
@abc.abstractmethod
|
45
|
-
def body(self) -> Union[str, bytes]: ...
|
46
|
-
|
47
|
-
@property
|
48
|
-
@abc.abstractmethod
|
49
|
-
def method(self) -> HTTPMethod: ...
|
50
|
-
|
51
|
-
@property
|
52
|
-
@abc.abstractmethod
|
53
|
-
def headers(self) -> Mapping[str, str]: ...
|
54
|
-
|
55
|
-
@property
|
56
|
-
@abc.abstractmethod
|
57
|
-
def content_type(self) -> Optional[str]: ...
|
58
|
-
|
59
|
-
@property
|
60
|
-
@abc.abstractmethod
|
61
|
-
def post_data(self) -> Mapping[str, Union[str, bytes]]: ...
|
62
|
-
|
63
|
-
@property
|
64
|
-
@abc.abstractmethod
|
65
|
-
def files(self) -> Mapping[str, Any]: ...
|
66
|
-
|
67
|
-
|
68
36
|
class SyncBaseHTTPView(
|
69
37
|
abc.ABC,
|
70
38
|
BaseView[Request],
|
@@ -92,15 +60,21 @@ class SyncBaseHTTPView(
|
|
92
60
|
|
93
61
|
@abc.abstractmethod
|
94
62
|
def create_response(
|
95
|
-
self,
|
63
|
+
self,
|
64
|
+
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
|
65
|
+
sub_response: SubResponse,
|
96
66
|
) -> Response: ...
|
97
67
|
|
98
68
|
@abc.abstractmethod
|
99
69
|
def render_graphql_ide(self, request: Request) -> Response: ...
|
100
70
|
|
101
71
|
def execute_operation(
|
102
|
-
self,
|
103
|
-
|
72
|
+
self,
|
73
|
+
request: Request,
|
74
|
+
context: Context,
|
75
|
+
root_value: Optional[RootValue],
|
76
|
+
sub_response: SubResponse,
|
77
|
+
) -> Union[ExecutionResult, list[ExecutionResult]]:
|
104
78
|
request_adapter = self.request_adapter_class(request)
|
105
79
|
|
106
80
|
try:
|
@@ -116,16 +90,64 @@ class SyncBaseHTTPView(
|
|
116
90
|
if not self.allow_queries_via_get and request_adapter.method == "GET":
|
117
91
|
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
118
92
|
|
119
|
-
|
120
|
-
|
93
|
+
if isinstance(request_data, list):
|
94
|
+
# batch GraphQL requests
|
95
|
+
return [
|
96
|
+
self.execute_single(
|
97
|
+
request=request,
|
98
|
+
request_adapter=request_adapter,
|
99
|
+
sub_response=sub_response,
|
100
|
+
context=context,
|
101
|
+
root_value=root_value,
|
102
|
+
request_data=data,
|
103
|
+
)
|
104
|
+
for data in request_data
|
105
|
+
]
|
106
|
+
|
107
|
+
return self.execute_single(
|
108
|
+
request=request,
|
109
|
+
request_adapter=request_adapter,
|
110
|
+
sub_response=sub_response,
|
111
|
+
context=context,
|
121
112
|
root_value=root_value,
|
122
|
-
|
123
|
-
context_value=context,
|
124
|
-
operation_name=request_data.operation_name,
|
125
|
-
allowed_operation_types=allowed_operation_types,
|
126
|
-
operation_extensions=request_data.extensions,
|
113
|
+
request_data=request_data,
|
127
114
|
)
|
128
115
|
|
116
|
+
def execute_single(
|
117
|
+
self,
|
118
|
+
request: Request,
|
119
|
+
request_adapter: SyncHTTPRequestAdapter,
|
120
|
+
sub_response: SubResponse,
|
121
|
+
context: Context,
|
122
|
+
root_value: Optional[RootValue],
|
123
|
+
request_data: GraphQLRequestData,
|
124
|
+
) -> ExecutionResult:
|
125
|
+
allowed_operation_types = OperationType.from_http(request_adapter.method)
|
126
|
+
|
127
|
+
if not self.allow_queries_via_get and request_adapter.method == "GET":
|
128
|
+
allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
|
129
|
+
|
130
|
+
try:
|
131
|
+
result = self.schema.execute_sync(
|
132
|
+
request_data.query,
|
133
|
+
root_value=root_value,
|
134
|
+
variable_values=request_data.variables,
|
135
|
+
context_value=context,
|
136
|
+
operation_name=request_data.operation_name,
|
137
|
+
allowed_operation_types=allowed_operation_types,
|
138
|
+
operation_extensions=request_data.extensions,
|
139
|
+
)
|
140
|
+
except CannotGetOperationTypeError as e:
|
141
|
+
raise HTTPException(400, e.as_http_error_reason()) from e
|
142
|
+
except InvalidOperationTypeError as e:
|
143
|
+
raise HTTPException(
|
144
|
+
400, e.as_http_error_reason(request_adapter.method)
|
145
|
+
) from e
|
146
|
+
except MissingQueryError as e:
|
147
|
+
raise HTTPException(400, "No GraphQL query found in the request") from e
|
148
|
+
|
149
|
+
return result
|
150
|
+
|
129
151
|
def parse_multipart(self, request: SyncHTTPRequestAdapter) -> dict[str, str]:
|
130
152
|
operations = self.parse_json(request.post_data.get("operations", "{}"))
|
131
153
|
files_map = self.parse_json(request.post_data.get("map", "{}"))
|
@@ -135,8 +157,18 @@ class SyncBaseHTTPView(
|
|
135
157
|
except KeyError as e:
|
136
158
|
raise HTTPException(400, "File(s) missing in form data") from e
|
137
159
|
|
138
|
-
def parse_http_body(
|
160
|
+
def parse_http_body(
|
161
|
+
self, request: SyncHTTPRequestAdapter
|
162
|
+
) -> Union[GraphQLRequestData, list[GraphQLRequestData]]:
|
163
|
+
headers = {key.lower(): value for key, value in request.headers.items()}
|
139
164
|
content_type, params = parse_content_type(request.content_type or "")
|
165
|
+
accept = headers.get("accept", "")
|
166
|
+
|
167
|
+
protocol: Literal["http", "multipart-subscription"] = (
|
168
|
+
"multipart-subscription"
|
169
|
+
if self._is_multipart_subscriptions(*parse_content_type(accept))
|
170
|
+
else "http"
|
171
|
+
)
|
140
172
|
|
141
173
|
if request.method == "GET":
|
142
174
|
data = self.parse_query_params(request.query_params)
|
@@ -152,6 +184,18 @@ class SyncBaseHTTPView(
|
|
152
184
|
else:
|
153
185
|
raise HTTPException(400, "Unsupported content type")
|
154
186
|
|
187
|
+
if isinstance(data, list):
|
188
|
+
self._validate_batch_request(data, protocol=protocol)
|
189
|
+
return [
|
190
|
+
GraphQLRequestData(
|
191
|
+
query=item.get("query"),
|
192
|
+
variables=item.get("variables"),
|
193
|
+
operation_name=item.get("operationName"),
|
194
|
+
extensions=item.get("extensions"),
|
195
|
+
)
|
196
|
+
for item in data
|
197
|
+
]
|
198
|
+
|
155
199
|
query = data.get("query")
|
156
200
|
if not isinstance(query, (str, type(None))):
|
157
201
|
raise HTTPException(
|
@@ -209,25 +253,29 @@ class SyncBaseHTTPView(
|
|
209
253
|
)
|
210
254
|
root_value = self.get_root_value(request) if root_value is UNSET else root_value
|
211
255
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
except CannotGetOperationTypeError as e:
|
219
|
-
raise HTTPException(400, e.as_http_error_reason()) from e
|
220
|
-
except InvalidOperationTypeError as e:
|
221
|
-
raise HTTPException(
|
222
|
-
400, e.as_http_error_reason(request_adapter.method)
|
223
|
-
) from e
|
224
|
-
except MissingQueryError as e:
|
225
|
-
raise HTTPException(400, "No GraphQL query found in the request") from e
|
256
|
+
result = self.execute_operation(
|
257
|
+
request=request,
|
258
|
+
context=context,
|
259
|
+
root_value=root_value,
|
260
|
+
sub_response=sub_response,
|
261
|
+
)
|
226
262
|
|
227
|
-
response_data
|
263
|
+
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]]
|
264
|
+
|
265
|
+
if isinstance(result, list):
|
266
|
+
response_data = []
|
267
|
+
for execution_result in result:
|
268
|
+
processed_result = self.process_result(
|
269
|
+
request=request, result=execution_result
|
270
|
+
)
|
271
|
+
if execution_result.errors:
|
272
|
+
self._handle_errors(execution_result.errors, processed_result)
|
273
|
+
response_data.append(processed_result)
|
274
|
+
else:
|
275
|
+
response_data = self.process_result(request=request, result=result)
|
228
276
|
|
229
|
-
|
230
|
-
|
277
|
+
if result.errors:
|
278
|
+
self._handle_errors(result.errors, response_data)
|
231
279
|
|
232
280
|
return self.create_response(
|
233
281
|
response_data=response_data, sub_response=sub_response
|
@@ -13,10 +13,10 @@ from typing import (
|
|
13
13
|
Optional,
|
14
14
|
TypedDict,
|
15
15
|
Union,
|
16
|
-
cast,
|
17
16
|
)
|
18
17
|
from typing_extensions import TypeGuard
|
19
18
|
|
19
|
+
from lia import HTTPException, LitestarRequestAdapter
|
20
20
|
from msgspec import Struct
|
21
21
|
|
22
22
|
from litestar import (
|
@@ -41,16 +41,13 @@ from litestar.status_codes import HTTP_200_OK
|
|
41
41
|
from strawberry.exceptions import InvalidCustomContext
|
42
42
|
from strawberry.http.async_base_view import (
|
43
43
|
AsyncBaseHTTPView,
|
44
|
-
AsyncHTTPRequestAdapter,
|
45
44
|
AsyncWebSocketAdapter,
|
46
45
|
)
|
47
46
|
from strawberry.http.exceptions import (
|
48
|
-
HTTPException,
|
49
47
|
NonJsonMessageReceived,
|
50
48
|
NonTextMessageReceived,
|
51
49
|
WebSocketDisconnected,
|
52
50
|
)
|
53
|
-
from strawberry.http.types import FormData, HTTPMethod, QueryParams
|
54
51
|
from strawberry.http.typevars import Context, RootValue
|
55
52
|
from strawberry.subscriptions import GRAPHQL_TRANSPORT_WS_PROTOCOL, GRAPHQL_WS_PROTOCOL
|
56
53
|
|
@@ -153,41 +150,6 @@ class GraphQLResource(Struct):
|
|
153
150
|
extensions: Optional[dict[str, object]]
|
154
151
|
|
155
152
|
|
156
|
-
class LitestarRequestAdapter(AsyncHTTPRequestAdapter):
|
157
|
-
def __init__(self, request: Request[Any, Any, Any]) -> None:
|
158
|
-
self.request = request
|
159
|
-
|
160
|
-
@property
|
161
|
-
def query_params(self) -> QueryParams:
|
162
|
-
return self.request.query_params
|
163
|
-
|
164
|
-
@property
|
165
|
-
def method(self) -> HTTPMethod:
|
166
|
-
return cast("HTTPMethod", self.request.method.upper())
|
167
|
-
|
168
|
-
@property
|
169
|
-
def headers(self) -> Mapping[str, str]:
|
170
|
-
return self.request.headers
|
171
|
-
|
172
|
-
@property
|
173
|
-
def content_type(self) -> Optional[str]:
|
174
|
-
content_type, params = self.request.content_type
|
175
|
-
|
176
|
-
# combine content type and params
|
177
|
-
if params:
|
178
|
-
content_type += "; " + "; ".join(f"{k}={v}" for k, v in params.items())
|
179
|
-
|
180
|
-
return content_type
|
181
|
-
|
182
|
-
async def get_body(self) -> bytes:
|
183
|
-
return await self.request.body()
|
184
|
-
|
185
|
-
async def get_form_data(self) -> FormData:
|
186
|
-
multipart_data = await self.request.form()
|
187
|
-
|
188
|
-
return FormData(form=multipart_data, files=multipart_data)
|
189
|
-
|
190
|
-
|
191
153
|
class LitestarWebSocketAdapter(AsyncWebSocketAdapter):
|
192
154
|
def __init__(
|
193
155
|
self, view: AsyncBaseHTTPView, request: WebSocket, response: WebSocket
|
@@ -302,7 +264,9 @@ class GraphQLController(
|
|
302
264
|
return Response(self.graphql_ide_html, media_type=MediaType.HTML)
|
303
265
|
|
304
266
|
def create_response(
|
305
|
-
self,
|
267
|
+
self,
|
268
|
+
response_data: Union[GraphQLHTTPResponse, list[GraphQLHTTPResponse]],
|
269
|
+
sub_response: Response[bytes],
|
306
270
|
) -> Response[bytes]:
|
307
271
|
response = Response(
|
308
272
|
self.encode_json(response_data).encode(),
|