vercel-queue-bundle 0.8.0__tar.gz → 0.9.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.8.0 → vercel_queue_bundle-0.9.0}/PKG-INFO +15 -7
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/README.md +9 -1
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/pyproject.toml +5 -4
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_common.py +0 -5
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/client.py +1 -11
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/devserver.py +63 -78
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/embedded.py +1 -1
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/http.py +15 -8
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/log.py +0 -4
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/devserver.py +0 -4
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/version.py +1 -1
- vercel_queue_bundle-0.8.0/vercel/queue/_internal/asgi.py +0 -145
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/LICENSE +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/_vercel_hatch_build.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/hatch_build.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/__main__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_async.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_sync.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/asynctools.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/cli.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/client_sync.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/config.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/constants.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/errors.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/lease.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/messages.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/multipart.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/names.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/polling.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/push.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/response.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/retry.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/streams.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/subscribers.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/transports.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/types.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/LICENSE.python-multipart.txt +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/decoders.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/exceptions.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/multipart.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/decoders.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/exceptions.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/multipart.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/embedded.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/py.typed +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/sync.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/testing/__init__.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/testing/pytest.py +0 -0
- {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/testing/state.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vercel-queue-bundle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
-
Requires-Dist: vercel-headers-bundle>=0.7.
|
|
10
|
-
Requires-Dist: vercel-internal-core-bundle<0.
|
|
11
|
-
Requires-Dist: vercel-internal-shared-vendored-deps>=0.1.
|
|
12
|
-
Requires-Dist: vercel-oidc-bundle>=0.
|
|
9
|
+
Requires-Dist: vercel-headers-bundle>=0.7.2
|
|
10
|
+
Requires-Dist: vercel-internal-core-bundle<0.3.0,>=0.2.0
|
|
11
|
+
Requires-Dist: vercel-internal-shared-vendored-deps>=0.1.2
|
|
12
|
+
Requires-Dist: vercel-oidc-bundle>=0.9.0
|
|
13
13
|
Provides-Extra: devserver
|
|
14
14
|
Requires-Dist: uvicorn; extra == 'devserver'
|
|
15
15
|
Provides-Extra: trio
|
|
@@ -29,7 +29,7 @@ designed for seamless integration with Vercel deployments.
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- **Simple API**: `send`, `subscribe`, and `
|
|
32
|
+
- **Simple API**: `send`, `subscribe`, and `poll` cover standard workflows.
|
|
33
33
|
- **Automatic Triggering on Vercel**: Vercel invokes your function when messages are ready.
|
|
34
34
|
- **Works Anywhere**: `send`, decorated subscribers, and manual `poll` loops work on Vercel, self-hosted workers, and locally.
|
|
35
35
|
- **Sync and Async Clients**: Prefer async for applications, use sync for scripts and blocking workers.
|
|
@@ -326,6 +326,8 @@ custom queue service URLs, create a `QueueClient` explicitly:
|
|
|
326
326
|
|
|
327
327
|
```python
|
|
328
328
|
# explicit_client.py
|
|
329
|
+
import httpx2
|
|
330
|
+
|
|
329
331
|
from vercel.queue import ALL_DEPLOYMENTS, QueueClient
|
|
330
332
|
|
|
331
333
|
queue = QueueClient(
|
|
@@ -340,6 +342,12 @@ queue = QueueClient(
|
|
|
340
342
|
await queue.send("my-topic", {"message": "Hello world"})
|
|
341
343
|
```
|
|
342
344
|
|
|
345
|
+
HTTPX2 is the default and the only HTTPX family installed by the SDK. For
|
|
346
|
+
migration, an application that explicitly installs legacy `httpx` may return a
|
|
347
|
+
complete `httpx.Client` or `httpx.AsyncClient` from `http_client_factory`.
|
|
348
|
+
Legacy transport objects must remain inside a legacy client; they cannot be
|
|
349
|
+
mounted in an `httpx2.Client`.
|
|
350
|
+
|
|
343
351
|
By default queue clients send requests to `https://<region>.vercel-queue.com/`. A custom endpoint
|
|
344
352
|
can be configured by passing a `base_url` keyword argument to the client constructor. The value
|
|
345
353
|
can be a fixed string, a `format()` template containing a `{region}` placeholder, or a callable
|
|
@@ -9,7 +9,7 @@ designed for seamless integration with Vercel deployments.
|
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
- **Simple API**: `send`, `subscribe`, and `
|
|
12
|
+
- **Simple API**: `send`, `subscribe`, and `poll` cover standard workflows.
|
|
13
13
|
- **Automatic Triggering on Vercel**: Vercel invokes your function when messages are ready.
|
|
14
14
|
- **Works Anywhere**: `send`, decorated subscribers, and manual `poll` loops work on Vercel, self-hosted workers, and locally.
|
|
15
15
|
- **Sync and Async Clients**: Prefer async for applications, use sync for scripts and blocking workers.
|
|
@@ -306,6 +306,8 @@ custom queue service URLs, create a `QueueClient` explicitly:
|
|
|
306
306
|
|
|
307
307
|
```python
|
|
308
308
|
# explicit_client.py
|
|
309
|
+
import httpx2
|
|
310
|
+
|
|
309
311
|
from vercel.queue import ALL_DEPLOYMENTS, QueueClient
|
|
310
312
|
|
|
311
313
|
queue = QueueClient(
|
|
@@ -320,6 +322,12 @@ queue = QueueClient(
|
|
|
320
322
|
await queue.send("my-topic", {"message": "Hello world"})
|
|
321
323
|
```
|
|
322
324
|
|
|
325
|
+
HTTPX2 is the default and the only HTTPX family installed by the SDK. For
|
|
326
|
+
migration, an application that explicitly installs legacy `httpx` may return a
|
|
327
|
+
complete `httpx.Client` or `httpx.AsyncClient` from `http_client_factory`.
|
|
328
|
+
Legacy transport objects must remain inside a legacy client; they cannot be
|
|
329
|
+
mounted in an `httpx2.Client`.
|
|
330
|
+
|
|
323
331
|
By default queue clients send requests to `https://<region>.vercel-queue.com/`. A custom endpoint
|
|
324
332
|
can be configured by passing a `base_url` keyword argument to the client constructor. The value
|
|
325
333
|
can be a fixed string, a `format()` template containing a `{region}` placeholder, or a callable
|
|
@@ -20,10 +20,10 @@ path = "hatch_build.py"
|
|
|
20
20
|
|
|
21
21
|
[tool.vercel.release.dependencies]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"vercel-headers-bundle>=0.7.
|
|
24
|
-
"vercel-internal-core-bundle>=0.
|
|
25
|
-
"vercel-oidc-bundle>=0.
|
|
26
|
-
"vercel-internal-shared-vendored-deps>=0.1.
|
|
23
|
+
"vercel-headers-bundle>=0.7.2",
|
|
24
|
+
"vercel-internal-core-bundle>=0.2.0,<0.3.0",
|
|
25
|
+
"vercel-oidc-bundle>=0.9.0",
|
|
26
|
+
"vercel-internal-shared-vendored-deps>=0.1.2",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
@@ -202,6 +202,7 @@ max-statements-in-try = 10
|
|
|
202
202
|
"asyncio.wait_for".msg = "prefer-anyio-primitives: use anyio fail_after or move_on_after instead."
|
|
203
203
|
|
|
204
204
|
[tool.pytest.ini_options]
|
|
205
|
+
pythonpath = ["../.."]
|
|
205
206
|
addopts = "--no-header --capture=tee-sys"
|
|
206
207
|
asyncio_mode = "strict"
|
|
207
208
|
testpaths = ["tests"]
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_common.py
RENAMED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
from .. import version as _version
|
|
5
5
|
from . import (
|
|
6
|
-
asgi as _asgi,
|
|
7
6
|
config as _config,
|
|
8
7
|
names as _names,
|
|
9
8
|
transports as _transports,
|
|
@@ -25,7 +24,6 @@ Handoff = _types.Handoff
|
|
|
25
24
|
Message = _types.Message
|
|
26
25
|
MessageID = _types.MessageID
|
|
27
26
|
MessageMetadata = _types.MessageMetadata
|
|
28
|
-
QueueClientAsgiApp = _asgi.QueueClientAsgiApp
|
|
29
27
|
QueueDirective = _types.QueueDirective
|
|
30
28
|
RawJsonTransport = _transports.RawJsonTransport
|
|
31
29
|
ReceiptHandle = _types.ReceiptHandle
|
|
@@ -38,7 +36,6 @@ Topic = _types.Topic
|
|
|
38
36
|
TopicPattern = _types.TopicPattern
|
|
39
37
|
TypedJsonTransport = _transports.TypedJsonTransport
|
|
40
38
|
__version__ = _version.__version__
|
|
41
|
-
asgi_app = _asgi.asgi_app
|
|
42
39
|
sanitize_name = _names.sanitize_name
|
|
43
40
|
|
|
44
41
|
# Only add public symbols to __all__; internal helpers must stay unexported.
|
|
@@ -75,7 +72,6 @@ __all__ = (
|
|
|
75
72
|
"MessageUnavailableError",
|
|
76
73
|
"PayloadValidationError",
|
|
77
74
|
"ProtocolError",
|
|
78
|
-
"QueueClientAsgiApp",
|
|
79
75
|
"QueueDirective",
|
|
80
76
|
"QueueError",
|
|
81
77
|
"QueueSubscriber",
|
|
@@ -99,7 +95,6 @@ __all__ = (
|
|
|
99
95
|
"UnauthorizedError",
|
|
100
96
|
"UnhandledMessageError",
|
|
101
97
|
"__version__",
|
|
102
|
-
"asgi_app",
|
|
103
98
|
"get_subscriptions",
|
|
104
99
|
"sanitize_name",
|
|
105
100
|
"subscribe",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any, Generic, TypeVar, cast, overload
|
|
4
4
|
|
|
5
5
|
import warnings
|
|
6
6
|
from collections.abc import (
|
|
@@ -112,10 +112,6 @@ T = TypeVar("T")
|
|
|
112
112
|
QUEUE_API_PATH = "/api/v3/topic"
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
if TYPE_CHECKING:
|
|
116
|
-
from .asgi import QueueClientAsgiApp
|
|
117
|
-
|
|
118
|
-
|
|
119
115
|
class _DuplicateMessageRedirect(Exception): # noqa: N818
|
|
120
116
|
def __init__(self, message_id: MessageID) -> None:
|
|
121
117
|
self.message_id = message_id
|
|
@@ -999,12 +995,6 @@ class QueueClient(BaseQueueClient):
|
|
|
999
995
|
"""
|
|
1000
996
|
await retry_message_after_async(message, delay, self._extend_lease)
|
|
1001
997
|
|
|
1002
|
-
def asgi_app(self) -> QueueClientAsgiApp:
|
|
1003
|
-
"""Return an ASGI push-callback app backed by this client."""
|
|
1004
|
-
from .asgi import QueueClientAsgiApp # noqa: PLC0415
|
|
1005
|
-
|
|
1006
|
-
return QueueClientAsgiApp(self)
|
|
1007
|
-
|
|
1008
998
|
|
|
1009
999
|
@dataclass(frozen=True, kw_only=True)
|
|
1010
1000
|
class _LeaseTarget:
|
|
@@ -12,29 +12,16 @@ import json
|
|
|
12
12
|
import signal
|
|
13
13
|
import threading
|
|
14
14
|
import time
|
|
15
|
-
from collections.abc import Callable, Iterator
|
|
16
|
-
|
|
15
|
+
from collections.abc import Callable, Iterator
|
|
16
|
+
|
|
17
|
+
from vercel.internal._vendor import anyio
|
|
18
|
+
from vercel.internal._vendor.anyio import from_thread
|
|
17
19
|
|
|
18
20
|
from ..embedded import create_embedded_queue_app
|
|
19
|
-
from .asgi import QueueClientAsgiApp, asgi_app
|
|
20
|
-
from .client import QueueClient
|
|
21
|
-
from .config import CURRENT_DEPLOYMENT, BaseUrl, DeploymentOption
|
|
22
21
|
from .embedded import EmbeddedQueueDevServer
|
|
23
|
-
from .http import AsyncHttpClientFactory
|
|
24
|
-
from .types import Duration
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dataclass(frozen=True, kw_only=True)
|
|
28
|
-
class QueueClientAsgiDevServer:
|
|
29
|
-
"""Running queue client ASGI dev server."""
|
|
30
|
-
|
|
31
|
-
base_url: str
|
|
32
|
-
app: QueueClientAsgiApp
|
|
33
|
-
_server: Any
|
|
34
|
-
_thread: threading.Thread
|
|
35
22
|
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS = 5.0
|
|
24
|
+
_SERVER_STOP_TIMEOUT_MARGIN_SECONDS = 1.0
|
|
38
25
|
|
|
39
26
|
|
|
40
27
|
@contextlib.contextmanager
|
|
@@ -55,8 +42,9 @@ def embedded_queue_dev_server(
|
|
|
55
42
|
log_level="warning",
|
|
56
43
|
lifespan="off",
|
|
57
44
|
ws="none",
|
|
45
|
+
timeout_graceful_shutdown=_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS,
|
|
58
46
|
)
|
|
59
|
-
server = uvicorn
|
|
47
|
+
server = _cancellable_server(uvicorn, config)
|
|
60
48
|
thread = threading.Thread(target=_profiled_server_run(server, profile), daemon=True)
|
|
61
49
|
thread.start()
|
|
62
50
|
_wait_for_server(server)
|
|
@@ -69,64 +57,15 @@ def embedded_queue_dev_server(
|
|
|
69
57
|
_thread=thread,
|
|
70
58
|
)
|
|
71
59
|
finally:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*,
|
|
82
|
-
client: QueueClient | None = None,
|
|
83
|
-
host: str = "127.0.0.1",
|
|
84
|
-
port: int = 0,
|
|
85
|
-
token: str | None = None,
|
|
86
|
-
region: str | None = None,
|
|
87
|
-
base_url: BaseUrl | None = None,
|
|
88
|
-
deployment: DeploymentOption = CURRENT_DEPLOYMENT,
|
|
89
|
-
headers: Mapping[str, str] | None = None,
|
|
90
|
-
timeout: Duration | None = 10.0,
|
|
91
|
-
http_client_factory: AsyncHttpClientFactory | None = None,
|
|
92
|
-
profile: str | None = None,
|
|
93
|
-
) -> Iterator[QueueClientAsgiDevServer]:
|
|
94
|
-
"""Run a queue client ASGI callback app on a localhost HTTP port."""
|
|
95
|
-
uvicorn = _uvicorn()
|
|
96
|
-
app = asgi_app(
|
|
97
|
-
client=client,
|
|
98
|
-
token=token,
|
|
99
|
-
region=region,
|
|
100
|
-
base_url=base_url,
|
|
101
|
-
deployment=deployment,
|
|
102
|
-
headers=headers,
|
|
103
|
-
timeout=timeout,
|
|
104
|
-
http_client_factory=http_client_factory,
|
|
105
|
-
)
|
|
106
|
-
config = uvicorn.Config(
|
|
107
|
-
app,
|
|
108
|
-
host=host,
|
|
109
|
-
port=port,
|
|
110
|
-
log_level="warning",
|
|
111
|
-
lifespan="on",
|
|
112
|
-
ws="none",
|
|
113
|
-
)
|
|
114
|
-
server = uvicorn.Server(config)
|
|
115
|
-
thread = threading.Thread(target=_profiled_server_run(server, profile), daemon=True)
|
|
116
|
-
thread.start()
|
|
117
|
-
_wait_for_server(server, "queue client ASGI dev server")
|
|
118
|
-
try:
|
|
119
|
-
yield QueueClientAsgiDevServer(
|
|
120
|
-
base_url=f"http://{host}:{_server_port(server)}",
|
|
121
|
-
app=app,
|
|
122
|
-
_server=server,
|
|
123
|
-
_thread=thread,
|
|
124
|
-
)
|
|
125
|
-
finally:
|
|
126
|
-
server.should_exit = True
|
|
127
|
-
thread.join(timeout=5)
|
|
128
|
-
if thread.is_alive():
|
|
129
|
-
raise RuntimeError("queue client ASGI dev server did not stop")
|
|
60
|
+
try:
|
|
61
|
+
_stop_server(
|
|
62
|
+
server,
|
|
63
|
+
thread,
|
|
64
|
+
"embedded queue dev server",
|
|
65
|
+
timeout=_GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS,
|
|
66
|
+
)
|
|
67
|
+
finally:
|
|
68
|
+
app.state.close()
|
|
130
69
|
|
|
131
70
|
|
|
132
71
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -181,6 +120,52 @@ def _wait_for_server(server: Any, name: str = "embedded queue dev server") -> No
|
|
|
181
120
|
raise RuntimeError(f"{name} did not start")
|
|
182
121
|
|
|
183
122
|
|
|
123
|
+
def _stop_server(
|
|
124
|
+
server: Any,
|
|
125
|
+
thread: threading.Thread,
|
|
126
|
+
name: str,
|
|
127
|
+
*,
|
|
128
|
+
timeout: float,
|
|
129
|
+
) -> None:
|
|
130
|
+
server.should_exit = True
|
|
131
|
+
thread.join(timeout=timeout + _SERVER_STOP_TIMEOUT_MARGIN_SECONDS)
|
|
132
|
+
if thread.is_alive():
|
|
133
|
+
server.force_exit = True
|
|
134
|
+
server.cancel()
|
|
135
|
+
thread.join(timeout=timeout)
|
|
136
|
+
if thread.is_alive():
|
|
137
|
+
raise RuntimeError(f"{name} did not stop")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _cancellable_server(uvicorn: Any, config: Any) -> Any:
|
|
141
|
+
class CancellableServer(uvicorn.Server):
|
|
142
|
+
_cancel_scope: anyio.CancelScope | None = None
|
|
143
|
+
_portal: from_thread.BlockingPortal | None = None
|
|
144
|
+
|
|
145
|
+
def run(self, sockets: list[Any] | None = None) -> None:
|
|
146
|
+
backend_options: dict[str, Any] = {}
|
|
147
|
+
get_loop_factory = getattr(self.config, "get_loop_factory", None)
|
|
148
|
+
if get_loop_factory is not None:
|
|
149
|
+
backend_options["loop_factory"] = get_loop_factory()
|
|
150
|
+
else:
|
|
151
|
+
self.config.setup_event_loop()
|
|
152
|
+
anyio.run(self._run, sockets, backend_options=backend_options)
|
|
153
|
+
|
|
154
|
+
async def _run(self, sockets: list[Any] | None) -> None:
|
|
155
|
+
async with from_thread.BlockingPortal() as self._portal:
|
|
156
|
+
with anyio.CancelScope() as self._cancel_scope:
|
|
157
|
+
await self.serve(sockets=sockets)
|
|
158
|
+
|
|
159
|
+
def cancel(self) -> None:
|
|
160
|
+
if self._portal is not None and self._cancel_scope is not None:
|
|
161
|
+
try:
|
|
162
|
+
self._portal.start_task_soon(self._cancel_scope.cancel)
|
|
163
|
+
except RuntimeError:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
return CancellableServer(config)
|
|
167
|
+
|
|
168
|
+
|
|
184
169
|
def _server_port(server: Any) -> int:
|
|
185
170
|
for asyncio_server in server.servers:
|
|
186
171
|
sockets = asyncio_server.sockets or ()
|
|
@@ -31,7 +31,7 @@ from tempfile import TemporaryDirectory
|
|
|
31
31
|
from urllib.parse import unquote
|
|
32
32
|
|
|
33
33
|
from vercel.internal._vendor import anyio
|
|
34
|
-
|
|
34
|
+
import vercel.internal._vendor.httpx2 as httpx
|
|
35
35
|
from vercel.internal._vendor.anyio.abc import TaskGroup
|
|
36
36
|
|
|
37
37
|
from .client import QueueClient
|
|
@@ -20,9 +20,14 @@ from email.utils import parsedate_to_datetime
|
|
|
20
20
|
from threading import Lock
|
|
21
21
|
from urllib.parse import urljoin, urlsplit
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import vercel.internal._vendor.httpx2 as httpx
|
|
24
24
|
from vercel.internal._vendor.anyio.lowlevel import get_async_backend
|
|
25
25
|
|
|
26
|
+
from vercel._internal.core.http._compat import (
|
|
27
|
+
response_not_read_errors,
|
|
28
|
+
transport_errors,
|
|
29
|
+
)
|
|
30
|
+
|
|
26
31
|
from .asynctools import iter_bytes_async
|
|
27
32
|
from .config import resolve_token, resolve_token_async
|
|
28
33
|
from .constants import HEADER_RETRY_AFTER
|
|
@@ -30,7 +35,7 @@ from .errors import CommunicationError
|
|
|
30
35
|
from .log import content_type, debug_enabled, debug_log, redact_text, safe_header_names, safe_url
|
|
31
36
|
from .types import Duration, Headers, RawHeaders, RequestContent, duration_to_float_seconds
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
_httpx_loggers = (logging.getLogger("httpx2"), logging.getLogger("httpx"))
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
@dataclass
|
|
@@ -296,7 +301,7 @@ class _SyncResponse:
|
|
|
296
301
|
async def response_text(response: AsyncHttpResponse) -> str:
|
|
297
302
|
try:
|
|
298
303
|
return response.text
|
|
299
|
-
except
|
|
304
|
+
except response_not_read_errors():
|
|
300
305
|
body = bytearray()
|
|
301
306
|
async for chunk in response.aiter_bytes():
|
|
302
307
|
body.extend(chunk)
|
|
@@ -452,7 +457,7 @@ class SyncQueueRuntime(BaseQueueRuntime):
|
|
|
452
457
|
client = self._acquire_client()
|
|
453
458
|
try:
|
|
454
459
|
response = client.request(method, url, **kwargs)
|
|
455
|
-
except
|
|
460
|
+
except transport_errors() as exc:
|
|
456
461
|
_log_http_error(method, url, exc)
|
|
457
462
|
raise CommunicationError(str(exc)) from exc
|
|
458
463
|
finally:
|
|
@@ -480,7 +485,7 @@ class SyncQueueRuntime(BaseQueueRuntime):
|
|
|
480
485
|
) as response:
|
|
481
486
|
_log_http_response(method, resolved_url, response.status_code, response.headers)
|
|
482
487
|
yield _SyncResponse(response)
|
|
483
|
-
except
|
|
488
|
+
except transport_errors() as exc:
|
|
484
489
|
_log_http_error(method, resolved_url, exc)
|
|
485
490
|
raise CommunicationError(str(exc)) from exc
|
|
486
491
|
finally:
|
|
@@ -525,7 +530,7 @@ class AsyncQueueRuntime(BaseQueueRuntime):
|
|
|
525
530
|
key, client = self._acquire_client()
|
|
526
531
|
try:
|
|
527
532
|
response = await client.request(method, url, **kwargs)
|
|
528
|
-
except
|
|
533
|
+
except transport_errors() as exc:
|
|
529
534
|
_log_http_error(method, url, exc)
|
|
530
535
|
raise CommunicationError(str(exc)) from exc
|
|
531
536
|
finally:
|
|
@@ -553,7 +558,7 @@ class AsyncQueueRuntime(BaseQueueRuntime):
|
|
|
553
558
|
) as response:
|
|
554
559
|
_log_http_response(method, resolved_url, response.status_code, response.headers)
|
|
555
560
|
yield response
|
|
556
|
-
except
|
|
561
|
+
except transport_errors() as exc:
|
|
557
562
|
_log_http_error(method, resolved_url, exc)
|
|
558
563
|
raise CommunicationError(str(exc)) from exc
|
|
559
564
|
finally:
|
|
@@ -630,5 +635,7 @@ def _install_httpx_request_log_filter() -> None:
|
|
|
630
635
|
with _httpx_filter_state.lock:
|
|
631
636
|
if _httpx_filter_state.installed:
|
|
632
637
|
return
|
|
633
|
-
|
|
638
|
+
request_filter = _QueueHttpxRequestLogFilter()
|
|
639
|
+
for logger in _httpx_loggers:
|
|
640
|
+
logger.addFilter(request_filter)
|
|
634
641
|
_httpx_filter_state.installed = True
|
|
@@ -40,10 +40,6 @@ def debug_enabled() -> bool:
|
|
|
40
40
|
return os.environ.get(_DEBUG_ENV) in {"1", "true"}
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def configure_asgi_logger() -> None:
|
|
44
|
-
logging.getLogger(_LOGGER_NAME).setLevel(logging.INFO if debug_enabled() else logging.WARNING)
|
|
45
|
-
|
|
46
|
-
|
|
47
43
|
def debug_log(event: str, **fields: Any) -> None:
|
|
48
44
|
if not debug_enabled():
|
|
49
45
|
return
|
|
@@ -4,19 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from ._internal.devserver import (
|
|
6
6
|
EmbeddedQueueDevServer,
|
|
7
|
-
QueueClientAsgiDevServer,
|
|
8
7
|
embedded_queue_dev_server,
|
|
9
8
|
main,
|
|
10
|
-
queue_client_asgi_dev_server,
|
|
11
9
|
)
|
|
12
10
|
|
|
13
11
|
# Only add public symbols to __all__; internal helpers must stay unexported.
|
|
14
12
|
__all__ = (
|
|
15
13
|
"EmbeddedQueueDevServer",
|
|
16
|
-
"QueueClientAsgiDevServer",
|
|
17
14
|
"embedded_queue_dev_server",
|
|
18
15
|
"main",
|
|
19
|
-
"queue_client_asgi_dev_server",
|
|
20
16
|
)
|
|
21
17
|
|
|
22
18
|
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, TypeAlias, TypedDict
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
|
|
7
|
-
|
|
8
|
-
from vercel.headers import HeadersContext, headers_from_asgi_scope
|
|
9
|
-
|
|
10
|
-
from .client import QueueClient
|
|
11
|
-
from .config import CURRENT_DEPLOYMENT, BaseUrl, DeploymentOption
|
|
12
|
-
from .errors import ProtocolError
|
|
13
|
-
from .http import AsyncHttpClientFactory
|
|
14
|
-
from .log import configure_asgi_logger, debug_log
|
|
15
|
-
from .types import Duration
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger("vercel.queue")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class AsgiScope(TypedDict, total=False):
|
|
21
|
-
type: str
|
|
22
|
-
method: str
|
|
23
|
-
headers: list[tuple[bytes, bytes]]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
AsgiMessage: TypeAlias = dict[str, Any]
|
|
27
|
-
AsgiReceive: TypeAlias = Callable[[], Awaitable[AsgiMessage]]
|
|
28
|
-
AsgiSend: TypeAlias = Callable[[AsgiMessage], Awaitable[None]]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class QueueClientAsgiApp:
|
|
32
|
-
"""ASGI push-callback app backed by an async queue client."""
|
|
33
|
-
|
|
34
|
-
def __init__(self, client: QueueClient) -> None:
|
|
35
|
-
configure_asgi_logger()
|
|
36
|
-
self.client = client
|
|
37
|
-
|
|
38
|
-
async def __call__(
|
|
39
|
-
self,
|
|
40
|
-
scope: AsgiScope,
|
|
41
|
-
receive: AsgiReceive,
|
|
42
|
-
send: AsgiSend,
|
|
43
|
-
) -> None:
|
|
44
|
-
scope_type = scope.get("type")
|
|
45
|
-
if scope_type == "http":
|
|
46
|
-
await self._handle_http(scope, receive, send)
|
|
47
|
-
return
|
|
48
|
-
if scope_type == "lifespan":
|
|
49
|
-
await self._handle_lifespan(receive, send)
|
|
50
|
-
return
|
|
51
|
-
raise RuntimeError(f"Unsupported ASGI scope type: {scope_type!r}")
|
|
52
|
-
|
|
53
|
-
async def _handle_http(
|
|
54
|
-
self,
|
|
55
|
-
scope: AsgiScope,
|
|
56
|
-
receive: AsgiReceive,
|
|
57
|
-
send: AsgiSend,
|
|
58
|
-
) -> None:
|
|
59
|
-
if scope.get("method") != "POST":
|
|
60
|
-
await _send_status(send, 405)
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
headers = headers_from_asgi_scope(scope)
|
|
64
|
-
try:
|
|
65
|
-
with HeadersContext(headers).use():
|
|
66
|
-
await self.client.accept_and_handle(
|
|
67
|
-
_body_chunks(receive),
|
|
68
|
-
headers,
|
|
69
|
-
)
|
|
70
|
-
except (ProtocolError, TypeError, ValueError) as exc:
|
|
71
|
-
debug_log("asgi.bad_request", error=repr(exc))
|
|
72
|
-
logger.warning("Vercel Queue push callback rejected: %s", exc)
|
|
73
|
-
await _send_status(send, 400)
|
|
74
|
-
except Exception as exc:
|
|
75
|
-
debug_log("asgi.delivery_failure", error=repr(exc))
|
|
76
|
-
logger.exception("Vercel Queue push callback failed")
|
|
77
|
-
await _send_status(send, 500)
|
|
78
|
-
else:
|
|
79
|
-
await _send_status(send, 204)
|
|
80
|
-
|
|
81
|
-
async def _handle_lifespan(self, receive: AsgiReceive, send: AsgiSend) -> None:
|
|
82
|
-
while True:
|
|
83
|
-
message = await receive()
|
|
84
|
-
message_type = message.get("type")
|
|
85
|
-
if message_type == "lifespan.startup":
|
|
86
|
-
await send({"type": "lifespan.startup.complete"})
|
|
87
|
-
elif message_type == "lifespan.shutdown":
|
|
88
|
-
await send({"type": "lifespan.shutdown.complete"})
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def asgi_app(
|
|
93
|
-
*,
|
|
94
|
-
client: QueueClient | None = None,
|
|
95
|
-
token: str | None = None,
|
|
96
|
-
region: str | None = None,
|
|
97
|
-
base_url: BaseUrl | None = None,
|
|
98
|
-
deployment: DeploymentOption = CURRENT_DEPLOYMENT,
|
|
99
|
-
headers: Mapping[str, str] | None = None,
|
|
100
|
-
timeout: Duration | None = 10.0,
|
|
101
|
-
http_client_factory: AsyncHttpClientFactory | None = None,
|
|
102
|
-
) -> QueueClientAsgiApp:
|
|
103
|
-
"""Create an ASGI queue push-callback app."""
|
|
104
|
-
configure_asgi_logger()
|
|
105
|
-
resolved_client = client or QueueClient(
|
|
106
|
-
token=token,
|
|
107
|
-
region=region,
|
|
108
|
-
base_url=base_url,
|
|
109
|
-
deployment=deployment,
|
|
110
|
-
headers=headers,
|
|
111
|
-
timeout=timeout,
|
|
112
|
-
http_client_factory=http_client_factory,
|
|
113
|
-
)
|
|
114
|
-
return QueueClientAsgiApp(resolved_client)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
async def _body_chunks(receive: AsgiReceive) -> AsyncIterator[bytes]:
|
|
118
|
-
while True:
|
|
119
|
-
message = await receive()
|
|
120
|
-
message_type = message.get("type")
|
|
121
|
-
if message_type == "http.disconnect":
|
|
122
|
-
raise ValueError("request body disconnected before completion")
|
|
123
|
-
if message_type != "http.request":
|
|
124
|
-
raise ValueError(f"unexpected ASGI message: {message_type!r}")
|
|
125
|
-
body = message.get("body", b"")
|
|
126
|
-
if body:
|
|
127
|
-
yield body
|
|
128
|
-
if not message.get("more_body", False):
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
async def _send_status(send: AsgiSend, status: int) -> None:
|
|
133
|
-
headers: list[tuple[bytes, bytes]] = []
|
|
134
|
-
if status == 405:
|
|
135
|
-
headers.append((b"allow", b"POST"))
|
|
136
|
-
await send({
|
|
137
|
-
"type": "http.response.start",
|
|
138
|
-
"status": status,
|
|
139
|
-
"headers": headers,
|
|
140
|
-
})
|
|
141
|
-
await send({"type": "http.response.body", "body": b""})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Only add public symbols to __all__; internal helpers must stay unexported.
|
|
145
|
-
__all__ = ("QueueClientAsgiApp", "asgi_app")
|
|
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.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/asynctools.py
RENAMED
|
File without changes
|
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.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
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/subscribers.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/transports.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/__init__.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/decoders.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/exceptions.py
RENAMED
|
File without changes
|
{vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.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
|