strawberry-graphql 0.238.1__py3-none-any.whl → 0.239.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.
@@ -7,10 +7,13 @@ from io import BytesIO
7
7
  from typing import (
8
8
  TYPE_CHECKING,
9
9
  Any,
10
+ AsyncGenerator,
11
+ Callable,
10
12
  Dict,
11
13
  Iterable,
12
14
  Mapping,
13
15
  Optional,
16
+ Union,
14
17
  cast,
15
18
  )
16
19
 
@@ -73,11 +76,17 @@ class AioHTTPRequestAdapter(AsyncHTTPRequestAdapter):
73
76
 
74
77
  @property
75
78
  def content_type(self) -> Optional[str]:
76
- return self.request.content_type
79
+ return self.headers.get("content-type")
77
80
 
78
81
 
79
82
  class GraphQLView(
80
- AsyncBaseHTTPView[web.Request, web.Response, web.Response, Context, RootValue]
83
+ AsyncBaseHTTPView[
84
+ web.Request,
85
+ Union[web.Response, web.StreamResponse],
86
+ web.Response,
87
+ Context,
88
+ RootValue,
89
+ ]
81
90
  ):
82
91
  # Mark the view as coroutine so that AIOHTTP does not confuse it with a deprecated
83
92
  # bare handler function.
@@ -180,5 +189,29 @@ class GraphQLView(
180
189
 
181
190
  return sub_response
182
191
 
192
+ async def create_multipart_response(
193
+ self,
194
+ request: web.Request,
195
+ stream: Callable[[], AsyncGenerator[str, None]],
196
+ sub_response: web.Response,
197
+ ) -> web.StreamResponse:
198
+ response = web.StreamResponse(
199
+ status=sub_response.status,
200
+ headers={
201
+ **sub_response.headers,
202
+ "Transfer-Encoding": "chunked",
203
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
204
+ },
205
+ )
206
+
207
+ await response.prepare(request)
208
+
209
+ async for data in stream():
210
+ await response.write(data.encode())
211
+
212
+ await response.write_eof()
213
+
214
+ return response
215
+
183
216
 
184
217
  __all__ = ["GraphQLView"]
@@ -5,6 +5,8 @@ from datetime import timedelta
5
5
  from typing import (
6
6
  TYPE_CHECKING,
7
7
  Any,
8
+ AsyncIterator,
9
+ Callable,
8
10
  Mapping,
9
11
  Optional,
10
12
  Sequence,
@@ -14,7 +16,12 @@ from typing import (
14
16
 
15
17
  from starlette import status
16
18
  from starlette.requests import Request
17
- from starlette.responses import HTMLResponse, PlainTextResponse, Response
19
+ from starlette.responses import (
20
+ HTMLResponse,
21
+ PlainTextResponse,
22
+ Response,
23
+ StreamingResponse,
24
+ )
18
25
  from starlette.websockets import WebSocket
19
26
 
20
27
  from strawberry.asgi.handlers import (
@@ -213,3 +220,19 @@ class GraphQL(
213
220
  response.status_code = sub_response.status_code
214
221
 
215
222
  return response
223
+
224
+ async def create_multipart_response(
225
+ self,
226
+ request: Request | WebSocket,
227
+ stream: Callable[[], AsyncIterator[str]],
228
+ sub_response: Response,
229
+ ) -> Response:
230
+ return StreamingResponse(
231
+ stream(),
232
+ status_code=sub_response.status_code,
233
+ headers={
234
+ **sub_response.headers,
235
+ "Transfer-Encoding": "chunked",
236
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
237
+ },
238
+ )
@@ -1,8 +1,3 @@
1
- """GraphQLHTTPHandler.
2
-
3
- A consumer to provide a graphql endpoint, and optionally graphiql.
4
- """
5
-
6
1
  from __future__ import annotations
7
2
 
8
3
  import dataclasses
@@ -10,7 +5,17 @@ import json
10
5
  import warnings
11
6
  from functools import cached_property
12
7
  from io import BytesIO
13
- from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Union
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ AsyncGenerator,
12
+ Callable,
13
+ Dict,
14
+ Mapping,
15
+ Optional,
16
+ Union,
17
+ )
18
+ from typing_extensions import assert_never
14
19
  from urllib.parse import parse_qs
15
20
 
16
21
  from django.conf import settings
@@ -44,6 +49,14 @@ class ChannelsResponse:
44
49
  headers: Dict[bytes, bytes] = dataclasses.field(default_factory=dict)
45
50
 
46
51
 
52
+ @dataclasses.dataclass
53
+ class MultipartChannelsResponse:
54
+ stream: Callable[[], AsyncGenerator[str, None]]
55
+ status: int = 200
56
+ content_type: str = "multipart/mixed;boundary=graphql;subscriptionSpec=1.0"
57
+ headers: Dict[bytes, bytes] = dataclasses.field(default_factory=dict)
58
+
59
+
47
60
  @dataclasses.dataclass
48
61
  class ChannelsRequest:
49
62
  consumer: ChannelsConsumer
@@ -186,16 +199,28 @@ class BaseGraphQLHTTPConsumer(ChannelsConsumer, AsyncHttpConsumer):
186
199
  async def handle(self, body: bytes) -> None:
187
200
  request = ChannelsRequest(consumer=self, body=body)
188
201
  try:
189
- response: ChannelsResponse = await self.run(request)
202
+ response = await self.run(request)
190
203
 
191
204
  if b"Content-Type" not in response.headers:
192
205
  response.headers[b"Content-Type"] = response.content_type.encode()
193
206
 
194
- await self.send_response(
195
- response.status,
196
- response.content,
197
- headers=response.headers,
198
- )
207
+ if isinstance(response, MultipartChannelsResponse):
208
+ response.headers[b"Transfer-Encoding"] = b"chunked"
209
+ await self.send_headers(headers=response.headers)
210
+
211
+ async for chunk in response.stream():
212
+ await self.send_body(chunk.encode("utf-8"), more_body=True)
213
+
214
+ await self.send_body(b"", more_body=False)
215
+
216
+ elif isinstance(response, ChannelsResponse):
217
+ await self.send_response(
218
+ response.status,
219
+ response.content,
220
+ headers=response.headers,
221
+ )
222
+ else:
223
+ assert_never(response)
199
224
  except HTTPException as e:
200
225
  await self.send_response(e.status_code, e.reason.encode())
201
226
 
@@ -204,7 +229,7 @@ class GraphQLHTTPConsumer(
204
229
  BaseGraphQLHTTPConsumer,
205
230
  AsyncBaseHTTPView[
206
231
  ChannelsRequest,
207
- ChannelsResponse,
232
+ Union[ChannelsResponse, MultipartChannelsResponse],
208
233
  TemporalResponse,
209
234
  Context,
210
235
  RootValue,
@@ -248,6 +273,16 @@ class GraphQLHTTPConsumer(
248
273
  async def get_sub_response(self, request: ChannelsRequest) -> TemporalResponse:
249
274
  return TemporalResponse()
250
275
 
276
+ async def create_multipart_response(
277
+ self,
278
+ request: ChannelsRequest,
279
+ stream: Callable[[], AsyncGenerator[str, None]],
280
+ sub_response: TemporalResponse,
281
+ ) -> MultipartChannelsResponse:
282
+ status = sub_response.status_code or 200
283
+ headers = {k.encode(): v.encode() for k, v in sub_response.headers.items()}
284
+ return MultipartChannelsResponse(stream=stream, status=status, headers=headers)
285
+
251
286
  async def render_graphql_ide(self, request: ChannelsRequest) -> ChannelsResponse:
252
287
  return ChannelsResponse(
253
288
  content=self.graphql_ide_html.encode(), content_type="text/html"
@@ -302,7 +337,7 @@ class SyncGraphQLHTTPConsumer(
302
337
  request: ChannelsRequest,
303
338
  context: Optional[Context] = UNSET,
304
339
  root_value: Optional[RootValue] = UNSET,
305
- ) -> ChannelsResponse:
340
+ ) -> ChannelsResponse | MultipartChannelsResponse:
306
341
  return super().run(request, context, root_value)
307
342
 
308
343
 
@@ -5,6 +5,7 @@ import warnings
5
5
  from typing import (
6
6
  TYPE_CHECKING,
7
7
  Any,
8
+ AsyncIterator,
8
9
  Callable,
9
10
  Mapping,
10
11
  Optional,
@@ -14,8 +15,14 @@ from typing import (
14
15
 
15
16
  from asgiref.sync import markcoroutinefunction
16
17
  from django.core.serializers.json import DjangoJSONEncoder
17
- from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse
18
- from django.http.response import HttpResponse
18
+ from django.http import (
19
+ HttpRequest,
20
+ HttpResponse,
21
+ HttpResponseNotAllowed,
22
+ JsonResponse,
23
+ StreamingHttpResponse,
24
+ )
25
+ from django.http.response import HttpResponseBase
19
26
  from django.template import RequestContext, Template
20
27
  from django.template.exceptions import TemplateDoesNotExist
21
28
  from django.template.loader import render_to_string
@@ -116,7 +123,7 @@ class AsyncDjangoHTTPRequestAdapter(AsyncHTTPRequestAdapter):
116
123
 
117
124
  @property
118
125
  def content_type(self) -> Optional[str]:
119
- return self.request.content_type
126
+ return self.headers.get("Content-type")
120
127
 
121
128
  async def get_body(self) -> str:
122
129
  return self.request.body.decode()
@@ -159,8 +166,9 @@ class BaseView:
159
166
 
160
167
  def create_response(
161
168
  self, response_data: GraphQLHTTPResponse, sub_response: HttpResponse
162
- ) -> HttpResponse:
169
+ ) -> HttpResponseBase:
163
170
  data = self.encode_json(response_data)
171
+
164
172
  response = HttpResponse(
165
173
  data,
166
174
  content_type="application/json",
@@ -177,6 +185,22 @@ class BaseView:
177
185
 
178
186
  return response
179
187
 
188
+ async def create_multipart_response(
189
+ self,
190
+ request: HttpRequest,
191
+ stream: Callable[[], AsyncIterator[Any]],
192
+ sub_response: TemporalHttpResponse,
193
+ ) -> HttpResponseBase:
194
+ return StreamingHttpResponse(
195
+ streaming_content=stream(),
196
+ status=sub_response.status_code,
197
+ headers={
198
+ **sub_response.headers,
199
+ "Transfer-Encoding": "chunked",
200
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
201
+ },
202
+ )
203
+
180
204
  def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
181
205
  return json.dumps(response_data, cls=DjangoJSONEncoder)
182
206
 
@@ -184,7 +208,7 @@ class BaseView:
184
208
  class GraphQLView(
185
209
  BaseView,
186
210
  SyncBaseHTTPView[
187
- HttpRequest, HttpResponse, TemporalHttpResponse, Context, RootValue
211
+ HttpRequest, HttpResponseBase, TemporalHttpResponse, Context, RootValue
188
212
  ],
189
213
  View,
190
214
  ):
@@ -207,7 +231,7 @@ class GraphQLView(
207
231
  @method_decorator(csrf_exempt)
208
232
  def dispatch(
209
233
  self, request: HttpRequest, *args: Any, **kwargs: Any
210
- ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponse]:
234
+ ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:
211
235
  try:
212
236
  return self.run(request=request)
213
237
  except HTTPException as e:
@@ -233,7 +257,7 @@ class GraphQLView(
233
257
  class AsyncGraphQLView(
234
258
  BaseView,
235
259
  AsyncBaseHTTPView[
236
- HttpRequest, HttpResponse, TemporalHttpResponse, Context, RootValue
260
+ HttpRequest, HttpResponseBase, TemporalHttpResponse, Context, RootValue
237
261
  ],
238
262
  View,
239
263
  ):
@@ -266,7 +290,7 @@ class AsyncGraphQLView(
266
290
  @method_decorator(csrf_exempt)
267
291
  async def dispatch( # pyright: ignore
268
292
  self, request: HttpRequest, *args: Any, **kwargs: Any
269
- ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponse]:
293
+ ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponseBase]:
270
294
  try:
271
295
  return await self.run(request=request)
272
296
  except HTTPException as e:
@@ -6,6 +6,7 @@ from inspect import signature
6
6
  from typing import (
7
7
  TYPE_CHECKING,
8
8
  Any,
9
+ AsyncIterator,
9
10
  Awaitable,
10
11
  Callable,
11
12
  Dict,
@@ -25,6 +26,7 @@ from starlette.responses import (
25
26
  JSONResponse,
26
27
  PlainTextResponse,
27
28
  Response,
29
+ StreamingResponse,
28
30
  )
29
31
  from starlette.websockets import WebSocket
30
32
 
@@ -330,5 +332,21 @@ class GraphQLRouter(
330
332
 
331
333
  return response
332
334
 
335
+ async def create_multipart_response(
336
+ self,
337
+ request: Request,
338
+ stream: Callable[[], AsyncIterator[str]],
339
+ sub_response: Response,
340
+ ) -> Response:
341
+ return StreamingResponse(
342
+ stream(),
343
+ status_code=sub_response.status_code,
344
+ headers={
345
+ **sub_response.headers,
346
+ "Transfer-Encoding": "chunked",
347
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
348
+ },
349
+ )
350
+
333
351
 
334
352
  __all__ = ["GraphQLRouter"]
strawberry/flask/views.py CHANGED
@@ -1,7 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Mapping,
8
+ Optional,
9
+ Union,
10
+ cast,
11
+ )
5
12
 
6
13
  from flask import Request, Response, render_template_string, request
7
14
  from flask.views import View
@@ -1,12 +1,17 @@
1
1
  import abc
2
+ import asyncio
3
+ import contextlib
2
4
  import json
3
5
  from typing import (
6
+ Any,
7
+ AsyncGenerator,
4
8
  Callable,
5
9
  Dict,
6
10
  Generic,
7
11
  List,
8
12
  Mapping,
9
13
  Optional,
14
+ Tuple,
10
15
  Union,
11
16
  )
12
17
 
@@ -15,15 +20,20 @@ from graphql import GraphQLError
15
20
  from strawberry import UNSET
16
21
  from strawberry.exceptions import MissingQueryError
17
22
  from strawberry.file_uploads.utils import replace_placeholders_with_files
18
- from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData, process_result
23
+ from strawberry.http import (
24
+ GraphQLHTTPResponse,
25
+ GraphQLRequestData,
26
+ process_result,
27
+ )
19
28
  from strawberry.http.ides import GraphQL_IDE
20
29
  from strawberry.schema.base import BaseSchema
21
30
  from strawberry.schema.exceptions import InvalidOperationTypeError
22
- from strawberry.types import ExecutionResult
31
+ from strawberry.types import ExecutionResult, SubscriptionExecutionResult
23
32
  from strawberry.types.graphql import OperationType
24
33
 
25
34
  from .base import BaseView
26
35
  from .exceptions import HTTPException
36
+ from .parse_content_type import parse_content_type
27
37
  from .types import FormData, HTTPMethod, QueryParams
28
38
  from .typevars import Context, Request, Response, RootValue, SubResponse
29
39
 
@@ -82,9 +92,17 @@ class AsyncBaseHTTPView(
82
92
  @abc.abstractmethod
83
93
  async def render_graphql_ide(self, request: Request) -> Response: ...
84
94
 
95
+ async def create_multipart_response(
96
+ self,
97
+ request: Request,
98
+ stream: Callable[[], AsyncGenerator[str, None]],
99
+ sub_response: SubResponse,
100
+ ) -> Response:
101
+ raise ValueError("Multipart responses are not supported")
102
+
85
103
  async def execute_operation(
86
104
  self, request: Request, context: Context, root_value: Optional[RootValue]
87
- ) -> ExecutionResult:
105
+ ) -> Union[ExecutionResult, SubscriptionExecutionResult]:
88
106
  request_adapter = self.request_adapter_class(request)
89
107
 
90
108
  try:
@@ -178,6 +196,11 @@ class AsyncBaseHTTPView(
178
196
  except MissingQueryError as e:
179
197
  raise HTTPException(400, "No GraphQL query found in the request") from e
180
198
 
199
+ if isinstance(result, SubscriptionExecutionResult):
200
+ stream = self._get_stream(request, result)
201
+
202
+ return await self.create_multipart_response(request, stream, sub_response)
203
+
181
204
  response_data = await self.process_result(request=request, result=result)
182
205
 
183
206
  if result.errors:
@@ -187,17 +210,107 @@ class AsyncBaseHTTPView(
187
210
  response_data=response_data, sub_response=sub_response
188
211
  )
189
212
 
213
+ def encode_multipart_data(self, data: Any, separator: str) -> str:
214
+ return "".join(
215
+ [
216
+ f"\r\n--{separator}\r\n",
217
+ "Content-Type: application/json\r\n\r\n",
218
+ self.encode_json(data),
219
+ "\n",
220
+ ]
221
+ )
222
+
223
+ def _stream_with_heartbeat(
224
+ self, stream: Callable[[], AsyncGenerator[str, None]]
225
+ ) -> Callable[[], AsyncGenerator[str, None]]:
226
+ """Adds a heartbeat to the stream, to prevent the connection from closing when there are no messages being sent."""
227
+ queue = asyncio.Queue[Tuple[bool, Any]](1)
228
+
229
+ cancelling = False
230
+
231
+ async def drain() -> None:
232
+ try:
233
+ async for item in stream():
234
+ await queue.put((False, item))
235
+ except Exception as e:
236
+ if not cancelling:
237
+ await queue.put((True, e))
238
+ else:
239
+ raise
240
+
241
+ async def heartbeat() -> None:
242
+ while True:
243
+ await queue.put((False, self.encode_multipart_data({}, "graphql")))
244
+
245
+ await asyncio.sleep(5)
246
+
247
+ async def merged() -> AsyncGenerator[str, None]:
248
+ heartbeat_task = asyncio.create_task(heartbeat())
249
+ task = asyncio.create_task(drain())
250
+
251
+ async def cancel_tasks() -> None:
252
+ nonlocal cancelling
253
+ cancelling = True
254
+ task.cancel()
255
+
256
+ with contextlib.suppress(asyncio.CancelledError):
257
+ await task
258
+
259
+ heartbeat_task.cancel()
260
+
261
+ with contextlib.suppress(asyncio.CancelledError):
262
+ await heartbeat_task
263
+
264
+ try:
265
+ while not task.done():
266
+ raised, data = await queue.get()
267
+
268
+ if raised:
269
+ await cancel_tasks()
270
+ raise data
271
+
272
+ yield data
273
+ finally:
274
+ await cancel_tasks()
275
+
276
+ return merged
277
+
278
+ def _get_stream(
279
+ self,
280
+ request: Request,
281
+ result: SubscriptionExecutionResult,
282
+ separator: str = "graphql",
283
+ ) -> Callable[[], AsyncGenerator[str, None]]:
284
+ async def stream() -> AsyncGenerator[str, None]:
285
+ async for value in result:
286
+ response = await self.process_result(request, value)
287
+ yield self.encode_multipart_data({"payload": response}, separator)
288
+
289
+ yield f"\r\n--{separator}--\r\n"
290
+
291
+ return self._stream_with_heartbeat(stream)
292
+
293
+ async def parse_multipart_subscriptions(
294
+ self, request: AsyncHTTPRequestAdapter
295
+ ) -> Dict[str, str]:
296
+ if request.method == "GET":
297
+ return self.parse_query_params(request.query_params)
298
+
299
+ return self.parse_json(await request.get_body())
300
+
190
301
  async def parse_http_body(
191
302
  self, request: AsyncHTTPRequestAdapter
192
303
  ) -> GraphQLRequestData:
193
- content_type = request.content_type or ""
304
+ content_type, params = parse_content_type(request.content_type or "")
194
305
 
195
306
  if request.method == "GET":
196
307
  data = self.parse_query_params(request.query_params)
197
308
  elif "application/json" in content_type:
198
309
  data = self.parse_json(await request.get_body())
199
- elif content_type.startswith("multipart/form-data"):
310
+ elif content_type == "multipart/form-data":
200
311
  data = await self.parse_multipart(request)
312
+ elif self._is_multipart_subscriptions(content_type, params):
313
+ data = await self.parse_multipart_subscriptions(request)
201
314
  else:
202
315
  raise HTTPException(400, "Unsupported content type")
203
316
 
strawberry/http/base.py CHANGED
@@ -69,5 +69,16 @@ class BaseView(Generic[Request]):
69
69
  graphql_ide=self.graphql_ide,
70
70
  )
71
71
 
72
+ def _is_multipart_subscriptions(
73
+ self, content_type: str, params: Dict[str, str]
74
+ ) -> bool:
75
+ if content_type != "multipart/mixed":
76
+ return False
77
+
78
+ if params.get("boundary") != "graphql":
79
+ return False
80
+
81
+ return params.get("subscriptionspec", "").startswith("1.0")
82
+
72
83
 
73
84
  __all__ = ["BaseView"]
@@ -0,0 +1,16 @@
1
+ from email.message import Message
2
+ from typing import Dict, Tuple
3
+
4
+
5
+ def parse_content_type(content_type: str) -> Tuple[str, Dict[str, str]]:
6
+ """Parse a content type header into a mime-type and a dictionary of parameters."""
7
+ email = Message()
8
+ email["content-type"] = content_type
9
+
10
+ params = email.get_params()
11
+
12
+ assert params
13
+
14
+ mime_type, _ = params.pop(0)
15
+
16
+ return mime_type, dict(params)
@@ -16,7 +16,11 @@ from graphql import GraphQLError
16
16
  from strawberry import UNSET
17
17
  from strawberry.exceptions import MissingQueryError
18
18
  from strawberry.file_uploads.utils import replace_placeholders_with_files
19
- from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData, process_result
19
+ from strawberry.http import (
20
+ GraphQLHTTPResponse,
21
+ GraphQLRequestData,
22
+ process_result,
23
+ )
20
24
  from strawberry.http.ides import GraphQL_IDE
21
25
  from strawberry.schema import BaseSchema
22
26
  from strawberry.schema.exceptions import InvalidOperationTypeError
@@ -25,6 +29,7 @@ from strawberry.types.graphql import OperationType
25
29
 
26
30
  from .base import BaseView
27
31
  from .exceptions import HTTPException
32
+ from .parse_content_type import parse_content_type
28
33
  from .types import HTTPMethod, QueryParams
29
34
  from .typevars import Context, Request, Response, RootValue, SubResponse
30
35
 
@@ -131,14 +136,19 @@ class SyncBaseHTTPView(
131
136
  raise HTTPException(400, "File(s) missing in form data") from e
132
137
 
133
138
  def parse_http_body(self, request: SyncHTTPRequestAdapter) -> GraphQLRequestData:
134
- content_type = request.content_type or ""
139
+ content_type, params = parse_content_type(request.content_type or "")
135
140
 
136
141
  if request.method == "GET":
137
142
  data = self.parse_query_params(request.query_params)
138
143
  elif "application/json" in content_type:
139
144
  data = self.parse_json(request.body)
140
- elif content_type.startswith("multipart/form-data"):
145
+ # TODO: multipart via get?
146
+ elif content_type == "multipart/form-data":
141
147
  data = self.parse_multipart(request)
148
+ elif self._is_multipart_subscriptions(content_type, params):
149
+ raise HTTPException(
150
+ 400, "Multipart subcriptions are not supported in sync mode"
151
+ )
142
152
  else:
143
153
  raise HTTPException(400, "Unsupported content type")
144
154
 
@@ -7,6 +7,8 @@ from datetime import timedelta
7
7
  from typing import (
8
8
  TYPE_CHECKING,
9
9
  Any,
10
+ AsyncIterator,
11
+ Callable,
10
12
  Dict,
11
13
  FrozenSet,
12
14
  List,
@@ -34,6 +36,7 @@ from litestar import (
34
36
  from litestar.background_tasks import BackgroundTasks
35
37
  from litestar.di import Provide
36
38
  from litestar.exceptions import NotFoundException, ValidationException
39
+ from litestar.response.streaming import Stream
37
40
  from litestar.status_codes import HTTP_200_OK
38
41
  from strawberry.exceptions import InvalidCustomContext
39
42
  from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
@@ -183,7 +186,13 @@ class LitestarRequestAdapter(AsyncHTTPRequestAdapter):
183
186
 
184
187
  @property
185
188
  def content_type(self) -> Optional[str]:
186
- return self.request.content_type[0]
189
+ content_type, params = self.request.content_type
190
+
191
+ # combine content type and params
192
+ if params:
193
+ content_type += "; " + "; ".join(f"{k}={v}" for k, v in params.items())
194
+
195
+ return content_type
187
196
 
188
197
  async def get_body(self) -> bytes:
189
198
  return await self.request.body()
@@ -271,6 +280,22 @@ class GraphQLController(
271
280
 
272
281
  return response
273
282
 
283
+ async def create_multipart_response(
284
+ self,
285
+ request: Request,
286
+ stream: Callable[[], AsyncIterator[str]],
287
+ sub_response: Response,
288
+ ) -> Response:
289
+ return Stream(
290
+ stream(),
291
+ status_code=sub_response.status_code,
292
+ headers={
293
+ **sub_response.headers,
294
+ "Transfer-Encoding": "chunked",
295
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
296
+ },
297
+ )
298
+
274
299
  @get(raises=[ValidationException, NotFoundException])
275
300
  async def handle_http_get(
276
301
  self,
strawberry/quart/views.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import warnings
2
2
  from collections.abc import Mapping
3
- from typing import TYPE_CHECKING, Optional, cast
3
+ from typing import TYPE_CHECKING, AsyncGenerator, Callable, Optional, cast
4
4
 
5
5
  from quart import Request, Response, request
6
6
  from quart.views import View
@@ -103,5 +103,21 @@ class GraphQLView(
103
103
  status=e.status_code,
104
104
  )
105
105
 
106
+ async def create_multipart_response(
107
+ self,
108
+ request: Request,
109
+ stream: Callable[[], AsyncGenerator[str, None]],
110
+ sub_response: Response,
111
+ ) -> Response:
112
+ return (
113
+ stream(),
114
+ sub_response.status_code,
115
+ { # type: ignore
116
+ **sub_response.headers,
117
+ "Transfer-Encoding": "chunked",
118
+ "Content-type": "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
119
+ },
120
+ )
121
+
106
122
 
107
123
  __all__ = ["GraphQLView"]
strawberry/sanic/views.py CHANGED
@@ -5,6 +5,8 @@ import warnings
5
5
  from typing import (
6
6
  TYPE_CHECKING,
7
7
  Any,
8
+ AsyncGenerator,
9
+ Callable,
8
10
  Dict,
9
11
  Mapping,
10
12
  Optional,
@@ -161,16 +163,46 @@ class GraphQLView(
161
163
  )
162
164
 
163
165
  async def post(self, request: Request) -> HTTPResponse:
166
+ self.request = request
167
+
164
168
  try:
165
169
  return await self.run(request)
166
170
  except HTTPException as e:
167
171
  return HTTPResponse(e.reason, status=e.status_code)
168
172
 
169
173
  async def get(self, request: Request) -> HTTPResponse: # type: ignore[override]
174
+ self.request = request
175
+
170
176
  try:
171
177
  return await self.run(request)
172
178
  except HTTPException as e:
173
179
  return HTTPResponse(e.reason, status=e.status_code)
174
180
 
181
+ async def create_multipart_response(
182
+ self,
183
+ request: Request,
184
+ stream: Callable[[], AsyncGenerator[str, None]],
185
+ sub_response: TemporalResponse,
186
+ ) -> HTTPResponse:
187
+ response = await self.request.respond(
188
+ content_type="multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json",
189
+ status=sub_response.status_code,
190
+ headers={
191
+ **sub_response.headers,
192
+ "Transfer-Encoding": "chunked",
193
+ },
194
+ )
195
+
196
+ async for chunk in stream():
197
+ await response.send(chunk)
198
+
199
+ await response.eof()
200
+
201
+ # returning the response will basically tell sanic to send it again
202
+ # to the client, so we return None to avoid that, and we ignore the type
203
+ # error mostly so we don't have to update the types everywhere for this
204
+ # corner case
205
+ return None # type: ignore
206
+
175
207
 
176
208
  __all__ = ["GraphQLView"]
strawberry/schema/base.py CHANGED
@@ -12,7 +12,11 @@ if TYPE_CHECKING:
12
12
 
13
13
  from strawberry.directive import StrawberryDirective
14
14
  from strawberry.schema.schema_converter import GraphQLCoreConverter
15
- from strawberry.types import ExecutionContext, ExecutionResult
15
+ from strawberry.types import (
16
+ ExecutionContext,
17
+ ExecutionResult,
18
+ SubscriptionExecutionResult,
19
+ )
16
20
  from strawberry.types.base import StrawberryObjectDefinition
17
21
  from strawberry.types.enum import EnumDefinition
18
22
  from strawberry.types.graphql import OperationType
@@ -39,7 +43,7 @@ class BaseSchema(Protocol):
39
43
  root_value: Optional[Any] = None,
40
44
  operation_name: Optional[str] = None,
41
45
  allowed_operation_types: Optional[Iterable[OperationType]] = None,
42
- ) -> ExecutionResult:
46
+ ) -> Union[ExecutionResult, SubscriptionExecutionResult]:
43
47
  raise NotImplementedError
44
48
 
45
49
  @abstractmethod
@@ -15,7 +15,7 @@ from typing import (
15
15
  Union,
16
16
  )
17
17
 
18
- from graphql import GraphQLError, parse
18
+ from graphql import GraphQLError, parse, subscribe
19
19
  from graphql import execute as original_execute
20
20
  from graphql.validation import validate
21
21
 
@@ -23,6 +23,7 @@ from strawberry.exceptions import MissingQueryError
23
23
  from strawberry.extensions.runner import SchemaExtensionsRunner
24
24
  from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule
25
25
  from strawberry.types import ExecutionResult
26
+ from strawberry.types.graphql import OperationType
26
27
 
27
28
  from .exceptions import InvalidOperationTypeError
28
29
 
@@ -36,7 +37,7 @@ if TYPE_CHECKING:
36
37
 
37
38
  from strawberry.extensions import SchemaExtension
38
39
  from strawberry.types import ExecutionContext
39
- from strawberry.types.graphql import OperationType
40
+ from strawberry.types.execution import SubscriptionExecutionResult
40
41
 
41
42
 
42
43
  # duplicated because of https://github.com/mkdocstrings/griffe-typingdoc/issues/7
@@ -84,7 +85,7 @@ async def execute(
84
85
  execution_context: ExecutionContext,
85
86
  execution_context_class: Optional[Type[GraphQLExecutionContext]] = None,
86
87
  process_errors: Callable[[List[GraphQLError], Optional[ExecutionContext]], None],
87
- ) -> ExecutionResult:
88
+ ) -> Union[ExecutionResult, SubscriptionExecutionResult]:
88
89
  extensions_runner = SchemaExtensionsRunner(
89
90
  execution_context=execution_context,
90
91
  extensions=list(extensions),
@@ -124,16 +125,28 @@ async def execute(
124
125
 
125
126
  async with extensions_runner.executing():
126
127
  if not execution_context.result:
127
- result = original_execute(
128
- schema,
129
- execution_context.graphql_document,
130
- root_value=execution_context.root_value,
131
- middleware=extensions_runner.as_middleware_manager(),
132
- variable_values=execution_context.variables,
133
- operation_name=execution_context.operation_name,
134
- context_value=execution_context.context,
135
- execution_context_class=execution_context_class,
136
- )
128
+ if execution_context.operation_type == OperationType.SUBSCRIPTION:
129
+ # TODO: should we process errors here?
130
+ # TODO: make our own wrapper?
131
+ return await subscribe( # type: ignore
132
+ schema,
133
+ execution_context.graphql_document,
134
+ root_value=execution_context.root_value,
135
+ context_value=execution_context.context,
136
+ variable_values=execution_context.variables,
137
+ operation_name=execution_context.operation_name,
138
+ )
139
+ else:
140
+ result = original_execute(
141
+ schema,
142
+ execution_context.graphql_document,
143
+ root_value=execution_context.root_value,
144
+ middleware=extensions_runner.as_middleware_manager(),
145
+ variable_values=execution_context.variables,
146
+ operation_name=execution_context.operation_name,
147
+ context_value=execution_context.context,
148
+ execution_context_class=execution_context_class,
149
+ )
137
150
 
138
151
  if isawaitable(result):
139
152
  result = await result
@@ -53,7 +53,7 @@ if TYPE_CHECKING:
53
53
 
54
54
  from strawberry.directive import StrawberryDirective
55
55
  from strawberry.extensions import SchemaExtension
56
- from strawberry.types import ExecutionResult
56
+ from strawberry.types import ExecutionResult, SubscriptionExecutionResult
57
57
  from strawberry.types.base import StrawberryType
58
58
  from strawberry.types.enum import EnumDefinition
59
59
  from strawberry.types.field import StrawberryField
@@ -284,7 +284,7 @@ class Schema(BaseSchema):
284
284
  root_value: Optional[Any] = None,
285
285
  operation_name: Optional[str] = None,
286
286
  allowed_operation_types: Optional[Iterable[OperationType]] = None,
287
- ) -> ExecutionResult:
287
+ ) -> Union[ExecutionResult, SubscriptionExecutionResult]:
288
288
  if allowed_operation_types is None:
289
289
  allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES
290
290
 
@@ -256,7 +256,7 @@ class BaseGraphQLTransportWSHandler(ABC):
256
256
  else:
257
257
  # create AsyncGenerator returning a single result
258
258
  async def get_result_source() -> AsyncIterator[ExecutionResult]:
259
- yield await self.schema.execute(
259
+ yield await self.schema.execute( # type: ignore
260
260
  query=message.payload.query,
261
261
  variable_values=message.payload.variables,
262
262
  context_value=context,
@@ -1,10 +1,12 @@
1
1
  from .base import get_object_definition, has_object_definition
2
- from .execution import ExecutionContext, ExecutionResult
2
+ from .execution import ExecutionContext, ExecutionResult, SubscriptionExecutionResult
3
3
  from .info import Info
4
4
 
5
5
  __all__ = [
6
6
  "ExecutionContext",
7
7
  "ExecutionResult",
8
+ "SubscriptionExecutionResult",
9
+ "Info",
8
10
  "Info",
9
11
  "get_object_definition",
10
12
  "has_object_definition",
@@ -9,8 +9,9 @@ from typing import (
9
9
  Optional,
10
10
  Tuple,
11
11
  Type,
12
+ runtime_checkable,
12
13
  )
13
- from typing_extensions import TypedDict
14
+ from typing_extensions import Protocol, TypedDict
14
15
 
15
16
  from graphql import specified_rules
16
17
 
@@ -96,4 +97,18 @@ class ParseOptions(TypedDict):
96
97
  max_tokens: NotRequired[int]
97
98
 
98
99
 
99
- __all__ = ["ExecutionContext", "ExecutionResult", "ParseOptions"]
100
+ @runtime_checkable
101
+ class SubscriptionExecutionResult(Protocol):
102
+ def __aiter__(self) -> SubscriptionExecutionResult: # pragma: no cover
103
+ ...
104
+
105
+ async def __anext__(self) -> Any: # pragma: no cover
106
+ ...
107
+
108
+
109
+ __all__ = [
110
+ "ExecutionContext",
111
+ "ExecutionResult",
112
+ "ParseOptions",
113
+ "SubscriptionExecutionResult",
114
+ ]
@@ -15,7 +15,11 @@ class OperationType(enum.Enum):
15
15
  @staticmethod
16
16
  def from_http(method: HTTPMethod) -> Set[OperationType]:
17
17
  if method == "GET":
18
- return {OperationType.QUERY}
18
+ return {
19
+ OperationType.QUERY,
20
+ # subscriptions are supported via GET in the multipart protocol
21
+ OperationType.SUBSCRIPTION,
22
+ }
19
23
 
20
24
  if method == "POST":
21
25
  return {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strawberry-graphql
3
- Version: 0.238.1
3
+ Version: 0.239.0
4
4
  Summary: A library for creating GraphQL APIs
5
5
  Home-page: https://strawberry.rocks/
6
6
  License: MIT
@@ -6,9 +6,9 @@ strawberry/aiohttp/handlers/graphql_transport_ws_handler.py,sha256=-dihpF3pueV6j
6
6
  strawberry/aiohttp/handlers/graphql_ws_handler.py,sha256=Ol8tBLIeal7bRiG_uxyJXnXo24xMDEh6M5XsuCyUpVQ,2275
7
7
  strawberry/aiohttp/test/__init__.py,sha256=4xxdUZtIISSOwjrcnmox7AvT4WWjowCm5bUuPdQneMg,71
8
8
  strawberry/aiohttp/test/client.py,sha256=4vjTDxtNVfpa74GfUUO7efPI6Ssh1vzfCYp3tPA-SLk,1357
9
- strawberry/aiohttp/views.py,sha256=q1y0zCuLNlwD_o4aosm7dNc-2F-ZJY95kiq1Ip0Prmw,6189
9
+ strawberry/aiohttp/views.py,sha256=aBt5xMZ_ocZfawy0v9EDHO8mtjQu1QUiI_oQ_W3AmaQ,7030
10
10
  strawberry/annotation.py,sha256=ftSxGZQUk4C5_5YRM_wNJFVVnZi0XOp71kD7zv4oxbU,13077
11
- strawberry/asgi/__init__.py,sha256=37w7FeDEmfq2YXKeyyBgjTAliz9ut5Y0-0dVgfE-PAk,6973
11
+ strawberry/asgi/__init__.py,sha256=7jMvOuKS-5V7FLCG4EyGCswAEN8AwjInxEnaVlgy52M,7588
12
12
  strawberry/asgi/handlers/__init__.py,sha256=rz5Gth2eJUn7tDq2--99KSNFeMdDPpLFCkfA7vge0cI,235
13
13
  strawberry/asgi/handlers/graphql_transport_ws_handler.py,sha256=_5N58XdtCZndU81ky1f5cwG9E4NhysxP4uHlqqZNzn4,2147
14
14
  strawberry/asgi/handlers/graphql_ws_handler.py,sha256=KQXmbk7uDwiG1yOadz65GsSzjAPUjKrpEQYAA94nGRM,2379
@@ -21,7 +21,7 @@ strawberry/channels/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
21
21
  strawberry/channels/handlers/base.py,sha256=DlMmzuZl-feceZZ7UHLW-zBSNVxHvaWTqwyWDP-e1Dg,7861
22
22
  strawberry/channels/handlers/graphql_transport_ws_handler.py,sha256=jMWmZ4Haoo3bf_bcM4ZheAjkreuhZBhwMhEX6GZ0Ue4,1995
23
23
  strawberry/channels/handlers/graphql_ws_handler.py,sha256=2W_KpXMmC-LbFLBMMFNarFIMyEBipmIOlQpfnfsyLbE,2538
24
- strawberry/channels/handlers/http_handler.py,sha256=XhP2CUMCcrKjHzwts3FimumghEMVqNXNb9cddbUwC90,9647
24
+ strawberry/channels/handlers/http_handler.py,sha256=rQEdmKnsYE5PTjG1t8AWA8mCC1FsIsvLZnFRiUSHUuc,10959
25
25
  strawberry/channels/handlers/ws_handler.py,sha256=rXWhLxDHSGJHYdCDMb-pckxka-rf4VZqaNSzkagTa6w,4693
26
26
  strawberry/channels/router.py,sha256=DKIbl4zuRBhfvViUVpyu0Rf_WRT41E6uZC-Yic9Ltvo,2024
27
27
  strawberry/channels/testing.py,sha256=f_PcBngLJXRmLftpr0IEoXiJzChsDPaB_ax7CK3oHmQ,6152
@@ -57,7 +57,7 @@ strawberry/django/apps.py,sha256=ZWw3Mzv1Cgy0T9xT8Jr2_dkCTZjT5WQABb34iqnu5pc,135
57
57
  strawberry/django/context.py,sha256=XL85jDGAVnb2pwgm5uRUvIXwlGia3i-8ZVfKihf0T24,655
58
58
  strawberry/django/test/__init__.py,sha256=4xxdUZtIISSOwjrcnmox7AvT4WWjowCm5bUuPdQneMg,71
59
59
  strawberry/django/test/client.py,sha256=6dorWECd0wdn8fu3dabE-dfGK3uza58mGrdJ-xPct-w,626
60
- strawberry/django/views.py,sha256=QIrAZr-XkY-hILokHPPTQkazzh11LXqAzm9d1jSCJKo,9235
60
+ strawberry/django/views.py,sha256=31D1xgtewURv2X5MuAtnsOjrxNhuMNrjYRlhAo_uwT8,9919
61
61
  strawberry/exceptions/__init__.py,sha256=DgdOJUs2xXHWcakr4tN6iIogltPi0MNnpu6MM6K0p5k,6347
62
62
  strawberry/exceptions/conflicting_arguments.py,sha256=68f6kMSXdjuEjZkoe8o2I9PSIjwTS1kXsSGaQBPk_hI,1587
63
63
  strawberry/exceptions/duplicated_type_name.py,sha256=-FG5qG_Mvkd7ROdOxCB9bijf8QR6Olryf07mbAFC0-U,2210
@@ -123,7 +123,7 @@ strawberry/fastapi/context.py,sha256=07991DShQoYMBVyQl9Mh5xvXQSNQI2RXT2ZQ5fWiga4
123
123
  strawberry/fastapi/handlers/__init__.py,sha256=TziBHyibYBOGiidjpCkjNThYbVY7K_nt0hnRLVHVh3I,241
124
124
  strawberry/fastapi/handlers/graphql_transport_ws_handler.py,sha256=5ivH7DJup0ZyGawAcj-n9VE_exBTje9Hou0_GIZCyD4,591
125
125
  strawberry/fastapi/handlers/graphql_ws_handler.py,sha256=OTloVUvEgXDpqjOlBAT1FnaNWRAglQgAt613YH06roc,537
126
- strawberry/fastapi/router.py,sha256=BCPQxLJb6439-KqOD6wj5dDkcY2RaUo7u8pLI1EuSkU,12438
126
+ strawberry/fastapi/router.py,sha256=5_IJZCO5pAH-F5ZsXxy2uwzQhL7HU24iRkZ20poVp60,13010
127
127
  strawberry/federation/__init__.py,sha256=FeUxLiBVuk9TKBmJJi51SeUaI8c80rS8hbl9No-hjII,535
128
128
  strawberry/federation/argument.py,sha256=2m_wgp7uFQDimTDDCBBqSoWeTop4MD1-KjjYrumEJIw,833
129
129
  strawberry/federation/enum.py,sha256=1rx50FiSMS-NjEFErhRSYB5no8TPsGMA_cH7hVbxAFI,3015
@@ -142,18 +142,19 @@ strawberry/file_uploads/__init__.py,sha256=v2-6FGBqnTnMPSUTFOiXpIutDMl-ga0PFtw5t
142
142
  strawberry/file_uploads/scalars.py,sha256=NRDeB7j8aotqIkz9r62ISTf4DrxQxEZYUuHsX5K16aU,161
143
143
  strawberry/file_uploads/utils.py,sha256=2zsXg3QsKgGLD7of2dW-vgQn_Naf7I3Men9PhEAFYwM,1160
144
144
  strawberry/flask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
- strawberry/flask/views.py,sha256=g1UxQUCmDRozzyDWmB3Hb8HbgpB2ZfAcAk2xmlEaGVI,5627
145
+ strawberry/flask/views.py,sha256=Ss3zpBEjY5W6bR9SUPDbuF7zXErgn_qcXm0MuhzPS7k,5656
146
146
  strawberry/http/__init__.py,sha256=lRHuYeDUvz7bpLsvBvTYPOXwD_uMz2LO78QaqGVSvEQ,1546
147
- strawberry/http/async_base_view.py,sha256=yjD9PVgFBn4ccpy396Kuty7GjfRCeBaMnv-Ls42aQF8,7154
148
- strawberry/http/base.py,sha256=Hnut_jIBmVy5jTDltFL7a-3J2RlOKdMNY5r7c_R962w,2246
147
+ strawberry/http/async_base_view.py,sha256=akBjdylFeVCO-tSH_1P8DMV497sLNN65_oBWMnLjmf4,10838
148
+ strawberry/http/base.py,sha256=ORw-0lk6UcOH80kdr4VElAvX75S3VTMpRddhVfooGDY,2569
149
149
  strawberry/http/exceptions.py,sha256=WdWO3RvZDax_yAdD0zlVex9tQgwNx7tjz8_A8kP4YHo,193
150
150
  strawberry/http/ides.py,sha256=3dqFRY8_9ZqyIYR_EyRdPZ1zhL3lxRYT2MPk84O_Tk8,874
151
- strawberry/http/sync_base_view.py,sha256=hZKgQYMeYiPUMmocJKXV0j1tPAEtvTmUJniD9lbf0NA,6849
151
+ strawberry/http/parse_content_type.py,sha256=sgtcOO_ZOFg7WWWibYyLc4SU58K-SErcW56kQczQmKU,412
152
+ strawberry/http/sync_base_view.py,sha256=OrW0PS-yLWoUnxE_xbgQgPHAcvToFTFGP3OH8LryNxs,7164
152
153
  strawberry/http/temporal_response.py,sha256=QrGYSg7Apu7Mh-X_uPKDZby-UicXw2J_ywxaqhS8a_4,220
153
154
  strawberry/http/types.py,sha256=cAuaiUuvaMI_XhZ2Ey6Ej23WyQKqMGFxzzpVHDjVazY,371
154
155
  strawberry/http/typevars.py,sha256=flx5KPWnTwYju7VwRSVhMmx15Rl1pQT1K57_GnK72Hg,282
155
156
  strawberry/litestar/__init__.py,sha256=zsXzg-mglCGUVO9iNXLm-yadoDSCK7k-zuyRqyvAh1w,237
156
- strawberry/litestar/controller.py,sha256=c3Z-XS_eNkTFNvvitsNaJRV0fswkCJju5ACY7ymwLRs,13369
157
+ strawberry/litestar/controller.py,sha256=gMQAhJH-knZHXBybuWmr-aoGQptwYMEyE6G-D4o5ggw,14156
157
158
  strawberry/litestar/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
159
  strawberry/litestar/handlers/graphql_transport_ws_handler.py,sha256=rDE-I02_fCov4FfpdBJBE2Xt-4FSNU8xJuQ6xedMF6E,2050
159
160
  strawberry/litestar/handlers/graphql_ws_handler.py,sha256=iCi2htoSgfk5H59gnw0tMwe9NxodqcaaxSsZ6TkQWYU,2283
@@ -164,7 +165,7 @@ strawberry/printer/ast_from_value.py,sha256=LgM5g2qvBOnAIf9znbiMEcRX0PGSQohR3Vr3
164
165
  strawberry/printer/printer.py,sha256=GntTBivg3fb_zPM41Q8DtWMiRmkmM9xwTF-aFWvnqTg,17524
165
166
  strawberry/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
167
  strawberry/quart/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
- strawberry/quart/views.py,sha256=5Y7JNv3oi9u__iQx2l2fQvz-Ha1H-ZhhYslaFsNUIs0,3432
168
+ strawberry/quart/views.py,sha256=GddrI25IFC43kZAjcmK0PfbPhdQlJAYFhA4-66Q77E0,3974
168
169
  strawberry/relay/__init__.py,sha256=Vi4btvA_g6Cj9Tk_F9GCSegapIf2WqkOWV8y3P0cTCs,553
169
170
  strawberry/relay/exceptions.py,sha256=KZSRJYlfutrAQALtBPnzJHRIMK6GZSnKAT_H4wIzGcI,4035
170
171
  strawberry/relay/fields.py,sha256=qrpDxDQ_bdzDUKtd8gxo9P3Oermxbux3yjFvHvHC1Ho,16927
@@ -174,16 +175,16 @@ strawberry/resolvers.py,sha256=Vdidc3YFc4-olSQZD_xQ1icyAFbyzqs_8I3eSpMFlA4,260
174
175
  strawberry/sanic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
175
176
  strawberry/sanic/context.py,sha256=qN7I9K_qIqgdbG_FbDl8XMb9aM1PyjIxSo8IAg2Uq8o,844
176
177
  strawberry/sanic/utils.py,sha256=r-tCX0JzELAdupF7fFy65JWAxj6pABSntNbGNaGZT8o,1080
177
- strawberry/sanic/views.py,sha256=Jxgqi9hz_26GFGaPaAB0lmVlYJZMNYNyqE7vIfhKtVk,5523
178
+ strawberry/sanic/views.py,sha256=q73BNJ_PVmnM-7_z3UwwPPpGQpQc_MQX2yI1l64__Nw,6552
178
179
  strawberry/scalars.py,sha256=c3y8EOmX-KUxSgRqk1TNercMA6_rgBHhQPp0z3C2zBU,2240
179
180
  strawberry/schema/__init__.py,sha256=u1QCyDVQExUVDA20kyosKPz3TS5HMCN2NrXclhiFAL4,92
180
- strawberry/schema/base.py,sha256=eH0nyrCKwdtvgzLS810LMTffpY5CsRd9qY-uZs9pZ3U,3644
181
+ strawberry/schema/base.py,sha256=y6nhXZ_-Oid79rS5uLLYn_azlZ3PzTOLGcO_7W5lovo,3742
181
182
  strawberry/schema/compat.py,sha256=rRqUm5-XgPXC018_u0Mrd4iad7tTRCNA45Ko4NaT6gk,1836
182
183
  strawberry/schema/config.py,sha256=Aa01oqnHb0ZPlw8Ti_O840LxlT827LNio15BQrc37A0,717
183
184
  strawberry/schema/exceptions.py,sha256=rqVNb_oYrKM0dHPgvAemqCG6Um282LPPu4zwQ5cZqs4,584
184
- strawberry/schema/execute.py,sha256=74eGjXrptVO_EP-luZmSHUeXtt3Lk5BbEoZv9v7w0jo,10840
185
+ strawberry/schema/execute.py,sha256=rLPY2F-VG0ZkAyg935UHdH0cKhDd6YO98MDyXko2_WA,11702
185
186
  strawberry/schema/name_converter.py,sha256=tpqw2XCSFvJI-H844iWhE2Z1sKic7DrjIZxt11eJN5Y,6574
186
- strawberry/schema/schema.py,sha256=ySwPFbUWhoo8G6GLZjsF9kxvQ9jGmw8ibYZFOTtwZRU,15896
187
+ strawberry/schema/schema.py,sha256=53F-4YCGVIiIf_YYLoBo1UC3JIv0HZ73SvTknk2PXyk,15961
187
188
  strawberry/schema/schema_converter.py,sha256=lckL2LoxAb6mNfJIVcerht2buzBG573ly3BHyl7wra4,36859
188
189
  strawberry/schema/types/__init__.py,sha256=oHO3COWhL3L1KLYCJNY1XFf5xt2GGtHiMC-UaYbFfnA,68
189
190
  strawberry/schema/types/base_scalars.py,sha256=NTj_tYqWLQLEOPDhBhSE1My4JXoieyg0jO8B6RNK-xA,1913
@@ -200,7 +201,7 @@ strawberry/static/pathfinder.html,sha256=0DPx9AmJ2C_sJstFXnWOz9k5tVQHeHaK7qdVY4l
200
201
  strawberry/subscriptions/__init__.py,sha256=1VGmiCzFepqRFyCikagkUoHHdoTG3XYlFu9GafoQMws,170
201
202
  strawberry/subscriptions/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
202
203
  strawberry/subscriptions/protocols/graphql_transport_ws/__init__.py,sha256=wN6dkMu6WiaIZTE19PGoN9xXpIN_RdDE_q7F7ZgjCxk,138
203
- strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py,sha256=P56QgUkiDuwoRHl8WKbtssDShQbgKuMY0FXYAPY6VgE,15133
204
+ strawberry/subscriptions/protocols/graphql_transport_ws/handlers.py,sha256=U8pZrdJeFQFjviZHdnBMJY5ofYcEzknMdqAkcylaflE,15149
204
205
  strawberry/subscriptions/protocols/graphql_transport_ws/types.py,sha256=rJQWeNcsHevAuxYYocz4F6DO3PR-IgWF_KYsx5VWN4s,2420
205
206
  strawberry/subscriptions/protocols/graphql_ws/__init__.py,sha256=ijn1A1O0Fzv5p9n-jw3T5H7M3oxbN4gbxRaepN7HyJk,553
206
207
  strawberry/subscriptions/protocols/graphql_ws/handlers.py,sha256=zptIxudunKCf-5FEB51n7t7KN0nRzSko6_-ya0gpYD4,7623
@@ -210,16 +211,16 @@ strawberry/test/client.py,sha256=-V_5DakR0NJ_Kd5ww4leIhMnIJ-Pf274HkNqYtGVfl8,604
210
211
  strawberry/tools/__init__.py,sha256=pdGpZx8wpq03VfUZJyF9JtYxZhGqzzxCiipsalWxJX4,127
211
212
  strawberry/tools/create_type.py,sha256=jY-6J4VTSrzqc-NbDZpNv7U9L1j1aun5KNGTaGgNcls,2041
212
213
  strawberry/tools/merge_types.py,sha256=noMPqGfbqUaMYMwT_db1rUMaPmmR_uHeFwx7Zd0rivE,1048
213
- strawberry/types/__init__.py,sha256=RRsWKBzsY2x1y1MKAYW0eMQRhE0AycomSoW-dpPPZXo,275
214
+ strawberry/types/__init__.py,sha256=8poIjLQ1cG2adyP0X6X6dLAVdxBGPebiGGl2vBoM4o8,351
214
215
  strawberry/types/arguments.py,sha256=vGWwKL_294rrZtg-GquW6h5t0FBAEm8Y14bD5_08FeI,9439
215
216
  strawberry/types/auto.py,sha256=c7XcB7seXd-cWAn5Ut3O0SUPOOCPG-Z2qcUAI9dRQIE,3008
216
217
  strawberry/types/base.py,sha256=W5OvqH0gnhkFaCcthTAOciUGTQhkWTnPdz4DzMfql5Y,15022
217
218
  strawberry/types/enum.py,sha256=E_ck94hgZP6YszWAso9UvCLUKRhd5KclwnIvo5PibZg,5965
218
- strawberry/types/execution.py,sha256=SoU_imuH9bu1onB9btkYfpB-zUgHES_E_M_WniraEXk,2851
219
+ strawberry/types/execution.py,sha256=qrWygrssnomKOaBbcDUOnpppogKEeJrdKpimnC0Ex9M,3159
219
220
  strawberry/types/field.py,sha256=jgliRsA4BLNKXXaj5fC7pBI8_USnTVjOF9OEVMV-hYk,21598
220
221
  strawberry/types/fields/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
222
  strawberry/types/fields/resolver.py,sha256=eDVUsNwteKZOK8fKPwESAz4hOAeDIhalH5BgHmqab_Y,14361
222
- strawberry/types/graphql.py,sha256=Hnt7RplzBdcthNYxYB4PA3LqffG99ICQSgSrDa28bJQ,717
223
+ strawberry/types/graphql.py,sha256=DNoXY42SJZHS-z91vGydWIKZ_FoacSbxPQZQ7BFeOSI,872
223
224
  strawberry/types/info.py,sha256=Y5nVRPJBwKaqT8CtSrMGpWdsgT_eW88dJXNwMAKNG0k,4750
224
225
  strawberry/types/lazy_type.py,sha256=sDQZYR-rEqVhsiBsnyY7y2qcC21luJgtJqrc0MbsnQM,5098
225
226
  strawberry/types/mutation.py,sha256=bS2WotWenRP-lKJazPIvuzpD4sfS3Rp5leq7-DPNS_I,11975
@@ -243,8 +244,8 @@ strawberry/utils/logging.py,sha256=U1cseHGquN09YFhFmRkiphfASKCyK0HUZREImPgVb0c,7
243
244
  strawberry/utils/operation.py,sha256=SSXxN-vMqdHO6W2OZtip-1z7y4_A-eTVFdhDvhKeLCk,1193
244
245
  strawberry/utils/str_converters.py,sha256=KGd7QH90RevaJjH6SQEkiVVsb8KuhJr_wv5AsI7UzQk,897
245
246
  strawberry/utils/typing.py,sha256=tUHHX2YTGX417EEQHB6j0B8-p-fg31ZI8csc9SUoq2I,14260
246
- strawberry_graphql-0.238.1.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
247
- strawberry_graphql-0.238.1.dist-info/METADATA,sha256=ruddJlJrH2DZIITGIVRKLD7wl9M9CfrIHIgtnZrKJUg,7707
248
- strawberry_graphql-0.238.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
249
- strawberry_graphql-0.238.1.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
250
- strawberry_graphql-0.238.1.dist-info/RECORD,,
247
+ strawberry_graphql-0.239.0.dist-info/LICENSE,sha256=m-XnIVUKqlG_AWnfi9NReh9JfKhYOB-gJfKE45WM1W8,1072
248
+ strawberry_graphql-0.239.0.dist-info/METADATA,sha256=UFNRnyQUMG9QbrayCz1BSLfI5iHibPp8OtEDJ4jnLfE,7707
249
+ strawberry_graphql-0.239.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
250
+ strawberry_graphql-0.239.0.dist-info/entry_points.txt,sha256=Nk7-aT3_uEwCgyqtHESV9H6Mc31cK-VAvhnQNTzTb4k,49
251
+ strawberry_graphql-0.239.0.dist-info/RECORD,,