vercel-queue 0.7.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.
Files changed (43) hide show
  1. vercel/queue/__init__.py +8 -0
  2. vercel/queue/__main__.py +10 -0
  3. vercel/queue/_internal/__init__.py +1 -0
  4. vercel/queue/_internal/api_async.py +219 -0
  5. vercel/queue/_internal/api_common.py +104 -0
  6. vercel/queue/_internal/api_sync.py +137 -0
  7. vercel/queue/_internal/asgi.py +145 -0
  8. vercel/queue/_internal/asynctools.py +40 -0
  9. vercel/queue/_internal/cli.py +313 -0
  10. vercel/queue/_internal/client.py +1109 -0
  11. vercel/queue/_internal/client_sync.py +432 -0
  12. vercel/queue/_internal/config.py +160 -0
  13. vercel/queue/_internal/constants.py +50 -0
  14. vercel/queue/_internal/devserver.py +205 -0
  15. vercel/queue/_internal/embedded.py +1900 -0
  16. vercel/queue/_internal/errors.py +262 -0
  17. vercel/queue/_internal/http.py +634 -0
  18. vercel/queue/_internal/lease.py +1007 -0
  19. vercel/queue/_internal/log.py +143 -0
  20. vercel/queue/_internal/messages.py +122 -0
  21. vercel/queue/_internal/multipart.py +255 -0
  22. vercel/queue/_internal/names.py +101 -0
  23. vercel/queue/_internal/polling.py +200 -0
  24. vercel/queue/_internal/push.py +246 -0
  25. vercel/queue/_internal/response.py +111 -0
  26. vercel/queue/_internal/retry.py +86 -0
  27. vercel/queue/_internal/streams.py +313 -0
  28. vercel/queue/_internal/subscribers.py +1025 -0
  29. vercel/queue/_internal/transports.py +363 -0
  30. vercel/queue/_internal/types.py +300 -0
  31. vercel/queue/_internal/typeutils.py +203 -0
  32. vercel/queue/devserver.py +24 -0
  33. vercel/queue/embedded.py +50 -0
  34. vercel/queue/py.typed +1 -0
  35. vercel/queue/sync.py +8 -0
  36. vercel/queue/testing/__init__.py +14 -0
  37. vercel/queue/testing/pytest.py +42 -0
  38. vercel/queue/testing/state.py +32 -0
  39. vercel/queue/version.py +3 -0
  40. vercel_queue-0.7.0.dist-info/METADATA +681 -0
  41. vercel_queue-0.7.0.dist-info/RECORD +43 -0
  42. vercel_queue-0.7.0.dist-info/WHEEL +4 -0
  43. vercel_queue-0.7.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,8 @@
1
+ """Vercel Queue client APIs."""
2
+ # ruff: noqa: F403
3
+
4
+ from ._internal import api_async as _api_async, api_common as _api_common
5
+ from ._internal.api_async import *
6
+ from ._internal.api_common import *
7
+
8
+ __all__ = _api_async.__all__ + _api_common.__all__
@@ -0,0 +1,10 @@
1
+ """Command-line entry point for ``python -m vercel.queue``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from ._internal.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Internal implementation for vercel.queue."""
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar, overload
4
+
5
+ from collections.abc import AsyncIterator, Mapping
6
+
7
+ from .client import Delivery, LeaseRenewal, QueueClient
8
+ from .config import (
9
+ CURRENT_DEPLOYMENT,
10
+ DeploymentOption,
11
+ )
12
+ from .http import (
13
+ AsyncHttpMessage,
14
+ AsyncPushDeliveryBody,
15
+ )
16
+ from .names import SanitizedName
17
+ from .subscribers import QueueSubscriber
18
+ from .types import (
19
+ Duration,
20
+ MessageID,
21
+ RawHeaders,
22
+ StrContainer,
23
+ Topic,
24
+ )
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ async def send(
30
+ topic: str | SanitizedName | Topic[T],
31
+ payload: T,
32
+ *,
33
+ idempotency_key: str | None = None,
34
+ retention: Duration | None = None,
35
+ delay: Duration | None = None,
36
+ deployment: DeploymentOption = CURRENT_DEPLOYMENT,
37
+ headers: Mapping[str, str] | None = None,
38
+ ) -> MessageID | None:
39
+ """Send a message with the default asynchronous client.
40
+
41
+ Args:
42
+ topic: Topic object or topic name.
43
+ payload: Payload accepted by the topic or inferred transport.
44
+ idempotency_key: Optional service-side deduplication key.
45
+ retention: Optional message retention duration.
46
+ delay: Optional delay before the message becomes visible.
47
+ deployment: Per-send deployment partition selection.
48
+ headers: Custom non-protected headers to include.
49
+
50
+ Returns:
51
+ Created message ID, or ``None`` if ingestion was deferred.
52
+
53
+ Raises:
54
+ QueueError: If the service rejects the request.
55
+
56
+ """
57
+ client = QueueClient()
58
+ return await client.send(
59
+ topic,
60
+ payload,
61
+ idempotency_key=idempotency_key,
62
+ retention=retention,
63
+ delay=delay,
64
+ deployment=deployment,
65
+ headers=headers,
66
+ )
67
+
68
+
69
+ @overload
70
+ async def accept_and_handle(
71
+ raw_body: AsyncPushDeliveryBody,
72
+ headers: RawHeaders,
73
+ *,
74
+ lease_duration: Duration | None = None,
75
+ ) -> None: ...
76
+
77
+
78
+ @overload
79
+ async def accept_and_handle(
80
+ raw_body: AsyncHttpMessage,
81
+ headers: None = None,
82
+ *,
83
+ lease_duration: Duration | None = None,
84
+ ) -> None: ...
85
+
86
+
87
+ async def accept_and_handle(
88
+ raw_body: AsyncPushDeliveryBody | AsyncHttpMessage,
89
+ headers: RawHeaders | None = None,
90
+ *,
91
+ lease_duration: Duration | None = None,
92
+ ) -> None:
93
+ """Accept a push callback and dispatch async subscribers.
94
+
95
+ Args:
96
+ raw_body: Callback body bytes, byte iterable, or response object.
97
+ headers: Callback request headers, unless ``raw_body`` is a response.
98
+ lease_duration: Processing timeout used while handlers run.
99
+
100
+ Raises:
101
+ UnhandledMessageError: If no subscription matches the topic.
102
+ QueueError: If fetching, acknowledging, or retry scheduling fails.
103
+
104
+ """
105
+ client = QueueClient()
106
+ await client._accept_and_handle( # noqa: SLF001
107
+ raw_body,
108
+ headers,
109
+ lease_duration=lease_duration,
110
+ )
111
+
112
+
113
+ @overload
114
+ def poll(
115
+ topic: Topic[T],
116
+ consumer_group: str | SanitizedName,
117
+ *,
118
+ limit: int = 1,
119
+ lease_duration: Duration | None = None,
120
+ ) -> AsyncIterator[Delivery[T]]: ...
121
+
122
+
123
+ @overload
124
+ def poll(
125
+ topic: str,
126
+ consumer_group: str | SanitizedName,
127
+ *,
128
+ limit: int = 1,
129
+ lease_duration: Duration | None = None,
130
+ ) -> AsyncIterator[Delivery[Any]]: ...
131
+
132
+
133
+ @overload
134
+ def poll(
135
+ topic: SanitizedName,
136
+ consumer_group: str | SanitizedName,
137
+ *,
138
+ limit: int = 1,
139
+ lease_duration: Duration | None = None,
140
+ ) -> AsyncIterator[Delivery[Any]]: ...
141
+
142
+
143
+ def poll(
144
+ topic: str | SanitizedName | Topic[T],
145
+ consumer_group: str | SanitizedName,
146
+ *,
147
+ limit: int = 1,
148
+ lease_duration: Duration | None = None,
149
+ ) -> AsyncIterator[Delivery[T]]:
150
+ """Poll available deliveries with the default asynchronous client.
151
+
152
+ Args:
153
+ topic: Topic object or topic name to receive from.
154
+ consumer_group: Consumer group to receive as.
155
+ limit: Maximum messages to claim, from 1 through 10.
156
+ lease_duration: Optional processing timeout for received messages.
157
+
158
+ Returns:
159
+ Async iterator of deliveries. Enter each delivery to process its message.
160
+
161
+ Raises:
162
+ InvalidLimitError: If ``limit`` is outside the service range.
163
+ QueueError: If the service rejects the request.
164
+
165
+ """
166
+
167
+ async def _iterate() -> AsyncIterator[Delivery[T]]:
168
+ client = QueueClient()
169
+ async for delivery in client.poll(
170
+ topic,
171
+ consumer_group,
172
+ limit=limit,
173
+ lease_duration=lease_duration,
174
+ ):
175
+ yield delivery
176
+
177
+ return _iterate()
178
+
179
+
180
+ async def poll_and_handle(
181
+ subscriber: QueueSubscriber[..., Any],
182
+ *,
183
+ topics: StrContainer | None = None,
184
+ interval: Duration = 1.0,
185
+ limit: int | None = None,
186
+ lease_duration: Duration | None = None,
187
+ ) -> None:
188
+ """Continuously poll messages with the default async client.
189
+
190
+ Args:
191
+ subscriber: Callback previously registered with ``@subscribe``.
192
+ topics: Concrete topic names to poll. Required for wildcard or prefix
193
+ subscription patterns; exact subscriptions infer their topic.
194
+ interval: Idle backoff when all configured topics are empty.
195
+ limit: Per-request maximum from 1 through 10. ``None`` drains until
196
+ empty before idling.
197
+ lease_duration: Optional processing timeout for received messages.
198
+
199
+ """
200
+ client = QueueClient()
201
+ await client.poll_and_handle(
202
+ subscriber,
203
+ topics=topics,
204
+ interval=interval,
205
+ limit=limit,
206
+ lease_duration=lease_duration,
207
+ )
208
+
209
+
210
+ # Only add public symbols to __all__; internal helpers must stay unexported.
211
+ __all__: tuple[str, ...] = (
212
+ "Delivery",
213
+ "LeaseRenewal",
214
+ "QueueClient",
215
+ "accept_and_handle",
216
+ "poll",
217
+ "poll_and_handle",
218
+ "send",
219
+ )
@@ -0,0 +1,104 @@
1
+ """Common public Vercel Queue API exports."""
2
+ # ruff: noqa: F403, F405
3
+
4
+ from .. import version as _version
5
+ from . import (
6
+ asgi as _asgi,
7
+ config as _config,
8
+ names as _names,
9
+ transports as _transports,
10
+ types as _types,
11
+ )
12
+ from .errors import *
13
+ from .subscribers import QueueSubscriber, Subscription, get_subscriptions, subscribe
14
+
15
+ ALL_DEPLOYMENTS = _config.ALL_DEPLOYMENTS
16
+ CURRENT_DEPLOYMENT = _config.CURRENT_DEPLOYMENT
17
+ AllDeployments = _config.AllDeployments
18
+ ByteBufferTransport = _transports.ByteBufferTransport
19
+ ByteStreamTransport = _transports.ByteStreamTransport
20
+ CurrentDeployment = _config.CurrentDeployment
21
+ DeploymentID = _config.DeploymentID
22
+ DeploymentOption = _config.DeploymentOption
23
+ Duration = _types.Duration
24
+ Handoff = _types.Handoff
25
+ Message = _types.Message
26
+ MessageID = _types.MessageID
27
+ MessageMetadata = _types.MessageMetadata
28
+ QueueClientAsgiApp = _asgi.QueueClientAsgiApp
29
+ QueueDirective = _types.QueueDirective
30
+ RawJsonTransport = _transports.RawJsonTransport
31
+ ReceiptHandle = _types.ReceiptHandle
32
+ RetryAfter = _types.RetryAfter
33
+ SanitizedName = _names.SanitizedName
34
+ StrContainer = _types.StrContainer
35
+ TextBufferTransport = _transports.TextBufferTransport
36
+ TextStreamTransport = _transports.TextStreamTransport
37
+ Topic = _types.Topic
38
+ TypedJsonTransport = _transports.TypedJsonTransport
39
+ __version__ = _version.__version__
40
+ asgi_app = _asgi.asgi_app
41
+ sanitize_name = _names.sanitize_name
42
+
43
+ # Only add public symbols to __all__; internal helpers must stay unexported.
44
+ __all__ = (
45
+ "ALL_DEPLOYMENTS",
46
+ "CURRENT_DEPLOYMENT",
47
+ "AllDeployments",
48
+ "BadRequestError",
49
+ "ByteBufferTransport",
50
+ "ByteStreamTransport",
51
+ "CommunicationError",
52
+ "ConsumerDiscoveryError",
53
+ "ConsumerRegistryNotConfiguredError",
54
+ "CurrentDeployment",
55
+ "DeploymentID",
56
+ "DeploymentOption",
57
+ "DeploymentResolutionError",
58
+ "DuplicateIdempotencyKeyError",
59
+ "DuplicateSubscriptionError",
60
+ "Duration",
61
+ "ForbiddenError",
62
+ "Handoff",
63
+ "InternalServerError",
64
+ "InvalidLimitError",
65
+ "Message",
66
+ "MessageAlreadyProcessedError",
67
+ "MessageCorruptedError",
68
+ "MessageID",
69
+ "MessageLeaseExpiredError",
70
+ "MessageLockedError",
71
+ "MessageMetadata",
72
+ "MessageNotFoundError",
73
+ "MessageNotInFlightError",
74
+ "MessageUnavailableError",
75
+ "PayloadValidationError",
76
+ "ProtocolError",
77
+ "QueueClientAsgiApp",
78
+ "QueueDirective",
79
+ "QueueError",
80
+ "QueueSubscriber",
81
+ "RawJsonTransport",
82
+ "ReceiptHandle",
83
+ "ReceiptHandleMismatchError",
84
+ "RetryAfter",
85
+ "RetryableError",
86
+ "SanitizedName",
87
+ "ServiceError",
88
+ "StrContainer",
89
+ "Subscription",
90
+ "SubscriptionError",
91
+ "TextBufferTransport",
92
+ "TextStreamTransport",
93
+ "ThrottledError",
94
+ "TokenResolutionError",
95
+ "Topic",
96
+ "TypedJsonTransport",
97
+ "UnauthorizedError",
98
+ "UnhandledMessageError",
99
+ "__version__",
100
+ "asgi_app",
101
+ "get_subscriptions",
102
+ "sanitize_name",
103
+ "subscribe",
104
+ )
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar, overload
4
+
5
+ from collections.abc import Iterator, Mapping
6
+
7
+ from .client import Delivery
8
+ from .client_sync import LeaseRenewal, QueueClient
9
+ from .config import CURRENT_DEPLOYMENT, DeploymentOption
10
+ from .http import HttpResponse, PushDeliveryBody
11
+ from .names import SanitizedName
12
+ from .types import (
13
+ Duration,
14
+ MessageID,
15
+ RawHeaders,
16
+ Topic,
17
+ )
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ def send(
23
+ topic: str | SanitizedName | Topic[T],
24
+ payload: T,
25
+ *,
26
+ idempotency_key: str | None = None,
27
+ retention: Duration | None = None,
28
+ delay: Duration | None = None,
29
+ deployment: DeploymentOption = CURRENT_DEPLOYMENT,
30
+ headers: Mapping[str, str] | None = None,
31
+ ) -> MessageID | None:
32
+ """Send a message with the default synchronous client."""
33
+ client = QueueClient()
34
+ return client.send(
35
+ topic,
36
+ payload,
37
+ idempotency_key=idempotency_key,
38
+ retention=retention,
39
+ delay=delay,
40
+ deployment=deployment,
41
+ headers=headers,
42
+ )
43
+
44
+
45
+ @overload
46
+ def accept_and_handle(
47
+ raw_body: PushDeliveryBody,
48
+ headers: RawHeaders,
49
+ *,
50
+ lease_duration: Duration | None = None,
51
+ ) -> None: ...
52
+
53
+
54
+ @overload
55
+ def accept_and_handle(
56
+ raw_body: HttpResponse,
57
+ headers: None = None,
58
+ *,
59
+ lease_duration: Duration | None = None,
60
+ ) -> None: ...
61
+
62
+
63
+ def accept_and_handle(
64
+ raw_body: PushDeliveryBody | HttpResponse,
65
+ headers: RawHeaders | None = None,
66
+ *,
67
+ lease_duration: Duration | None = None,
68
+ ) -> None:
69
+ """Accept a push callback and dispatch sync subscribers."""
70
+ client = QueueClient()
71
+ client._accept_and_handle( # noqa: SLF001
72
+ raw_body,
73
+ headers,
74
+ lease_duration=lease_duration,
75
+ )
76
+
77
+
78
+ @overload
79
+ def poll(
80
+ topic: Topic[T],
81
+ consumer_group: str | SanitizedName,
82
+ *,
83
+ limit: int = 1,
84
+ lease_duration: Duration | None = None,
85
+ ) -> Iterator[Delivery[T]]: ...
86
+
87
+
88
+ @overload
89
+ def poll(
90
+ topic: str,
91
+ consumer_group: str | SanitizedName,
92
+ *,
93
+ limit: int = 1,
94
+ lease_duration: Duration | None = None,
95
+ ) -> Iterator[Delivery[Any]]: ...
96
+
97
+
98
+ @overload
99
+ def poll(
100
+ topic: SanitizedName,
101
+ consumer_group: str | SanitizedName,
102
+ *,
103
+ limit: int = 1,
104
+ lease_duration: Duration | None = None,
105
+ ) -> Iterator[Delivery[Any]]: ...
106
+
107
+
108
+ def poll(
109
+ topic: str | SanitizedName | Topic[Any],
110
+ consumer_group: str | SanitizedName,
111
+ *,
112
+ limit: int = 1,
113
+ lease_duration: Duration | None = None,
114
+ ) -> Iterator[Delivery[Any]]:
115
+ """Poll available deliveries with the default synchronous client."""
116
+
117
+ def _iterate() -> Iterator[Delivery[Any]]:
118
+ client = QueueClient()
119
+ yield from client.poll(
120
+ topic,
121
+ consumer_group,
122
+ limit=limit,
123
+ lease_duration=lease_duration,
124
+ )
125
+
126
+ return _iterate()
127
+
128
+
129
+ # Only add public symbols to __all__; internal helpers must stay unexported.
130
+ __all__ = (
131
+ "Delivery",
132
+ "LeaseRenewal",
133
+ "QueueClient",
134
+ "accept_and_handle",
135
+ "poll",
136
+ "send",
137
+ )
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeAlias, TypedDict
4
+
5
+ import logging
6
+ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
7
+
8
+ from vercel.headers import HeadersContext, headers_from_asgi_scope
9
+
10
+ from .client import QueueClient
11
+ from .config import CURRENT_DEPLOYMENT, BaseUrl, DeploymentOption
12
+ from .errors import ProtocolError
13
+ from .http import AsyncHttpClientFactory
14
+ from .log import configure_asgi_logger, debug_log
15
+ from .types import Duration
16
+
17
+ logger = logging.getLogger("vercel.queue")
18
+
19
+
20
+ class AsgiScope(TypedDict, total=False):
21
+ type: str
22
+ method: str
23
+ headers: list[tuple[bytes, bytes]]
24
+
25
+
26
+ AsgiMessage: TypeAlias = dict[str, Any]
27
+ AsgiReceive: TypeAlias = Callable[[], Awaitable[AsgiMessage]]
28
+ AsgiSend: TypeAlias = Callable[[AsgiMessage], Awaitable[None]]
29
+
30
+
31
+ class QueueClientAsgiApp:
32
+ """ASGI push-callback app backed by an async queue client."""
33
+
34
+ def __init__(self, client: QueueClient) -> None:
35
+ configure_asgi_logger()
36
+ self.client = client
37
+
38
+ async def __call__(
39
+ self,
40
+ scope: AsgiScope,
41
+ receive: AsgiReceive,
42
+ send: AsgiSend,
43
+ ) -> None:
44
+ scope_type = scope.get("type")
45
+ if scope_type == "http":
46
+ await self._handle_http(scope, receive, send)
47
+ return
48
+ if scope_type == "lifespan":
49
+ await self._handle_lifespan(receive, send)
50
+ return
51
+ raise RuntimeError(f"Unsupported ASGI scope type: {scope_type!r}")
52
+
53
+ async def _handle_http(
54
+ self,
55
+ scope: AsgiScope,
56
+ receive: AsgiReceive,
57
+ send: AsgiSend,
58
+ ) -> None:
59
+ if scope.get("method") != "POST":
60
+ await _send_status(send, 405)
61
+ return
62
+
63
+ headers = headers_from_asgi_scope(scope)
64
+ try:
65
+ with HeadersContext(headers).use():
66
+ await self.client.accept_and_handle(
67
+ _body_chunks(receive),
68
+ headers,
69
+ )
70
+ except (ProtocolError, TypeError, ValueError) as exc:
71
+ debug_log("asgi.bad_request", error=repr(exc))
72
+ logger.warning("Vercel Queue push callback rejected: %s", exc)
73
+ await _send_status(send, 400)
74
+ except Exception as exc:
75
+ debug_log("asgi.delivery_failure", error=repr(exc))
76
+ logger.exception("Vercel Queue push callback failed")
77
+ await _send_status(send, 500)
78
+ else:
79
+ await _send_status(send, 204)
80
+
81
+ async def _handle_lifespan(self, receive: AsgiReceive, send: AsgiSend) -> None:
82
+ while True:
83
+ message = await receive()
84
+ message_type = message.get("type")
85
+ if message_type == "lifespan.startup":
86
+ await send({"type": "lifespan.startup.complete"})
87
+ elif message_type == "lifespan.shutdown":
88
+ await send({"type": "lifespan.shutdown.complete"})
89
+ return
90
+
91
+
92
+ def asgi_app(
93
+ *,
94
+ client: QueueClient | None = None,
95
+ token: str | None = None,
96
+ region: str | None = None,
97
+ base_url: BaseUrl | None = None,
98
+ deployment: DeploymentOption = CURRENT_DEPLOYMENT,
99
+ headers: Mapping[str, str] | None = None,
100
+ timeout: Duration | None = 10.0,
101
+ http_client_factory: AsyncHttpClientFactory | None = None,
102
+ ) -> QueueClientAsgiApp:
103
+ """Create an ASGI queue push-callback app."""
104
+ configure_asgi_logger()
105
+ resolved_client = client or QueueClient(
106
+ token=token,
107
+ region=region,
108
+ base_url=base_url,
109
+ deployment=deployment,
110
+ headers=headers,
111
+ timeout=timeout,
112
+ http_client_factory=http_client_factory,
113
+ )
114
+ return QueueClientAsgiApp(resolved_client)
115
+
116
+
117
+ async def _body_chunks(receive: AsgiReceive) -> AsyncIterator[bytes]:
118
+ while True:
119
+ message = await receive()
120
+ message_type = message.get("type")
121
+ if message_type == "http.disconnect":
122
+ raise ValueError("request body disconnected before completion")
123
+ if message_type != "http.request":
124
+ raise ValueError(f"unexpected ASGI message: {message_type!r}")
125
+ body = message.get("body", b"")
126
+ if body:
127
+ yield body
128
+ if not message.get("more_body", False):
129
+ return
130
+
131
+
132
+ async def _send_status(send: AsgiSend, status: int) -> None:
133
+ headers: list[tuple[bytes, bytes]] = []
134
+ if status == 405:
135
+ headers.append((b"allow", b"POST"))
136
+ await send({
137
+ "type": "http.response.start",
138
+ "status": status,
139
+ "headers": headers,
140
+ })
141
+ await send({"type": "http.response.body", "body": b""})
142
+
143
+
144
+ # Only add public symbols to __all__; internal helpers must stay unexported.
145
+ __all__ = ("QueueClientAsgiApp", "asgi_app")
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeGuard, TypeVar
4
+
5
+ from collections.abc import AsyncIterator, Awaitable, Coroutine, Iterable, Iterator
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ def _is_coroutine(value: Awaitable[T]) -> TypeGuard[Coroutine[Any, Any, T]]:
11
+ return isinstance(value, Coroutine)
12
+
13
+
14
+ def iter_coroutine(coro: Coroutine[Any, Any, T]) -> T:
15
+ """Execute a non-suspending coroutine synchronously."""
16
+ try:
17
+ coro.send(None)
18
+ except StopIteration as exc:
19
+ return exc.value
20
+ else:
21
+ raise RuntimeError(f"coroutine {coro!r} did not stop after one iteration!")
22
+ finally:
23
+ coro.close()
24
+
25
+
26
+ def iter_async_iterator(iterator: AsyncIterator[T]) -> Iterator[T]:
27
+ """Iterate over a non-suspending async iterator synchronously."""
28
+ while True:
29
+ try:
30
+ next_item = anext(iterator)
31
+ if not _is_coroutine(next_item):
32
+ raise TypeError("async iterator __anext__() must return a coroutine")
33
+ yield iter_coroutine(next_item)
34
+ except StopAsyncIteration:
35
+ return
36
+
37
+
38
+ async def iter_bytes_async(payload: Iterable[bytes]) -> AsyncIterator[bytes]:
39
+ for chunk in payload:
40
+ yield chunk