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.
Files changed (54) hide show
  1. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/PKG-INFO +15 -7
  2. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/README.md +9 -1
  3. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/pyproject.toml +5 -4
  4. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_common.py +0 -5
  5. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/client.py +1 -11
  6. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/devserver.py +63 -78
  7. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/embedded.py +1 -1
  8. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/http.py +15 -8
  9. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/log.py +0 -4
  10. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/devserver.py +0 -4
  11. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/version.py +1 -1
  12. vercel_queue_bundle-0.8.0/vercel/queue/_internal/asgi.py +0 -145
  13. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/LICENSE +0 -0
  14. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/_vercel_hatch_build.py +0 -0
  15. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/hatch_build.py +0 -0
  16. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/__init__.py +0 -0
  17. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/__main__.py +0 -0
  18. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/__init__.py +0 -0
  19. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_async.py +0 -0
  20. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/api_sync.py +0 -0
  21. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/asynctools.py +0 -0
  22. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/cli.py +0 -0
  23. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/client_sync.py +0 -0
  24. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/config.py +0 -0
  25. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/constants.py +0 -0
  26. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/errors.py +0 -0
  27. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/lease.py +0 -0
  28. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/messages.py +0 -0
  29. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/multipart.py +0 -0
  30. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/names.py +0 -0
  31. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/polling.py +0 -0
  32. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/push.py +0 -0
  33. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/response.py +0 -0
  34. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/retry.py +0 -0
  35. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/streams.py +0 -0
  36. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/subscribers.py +0 -0
  37. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/transports.py +0 -0
  38. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_internal/types.py +0 -0
  39. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/LICENSE.python-multipart.txt +0 -0
  40. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/__init__.py +0 -0
  41. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/__init__.py +0 -0
  42. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/decoders.py +0 -0
  43. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/exceptions.py +0 -0
  44. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/multipart/multipart.py +0 -0
  45. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/__init__.py +0 -0
  46. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/decoders.py +0 -0
  47. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/exceptions.py +0 -0
  48. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/_vendor/python_multipart/multipart.py +0 -0
  49. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/embedded.py +0 -0
  50. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/py.typed +0 -0
  51. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/sync.py +0 -0
  52. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/testing/__init__.py +0 -0
  53. {vercel_queue_bundle-0.8.0 → vercel_queue_bundle-0.9.0}/vercel/queue/testing/pytest.py +0 -0
  54. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: vercel-queue-bundle
3
- Version: 0.8.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.1
10
- Requires-Dist: vercel-internal-core-bundle<0.2.0,>=0.1.2
11
- Requires-Dist: vercel-internal-shared-vendored-deps>=0.1.1
12
- Requires-Dist: vercel-oidc-bundle>=0.8.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 `asgi_app` cover standard workflows.
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 `asgi_app` cover standard workflows.
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.1",
24
- "vercel-internal-core-bundle>=0.1.2,<0.2.0",
25
- "vercel-oidc-bundle>=0.8.0",
26
- "vercel-internal-shared-vendored-deps>=0.1.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"]
@@ -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 TYPE_CHECKING, Any, Generic, TypeVar, cast, overload
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, Mapping
16
- from dataclasses import dataclass
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
- def is_running(self) -> bool:
37
- return self._thread.is_alive() and not self._server.should_exit
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.Server(config)
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
- server.should_exit = True
73
- thread.join(timeout=5)
74
- if thread.is_alive():
75
- raise RuntimeError("embedded queue dev server did not stop")
76
- app.state.close()
77
-
78
-
79
- @contextlib.contextmanager
80
- def queue_client_asgi_dev_server( # noqa: PLR0913
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
- from vercel.internal._vendor import httpx
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
- from vercel.internal._vendor import httpx
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
- _httpx_logger = logging.getLogger("httpx")
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 httpx.ResponseNotRead:
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 httpx.TransportError as exc:
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 httpx.TransportError as exc:
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 httpx.TransportError as exc:
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 httpx.TransportError as exc:
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
- _httpx_logger.addFilter(_QueueHttpxRequestLogFilter())
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,3 +1,3 @@
1
1
  """Package version metadata."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.9.0"
@@ -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")