vercel-queue 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vercel/queue/__init__.py +8 -0
- vercel/queue/__main__.py +10 -0
- vercel/queue/_internal/__init__.py +1 -0
- vercel/queue/_internal/api_async.py +219 -0
- vercel/queue/_internal/api_common.py +104 -0
- vercel/queue/_internal/api_sync.py +137 -0
- vercel/queue/_internal/asgi.py +145 -0
- vercel/queue/_internal/asynctools.py +40 -0
- vercel/queue/_internal/cli.py +313 -0
- vercel/queue/_internal/client.py +1109 -0
- vercel/queue/_internal/client_sync.py +432 -0
- vercel/queue/_internal/config.py +160 -0
- vercel/queue/_internal/constants.py +50 -0
- vercel/queue/_internal/devserver.py +205 -0
- vercel/queue/_internal/embedded.py +1900 -0
- vercel/queue/_internal/errors.py +262 -0
- vercel/queue/_internal/http.py +634 -0
- vercel/queue/_internal/lease.py +1007 -0
- vercel/queue/_internal/log.py +143 -0
- vercel/queue/_internal/messages.py +122 -0
- vercel/queue/_internal/multipart.py +255 -0
- vercel/queue/_internal/names.py +101 -0
- vercel/queue/_internal/polling.py +200 -0
- vercel/queue/_internal/push.py +246 -0
- vercel/queue/_internal/response.py +111 -0
- vercel/queue/_internal/retry.py +86 -0
- vercel/queue/_internal/streams.py +313 -0
- vercel/queue/_internal/subscribers.py +1025 -0
- vercel/queue/_internal/transports.py +363 -0
- vercel/queue/_internal/types.py +300 -0
- vercel/queue/_internal/typeutils.py +203 -0
- vercel/queue/devserver.py +24 -0
- vercel/queue/embedded.py +50 -0
- vercel/queue/py.typed +1 -0
- vercel/queue/sync.py +8 -0
- vercel/queue/testing/__init__.py +14 -0
- vercel/queue/testing/pytest.py +42 -0
- vercel/queue/testing/state.py +32 -0
- vercel/queue/version.py +3 -0
- vercel_queue-0.7.0.dist-info/METADATA +681 -0
- vercel_queue-0.7.0.dist-info/RECORD +43 -0
- vercel_queue-0.7.0.dist-info/WHEEL +4 -0
- vercel_queue-0.7.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypeVar, overload
|
|
4
|
+
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
from collections.abc import Iterator, Mapping
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
|
|
9
|
+
from vercel.headers import HeadersContext
|
|
10
|
+
|
|
11
|
+
from .asynctools import iter_async_iterator, iter_coroutine
|
|
12
|
+
from .client import BaseQueueClient, Delivery
|
|
13
|
+
from .config import CURRENT_DEPLOYMENT, BaseUrl, DeploymentOption
|
|
14
|
+
from .http import (
|
|
15
|
+
AsyncQueueRuntime,
|
|
16
|
+
HttpResponse,
|
|
17
|
+
PushDeliveryBody,
|
|
18
|
+
SyncHttpClientFactory,
|
|
19
|
+
SyncQueueRuntime,
|
|
20
|
+
)
|
|
21
|
+
from .lease import (
|
|
22
|
+
LeaseRenewal,
|
|
23
|
+
finalize_payload_sync,
|
|
24
|
+
processing_lease_seconds,
|
|
25
|
+
retry_message_after_sync,
|
|
26
|
+
)
|
|
27
|
+
from .log import debug_log_for_msg
|
|
28
|
+
from .messages import sync_message_payload
|
|
29
|
+
from .names import SanitizedName
|
|
30
|
+
from .polling import run_poll_and_handle_sync, start_sync_polling_thread
|
|
31
|
+
from .push import accept_input_sync, parse_push_delivery_metadata
|
|
32
|
+
from .retry import retry_sync_follow_up
|
|
33
|
+
from .streams import SyncStreamPayload, SyncTextStreamPayload
|
|
34
|
+
from .subscribers import (
|
|
35
|
+
QueueSubscriber,
|
|
36
|
+
call_subscribers_sync,
|
|
37
|
+
infer_subscriber_transport,
|
|
38
|
+
reject_async_subscriber_for_sync,
|
|
39
|
+
)
|
|
40
|
+
from .types import (
|
|
41
|
+
Duration,
|
|
42
|
+
Handoff,
|
|
43
|
+
Message,
|
|
44
|
+
MessageID,
|
|
45
|
+
MessageMetadata,
|
|
46
|
+
RawHeaders,
|
|
47
|
+
RetryAfter,
|
|
48
|
+
StrContainer,
|
|
49
|
+
Topic,
|
|
50
|
+
Transport,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
T = TypeVar("T")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class QueueClient(BaseQueueClient):
|
|
57
|
+
"""Synchronous Vercel Queues client.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
token: Vercel OIDC token. Defaults to environment/OIDC resolution.
|
|
61
|
+
region: Queue region, such as ``"iad1"``.
|
|
62
|
+
base_url: Custom service base URL or region resolver for tests or proxies.
|
|
63
|
+
deployment: Deployment partition selection. Use ``ALL_DEPLOYMENTS``
|
|
64
|
+
to omit ``Vqs-Deployment-Id``.
|
|
65
|
+
headers: Default custom headers for send requests.
|
|
66
|
+
timeout: HTTP timeout in seconds.
|
|
67
|
+
http_client_factory: Optional factory for the underlying HTTP client.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
token: str | None = None,
|
|
75
|
+
region: str | None = None,
|
|
76
|
+
base_url: BaseUrl | None = None,
|
|
77
|
+
deployment: DeploymentOption = CURRENT_DEPLOYMENT,
|
|
78
|
+
headers: Mapping[str, str] | None = None,
|
|
79
|
+
timeout: Duration | None = 10.0,
|
|
80
|
+
http_client_factory: SyncHttpClientFactory | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(
|
|
83
|
+
runtime=SyncQueueRuntime(
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
client_factory=http_client_factory,
|
|
86
|
+
),
|
|
87
|
+
token=token,
|
|
88
|
+
region=region,
|
|
89
|
+
base_url=base_url,
|
|
90
|
+
deployment=deployment,
|
|
91
|
+
headers=headers,
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def send(
|
|
96
|
+
self,
|
|
97
|
+
topic: str | SanitizedName | Topic[T],
|
|
98
|
+
payload: T,
|
|
99
|
+
*,
|
|
100
|
+
idempotency_key: str | None = None,
|
|
101
|
+
retention: Duration | None = None,
|
|
102
|
+
delay: Duration | None = None,
|
|
103
|
+
deployment: DeploymentOption = CURRENT_DEPLOYMENT,
|
|
104
|
+
headers: Mapping[str, str] | None = None,
|
|
105
|
+
) -> MessageID | None:
|
|
106
|
+
"""Send a message to a topic."""
|
|
107
|
+
return iter_coroutine(
|
|
108
|
+
self._send(
|
|
109
|
+
topic,
|
|
110
|
+
payload,
|
|
111
|
+
idempotency_key=idempotency_key,
|
|
112
|
+
retention=retention,
|
|
113
|
+
delay=delay,
|
|
114
|
+
deployment=deployment,
|
|
115
|
+
headers=headers,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _accept_impl(
|
|
120
|
+
self,
|
|
121
|
+
raw_body: PushDeliveryBody | HttpResponse,
|
|
122
|
+
headers: RawHeaders | None = None,
|
|
123
|
+
*,
|
|
124
|
+
transport: Transport[T] | None = None,
|
|
125
|
+
lease_duration: Duration | None = None,
|
|
126
|
+
) -> Message[T]:
|
|
127
|
+
raw_body, headers = accept_input_sync(raw_body, headers)
|
|
128
|
+
message = iter_coroutine(
|
|
129
|
+
self._accept(
|
|
130
|
+
raw_body,
|
|
131
|
+
headers,
|
|
132
|
+
transport=transport,
|
|
133
|
+
lease_duration=lease_duration,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return sync_message_payload(message)
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def accept_and_handle(
|
|
140
|
+
self,
|
|
141
|
+
raw_body: PushDeliveryBody,
|
|
142
|
+
headers: RawHeaders,
|
|
143
|
+
*,
|
|
144
|
+
lease_duration: Duration | None = None,
|
|
145
|
+
) -> None: ...
|
|
146
|
+
|
|
147
|
+
@overload
|
|
148
|
+
def accept_and_handle(
|
|
149
|
+
self,
|
|
150
|
+
raw_body: HttpResponse,
|
|
151
|
+
headers: None = None,
|
|
152
|
+
*,
|
|
153
|
+
lease_duration: Duration | None = None,
|
|
154
|
+
) -> None: ...
|
|
155
|
+
|
|
156
|
+
def accept_and_handle(
|
|
157
|
+
self,
|
|
158
|
+
raw_body: PushDeliveryBody | HttpResponse,
|
|
159
|
+
headers: RawHeaders | None = None,
|
|
160
|
+
*,
|
|
161
|
+
lease_duration: Duration | None = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Accept a push callback and dispatch matching subscribers."""
|
|
164
|
+
self._accept_and_handle(
|
|
165
|
+
raw_body,
|
|
166
|
+
headers,
|
|
167
|
+
lease_duration=lease_duration,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _accept_and_handle(
|
|
171
|
+
self,
|
|
172
|
+
raw_body: PushDeliveryBody | HttpResponse,
|
|
173
|
+
headers: RawHeaders | None = None,
|
|
174
|
+
*,
|
|
175
|
+
transport: Transport[T] | None = None,
|
|
176
|
+
lease_duration: Duration | None = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
raw_body, headers = accept_input_sync(raw_body, headers)
|
|
179
|
+
processing_lease_duration = processing_lease_seconds(lease_duration)
|
|
180
|
+
if transport is None:
|
|
181
|
+
metadata = parse_push_delivery_metadata(headers)
|
|
182
|
+
transport = infer_subscriber_transport(metadata)
|
|
183
|
+
# Delivery headers carry the auth context for follow-up queue calls,
|
|
184
|
+
# including lease renewals running outside this call stack.
|
|
185
|
+
with HeadersContext(headers).use():
|
|
186
|
+
message = sync_message_payload(
|
|
187
|
+
iter_coroutine(
|
|
188
|
+
self._accept(
|
|
189
|
+
raw_body,
|
|
190
|
+
headers,
|
|
191
|
+
transport=transport,
|
|
192
|
+
lease_duration=processing_lease_duration,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
lifecycle = _MessageLifecycle(
|
|
197
|
+
message,
|
|
198
|
+
client=self,
|
|
199
|
+
lease_duration=processing_lease_duration,
|
|
200
|
+
)
|
|
201
|
+
with lifecycle:
|
|
202
|
+
call_subscribers_sync(message)
|
|
203
|
+
|
|
204
|
+
@overload
|
|
205
|
+
def poll(
|
|
206
|
+
self,
|
|
207
|
+
topic: Topic[SyncStreamPayload],
|
|
208
|
+
consumer_group: str | SanitizedName,
|
|
209
|
+
*,
|
|
210
|
+
limit: int = 1,
|
|
211
|
+
lease_duration: Duration | None = None,
|
|
212
|
+
) -> Iterator[Delivery[SyncStreamPayload]]: ...
|
|
213
|
+
|
|
214
|
+
@overload
|
|
215
|
+
def poll(
|
|
216
|
+
self,
|
|
217
|
+
topic: Topic[SyncTextStreamPayload],
|
|
218
|
+
consumer_group: str | SanitizedName,
|
|
219
|
+
*,
|
|
220
|
+
limit: int = 1,
|
|
221
|
+
lease_duration: Duration | None = None,
|
|
222
|
+
) -> Iterator[Delivery[SyncTextStreamPayload]]: ...
|
|
223
|
+
|
|
224
|
+
@overload
|
|
225
|
+
def poll(
|
|
226
|
+
self,
|
|
227
|
+
topic: Topic[T],
|
|
228
|
+
consumer_group: str | SanitizedName,
|
|
229
|
+
*,
|
|
230
|
+
limit: int = 1,
|
|
231
|
+
lease_duration: Duration | None = None,
|
|
232
|
+
) -> Iterator[Delivery[T]]: ...
|
|
233
|
+
|
|
234
|
+
@overload
|
|
235
|
+
def poll(
|
|
236
|
+
self,
|
|
237
|
+
topic: str,
|
|
238
|
+
consumer_group: str | SanitizedName,
|
|
239
|
+
*,
|
|
240
|
+
limit: int = 1,
|
|
241
|
+
lease_duration: Duration | None = None,
|
|
242
|
+
) -> Iterator[Delivery[Any]]: ...
|
|
243
|
+
|
|
244
|
+
@overload
|
|
245
|
+
def poll(
|
|
246
|
+
self,
|
|
247
|
+
topic: SanitizedName,
|
|
248
|
+
consumer_group: str | SanitizedName,
|
|
249
|
+
*,
|
|
250
|
+
limit: int = 1,
|
|
251
|
+
lease_duration: Duration | None = None,
|
|
252
|
+
) -> Iterator[Delivery[Any]]: ...
|
|
253
|
+
|
|
254
|
+
def poll(
|
|
255
|
+
self,
|
|
256
|
+
topic: str | SanitizedName | Topic[Any],
|
|
257
|
+
consumer_group: str | SanitizedName,
|
|
258
|
+
*,
|
|
259
|
+
limit: int = 1,
|
|
260
|
+
lease_duration: Duration | None = None,
|
|
261
|
+
) -> Iterator[Delivery[Any]]:
|
|
262
|
+
"""Poll available deliveries for a consumer group."""
|
|
263
|
+
messages = self._receive(
|
|
264
|
+
topic,
|
|
265
|
+
consumer_group,
|
|
266
|
+
limit=limit,
|
|
267
|
+
lease_duration=lease_duration,
|
|
268
|
+
)
|
|
269
|
+
for message in iter_async_iterator(messages):
|
|
270
|
+
yield Delivery(
|
|
271
|
+
sync_message_payload(message),
|
|
272
|
+
client=self,
|
|
273
|
+
lease_duration=lease_duration,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def poll_and_handle(
|
|
277
|
+
self,
|
|
278
|
+
subscriber: QueueSubscriber[..., Any],
|
|
279
|
+
*,
|
|
280
|
+
topics: StrContainer | None = None,
|
|
281
|
+
interval: Duration = 1.0,
|
|
282
|
+
limit: int | None = None,
|
|
283
|
+
lease_duration: Duration | None = None,
|
|
284
|
+
) -> concurrent.futures.Future[None]:
|
|
285
|
+
"""Start a background thread that polls and dispatches one subscriber."""
|
|
286
|
+
reject_async_subscriber_for_sync(subscriber)
|
|
287
|
+
return start_sync_polling_thread(
|
|
288
|
+
lambda stop: run_poll_and_handle_sync(
|
|
289
|
+
poll=self.poll,
|
|
290
|
+
subscriber=subscriber,
|
|
291
|
+
topics=topics,
|
|
292
|
+
interval=interval,
|
|
293
|
+
limit=limit,
|
|
294
|
+
lease_duration=lease_duration,
|
|
295
|
+
stop=stop,
|
|
296
|
+
),
|
|
297
|
+
name="vercel-queue-poll-and-handle",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def acknowledge(self, message: Message[T] | MessageMetadata) -> None:
|
|
301
|
+
"""Acknowledge a received message.
|
|
302
|
+
|
|
303
|
+
This is a lower-level API for integrations that manage message
|
|
304
|
+
lifecycles manually. Application code should usually process messages
|
|
305
|
+
through ``poll`` delivery context managers or ``accept_and_handle`` so
|
|
306
|
+
acknowledgement happens automatically.
|
|
307
|
+
"""
|
|
308
|
+
retry_sync_follow_up(
|
|
309
|
+
lambda: iter_coroutine(self._acknowledge(message)),
|
|
310
|
+
event_prefix="ack",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def extend_lease(
|
|
314
|
+
self,
|
|
315
|
+
message: Message[T] | MessageMetadata,
|
|
316
|
+
duration: Duration,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Extend message processing.
|
|
319
|
+
|
|
320
|
+
This is a lower-level API for integrations that manage message
|
|
321
|
+
lifecycles manually. Application code should usually rely on delivery
|
|
322
|
+
context managers or ``LeaseRenewal`` to keep leases alive while a
|
|
323
|
+
handler runs.
|
|
324
|
+
"""
|
|
325
|
+
retry_sync_follow_up(
|
|
326
|
+
lambda: iter_coroutine(self._extend_lease(message, duration)),
|
|
327
|
+
event_prefix="visibility",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def retry_after(
|
|
331
|
+
self,
|
|
332
|
+
message: Message[T] | MessageMetadata,
|
|
333
|
+
delay: Duration,
|
|
334
|
+
) -> None:
|
|
335
|
+
"""Request redelivery of a received message after ``delay``.
|
|
336
|
+
|
|
337
|
+
This is a lower-level API for integrations that manage message
|
|
338
|
+
lifecycles manually. Application code should usually raise
|
|
339
|
+
``RetryAfter`` from a handler instead of calling this directly.
|
|
340
|
+
|
|
341
|
+
Unlike ``extend_lease``, this is a settlement-style follow-up:
|
|
342
|
+
transient failures are retried, and a lease that no longer exists is
|
|
343
|
+
tolerated with a warning because the message will be redelivered
|
|
344
|
+
anyway.
|
|
345
|
+
"""
|
|
346
|
+
retry_message_after_sync(message, delay, self._extend_lease_sync)
|
|
347
|
+
|
|
348
|
+
def _extend_lease_sync(
|
|
349
|
+
self,
|
|
350
|
+
message: Message[Any] | MessageMetadata,
|
|
351
|
+
duration: Duration,
|
|
352
|
+
) -> None:
|
|
353
|
+
iter_coroutine(self._extend_lease(message, duration))
|
|
354
|
+
|
|
355
|
+
async def _renew_lease(self, message: Message[Any], duration: Duration) -> None:
|
|
356
|
+
runtime = AsyncQueueRuntime(timeout=self.timeout)
|
|
357
|
+
runtime.configure_base_url(self.base_url)
|
|
358
|
+
await self._extend_lease(message, duration, runtime=runtime)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class _MessageLifecycle:
|
|
362
|
+
def __init__(
|
|
363
|
+
self,
|
|
364
|
+
message: Message[Any],
|
|
365
|
+
*,
|
|
366
|
+
client: QueueClient,
|
|
367
|
+
lease_duration: Duration | None = None,
|
|
368
|
+
) -> None:
|
|
369
|
+
self._message = message
|
|
370
|
+
self._client = client
|
|
371
|
+
self._renewal = LeaseRenewal(
|
|
372
|
+
message,
|
|
373
|
+
client=client,
|
|
374
|
+
lease_duration=processing_lease_seconds(lease_duration),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def __enter__(self) -> LeaseRenewal:
|
|
378
|
+
self._renewal.__enter__()
|
|
379
|
+
return self._renewal
|
|
380
|
+
|
|
381
|
+
def __exit__(
|
|
382
|
+
self,
|
|
383
|
+
exc_type: type[BaseException] | None,
|
|
384
|
+
exc: BaseException | None,
|
|
385
|
+
traceback: TracebackType | None,
|
|
386
|
+
) -> bool | None:
|
|
387
|
+
# ACK, Handoff, and RetryAfter deliberately use different renewal shutdown
|
|
388
|
+
# semantics. Server-side, background renewal and final directives are
|
|
389
|
+
# all conditional writes against the same active lease record. If a
|
|
390
|
+
# renewal races with ACK, the final state is safe either way: renewal
|
|
391
|
+
# first still leaves the same receipt handle valid for ACK, and ACK
|
|
392
|
+
# first makes the later renewal fail with a 4xx because the message is
|
|
393
|
+
# no longer INFLIGHT. Waiting for cancellation in the success path only
|
|
394
|
+
# adds tail latency without improving correctness.
|
|
395
|
+
#
|
|
396
|
+
# Handoff leaves the delivery open for another owner, so a best-effort
|
|
397
|
+
# stop failure can keep the hidden renewal alive past handoff.
|
|
398
|
+
#
|
|
399
|
+
# RetryAfter is different because it is itself a visibility update. A
|
|
400
|
+
# late automatic renewal can overwrite the shorter retry delay the
|
|
401
|
+
# handler requested, delaying redelivery until the normal processing
|
|
402
|
+
# lease. For that path we wait for the renewal worker to finish or
|
|
403
|
+
# cancel any in-flight extension before applying the RetryAfter
|
|
404
|
+
# visibility change. If stopping times out, we still apply the
|
|
405
|
+
# directive: the worst remaining case is the pre-existing race, while
|
|
406
|
+
# refusing to apply RetryAfter would always leave the long lease behind.
|
|
407
|
+
wait_for_renewal_stop = isinstance(exc, (Handoff, RetryAfter))
|
|
408
|
+
try:
|
|
409
|
+
self._renewal.stop(wait=wait_for_renewal_stop)
|
|
410
|
+
finally:
|
|
411
|
+
finalize_payload_sync(self._message.payload)
|
|
412
|
+
|
|
413
|
+
if isinstance(exc, Handoff):
|
|
414
|
+
debug_log_for_msg("message.handoff", self._message)
|
|
415
|
+
return True
|
|
416
|
+
if isinstance(exc, RetryAfter):
|
|
417
|
+
self._renewal.extend(exc.timeout_seconds)
|
|
418
|
+
debug_log_for_msg(
|
|
419
|
+
"message.retry_after",
|
|
420
|
+
self._message,
|
|
421
|
+
retry_after_seconds=exc.timeout_seconds,
|
|
422
|
+
)
|
|
423
|
+
return True
|
|
424
|
+
if exc is None:
|
|
425
|
+
# acknowledge() already retries transient failures internally.
|
|
426
|
+
self._client.acknowledge(self._message)
|
|
427
|
+
debug_log_for_msg("message.ack", self._message)
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# Only add public symbols to __all__; internal helpers must stay unexported.
|
|
432
|
+
__all__ = ("LeaseRenewal", "QueueClient")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Final, TypeAlias, final
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable, Mapping
|
|
8
|
+
|
|
9
|
+
from vercel.oidc import get_vercel_oidc_token
|
|
10
|
+
from vercel.oidc.aio import get_vercel_oidc_token as get_vercel_oidc_token_async
|
|
11
|
+
|
|
12
|
+
from .constants import (
|
|
13
|
+
HEADER_AUTHORIZATION,
|
|
14
|
+
HEADER_CONTENT_TYPE,
|
|
15
|
+
VQS_HEADER_DELAY_SECONDS,
|
|
16
|
+
VQS_HEADER_DEPLOYMENT_ID,
|
|
17
|
+
VQS_HEADER_IDEMPOTENCY_KEY,
|
|
18
|
+
VQS_HEADER_MAX_MESSAGES,
|
|
19
|
+
VQS_HEADER_RETENTION_SECONDS,
|
|
20
|
+
VQS_HEADER_VISIBILITY_TIMEOUT_SECONDS,
|
|
21
|
+
)
|
|
22
|
+
from .errors import DeploymentResolutionError, TokenResolutionError
|
|
23
|
+
|
|
24
|
+
DeploymentID: TypeAlias = str
|
|
25
|
+
BaseUrl: TypeAlias = str | Callable[[str], str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@final
|
|
29
|
+
class CurrentDeployment:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@final
|
|
34
|
+
class AllDeployments:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
CURRENT_DEPLOYMENT: Final = CurrentDeployment()
|
|
39
|
+
ALL_DEPLOYMENTS: Final = AllDeployments()
|
|
40
|
+
DeploymentOption: TypeAlias = DeploymentID | CurrentDeployment | AllDeployments
|
|
41
|
+
REGION_PATTERN = re.compile(r"^[a-z]{2,5}[0-9]{1,2}$")
|
|
42
|
+
|
|
43
|
+
PROTECTED_HEADERS = {
|
|
44
|
+
HEADER_AUTHORIZATION.lower(),
|
|
45
|
+
HEADER_CONTENT_TYPE.lower(),
|
|
46
|
+
VQS_HEADER_DELAY_SECONDS.lower(),
|
|
47
|
+
VQS_HEADER_DEPLOYMENT_ID.lower(),
|
|
48
|
+
VQS_HEADER_IDEMPOTENCY_KEY.lower(),
|
|
49
|
+
VQS_HEADER_MAX_MESSAGES.lower(),
|
|
50
|
+
VQS_HEADER_RETENTION_SECONDS.lower(),
|
|
51
|
+
VQS_HEADER_VISIBILITY_TIMEOUT_SECONDS.lower(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_region(region: str | None = None) -> str:
|
|
56
|
+
resolved = region or os.environ.get("VERCEL_REGION")
|
|
57
|
+
if resolved is None:
|
|
58
|
+
raise ValueError("Queue region is required. Provide 'region' or set VERCEL_REGION.")
|
|
59
|
+
if not REGION_PATTERN.fullmatch(resolved):
|
|
60
|
+
raise ValueError(f"Invalid queue region: {resolved!r}")
|
|
61
|
+
return resolved
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_region(region: str | None) -> str | None:
|
|
65
|
+
if region is None:
|
|
66
|
+
return None
|
|
67
|
+
if not REGION_PATTERN.fullmatch(region):
|
|
68
|
+
raise ValueError(f"Invalid queue region: {region!r}")
|
|
69
|
+
return region
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_base_url(base_url: BaseUrl | None = None, *, region: str | None = None) -> str:
|
|
73
|
+
resolved_region = resolve_region(region)
|
|
74
|
+
if base_url is not None:
|
|
75
|
+
if isinstance(base_url, str):
|
|
76
|
+
resolved = base_url.format(region=resolved_region)
|
|
77
|
+
else:
|
|
78
|
+
resolved = base_url(resolved_region)
|
|
79
|
+
return resolved.rstrip("/")
|
|
80
|
+
env_base_url = os.environ.get("VERCEL_QUEUE_BASE_URL")
|
|
81
|
+
if env_base_url:
|
|
82
|
+
return env_base_url.format(region=resolved_region).rstrip("/")
|
|
83
|
+
return f"https://{resolved_region}.vercel-queue.com"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve_deployment(deployment: DeploymentOption = CURRENT_DEPLOYMENT) -> str | None:
|
|
87
|
+
if deployment is ALL_DEPLOYMENTS:
|
|
88
|
+
return None
|
|
89
|
+
if isinstance(deployment, str):
|
|
90
|
+
return deployment
|
|
91
|
+
env_deployment = os.environ.get("VERCEL_DEPLOYMENT_ID")
|
|
92
|
+
if env_deployment:
|
|
93
|
+
return env_deployment
|
|
94
|
+
raise DeploymentResolutionError(
|
|
95
|
+
"Failed to resolve queue deployment ID. Provide 'deployment', "
|
|
96
|
+
"set VERCEL_DEPLOYMENT_ID, or pass deployment=ALL_DEPLOYMENTS "
|
|
97
|
+
"to explicitly omit it."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_token(token: str | None = None) -> str:
|
|
102
|
+
if token:
|
|
103
|
+
return token
|
|
104
|
+
env_token = os.environ.get("VERCEL_QUEUE_TOKEN")
|
|
105
|
+
if env_token:
|
|
106
|
+
return env_token
|
|
107
|
+
try:
|
|
108
|
+
resolved = get_vercel_oidc_token()
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
raise TokenResolutionError(
|
|
111
|
+
"Failed to resolve queue token. Provide 'token', set VERCEL_QUEUE_TOKEN, "
|
|
112
|
+
"or make a Vercel OIDC token available."
|
|
113
|
+
) from exc
|
|
114
|
+
if not resolved:
|
|
115
|
+
raise TokenResolutionError("Failed to resolve queue token: OIDC returned an empty token")
|
|
116
|
+
return resolved
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def resolve_token_async(token: str | None = None) -> str:
|
|
120
|
+
if token:
|
|
121
|
+
return token
|
|
122
|
+
env_token = os.environ.get("VERCEL_QUEUE_TOKEN")
|
|
123
|
+
if env_token:
|
|
124
|
+
return env_token
|
|
125
|
+
try:
|
|
126
|
+
resolved = await get_vercel_oidc_token_async()
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
raise TokenResolutionError(
|
|
129
|
+
"Failed to resolve queue token. Provide 'token', set VERCEL_QUEUE_TOKEN, "
|
|
130
|
+
"or make a Vercel OIDC token available."
|
|
131
|
+
) from exc
|
|
132
|
+
if not resolved:
|
|
133
|
+
raise TokenResolutionError("Failed to resolve queue token: OIDC returned an empty token")
|
|
134
|
+
return resolved
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def apply_custom_headers(headers: dict[str, str], custom_headers: Mapping[str, str] | None) -> None:
|
|
138
|
+
for key, value in (custom_headers or {}).items():
|
|
139
|
+
if key.lower() in PROTECTED_HEADERS or key.lower().startswith("vqs-"):
|
|
140
|
+
continue
|
|
141
|
+
headers[str(key)] = str(value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Only add public symbols to __all__; internal helpers must stay unexported.
|
|
145
|
+
__all__ = (
|
|
146
|
+
"ALL_DEPLOYMENTS",
|
|
147
|
+
"CURRENT_DEPLOYMENT",
|
|
148
|
+
"AllDeployments",
|
|
149
|
+
"BaseUrl",
|
|
150
|
+
"CurrentDeployment",
|
|
151
|
+
"DeploymentID",
|
|
152
|
+
"DeploymentOption",
|
|
153
|
+
"apply_custom_headers",
|
|
154
|
+
"resolve_base_url",
|
|
155
|
+
"resolve_deployment",
|
|
156
|
+
"resolve_region",
|
|
157
|
+
"resolve_token",
|
|
158
|
+
"resolve_token_async",
|
|
159
|
+
"validate_region",
|
|
160
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ..version import __version__ as _ver
|
|
7
|
+
|
|
8
|
+
CONTENT_TYPE_JSON = "application/json"
|
|
9
|
+
CONTENT_TYPE_NDJSON = "application/x-ndjson"
|
|
10
|
+
CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"
|
|
11
|
+
CONTENT_TYPE_TEXT = "text/plain; charset=utf-8"
|
|
12
|
+
CONTENT_TYPE_MULTIPART_MIXED = "multipart/mixed"
|
|
13
|
+
|
|
14
|
+
HEADER_ACCEPT = "Accept"
|
|
15
|
+
HEADER_AUTHORIZATION = "Authorization"
|
|
16
|
+
HEADER_CONTENT_TYPE = "Content-Type"
|
|
17
|
+
HEADER_RETRY_AFTER = "Retry-After"
|
|
18
|
+
HEADER_USER_AGENT = "User-Agent"
|
|
19
|
+
|
|
20
|
+
VQS_NAME_PATTERN = r"^[A-Za-z0-9_-]+$"
|
|
21
|
+
|
|
22
|
+
DEFAULT_RETRY_AFTER_SECONDS = 60
|
|
23
|
+
|
|
24
|
+
VQS_HEADER_DELAY_SECONDS = "Vqs-Delay-Seconds"
|
|
25
|
+
VQS_HEADER_CLIENT_TS = "Vqs-Client-Ts"
|
|
26
|
+
VQS_HEADER_DELIVERY_COUNT = "Vqs-Delivery-Count"
|
|
27
|
+
VQS_HEADER_DEPLOYMENT_ID = "Vqs-Deployment-Id"
|
|
28
|
+
VQS_HEADER_EXPIRES_AT = "Vqs-Expires-At"
|
|
29
|
+
VQS_HEADER_IDEMPOTENCY_KEY = "Vqs-Idempotency-Key"
|
|
30
|
+
VQS_HEADER_MAX_MESSAGES = "Vqs-Max-Messages"
|
|
31
|
+
VQS_HEADER_MESSAGE_ID = "Vqs-Message-Id"
|
|
32
|
+
VQS_HEADER_RECEIPT_HANDLE = "Vqs-Receipt-Handle"
|
|
33
|
+
VQS_HEADER_RETENTION_SECONDS = "Vqs-Retention-Seconds"
|
|
34
|
+
VQS_HEADER_TIMESTAMP = "Vqs-Timestamp"
|
|
35
|
+
VQS_HEADER_VISIBILITY_TIMEOUT_SECONDS = "Vqs-Visibility-Timeout-Seconds"
|
|
36
|
+
|
|
37
|
+
CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta"
|
|
38
|
+
CLOUD_EVENT_HEADER_TYPE = "ce-type"
|
|
39
|
+
CLOUD_EVENT_HEADER_VQS_CONSUMER_GROUP = "ce-vqsconsumergroup"
|
|
40
|
+
CLOUD_EVENT_HEADER_VQS_CREATED_AT = "ce-vqscreatedat"
|
|
41
|
+
CLOUD_EVENT_HEADER_VQS_DELIVERY_COUNT = "ce-vqsdeliverycount"
|
|
42
|
+
CLOUD_EVENT_HEADER_VQS_EXPIRES_AT = "ce-vqsexpiresat"
|
|
43
|
+
CLOUD_EVENT_HEADER_VQS_MESSAGE_ID = "ce-vqsmessageid"
|
|
44
|
+
CLOUD_EVENT_HEADER_VQS_TOPIC = "ce-vqsqueuename"
|
|
45
|
+
CLOUD_EVENT_HEADER_VQS_RECEIPT_HANDLE = "ce-vqsreceipthandle"
|
|
46
|
+
CLOUD_EVENT_HEADER_VQS_REGION = "ce-vqsregion"
|
|
47
|
+
CLOUD_EVENT_HEADER_VQS_VISIBILITY_DEADLINE = "ce-vqsvisibilitydeadline"
|
|
48
|
+
|
|
49
|
+
PLATFORM = platform.uname()
|
|
50
|
+
USER_AGENT = f"vercel/queue/{_ver} (Python/{sys.version}; {PLATFORM.system}/{PLATFORM.machine})"
|