vercel-queue-bundle 0.7.2__tar.gz → 0.7.3__tar.gz

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 (55) hide show
  1. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/PKG-INFO +46 -47
  2. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/README.md +43 -45
  3. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/_vercel_hatch_build.py +38 -3
  4. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/pyproject.toml +7 -1
  5. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/subscribers.py +9 -8
  6. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/transports.py +11 -10
  7. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/version.py +1 -1
  8. vercel_queue_bundle-0.7.2/vercel/queue/_internal/typeutils.py +0 -203
  9. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/LICENSE +0 -0
  10. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/hatch_build.py +0 -0
  11. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/__init__.py +0 -0
  12. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/__main__.py +0 -0
  13. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/__init__.py +0 -0
  14. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/api_async.py +0 -0
  15. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/api_common.py +0 -0
  16. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/api_sync.py +0 -0
  17. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/asgi.py +0 -0
  18. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/asynctools.py +0 -0
  19. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/cli.py +0 -0
  20. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/client.py +0 -0
  21. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/client_sync.py +0 -0
  22. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/config.py +0 -0
  23. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/constants.py +0 -0
  24. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/devserver.py +0 -0
  25. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/embedded.py +0 -0
  26. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/errors.py +0 -0
  27. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/http.py +0 -0
  28. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/lease.py +0 -0
  29. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/log.py +0 -0
  30. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/messages.py +0 -0
  31. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/multipart.py +0 -0
  32. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/names.py +0 -0
  33. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/polling.py +0 -0
  34. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/push.py +0 -0
  35. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/response.py +0 -0
  36. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/retry.py +0 -0
  37. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/streams.py +0 -0
  38. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_internal/types.py +0 -0
  39. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/LICENSE.python-multipart.txt +0 -0
  40. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/__init__.py +0 -0
  41. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/multipart/__init__.py +0 -0
  42. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/multipart/decoders.py +0 -0
  43. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/multipart/exceptions.py +0 -0
  44. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/multipart/multipart.py +0 -0
  45. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/python_multipart/__init__.py +0 -0
  46. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/python_multipart/decoders.py +0 -0
  47. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/python_multipart/exceptions.py +0 -0
  48. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/_vendor/python_multipart/multipart.py +0 -0
  49. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/devserver.py +0 -0
  50. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/embedded.py +0 -0
  51. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/py.typed +0 -0
  52. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/sync.py +0 -0
  53. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/testing/__init__.py +0 -0
  54. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/testing/pytest.py +0 -0
  55. {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.7.3}/vercel/queue/testing/state.py +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vercel-queue-bundle
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Vercel Queue client for Python
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  License-File: vercel/queue/_vendor/LICENSE.python-multipart.txt
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: vercel-headers-bundle>=0.7.1
10
+ Requires-Dist: vercel-internal-core-bundle<0.2.0,>=0.1.2
10
11
  Requires-Dist: vercel-internal-shared-vendored-deps>=0.1.1
11
- Requires-Dist: vercel-oidc-bundle>=0.7.1
12
+ Requires-Dist: vercel-oidc-bundle>=0.8.0
12
13
  Provides-Extra: devserver
13
14
  Requires-Dist: uvicorn; extra == 'devserver'
14
15
  Provides-Extra: trio
@@ -69,35 +70,29 @@ from vercel.queue import send
69
70
  message_id = await send("my-topic", {"message": "Hello world"})
70
71
  ```
71
72
 
72
- **3. Handle incoming messages with an API route function:**
73
+ **3. Handle incoming messages in a subscriber module:**
73
74
 
74
75
  ```python
75
- # api/queue.py
76
- from vercel.queue import asgi_app, subscribe
76
+ # worker.py
77
+ from vercel.queue import subscribe
77
78
 
78
79
 
79
- @subscribe(topic="my-topic", consumer_group="api/queue.py")
80
+ @subscribe(topic="my-topic")
80
81
  async def process_message(message):
81
82
  print("Processing:", message)
82
-
83
-
84
- # An ASGI app instance that converts incoming message callbacks
85
- # sent by Vercel Queues and routes them to handlers.
86
- app = asgi_app()
87
83
  ```
88
84
 
89
- **4. Configure `vercel.json`:**
85
+ **4. Declare the subscriber in `pyproject.toml`:**
90
86
 
91
- ```json
92
- {
93
- "functions": {
94
- "api/queue.py": {
95
- "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
96
- }
97
- }
98
- }
87
+ ```toml
88
+ [[tool.vercel.subscribers]]
89
+ entrypoint = "worker:process_message"
99
90
  ```
100
91
 
92
+ The Vercel build introspects the entrypoint module's registered
93
+ subscriptions and compiles it into a queue-triggered function. No
94
+ `vercel.json` trigger configuration is needed.
95
+
101
96
  **5. Deploy:**
102
97
 
103
98
  ```bash
@@ -165,42 +160,46 @@ and running polling loops on other infrastructure.
165
160
 
166
161
  ### Auto-scaled push-mode on Vercel
167
162
 
168
- The recommended way of deploying queue subscribers is to deploy them as Vercel Functions
169
-
170
- **Vercel Function (plain `/api` directory):**
163
+ The recommended way of deploying queue subscribers is to declare them in
164
+ `pyproject.toml` with `[[tool.vercel.subscribers]]`. Each entry points at a
165
+ module that registers subscriptions with `@subscribe`:
171
166
 
172
167
  ```python
173
- # api/handle_orders.py
174
- from vercel.queue import asgi_app, subscribe
168
+ # worker.py
169
+ from vercel.queue import subscribe
175
170
 
176
171
 
177
- @subscribe(topic="orders", consumer_group="api/handle_orders.py")
172
+ @subscribe(topic="orders", retry_after=60)
178
173
  async def handle_order(message):
179
174
  print("Processing:", message)
175
+ ```
180
176
 
177
+ **pyproject.toml**:
181
178
 
182
- # An ASGI app instance that converts incoming message callbacks
183
- # sent by Vercel Queues and routes them to handlers.
184
- app = asgi_app()
179
+ ```toml
180
+ [[tool.vercel.subscribers]]
181
+ entrypoint = "worker:handle_order"
185
182
  ```
186
183
 
187
- **vercel.json**:
188
-
189
- ```json
190
- {
191
- "functions": {
192
- "api/queue/orders.py": {
193
- "experimentalTriggers": [
194
- {
195
- "type": "queue/v2beta",
196
- "topic": "orders",
197
- "retryAfterSeconds": 60,
198
- "initialDelaySeconds": 0
199
- }
200
- ]
201
- }
202
- }
203
- }
184
+ At build time, Vercel imports the entrypoint module, introspects every
185
+ subscription it registers, and compiles the subscriber into its own
186
+ queue-triggered function. Delivery tuning lives on the `@subscribe`
187
+ decorator (`retry_after`, `initial_delay`, `max_concurrency`,
188
+ `max_attempts`, `consumer_group`), so no `vercel.json` configuration is
189
+ needed.
190
+
191
+ A subscriber consumes every subscription registered by its entrypoint
192
+ module. Add an optional `topics` filter to split subscriptions across
193
+ several subscriber functions:
194
+
195
+ ```toml
196
+ [[tool.vercel.subscribers]]
197
+ entrypoint = "worker:handle_order"
198
+ topics = ["orders"]
199
+
200
+ [[tool.vercel.subscribers]]
201
+ entrypoint = "worker:handle_refund"
202
+ topics = ["refunds"]
204
203
  ```
205
204
 
206
205
  ### Automatic Polling Loop
@@ -287,7 +286,7 @@ polling, because it can distribute messages across regions unpredictably.
287
286
  ## Retry and Backoff
288
287
 
289
288
  When a topic handler raises, the message is not acknowledged and becomes available for redelivery
290
- after the `retryAfterSeconds` interval configured in `vercel.json`. Retries continue until the
289
+ after the `retry_after` interval configured on the subscriber. Retries continue until the
291
290
  handler succeeds or the message expires.
292
291
 
293
292
  For finer control over retry timing, raise `RetryAfter` from a subscriber:
@@ -50,35 +50,29 @@ from vercel.queue import send
50
50
  message_id = await send("my-topic", {"message": "Hello world"})
51
51
  ```
52
52
 
53
- **3. Handle incoming messages with an API route function:**
53
+ **3. Handle incoming messages in a subscriber module:**
54
54
 
55
55
  ```python
56
- # api/queue.py
57
- from vercel.queue import asgi_app, subscribe
56
+ # worker.py
57
+ from vercel.queue import subscribe
58
58
 
59
59
 
60
- @subscribe(topic="my-topic", consumer_group="api/queue.py")
60
+ @subscribe(topic="my-topic")
61
61
  async def process_message(message):
62
62
  print("Processing:", message)
63
-
64
-
65
- # An ASGI app instance that converts incoming message callbacks
66
- # sent by Vercel Queues and routes them to handlers.
67
- app = asgi_app()
68
63
  ```
69
64
 
70
- **4. Configure `vercel.json`:**
65
+ **4. Declare the subscriber in `pyproject.toml`:**
71
66
 
72
- ```json
73
- {
74
- "functions": {
75
- "api/queue.py": {
76
- "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
77
- }
78
- }
79
- }
67
+ ```toml
68
+ [[tool.vercel.subscribers]]
69
+ entrypoint = "worker:process_message"
80
70
  ```
81
71
 
72
+ The Vercel build introspects the entrypoint module's registered
73
+ subscriptions and compiles it into a queue-triggered function. No
74
+ `vercel.json` trigger configuration is needed.
75
+
82
76
  **5. Deploy:**
83
77
 
84
78
  ```bash
@@ -146,42 +140,46 @@ and running polling loops on other infrastructure.
146
140
 
147
141
  ### Auto-scaled push-mode on Vercel
148
142
 
149
- The recommended way of deploying queue subscribers is to deploy them as Vercel Functions
150
-
151
- **Vercel Function (plain `/api` directory):**
143
+ The recommended way of deploying queue subscribers is to declare them in
144
+ `pyproject.toml` with `[[tool.vercel.subscribers]]`. Each entry points at a
145
+ module that registers subscriptions with `@subscribe`:
152
146
 
153
147
  ```python
154
- # api/handle_orders.py
155
- from vercel.queue import asgi_app, subscribe
148
+ # worker.py
149
+ from vercel.queue import subscribe
156
150
 
157
151
 
158
- @subscribe(topic="orders", consumer_group="api/handle_orders.py")
152
+ @subscribe(topic="orders", retry_after=60)
159
153
  async def handle_order(message):
160
154
  print("Processing:", message)
155
+ ```
161
156
 
157
+ **pyproject.toml**:
162
158
 
163
- # An ASGI app instance that converts incoming message callbacks
164
- # sent by Vercel Queues and routes them to handlers.
165
- app = asgi_app()
159
+ ```toml
160
+ [[tool.vercel.subscribers]]
161
+ entrypoint = "worker:handle_order"
166
162
  ```
167
163
 
168
- **vercel.json**:
169
-
170
- ```json
171
- {
172
- "functions": {
173
- "api/queue/orders.py": {
174
- "experimentalTriggers": [
175
- {
176
- "type": "queue/v2beta",
177
- "topic": "orders",
178
- "retryAfterSeconds": 60,
179
- "initialDelaySeconds": 0
180
- }
181
- ]
182
- }
183
- }
184
- }
164
+ At build time, Vercel imports the entrypoint module, introspects every
165
+ subscription it registers, and compiles the subscriber into its own
166
+ queue-triggered function. Delivery tuning lives on the `@subscribe`
167
+ decorator (`retry_after`, `initial_delay`, `max_concurrency`,
168
+ `max_attempts`, `consumer_group`), so no `vercel.json` configuration is
169
+ needed.
170
+
171
+ A subscriber consumes every subscription registered by its entrypoint
172
+ module. Add an optional `topics` filter to split subscriptions across
173
+ several subscriber functions:
174
+
175
+ ```toml
176
+ [[tool.vercel.subscribers]]
177
+ entrypoint = "worker:handle_order"
178
+ topics = ["orders"]
179
+
180
+ [[tool.vercel.subscribers]]
181
+ entrypoint = "worker:handle_refund"
182
+ topics = ["refunds"]
185
183
  ```
186
184
 
187
185
  ### Automatic Polling Loop
@@ -268,7 +266,7 @@ polling, because it can distribute messages across regions unpredictably.
268
266
  ## Retry and Backoff
269
267
 
270
268
  When a topic handler raises, the message is not acknowledged and becomes available for redelivery
271
- after the `retryAfterSeconds` interval configured in `vercel.json`. Retries continue until the
269
+ after the `retry_after` interval configured on the subscriber. Retries continue until the
272
270
  handler succeeds or the message expires.
273
271
 
274
272
  For finer control over retry timing, raise `RetryAfter` from a subscriber:
@@ -8,6 +8,7 @@ from typing import Any
8
8
 
9
9
  from hatchling.metadata.plugin.interface import MetadataHookInterface
10
10
  from packaging.requirements import Requirement
11
+ from packaging.specifiers import SpecifierSet
11
12
 
12
13
  try:
13
14
  import tomllib
@@ -29,8 +30,10 @@ class WorkspaceDependenciesMetadataHook(MetadataHookInterface):
29
30
  }
30
31
  workspace_root = _find_workspace_root(Path(self.root))
31
32
 
33
+ package = pyproject.get("project", {}).get("name", str(self.root))
34
+
32
35
  metadata["dependencies"] = [
33
- _rewrite_dependency(requirement, workspace_names, workspace_root)
36
+ _rewrite_dependency(requirement, workspace_names, workspace_root, package)
34
37
  for requirement in dependency_table.get("dependencies", [])
35
38
  ]
36
39
 
@@ -55,24 +58,56 @@ def _rewrite_dependency(
55
58
  requirement: str,
56
59
  workspace_names: set[str],
57
60
  workspace_root: Path | None,
61
+ package: str,
58
62
  ) -> str:
59
63
  parsed = Requirement(requirement)
60
64
  normalized = parsed.name.lower().replace("_", "-")
61
65
  if normalized not in workspace_names or workspace_root is None:
62
66
  return requirement
63
- return _with_lower_bound(parsed, _read_workspace_version(workspace_root, normalized))
67
+ version = _read_workspace_version(workspace_root, normalized)
68
+ return _with_lower_bound(parsed, version, requirement, package)
64
69
 
65
70
 
66
- def _with_lower_bound(requirement: Requirement, version: str) -> str:
71
+ def _with_lower_bound(requirement: Requirement, version: str, declared: str, package: str) -> str:
67
72
  extras = f"[{','.join(sorted(requirement.extras))}]" if requirement.extras else ""
68
73
  specifiers = [
69
74
  str(specifier) for specifier in requirement.specifier if specifier.operator != ">="
70
75
  ]
71
76
  specifier_text = ",".join([f">={version}", *specifiers])
77
+ _reject_unsatisfiable(package, requirement.name, declared, specifier_text, version)
72
78
  marker = f" ; {requirement.marker}" if requirement.marker else ""
73
79
  return f"{requirement.name}{extras}{specifier_text}{marker}"
74
80
 
75
81
 
82
+ def _reject_unsatisfiable(
83
+ package: str, dependency: str, declared: str, specifier_text: str, version: str
84
+ ) -> None:
85
+ """Refuse to publish a bound that no version of *dependency* can satisfy.
86
+
87
+ The lower bound is generated from the sibling's current version while the
88
+ rest of the specifier is whatever the package declared, so a hand-written
89
+ upper bound that the sibling has since reached produces something like
90
+ ``>=0.3.0,<0.3.0``. Nothing rejects that later: the wheel builds, uploads,
91
+ and only fails when someone tries to install it. Fail the build instead --
92
+ CI builds every package, so the bump that crosses the bound is caught by
93
+ its own pull request.
94
+
95
+ The test is that the sibling version *being released* satisfies the bound,
96
+ which is narrower than the range being non-empty: `>=0.7.1,!=0.7.1` leaves
97
+ room for a later version, but says the release under way is unusable.
98
+ """
99
+ # `prereleases=True` so a workspace version like `0.4.0b1` is judged
100
+ # against its own bound rather than excluded for being a prerelease.
101
+ if SpecifierSet(specifier_text).contains(version, prereleases=True):
102
+ return
103
+ raise RuntimeError(
104
+ f"{package} declares {declared!r}, but {dependency} is at {version} in "
105
+ f"the workspace, so publishing would pin {dependency}{specifier_text} — "
106
+ f"which excludes {version} itself, the version being released. "
107
+ f"Raise the upper bound in [tool.vercel.release.dependencies] of {package}."
108
+ )
109
+
110
+
76
111
  def _read_workspace_version(workspace_root: Path, package_name: str) -> str:
77
112
  for pattern in ("src/*/pyproject.toml", "integrations/*/pyproject.toml"):
78
113
  for pyproject_path in workspace_root.glob(pattern):
@@ -21,7 +21,8 @@ path = "hatch_build.py"
21
21
  [tool.vercel.release.dependencies]
22
22
  dependencies = [
23
23
  "vercel-headers-bundle>=0.7.1",
24
- "vercel-oidc-bundle>=0.7.1",
24
+ "vercel-internal-core-bundle>=0.1.2,<0.2.0",
25
+ "vercel-oidc-bundle>=0.8.0",
25
26
  "vercel-internal-shared-vendored-deps>=0.1.1",
26
27
  ]
27
28
 
@@ -33,6 +34,7 @@ trio = ["anyio[trio]>=4.0.0"]
33
34
  [tool.uv.sources]
34
35
  vercel-oidc = { workspace = true }
35
36
  vercel-headers = { workspace = true }
37
+ vercel-internal-core = { workspace = true }
36
38
 
37
39
  [tool.hatch.version]
38
40
  path = "vercel/queue/version.py"
@@ -65,6 +67,10 @@ exclude = [
65
67
  extend = "../../pyproject.toml"
66
68
  cache-dir = "../../.ruff_cache"
67
69
  preview = true
70
+ # `vercel` is a PEP 420 namespace package split across workspace members, so
71
+ # `vercel._internal.core` is this package's own shared internals rather than
72
+ # another project's privates. import-private-name needs telling.
73
+ namespace-packages = ["vercel"]
68
74
 
69
75
  [tool.ruff.lint]
70
76
  extend-select = [
@@ -12,6 +12,15 @@ from importlib import import_module
12
12
  from itertools import count
13
13
  from types import MappingProxyType
14
14
 
15
+ from vercel._internal.core.typeutils import (
16
+ ResolvedAnnotation,
17
+ TypeAnnotationResolutionError,
18
+ args,
19
+ origin_is,
20
+ resolve_annotation_with_namespace_from_call_stack,
21
+ strip_annotated,
22
+ )
23
+
15
24
  from .errors import (
16
25
  DuplicateSubscriptionError,
17
26
  PayloadValidationError,
@@ -44,14 +53,6 @@ from .types import (
44
53
  Transport,
45
54
  duration_to_seconds,
46
55
  )
47
- from .typeutils import (
48
- ResolvedAnnotation,
49
- TypeAnnotationResolutionError,
50
- args,
51
- origin_is,
52
- resolve_annotation_with_namespace_from_call_stack,
53
- strip_annotated,
54
- )
55
56
 
56
57
  _Subscriber: TypeAlias = Callable[..., Any | Awaitable[Any]]
57
58
  _SubscriberRef: TypeAlias = weakref.ReferenceType[_Subscriber]
@@ -7,6 +7,17 @@ import json
7
7
  from collections.abc import AsyncIterable, AsyncIterator, Callable, Iterable
8
8
  from importlib import import_module
9
9
 
10
+ from vercel._internal.core.typeutils import (
11
+ annotation_needs_resolution,
12
+ args,
13
+ is_classvar,
14
+ is_final,
15
+ is_type_var,
16
+ is_union_type,
17
+ origin_is,
18
+ strip_annotated,
19
+ )
20
+
10
21
  from .constants import CONTENT_TYPE_JSON, CONTENT_TYPE_OCTET_STREAM, CONTENT_TYPE_TEXT
11
22
  from .errors import SubscriptionError
12
23
  from .streams import (
@@ -20,16 +31,6 @@ from .types import (
20
31
  Topic,
21
32
  Transport,
22
33
  )
23
- from .typeutils import (
24
- annotation_needs_resolution,
25
- args,
26
- is_classvar,
27
- is_final,
28
- is_type_var,
29
- is_union_type,
30
- origin_is,
31
- strip_annotated,
32
- )
33
34
 
34
35
  T = TypeVar("T")
35
36
 
@@ -1,3 +1,3 @@
1
1
  """Package version metadata."""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.7.3"
@@ -1,203 +0,0 @@
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