vercel-queue-bundle 0.7.2__tar.gz → 0.8.0__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_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/PKG-INFO +46 -47
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/README.md +43 -45
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/_vercel_hatch_build.py +38 -3
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/pyproject.toml +7 -1
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/api_common.py +2 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/subscribers.py +81 -30
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/transports.py +28 -14
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/types.py +81 -20
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/version.py +1 -1
- vercel_queue_bundle-0.7.2/vercel/queue/_internal/typeutils.py +0 -203
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/LICENSE +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/hatch_build.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/__main__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/api_async.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/api_sync.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/asgi.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/asynctools.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/cli.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/client.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/client_sync.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/config.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/constants.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/devserver.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/embedded.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/errors.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/http.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/lease.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/log.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/messages.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/multipart.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/names.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/polling.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/push.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/response.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/retry.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/streams.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/LICENSE.python-multipart.txt +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/decoders.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/exceptions.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/multipart.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/python_multipart/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/python_multipart/decoders.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/python_multipart/exceptions.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/python_multipart/multipart.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/devserver.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/embedded.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/py.typed +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/sync.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/testing/__init__.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/testing/pytest.py +0 -0
- {vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/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.
|
|
3
|
+
Version: 0.8.0
|
|
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.
|
|
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
|
|
73
|
+
**3. Handle incoming messages in a subscriber module:**
|
|
73
74
|
|
|
74
75
|
```python
|
|
75
|
-
#
|
|
76
|
-
from vercel.queue import
|
|
76
|
+
# worker.py
|
|
77
|
+
from vercel.queue import subscribe
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
@subscribe(topic="my-topic"
|
|
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.
|
|
85
|
+
**4. Declare the subscriber in `pyproject.toml`:**
|
|
90
86
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
#
|
|
174
|
-
from vercel.queue import
|
|
168
|
+
# worker.py
|
|
169
|
+
from vercel.queue import subscribe
|
|
175
170
|
|
|
176
171
|
|
|
177
|
-
@subscribe(topic="orders",
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
```toml
|
|
180
|
+
[[tool.vercel.subscribers]]
|
|
181
|
+
entrypoint = "worker:handle_order"
|
|
185
182
|
```
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 `
|
|
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
|
|
53
|
+
**3. Handle incoming messages in a subscriber module:**
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
|
-
#
|
|
57
|
-
from vercel.queue import
|
|
56
|
+
# worker.py
|
|
57
|
+
from vercel.queue import subscribe
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
@subscribe(topic="my-topic"
|
|
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.
|
|
65
|
+
**4. Declare the subscriber in `pyproject.toml`:**
|
|
71
66
|
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
#
|
|
155
|
-
from vercel.queue import
|
|
148
|
+
# worker.py
|
|
149
|
+
from vercel.queue import subscribe
|
|
156
150
|
|
|
157
151
|
|
|
158
|
-
@subscribe(topic="orders",
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
```toml
|
|
160
|
+
[[tool.vercel.subscribers]]
|
|
161
|
+
entrypoint = "worker:handle_order"
|
|
166
162
|
```
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 `
|
|
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
|
-
|
|
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-
|
|
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 = [
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/api_common.py
RENAMED
|
@@ -35,6 +35,7 @@ StrContainer = _types.StrContainer
|
|
|
35
35
|
TextBufferTransport = _transports.TextBufferTransport
|
|
36
36
|
TextStreamTransport = _transports.TextStreamTransport
|
|
37
37
|
Topic = _types.Topic
|
|
38
|
+
TopicPattern = _types.TopicPattern
|
|
38
39
|
TypedJsonTransport = _transports.TypedJsonTransport
|
|
39
40
|
__version__ = _version.__version__
|
|
40
41
|
asgi_app = _asgi.asgi_app
|
|
@@ -93,6 +94,7 @@ __all__ = (
|
|
|
93
94
|
"ThrottledError",
|
|
94
95
|
"TokenResolutionError",
|
|
95
96
|
"Topic",
|
|
97
|
+
"TopicPattern",
|
|
96
98
|
"TypedJsonTransport",
|
|
97
99
|
"UnauthorizedError",
|
|
98
100
|
"UnhandledMessageError",
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/subscribers.py
RENAMED
|
@@ -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,
|
|
@@ -31,9 +40,11 @@ from .transports import (
|
|
|
31
40
|
is_untyped_payload_annotation,
|
|
32
41
|
payload_transport_kind,
|
|
33
42
|
reject_invalid_payload_annotation,
|
|
43
|
+
topic_payload_annotation,
|
|
34
44
|
transport_for_kind,
|
|
35
45
|
)
|
|
36
46
|
from .types import (
|
|
47
|
+
BaseTopic,
|
|
37
48
|
Duration,
|
|
38
49
|
Message,
|
|
39
50
|
MessageMetadata,
|
|
@@ -41,20 +52,15 @@ from .types import (
|
|
|
41
52
|
RetryAfter,
|
|
42
53
|
StrContainer,
|
|
43
54
|
Topic,
|
|
55
|
+
TopicPattern,
|
|
44
56
|
Transport,
|
|
45
57
|
duration_to_seconds,
|
|
46
58
|
)
|
|
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
59
|
|
|
56
60
|
_Subscriber: TypeAlias = Callable[..., Any | Awaitable[Any]]
|
|
57
61
|
_SubscriberRef: TypeAlias = weakref.ReferenceType[_Subscriber]
|
|
62
|
+
_SubscriptionTopic: TypeAlias = "str | SanitizedName | Topic[Any] | TopicPattern[Any]"
|
|
63
|
+
"""Anything ``subscribe()`` accepts: one topic, or a pattern matching several."""
|
|
58
64
|
P = ParamSpec("P")
|
|
59
65
|
R = TypeVar("R")
|
|
60
66
|
R_co = TypeVar("R_co", covariant=True)
|
|
@@ -65,6 +71,14 @@ class PayloadAdapter(Protocol):
|
|
|
65
71
|
def validate_python(self, value: Any, /) -> Any: ...
|
|
66
72
|
|
|
67
73
|
|
|
74
|
+
class _TransportPayloadAdapter:
|
|
75
|
+
def __init__(self, validate_payload: Callable[[Any], Any]) -> None:
|
|
76
|
+
self._validate_payload = validate_payload
|
|
77
|
+
|
|
78
|
+
def validate_python(self, value: Any, /) -> Any:
|
|
79
|
+
return self._validate_payload(value)
|
|
80
|
+
|
|
81
|
+
|
|
68
82
|
class EmbeddedDispatcher(Protocol):
|
|
69
83
|
def register_subscription(
|
|
70
84
|
self,
|
|
@@ -106,6 +120,7 @@ class InvocationPlan:
|
|
|
106
120
|
payload_adapter: PayloadAdapter | None
|
|
107
121
|
mode: InvocationMode
|
|
108
122
|
transport_kind: TransportKind
|
|
123
|
+
transport: Transport[Any] | None = None
|
|
109
124
|
|
|
110
125
|
def prepare_payload(self, payload: Any) -> Any:
|
|
111
126
|
if self.payload_adapter is None:
|
|
@@ -119,6 +134,33 @@ class InvocationPlan:
|
|
|
119
134
|
raise
|
|
120
135
|
|
|
121
136
|
|
|
137
|
+
class _TransportOrKind:
|
|
138
|
+
def __init__(self, plan: InvocationPlan) -> None:
|
|
139
|
+
self.transport_kind = plan.transport_kind
|
|
140
|
+
self.transport = plan.transport
|
|
141
|
+
|
|
142
|
+
def __hash__(self) -> int:
|
|
143
|
+
return hash(self.transport_kind) if self.transport is None else id(self.transport)
|
|
144
|
+
|
|
145
|
+
def __eq__(self, other: object) -> bool:
|
|
146
|
+
if not isinstance(other, _TransportOrKind):
|
|
147
|
+
return NotImplemented
|
|
148
|
+
if self.transport is None and other.transport is None:
|
|
149
|
+
return self.transport_kind == other.transport_kind
|
|
150
|
+
# Two instances of the same class can decode differently
|
|
151
|
+
return self.transport is other.transport
|
|
152
|
+
|
|
153
|
+
def __repr__(self) -> str:
|
|
154
|
+
if self.transport is None:
|
|
155
|
+
return self.transport_kind
|
|
156
|
+
return repr(self.transport)
|
|
157
|
+
|
|
158
|
+
def get_transport(self) -> Transport[Any]:
|
|
159
|
+
if self.transport is None:
|
|
160
|
+
return _transport_for_kind(self.transport_kind)
|
|
161
|
+
return self.transport
|
|
162
|
+
|
|
163
|
+
|
|
122
164
|
@dataclass(frozen=True, kw_only=True)
|
|
123
165
|
class _Subscription:
|
|
124
166
|
func_ref: _SubscriberRef
|
|
@@ -313,23 +355,17 @@ def _message_payload_annotation(annotation: Any) -> Any:
|
|
|
313
355
|
return message_args[0]
|
|
314
356
|
|
|
315
357
|
|
|
316
|
-
def
|
|
317
|
-
if not isinstance(topic, Topic):
|
|
318
|
-
return inspect.Signature.empty
|
|
319
|
-
if getattr(type(topic), "__topic_origin__", None) is not Topic:
|
|
320
|
-
return inspect.Signature.empty
|
|
321
|
-
return type(topic).__topic_payload_type__
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def _normalize_subscription_topic(topic: str | SanitizedName | Topic[Any]) -> str:
|
|
358
|
+
def _normalize_subscription_topic(topic: _SubscriptionTopic) -> str:
|
|
325
359
|
if isinstance(topic, SanitizedName):
|
|
326
360
|
return str(topic)
|
|
327
361
|
if isinstance(topic, str):
|
|
328
362
|
validate_subscription_pattern(topic)
|
|
329
363
|
return topic
|
|
364
|
+
if isinstance(topic, TopicPattern):
|
|
365
|
+
return str(topic.name)
|
|
330
366
|
if isinstance(topic, Topic):
|
|
331
367
|
return validate_topic_name(topic)
|
|
332
|
-
raise TypeError("topic must be a string or
|
|
368
|
+
raise TypeError("topic must be a string, Topic, or TopicPattern")
|
|
333
369
|
|
|
334
370
|
|
|
335
371
|
def _resolve_invocation_payload_annotation(
|
|
@@ -355,7 +391,11 @@ def _payload_adapter(
|
|
|
355
391
|
annotation: Any,
|
|
356
392
|
*,
|
|
357
393
|
localns: dict[str, Any] | None = None,
|
|
394
|
+
transport: Transport[Any] | None = None,
|
|
358
395
|
) -> PayloadAdapter | None:
|
|
396
|
+
validate_payload = getattr(transport, "validate_payload", None)
|
|
397
|
+
if callable(validate_payload):
|
|
398
|
+
return _TransportPayloadAdapter(validate_payload)
|
|
359
399
|
annotation = strip_annotated(annotation)
|
|
360
400
|
if is_untyped_payload_annotation(annotation):
|
|
361
401
|
return None
|
|
@@ -406,6 +446,7 @@ def _build_invocation_plan(
|
|
|
406
446
|
func: _Subscriber,
|
|
407
447
|
*,
|
|
408
448
|
topic_payload_annotation: Any = inspect.Signature.empty,
|
|
449
|
+
transport: Transport[Any] | None = None,
|
|
409
450
|
) -> InvocationPlan:
|
|
410
451
|
signature = inspect.signature(func)
|
|
411
452
|
input_params: list[inspect.Parameter] = []
|
|
@@ -445,9 +486,11 @@ def _build_invocation_plan(
|
|
|
445
486
|
payload_adapter=_payload_adapter(
|
|
446
487
|
payload_annotation,
|
|
447
488
|
localns=resolved_annotation.localns,
|
|
489
|
+
transport=transport,
|
|
448
490
|
),
|
|
449
491
|
mode=mode,
|
|
450
492
|
transport_kind=_transport_kind(payload_annotation),
|
|
493
|
+
transport=transport,
|
|
451
494
|
)
|
|
452
495
|
|
|
453
496
|
|
|
@@ -592,13 +635,13 @@ def infer_subscriber_transport(metadata: MessageMetadata) -> Transport[Any]:
|
|
|
592
635
|
if not matching:
|
|
593
636
|
raise _no_matching_subscriptions_error(metadata.topic)
|
|
594
637
|
|
|
595
|
-
|
|
596
|
-
if len(
|
|
638
|
+
tks = {_TransportOrKind(matched.subscription.invocation) for matched in matching}
|
|
639
|
+
if len(tks) != 1:
|
|
597
640
|
raise SubscriptionError(
|
|
598
641
|
"matching queue subscribers require incompatible payload transports: "
|
|
599
|
-
+ ", ".join(sorted(
|
|
642
|
+
+ ", ".join(sorted(map(repr, tks)))
|
|
600
643
|
)
|
|
601
|
-
return
|
|
644
|
+
return tks.pop().get_transport()
|
|
602
645
|
|
|
603
646
|
|
|
604
647
|
async def _maybe_await_result(result: Any) -> Any:
|
|
@@ -701,14 +744,14 @@ def _register_subscription(
|
|
|
701
744
|
func: Callable[P, R],
|
|
702
745
|
*,
|
|
703
746
|
consumer_group: str | SanitizedName | None = None,
|
|
704
|
-
topic:
|
|
747
|
+
topic: _SubscriptionTopic,
|
|
705
748
|
retry_after: Duration | None = None,
|
|
706
749
|
initial_delay: Duration | None = None,
|
|
707
750
|
max_concurrency: int | None = None,
|
|
708
751
|
max_attempts: int | None = None,
|
|
709
752
|
) -> QueueSubscriber[P, R]:
|
|
710
753
|
topic_name = _normalize_subscription_topic(topic)
|
|
711
|
-
|
|
754
|
+
payload_annotation = topic_payload_annotation(topic)
|
|
712
755
|
|
|
713
756
|
resolved_consumer_group = (
|
|
714
757
|
_default_consumer_group(func)
|
|
@@ -724,7 +767,10 @@ def _register_subscription(
|
|
|
724
767
|
consumer_group=resolved_consumer_group,
|
|
725
768
|
invocation=_build_invocation_plan(
|
|
726
769
|
cast("_Subscriber", func),
|
|
727
|
-
topic_payload_annotation=
|
|
770
|
+
topic_payload_annotation=payload_annotation,
|
|
771
|
+
# This is not receive_transport_for_topic() because we are doing
|
|
772
|
+
# more than that in _build_invocation_plan()
|
|
773
|
+
transport=topic.transport if isinstance(topic, BaseTopic) else None,
|
|
728
774
|
),
|
|
729
775
|
topic=topic_name,
|
|
730
776
|
retry_after_seconds=_optional_bounded_duration(
|
|
@@ -768,7 +814,12 @@ def subscribe(
|
|
|
768
814
|
|
|
769
815
|
|
|
770
816
|
@overload
|
|
771
|
-
def subscribe(
|
|
817
|
+
def subscribe(
|
|
818
|
+
func: Callable[[T], R],
|
|
819
|
+
/,
|
|
820
|
+
*,
|
|
821
|
+
topic: Topic[T] | TopicPattern[T],
|
|
822
|
+
) -> QueueSubscriber[[T], R]: ...
|
|
772
823
|
|
|
773
824
|
|
|
774
825
|
@overload
|
|
@@ -776,7 +827,7 @@ def subscribe(
|
|
|
776
827
|
func: Callable[[Message[T]], R],
|
|
777
828
|
/,
|
|
778
829
|
*,
|
|
779
|
-
topic: Topic[T],
|
|
830
|
+
topic: Topic[T] | TopicPattern[T],
|
|
780
831
|
) -> QueueSubscriber[[Message[T]], R]: ...
|
|
781
832
|
|
|
782
833
|
|
|
@@ -807,7 +858,7 @@ def subscribe(
|
|
|
807
858
|
@overload
|
|
808
859
|
def subscribe(
|
|
809
860
|
*,
|
|
810
|
-
topic: Topic[T],
|
|
861
|
+
topic: Topic[T] | TopicPattern[T],
|
|
811
862
|
consumer_group: str | SanitizedName | None = None,
|
|
812
863
|
retry_after: Duration | None = None,
|
|
813
864
|
initial_delay: Duration | None = None,
|
|
@@ -849,7 +900,7 @@ def subscribe(
|
|
|
849
900
|
func: None,
|
|
850
901
|
/,
|
|
851
902
|
*,
|
|
852
|
-
topic: Topic[T],
|
|
903
|
+
topic: Topic[T] | TopicPattern[T],
|
|
853
904
|
consumer_group: str | SanitizedName | None = None,
|
|
854
905
|
retry_after: Duration | None = None,
|
|
855
906
|
initial_delay: Duration | None = None,
|
|
@@ -862,7 +913,7 @@ def subscribe(
|
|
|
862
913
|
func: _Subscriber | None = None,
|
|
863
914
|
/,
|
|
864
915
|
*,
|
|
865
|
-
topic:
|
|
916
|
+
topic: _SubscriptionTopic,
|
|
866
917
|
consumer_group: str | SanitizedName | None = None,
|
|
867
918
|
retry_after: Duration | None = None,
|
|
868
919
|
initial_delay: Duration | None = None,
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/transports.py
RENAMED
|
@@ -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 (
|
|
@@ -16,20 +27,12 @@ from .streams import (
|
|
|
16
27
|
SyncTextStreamPayload,
|
|
17
28
|
)
|
|
18
29
|
from .types import (
|
|
30
|
+
BaseTopic,
|
|
19
31
|
RequestContent,
|
|
20
32
|
Topic,
|
|
33
|
+
TopicPattern,
|
|
21
34
|
Transport,
|
|
22
35
|
)
|
|
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
36
|
|
|
34
37
|
T = TypeVar("T")
|
|
35
38
|
|
|
@@ -303,14 +306,24 @@ def receive_transport_for_annotation(annotation: Any) -> Transport[Any]:
|
|
|
303
306
|
return transport_for_kind(kind)
|
|
304
307
|
|
|
305
308
|
|
|
309
|
+
def topic_payload_annotation(topic: object) -> Any:
|
|
310
|
+
"""Return the payload type ``topic`` was specialized with, if any.
|
|
311
|
+
|
|
312
|
+
A bare name, an unspecialized topic, and a user subclass all report "no
|
|
313
|
+
annotation": none of them carries a payload type we put there.
|
|
314
|
+
"""
|
|
315
|
+
origin = getattr(type(topic), "__topic_origin__", None)
|
|
316
|
+
if origin is not Topic and origin is not TopicPattern:
|
|
317
|
+
return inspect.Signature.empty
|
|
318
|
+
return getattr(type(topic), "__topic_payload_type__", inspect.Signature.empty)
|
|
319
|
+
|
|
320
|
+
|
|
306
321
|
def receive_transport_for_topic(topic: object) -> Transport[Any]:
|
|
307
|
-
if not isinstance(topic,
|
|
322
|
+
if not isinstance(topic, BaseTopic):
|
|
308
323
|
return RawJsonTransport[Any]()
|
|
309
324
|
if topic.transport is not None:
|
|
310
325
|
return topic.transport
|
|
311
|
-
|
|
312
|
-
return RawJsonTransport[Any]()
|
|
313
|
-
return receive_transport_for_annotation(type(topic).__topic_payload_type__)
|
|
326
|
+
return receive_transport_for_annotation(topic_payload_annotation(topic))
|
|
314
327
|
|
|
315
328
|
|
|
316
329
|
def send_transport_for_topic(topic: object) -> Transport[Any] | None:
|
|
@@ -359,5 +372,6 @@ __all__ = (
|
|
|
359
372
|
"receive_transport_for_topic",
|
|
360
373
|
"reject_invalid_payload_annotation",
|
|
361
374
|
"send_transport_for_topic",
|
|
375
|
+
"topic_payload_annotation",
|
|
362
376
|
"transport_for_kind",
|
|
363
377
|
)
|
|
@@ -12,8 +12,15 @@ from collections.abc import (
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from datetime import datetime, timedelta
|
|
14
14
|
|
|
15
|
+
from vercel._internal.core.polyfills import Self
|
|
16
|
+
|
|
15
17
|
from .constants import DEFAULT_RETRY_AFTER_SECONDS
|
|
16
|
-
from .names import
|
|
18
|
+
from .names import (
|
|
19
|
+
SanitizedName,
|
|
20
|
+
validate_name,
|
|
21
|
+
validate_subscription_pattern,
|
|
22
|
+
validate_topic_name,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
T = TypeVar("T")
|
|
19
26
|
_TYPE_VAR_TYPE = type(T)
|
|
@@ -62,47 +69,62 @@ def duration_to_float_seconds(duration: Duration) -> float:
|
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
@dataclass(frozen=True, kw_only=True, eq=False)
|
|
65
|
-
class
|
|
66
|
-
"""
|
|
72
|
+
class BaseTopic(Generic[T]):
|
|
73
|
+
"""Shared behaviour of :class:`Topic` and :class:`TopicPattern`.
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
Use ``Topic`` to name one topic and ``TopicPattern`` to match several; the
|
|
76
|
+
two are separate types so that a pattern cannot reach an operation that
|
|
77
|
+
needs a single, concrete topic.
|
|
69
78
|
"""
|
|
70
79
|
|
|
71
|
-
name: SanitizedName
|
|
72
|
-
"""
|
|
80
|
+
name: SanitizedName | str
|
|
81
|
+
"""The topic name, or the pattern that selects topic names.
|
|
82
|
+
|
|
83
|
+
A pattern is a plain ``str``: ``SanitizedName`` means "safe to put in a
|
|
84
|
+
request path", which a pattern is not.
|
|
85
|
+
"""
|
|
73
86
|
|
|
74
87
|
transport: Transport[Any] | None = None
|
|
75
|
-
"""Optional transport used when sending to or
|
|
88
|
+
"""Optional transport used when sending to, polling, or subscribing to this topic."""
|
|
76
89
|
|
|
77
|
-
__topic_origin__: ClassVar[type[
|
|
90
|
+
__topic_origin__: ClassVar[type[BaseTopic[Any]] | None] = None
|
|
78
91
|
__topic_payload_type__: ClassVar[Any] = None
|
|
79
|
-
_specializations: ClassVar[dict[Any, type[
|
|
80
|
-
|
|
81
|
-
def
|
|
92
|
+
_specializations: ClassVar[dict[Any, type[BaseTopic[Any]]]] = {}
|
|
93
|
+
|
|
94
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
95
|
+
super().__init_subclass__(**kwargs)
|
|
96
|
+
# `Topic` and `TopicPattern` each need their own cache, or the first
|
|
97
|
+
# `Topic[bytes]` would be handed back for `TopicPattern[bytes]`. A
|
|
98
|
+
# specialization (the only thing carrying `__topic_origin__` in its
|
|
99
|
+
# own namespace) keeps sharing the cache of the class it came from.
|
|
100
|
+
if "__topic_origin__" not in cls.__dict__:
|
|
101
|
+
cls._specializations = {}
|
|
102
|
+
|
|
103
|
+
def __class_getitem__(cls, params: Any) -> type[Self]:
|
|
82
104
|
if isinstance(params, tuple):
|
|
83
105
|
if len(params) != 1:
|
|
84
|
-
raise TypeError("
|
|
106
|
+
raise TypeError(f"{cls.__name__} expects exactly one type argument")
|
|
85
107
|
params = params[0]
|
|
86
108
|
if isinstance(params, _TYPE_VAR_TYPE):
|
|
87
|
-
return
|
|
109
|
+
return cls
|
|
88
110
|
|
|
89
111
|
try:
|
|
90
|
-
return cls._specializations[params]
|
|
112
|
+
return cast("type[Self]", cls._specializations[params])
|
|
91
113
|
except KeyError:
|
|
92
114
|
pass
|
|
93
115
|
|
|
94
116
|
payload_repr = _topic_payload_type_repr(params)
|
|
95
117
|
specialization = type(
|
|
96
|
-
f"
|
|
118
|
+
f"{cls.__name__}[{payload_repr}]",
|
|
97
119
|
(cls,),
|
|
98
120
|
{
|
|
99
121
|
"__module__": cls.__module__,
|
|
100
|
-
"__topic_origin__":
|
|
122
|
+
"__topic_origin__": cls,
|
|
101
123
|
"__topic_payload_type__": params,
|
|
102
124
|
},
|
|
103
125
|
)
|
|
104
126
|
cls._specializations[params] = specialization
|
|
105
|
-
return cast("type[
|
|
127
|
+
return cast("type[Self]", specialization)
|
|
106
128
|
|
|
107
129
|
def __init__(
|
|
108
130
|
self,
|
|
@@ -110,14 +132,22 @@ class Topic(Generic[T]):
|
|
|
110
132
|
*,
|
|
111
133
|
transport: Transport[Any] | None = None,
|
|
112
134
|
) -> None:
|
|
113
|
-
object.__setattr__(self, "name",
|
|
135
|
+
object.__setattr__(self, "name", self._validated_name(name))
|
|
114
136
|
object.__setattr__(self, "transport", transport)
|
|
115
137
|
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _validated_name(name: str | SanitizedName) -> SanitizedName | str:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
116
142
|
def __repr__(self) -> str:
|
|
117
|
-
|
|
143
|
+
# Report the class the user named, not the synthesized specialization.
|
|
144
|
+
cls = type(self)
|
|
145
|
+
return f"{(cls.__topic_origin__ or cls).__name__}(name={self.name!r})"
|
|
118
146
|
|
|
119
147
|
def __eq__(self, other: object) -> bool:
|
|
120
|
-
|
|
148
|
+
# A pattern always ends in `*`, which a topic name can never contain,
|
|
149
|
+
# so comparing names is enough to keep the two kinds apart.
|
|
150
|
+
if not isinstance(other, BaseTopic):
|
|
121
151
|
return NotImplemented
|
|
122
152
|
return self.name == other.name
|
|
123
153
|
|
|
@@ -125,6 +155,36 @@ class Topic(Generic[T]):
|
|
|
125
155
|
return hash(self.name)
|
|
126
156
|
|
|
127
157
|
|
|
158
|
+
class Topic(BaseTopic[T]):
|
|
159
|
+
"""A named Vercel Queues topic.
|
|
160
|
+
|
|
161
|
+
Topics identify the stream that messages are sent to and received from.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
name: SanitizedName
|
|
165
|
+
"""Topic name to send to, poll, or subscribe to."""
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _validated_name(name: str | SanitizedName) -> SanitizedName | str:
|
|
169
|
+
return SanitizedName(validate_topic_name(name))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TopicPattern(BaseTopic[T]):
|
|
173
|
+
"""A pattern selecting every Vercel Queues topic it matches.
|
|
174
|
+
|
|
175
|
+
``"*"`` matches every topic and a prefix ending in ``*`` matches by
|
|
176
|
+
prefix. A pattern can only be subscribed to: sending and polling name one
|
|
177
|
+
topic, so they take a :class:`Topic`.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
name: str
|
|
181
|
+
"""The pattern this subscription matches topics with."""
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _validated_name(name: str | SanitizedName) -> SanitizedName | str:
|
|
185
|
+
return validate_subscription_pattern(str(name))
|
|
186
|
+
|
|
187
|
+
|
|
128
188
|
def _topic_payload_type_repr(payload_type: object) -> str:
|
|
129
189
|
name = getattr(payload_type, "__qualname__", None)
|
|
130
190
|
if isinstance(name, str):
|
|
@@ -297,4 +357,5 @@ __all__: tuple[str, ...] = (
|
|
|
297
357
|
"RetryAfter",
|
|
298
358
|
"StrContainer",
|
|
299
359
|
"Topic",
|
|
360
|
+
"TopicPattern",
|
|
300
361
|
)
|
|
@@ -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
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/asynctools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_internal/client_sync.py
RENAMED
|
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
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/__init__.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/decoders.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/exceptions.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.7.2 → vercel_queue_bundle-0.8.0}/vercel/queue/_vendor/multipart/multipart.py
RENAMED
|
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
|