vercel-queue 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.
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/PKG-INFO +46 -47
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/README.md +43 -45
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/_vercel_hatch_build.py +38 -3
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/pyproject.toml +6 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/subscribers.py +9 -8
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/transports.py +11 -10
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/version.py +1 -1
- vercel_queue-0.7.2/vercel/queue/_internal/typeutils.py +0 -203
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/.gitignore +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/LICENSE +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/hatch_build.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/__init__.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/__main__.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/__init__.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/api_async.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/api_common.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/api_sync.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/asgi.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/asynctools.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/cli.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/client.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/client_sync.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/config.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/constants.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/devserver.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/embedded.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/errors.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/http.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/lease.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/log.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/messages.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/multipart.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/names.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/polling.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/push.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/response.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/retry.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/streams.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/_internal/types.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/devserver.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/embedded.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/py.typed +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/sync.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/testing/__init__.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/testing/pytest.py +0 -0
- {vercel_queue-0.7.2 → vercel_queue-0.7.3}/vercel/queue/testing/state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vercel-queue
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Vercel Queue client for Python
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -10,7 +10,8 @@ Requires-Dist: httpx[http2]>=0.27.0
|
|
|
10
10
|
Requires-Dist: python-multipart>=0.0.20
|
|
11
11
|
Requires-Dist: typing-extensions>=4.0.0
|
|
12
12
|
Requires-Dist: vercel-headers>=0.7.1
|
|
13
|
-
Requires-Dist: vercel-
|
|
13
|
+
Requires-Dist: vercel-internal-core<0.2.0,>=0.1.2
|
|
14
|
+
Requires-Dist: vercel-oidc>=0.8.0
|
|
14
15
|
Provides-Extra: devserver
|
|
15
16
|
Requires-Dist: uvicorn; extra == 'devserver'
|
|
16
17
|
Provides-Extra: trio
|
|
@@ -67,35 +68,29 @@ from vercel.queue import send
|
|
|
67
68
|
message_id = await send("my-topic", {"message": "Hello world"})
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
**3. Handle incoming messages
|
|
71
|
+
**3. Handle incoming messages in a subscriber module:**
|
|
71
72
|
|
|
72
73
|
```python
|
|
73
|
-
#
|
|
74
|
-
from vercel.queue import
|
|
74
|
+
# worker.py
|
|
75
|
+
from vercel.queue import subscribe
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
@subscribe(topic="my-topic"
|
|
78
|
+
@subscribe(topic="my-topic")
|
|
78
79
|
async def process_message(message):
|
|
79
80
|
print("Processing:", message)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# An ASGI app instance that converts incoming message callbacks
|
|
83
|
-
# sent by Vercel Queues and routes them to handlers.
|
|
84
|
-
app = asgi_app()
|
|
85
81
|
```
|
|
86
82
|
|
|
87
|
-
**4.
|
|
83
|
+
**4. Declare the subscriber in `pyproject.toml`:**
|
|
88
84
|
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"api/queue.py": {
|
|
93
|
-
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
85
|
+
```toml
|
|
86
|
+
[[tool.vercel.subscribers]]
|
|
87
|
+
entrypoint = "worker:process_message"
|
|
97
88
|
```
|
|
98
89
|
|
|
90
|
+
The Vercel build introspects the entrypoint module's registered
|
|
91
|
+
subscriptions and compiles it into a queue-triggered function. No
|
|
92
|
+
`vercel.json` trigger configuration is needed.
|
|
93
|
+
|
|
99
94
|
**5. Deploy:**
|
|
100
95
|
|
|
101
96
|
```bash
|
|
@@ -163,42 +158,46 @@ and running polling loops on other infrastructure.
|
|
|
163
158
|
|
|
164
159
|
### Auto-scaled push-mode on Vercel
|
|
165
160
|
|
|
166
|
-
The recommended way of deploying queue subscribers is to
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
The recommended way of deploying queue subscribers is to declare them in
|
|
162
|
+
`pyproject.toml` with `[[tool.vercel.subscribers]]`. Each entry points at a
|
|
163
|
+
module that registers subscriptions with `@subscribe`:
|
|
169
164
|
|
|
170
165
|
```python
|
|
171
|
-
#
|
|
172
|
-
from vercel.queue import
|
|
166
|
+
# worker.py
|
|
167
|
+
from vercel.queue import subscribe
|
|
173
168
|
|
|
174
169
|
|
|
175
|
-
@subscribe(topic="orders",
|
|
170
|
+
@subscribe(topic="orders", retry_after=60)
|
|
176
171
|
async def handle_order(message):
|
|
177
172
|
print("Processing:", message)
|
|
173
|
+
```
|
|
178
174
|
|
|
175
|
+
**pyproject.toml**:
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
```toml
|
|
178
|
+
[[tool.vercel.subscribers]]
|
|
179
|
+
entrypoint = "worker:handle_order"
|
|
183
180
|
```
|
|
184
181
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
182
|
+
At build time, Vercel imports the entrypoint module, introspects every
|
|
183
|
+
subscription it registers, and compiles the subscriber into its own
|
|
184
|
+
queue-triggered function. Delivery tuning lives on the `@subscribe`
|
|
185
|
+
decorator (`retry_after`, `initial_delay`, `max_concurrency`,
|
|
186
|
+
`max_attempts`, `consumer_group`), so no `vercel.json` configuration is
|
|
187
|
+
needed.
|
|
188
|
+
|
|
189
|
+
A subscriber consumes every subscription registered by its entrypoint
|
|
190
|
+
module. Add an optional `topics` filter to split subscriptions across
|
|
191
|
+
several subscriber functions:
|
|
192
|
+
|
|
193
|
+
```toml
|
|
194
|
+
[[tool.vercel.subscribers]]
|
|
195
|
+
entrypoint = "worker:handle_order"
|
|
196
|
+
topics = ["orders"]
|
|
197
|
+
|
|
198
|
+
[[tool.vercel.subscribers]]
|
|
199
|
+
entrypoint = "worker:handle_refund"
|
|
200
|
+
topics = ["refunds"]
|
|
202
201
|
```
|
|
203
202
|
|
|
204
203
|
### Automatic Polling Loop
|
|
@@ -285,7 +284,7 @@ polling, because it can distribute messages across regions unpredictably.
|
|
|
285
284
|
## Retry and Backoff
|
|
286
285
|
|
|
287
286
|
When a topic handler raises, the message is not acknowledged and becomes available for redelivery
|
|
288
|
-
after the `
|
|
287
|
+
after the `retry_after` interval configured on the subscriber. Retries continue until the
|
|
289
288
|
handler succeeds or the message expires.
|
|
290
289
|
|
|
291
290
|
For finer control over retry timing, raise `RetryAfter` from a subscriber:
|
|
@@ -46,35 +46,29 @@ from vercel.queue import send
|
|
|
46
46
|
message_id = await send("my-topic", {"message": "Hello world"})
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
**3. Handle incoming messages
|
|
49
|
+
**3. Handle incoming messages in a subscriber module:**
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
|
-
#
|
|
53
|
-
from vercel.queue import
|
|
52
|
+
# worker.py
|
|
53
|
+
from vercel.queue import subscribe
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
@subscribe(topic="my-topic"
|
|
56
|
+
@subscribe(topic="my-topic")
|
|
57
57
|
async def process_message(message):
|
|
58
58
|
print("Processing:", message)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# An ASGI app instance that converts incoming message callbacks
|
|
62
|
-
# sent by Vercel Queues and routes them to handlers.
|
|
63
|
-
app = asgi_app()
|
|
64
59
|
```
|
|
65
60
|
|
|
66
|
-
**4.
|
|
61
|
+
**4. Declare the subscriber in `pyproject.toml`:**
|
|
67
62
|
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"api/queue.py": {
|
|
72
|
-
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
63
|
+
```toml
|
|
64
|
+
[[tool.vercel.subscribers]]
|
|
65
|
+
entrypoint = "worker:process_message"
|
|
76
66
|
```
|
|
77
67
|
|
|
68
|
+
The Vercel build introspects the entrypoint module's registered
|
|
69
|
+
subscriptions and compiles it into a queue-triggered function. No
|
|
70
|
+
`vercel.json` trigger configuration is needed.
|
|
71
|
+
|
|
78
72
|
**5. Deploy:**
|
|
79
73
|
|
|
80
74
|
```bash
|
|
@@ -142,42 +136,46 @@ and running polling loops on other infrastructure.
|
|
|
142
136
|
|
|
143
137
|
### Auto-scaled push-mode on Vercel
|
|
144
138
|
|
|
145
|
-
The recommended way of deploying queue subscribers is to
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
The recommended way of deploying queue subscribers is to declare them in
|
|
140
|
+
`pyproject.toml` with `[[tool.vercel.subscribers]]`. Each entry points at a
|
|
141
|
+
module that registers subscriptions with `@subscribe`:
|
|
148
142
|
|
|
149
143
|
```python
|
|
150
|
-
#
|
|
151
|
-
from vercel.queue import
|
|
144
|
+
# worker.py
|
|
145
|
+
from vercel.queue import subscribe
|
|
152
146
|
|
|
153
147
|
|
|
154
|
-
@subscribe(topic="orders",
|
|
148
|
+
@subscribe(topic="orders", retry_after=60)
|
|
155
149
|
async def handle_order(message):
|
|
156
150
|
print("Processing:", message)
|
|
151
|
+
```
|
|
157
152
|
|
|
153
|
+
**pyproject.toml**:
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
```toml
|
|
156
|
+
[[tool.vercel.subscribers]]
|
|
157
|
+
entrypoint = "worker:handle_order"
|
|
162
158
|
```
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
160
|
+
At build time, Vercel imports the entrypoint module, introspects every
|
|
161
|
+
subscription it registers, and compiles the subscriber into its own
|
|
162
|
+
queue-triggered function. Delivery tuning lives on the `@subscribe`
|
|
163
|
+
decorator (`retry_after`, `initial_delay`, `max_concurrency`,
|
|
164
|
+
`max_attempts`, `consumer_group`), so no `vercel.json` configuration is
|
|
165
|
+
needed.
|
|
166
|
+
|
|
167
|
+
A subscriber consumes every subscription registered by its entrypoint
|
|
168
|
+
module. Add an optional `topics` filter to split subscriptions across
|
|
169
|
+
several subscriber functions:
|
|
170
|
+
|
|
171
|
+
```toml
|
|
172
|
+
[[tool.vercel.subscribers]]
|
|
173
|
+
entrypoint = "worker:handle_order"
|
|
174
|
+
topics = ["orders"]
|
|
175
|
+
|
|
176
|
+
[[tool.vercel.subscribers]]
|
|
177
|
+
entrypoint = "worker:handle_refund"
|
|
178
|
+
topics = ["refunds"]
|
|
181
179
|
```
|
|
182
180
|
|
|
183
181
|
### Automatic Polling Loop
|
|
@@ -264,7 +262,7 @@ polling, because it can distribute messages across regions unpredictably.
|
|
|
264
262
|
## Retry and Backoff
|
|
265
263
|
|
|
266
264
|
When a topic handler raises, the message is not acknowledged and becomes available for redelivery
|
|
267
|
-
after the `
|
|
265
|
+
after the `retry_after` interval configured on the subscriber. Retries continue until the
|
|
268
266
|
handler succeeds or the message expires.
|
|
269
267
|
|
|
270
268
|
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
|
-
|
|
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,6 +21,7 @@ dependencies = [
|
|
|
21
21
|
"python-multipart>=0.0.20",
|
|
22
22
|
"typing_extensions>=4.0.0",
|
|
23
23
|
"vercel-headers>=0.6.0",
|
|
24
|
+
"vercel-internal-core>=0.1.0,<0.2.0",
|
|
24
25
|
"vercel-oidc>=0.6.0",
|
|
25
26
|
]
|
|
26
27
|
|
|
@@ -32,6 +33,7 @@ trio = ["anyio[trio]>=4.0.0"]
|
|
|
32
33
|
[tool.uv.sources]
|
|
33
34
|
vercel-oidc = { workspace = true }
|
|
34
35
|
vercel-headers = { workspace = true }
|
|
36
|
+
vercel-internal-core = { workspace = true }
|
|
35
37
|
|
|
36
38
|
[tool.hatch.version]
|
|
37
39
|
path = "vercel/queue/version.py"
|
|
@@ -63,6 +65,10 @@ exclude = [
|
|
|
63
65
|
extend = "../../pyproject.toml"
|
|
64
66
|
cache-dir = "../../.ruff_cache"
|
|
65
67
|
preview = true
|
|
68
|
+
# `vercel` is a PEP 420 namespace package split across workspace members, so
|
|
69
|
+
# `vercel._internal.core` is this package's own shared internals rather than
|
|
70
|
+
# another project's privates. import-private-name needs telling.
|
|
71
|
+
namespace-packages = ["vercel"]
|
|
66
72
|
|
|
67
73
|
[tool.ruff.lint]
|
|
68
74
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|