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,203 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ Annotated,
5
+ Any,
6
+ ClassVar,
7
+ Final,
8
+ ForwardRef,
9
+ Literal,
10
+ TypeVar,
11
+ Union,
12
+ get_args,
13
+ get_origin,
14
+ get_type_hints,
15
+ )
16
+
17
+ import inspect
18
+ import types
19
+ from dataclasses import dataclass
20
+ from types import FrameType
21
+
22
+ _T = TypeVar("_T")
23
+ _TYPE_VAR_TYPE = type(_T)
24
+
25
+
26
+ class TypeAnnotationResolutionError(TypeError):
27
+ """Raised when a runtime annotation cannot be resolved."""
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ResolvedAnnotation:
32
+ annotation: Any
33
+ localns: dict[str, Any] | None = None
34
+
35
+
36
+ def is_generic_alias(tp: Any) -> bool:
37
+ return get_origin(tp) is not None
38
+
39
+
40
+ def is_annotated(tp: Any) -> bool:
41
+ return get_origin(tp) is Annotated
42
+
43
+
44
+ def strip_annotated(tp: Any) -> Any:
45
+ while is_annotated(tp):
46
+ tp = get_args(tp)[0]
47
+ return tp
48
+
49
+
50
+ def is_type_var(tp: Any) -> bool:
51
+ return isinstance(tp, _TYPE_VAR_TYPE)
52
+
53
+
54
+ def is_classvar(tp: Any) -> bool:
55
+ return get_origin(tp) is ClassVar
56
+
57
+
58
+ def is_final(tp: Any) -> bool:
59
+ return get_origin(tp) is Final
60
+
61
+
62
+ def is_union_type(tp: Any) -> bool:
63
+ return get_origin(tp) in {Union, types.UnionType}
64
+
65
+
66
+ def origin_is(tp: Any, *origins: Any) -> bool:
67
+ return get_origin(tp) in origins
68
+
69
+
70
+ def args(tp: Any) -> tuple[Any, ...]:
71
+ return get_args(tp)
72
+
73
+
74
+ def annotation_needs_resolution(annotation: Any) -> bool:
75
+ if isinstance(annotation, str | ForwardRef):
76
+ return True
77
+ if is_type_var(annotation):
78
+ return False
79
+ if origin_is(annotation, Literal):
80
+ return False
81
+ if origin_is(annotation, Annotated):
82
+ annotation_args = args(annotation)
83
+ return bool(annotation_args) and annotation_needs_resolution(annotation_args[0])
84
+ return any(annotation_needs_resolution(item) for item in args(annotation))
85
+
86
+
87
+ def _normalize_forward_refs(annotation: Any) -> Any:
88
+ if isinstance(annotation, str):
89
+ return ForwardRef(annotation)
90
+ if isinstance(annotation, ForwardRef) or is_type_var(annotation):
91
+ return annotation
92
+
93
+ annotation_args = args(annotation)
94
+ if not annotation_args or origin_is(annotation, Literal):
95
+ return annotation
96
+
97
+ origin = get_origin(annotation)
98
+ if origin is Annotated:
99
+ first_arg = _normalize_forward_refs(annotation_args[0])
100
+ if first_arg is annotation_args[0]:
101
+ return annotation
102
+ return Annotated.__class_getitem__((first_arg, *annotation_args[1:]))
103
+
104
+ normalized_args = tuple(_normalize_forward_refs(item) for item in annotation_args)
105
+ if normalized_args == annotation_args:
106
+ return annotation
107
+
108
+ if isinstance(annotation, types.GenericAlias):
109
+ return origin[normalized_args]
110
+ copy_with = getattr(annotation, "copy_with", None)
111
+ if copy_with is not None:
112
+ return copy_with(normalized_args)
113
+ return annotation
114
+
115
+
116
+ def _resolve_annotation_fully(
117
+ annotation: Any,
118
+ *,
119
+ globalns: dict[str, Any],
120
+ localns: dict[str, Any] | None = None,
121
+ ) -> ResolvedAnnotation:
122
+ resolved = resolve_annotation(annotation, globalns=globalns, localns=localns)
123
+ if not annotation_needs_resolution(resolved):
124
+ return ResolvedAnnotation(resolved, localns)
125
+
126
+ normalized = _normalize_forward_refs(resolved)
127
+ return ResolvedAnnotation(
128
+ resolve_annotation(normalized, globalns=globalns, localns=localns),
129
+ localns,
130
+ )
131
+
132
+
133
+ def _call_stack_localns() -> dict[str, Any]:
134
+ frame: FrameType | None = inspect.currentframe()
135
+ localns: dict[str, Any] = {}
136
+ try:
137
+ frame = frame.f_back if frame is not None else None
138
+ while frame is not None:
139
+ localns = {**frame.f_locals, **localns}
140
+ frame = frame.f_back
141
+ finally:
142
+ del frame
143
+ return localns
144
+
145
+
146
+ def resolve_annotation(
147
+ annotation: Any,
148
+ *,
149
+ globalns: dict[str, Any],
150
+ localns: dict[str, Any] | None = None,
151
+ ) -> Any:
152
+ class AnnotationShim:
153
+ pass
154
+
155
+ AnnotationShim.__annotations__ = {"value": annotation}
156
+ return get_type_hints(
157
+ AnnotationShim,
158
+ globalns=globalns,
159
+ localns=localns,
160
+ include_extras=True,
161
+ )["value"]
162
+
163
+
164
+ def resolve_annotation_from_call_stack(
165
+ annotation: Any,
166
+ *,
167
+ globalns: dict[str, Any],
168
+ ) -> Any:
169
+ return resolve_annotation_with_namespace_from_call_stack(
170
+ annotation,
171
+ globalns=globalns,
172
+ ).annotation
173
+
174
+
175
+ def resolve_annotation_with_namespace_from_call_stack(
176
+ annotation: Any,
177
+ *,
178
+ globalns: dict[str, Any],
179
+ ) -> ResolvedAnnotation:
180
+ if annotation is Any or not annotation_needs_resolution(annotation):
181
+ return ResolvedAnnotation(annotation, _call_stack_localns())
182
+
183
+ resolution_error: BaseException | None = None
184
+ try:
185
+ return _resolve_annotation_fully(annotation, globalns=globalns)
186
+ except (NameError, TypeError, AttributeError) as exc:
187
+ resolution_error = exc
188
+
189
+ localns = _call_stack_localns()
190
+
191
+ try:
192
+ return _resolve_annotation_fully(
193
+ annotation,
194
+ globalns=globalns,
195
+ localns=localns,
196
+ )
197
+ except (NameError, TypeError, AttributeError) as exc:
198
+ resolution_error = exc
199
+
200
+ if not annotation_needs_resolution(annotation):
201
+ return ResolvedAnnotation(annotation)
202
+
203
+ raise TypeAnnotationResolutionError from resolution_error
@@ -0,0 +1,24 @@
1
+ """Standalone queue development server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._internal.devserver import (
6
+ EmbeddedQueueDevServer,
7
+ QueueClientAsgiDevServer,
8
+ embedded_queue_dev_server,
9
+ main,
10
+ queue_client_asgi_dev_server,
11
+ )
12
+
13
+ # Only add public symbols to __all__; internal helpers must stay unexported.
14
+ __all__ = (
15
+ "EmbeddedQueueDevServer",
16
+ "QueueClientAsgiDevServer",
17
+ "embedded_queue_dev_server",
18
+ "main",
19
+ "queue_client_asgi_dev_server",
20
+ )
21
+
22
+
23
+ if __name__ == "__main__":
24
+ raise SystemExit(main())
@@ -0,0 +1,50 @@
1
+ """Embedded local queue server for in-process Queue API development.
2
+
3
+ This module provides a dependency-light Queue API implementation for embedded
4
+ local runtime code. It preserves the same client and push-delivery path as the
5
+ hosted service: callers send through ``QueueClient``, messages are stored
6
+ in a local server, visible messages are leased, and a dispatcher invokes
7
+ subscribers through ``accept_and_handle`` with synthetic Vercel Queue push
8
+ headers. Successful handlers are acknowledged by the normal client follow-up
9
+ path, and failures retry through lease expiry or explicit visibility changes.
10
+
11
+ Use this for embedded, single-process local mode and tests.
12
+
13
+ Run an embedded queue server in a test and use a client that sends to
14
+ it directly:
15
+
16
+ import anyio
17
+ import vercel.queue as vq
18
+ import vercel.queue.embedded as vq_embedded
19
+
20
+ @vq.subscribe(topic="jobs*", consumer_group="just-testing")
21
+ async def handle_job(message: vq.Message[dict[str, object]]) -> None:
22
+ print(message.payload)
23
+
24
+ async def main() -> None:
25
+ async with vq_embedded.embedded_queue_service() as service:
26
+ client = service.get_async_client()
27
+ await client.send("email", {"subject": "hi"})
28
+
29
+ anyio.run(main)
30
+
31
+
32
+ Enable the pytest fixture and use it in tests:
33
+
34
+ pytest_plugins = ["vercel.queue.testing.pytest"]
35
+
36
+ async def test_queue(embedded_queue_server):
37
+ client = embedded_queue_server.get_async_client()
38
+ message_id = await client.send("emails", {"subject": "hi"}, retention=60)
39
+ """
40
+
41
+ from ._internal.embedded import (
42
+ create_embedded_queue_app,
43
+ embedded_queue_service,
44
+ )
45
+
46
+ # Only add public symbols to __all__; internal helpers must stay unexported.
47
+ __all__ = (
48
+ "create_embedded_queue_app",
49
+ "embedded_queue_service",
50
+ )
vercel/queue/py.typed ADDED
@@ -0,0 +1 @@
1
+
vercel/queue/sync.py ADDED
@@ -0,0 +1,8 @@
1
+ """Synchronous Vercel Queue client APIs."""
2
+ # ruff: noqa: F403
3
+
4
+ from ._internal import api_common as _api_common, api_sync as _api_sync
5
+ from ._internal.api_common import *
6
+ from ._internal.api_sync import *
7
+
8
+ __all__ = _api_common.__all__ + _api_sync.__all__
@@ -0,0 +1,14 @@
1
+ """Testing helpers for Vercel Queue."""
2
+
3
+ from .state import (
4
+ clear_subscriptions,
5
+ reset_default_async_queue_clients,
6
+ reset_default_queue_clients,
7
+ )
8
+
9
+ # Only add public symbols to __all__; internal helpers must stay unexported.
10
+ __all__ = (
11
+ "clear_subscriptions",
12
+ "reset_default_async_queue_clients",
13
+ "reset_default_queue_clients",
14
+ )
@@ -0,0 +1,42 @@
1
+ """Pytest fixtures for the embedded queue server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+
7
+ import pytest
8
+
9
+ from vercel.queue.devserver import EmbeddedQueueDevServer, embedded_queue_dev_server
10
+
11
+
12
+ @pytest.fixture(scope="session")
13
+ def _embedded_queue_server_session() -> Iterator[EmbeddedQueueDevServer]:
14
+ """Run one embedded queue server process for the test session."""
15
+ with embedded_queue_dev_server() as server:
16
+ yield server
17
+
18
+
19
+ @pytest.fixture
20
+ def embedded_queue_server(
21
+ _embedded_queue_server_session: EmbeddedQueueDevServer,
22
+ ) -> Iterator[EmbeddedQueueDevServer]:
23
+ """Return an isolated embedded queue server state for a test."""
24
+ _embedded_queue_server_session.reset()
25
+ try:
26
+ yield _embedded_queue_server_session
27
+ finally:
28
+ _embedded_queue_server_session.reset()
29
+
30
+
31
+ @pytest.fixture
32
+ def eqs(embedded_queue_server: EmbeddedQueueDevServer) -> EmbeddedQueueDevServer:
33
+ """Alias for the embedded queue server fixture."""
34
+ return embedded_queue_server
35
+
36
+
37
+ # Only add public symbols to __all__; internal helpers must stay unexported.
38
+ __all__ = (
39
+ "EmbeddedQueueDevServer",
40
+ "embedded_queue_server",
41
+ "eqs",
42
+ )
@@ -0,0 +1,32 @@
1
+ """Queue test state helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .._internal import subscribers as queue_subscribers
6
+ from .._internal.http import (
7
+ reset_async_http_client_pool_for_tests,
8
+ reset_http_client_pools_for_tests,
9
+ )
10
+
11
+
12
+ def reset_default_queue_clients() -> None:
13
+ """Reset cached default Queue clients for tests."""
14
+ reset_http_client_pools_for_tests()
15
+
16
+
17
+ async def reset_default_async_queue_clients() -> None:
18
+ """Reset cached default asynchronous Queue clients for tests."""
19
+ await reset_async_http_client_pool_for_tests()
20
+
21
+
22
+ def clear_subscriptions() -> None:
23
+ """Clear registered subscribers and embedded dispatchers for tests."""
24
+ queue_subscribers.clear_subscriptions_for_tests()
25
+
26
+
27
+ # Only add public symbols to __all__; internal helpers must stay unexported.
28
+ __all__ = (
29
+ "clear_subscriptions",
30
+ "reset_default_async_queue_clients",
31
+ "reset_default_queue_clients",
32
+ )
@@ -0,0 +1,3 @@
1
+ """Package version metadata."""
2
+
3
+ __version__ = "0.7.0"