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,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import base64
|
|
7
|
+
import binascii
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess # noqa: S404 - CLI delegates deployment lookup to the Vercel CLI.
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from vercel.queue.sync import QueueClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
19
|
+
"""Run the ``python -m vercel.queue`` command."""
|
|
20
|
+
parser = _build_parser()
|
|
21
|
+
try:
|
|
22
|
+
args = parser.parse_args(argv)
|
|
23
|
+
except SystemExit as exc:
|
|
24
|
+
return int(exc.code) if isinstance(exc.code, int) else 2
|
|
25
|
+
|
|
26
|
+
if args.command == "send":
|
|
27
|
+
return _send(args, parser)
|
|
28
|
+
|
|
29
|
+
parser.error("unknown command")
|
|
30
|
+
return 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
parser = argparse.ArgumentParser(prog="python -m vercel.queue")
|
|
35
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
36
|
+
|
|
37
|
+
send = subparsers.add_parser("send", help="send a queue message")
|
|
38
|
+
send.add_argument("--topic", required=True, help="queue topic name")
|
|
39
|
+
send.add_argument("--region", help="queue region, such as iad1")
|
|
40
|
+
send.add_argument("--deployment", help="deployment ID, such as dpl_123")
|
|
41
|
+
send.add_argument(
|
|
42
|
+
"--output-format",
|
|
43
|
+
choices=["text", "json"],
|
|
44
|
+
default="text",
|
|
45
|
+
help="output format",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
payload = send.add_mutually_exclusive_group(required=True)
|
|
49
|
+
payload.add_argument("--json", dest="json_value", help="JSON payload")
|
|
50
|
+
payload.add_argument("--json-from", help="path to a UTF-8 JSON payload file")
|
|
51
|
+
payload.add_argument("--text", help="text payload")
|
|
52
|
+
payload.add_argument("--text-from", help="path to a UTF-8 text payload file")
|
|
53
|
+
payload.add_argument("--binary", help="base64-encoded binary payload")
|
|
54
|
+
payload.add_argument("--binary-from", help="path to a raw binary payload file")
|
|
55
|
+
return parser
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _send(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
59
|
+
try:
|
|
60
|
+
payload = _payload_from_args(args)
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
parser.print_usage(sys.stderr)
|
|
63
|
+
sys.stderr.write(f"{parser.prog}: error: {exc}\n")
|
|
64
|
+
return 2
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
_load_dotenv_local()
|
|
68
|
+
deployment = args.deployment or _resolve_current_production_deployment()
|
|
69
|
+
queue = QueueClient(region=args.region, deployment=deployment)
|
|
70
|
+
message_id = queue.send(args.topic, payload)
|
|
71
|
+
except Exception as exc: # noqa: BLE001 - CLI boundary converts runtime failures to exit codes.
|
|
72
|
+
sys.stderr.write(f"vercel.queue: {exc}\n")
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
_write_output(message_id, deployment, args.output_format)
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _payload_from_args(args: argparse.Namespace) -> Any:
|
|
80
|
+
if args.json_value is not None:
|
|
81
|
+
return _parse_json(args.json_value, "--json")
|
|
82
|
+
if args.json_from is not None:
|
|
83
|
+
return _parse_json(_read_text(args.json_from, "--json-from"), "--json-from")
|
|
84
|
+
if args.text is not None:
|
|
85
|
+
return args.text
|
|
86
|
+
if args.text_from is not None:
|
|
87
|
+
return _read_text(args.text_from, "--text-from")
|
|
88
|
+
if args.binary is not None:
|
|
89
|
+
return _decode_base64(args.binary)
|
|
90
|
+
if args.binary_from is not None:
|
|
91
|
+
return _read_bytes(args.binary_from, "--binary-from")
|
|
92
|
+
raise ValueError("one payload option is required")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_json(value: str, option: str) -> Any:
|
|
96
|
+
try:
|
|
97
|
+
return json.loads(value)
|
|
98
|
+
except json.JSONDecodeError as exc:
|
|
99
|
+
raise ValueError(f"{option} must contain valid JSON: {exc.msg}") from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _decode_base64(value: str) -> bytes:
|
|
103
|
+
try:
|
|
104
|
+
return base64.b64decode(value, validate=True)
|
|
105
|
+
except binascii.Error as exc:
|
|
106
|
+
raise ValueError(f"--binary must contain valid base64: {exc}") from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _read_text(path: str, option: str) -> str:
|
|
110
|
+
try:
|
|
111
|
+
return Path(path).read_text(encoding="utf-8")
|
|
112
|
+
except OSError as exc:
|
|
113
|
+
raise ValueError(f"{option} could not be read: {exc}") from exc
|
|
114
|
+
except UnicodeDecodeError as exc:
|
|
115
|
+
raise ValueError(f"{option} must be UTF-8 text: {exc}") from exc
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _read_bytes(path: str, option: str) -> bytes:
|
|
119
|
+
try:
|
|
120
|
+
return Path(path).read_bytes()
|
|
121
|
+
except OSError as exc:
|
|
122
|
+
raise ValueError(f"{option} could not be read: {exc}") from exc
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resolve_current_production_deployment() -> str:
|
|
126
|
+
project = _load_linked_project()
|
|
127
|
+
scope = project.get("teamId")
|
|
128
|
+
deployment_url = _find_current_production_deployment_url(scope)
|
|
129
|
+
if deployment_url is None:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
"Failed to resolve deployment ID: no production deployment was found for the "
|
|
132
|
+
"linked project. Pass --deployment to choose one explicitly."
|
|
133
|
+
)
|
|
134
|
+
return _inspect_deployment_id(deployment_url, scope)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _load_linked_project(start: Path | None = None) -> dict[str, str]:
|
|
138
|
+
project_path = _find_project_json(start or Path.cwd())
|
|
139
|
+
if project_path is None:
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
"Failed to resolve deployment ID: .vercel/project.json was not found. "
|
|
142
|
+
"Run `vercel link` or pass --deployment."
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
data = json.loads(project_path.read_text(encoding="utf-8"))
|
|
146
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
"Failed to resolve deployment ID: .vercel/project.json is invalid. "
|
|
149
|
+
"Run `vercel link` or pass --deployment."
|
|
150
|
+
) from exc
|
|
151
|
+
if not isinstance(data, dict):
|
|
152
|
+
raise TypeError(
|
|
153
|
+
"Failed to resolve deployment ID: .vercel/project.json is invalid. "
|
|
154
|
+
"Run `vercel link` or pass --deployment."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
project_id = data.get("projectId")
|
|
158
|
+
team_id = data.get("teamId") or data.get("orgId")
|
|
159
|
+
if not isinstance(project_id, str) or not project_id:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"Failed to resolve deployment ID: .vercel/project.json is missing projectId. "
|
|
162
|
+
"Run `vercel link` or pass --deployment."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
project = {"projectId": project_id}
|
|
166
|
+
if isinstance(team_id, str) and team_id:
|
|
167
|
+
project["teamId"] = team_id
|
|
168
|
+
return project
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _find_project_json(start: Path) -> Path | None:
|
|
172
|
+
current = start.resolve()
|
|
173
|
+
if current.is_file():
|
|
174
|
+
current = current.parent
|
|
175
|
+
while True:
|
|
176
|
+
candidate = current / ".vercel" / "project.json"
|
|
177
|
+
if candidate.is_file():
|
|
178
|
+
return candidate
|
|
179
|
+
if current.parent == current:
|
|
180
|
+
return None
|
|
181
|
+
current = current.parent
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _find_current_production_deployment_url(scope: str | None = None) -> str | None:
|
|
185
|
+
args = [
|
|
186
|
+
"list",
|
|
187
|
+
"--environment",
|
|
188
|
+
"production",
|
|
189
|
+
"--status",
|
|
190
|
+
"READY",
|
|
191
|
+
"--format",
|
|
192
|
+
"json",
|
|
193
|
+
"--non-interactive",
|
|
194
|
+
]
|
|
195
|
+
if scope is not None:
|
|
196
|
+
args.extend(["--scope", scope])
|
|
197
|
+
data = _run_vercel_json(args)
|
|
198
|
+
if not isinstance(data, dict):
|
|
199
|
+
raise TypeError("Failed to resolve deployment ID: Vercel CLI returned an invalid response.")
|
|
200
|
+
deployments = data.get("deployments")
|
|
201
|
+
if not isinstance(deployments, list):
|
|
202
|
+
raise TypeError("Failed to resolve deployment ID: Vercel CLI returned an invalid response.")
|
|
203
|
+
if not deployments:
|
|
204
|
+
return None
|
|
205
|
+
first = deployments[0]
|
|
206
|
+
url = first.get("url") if isinstance(first, dict) else None
|
|
207
|
+
if not isinstance(url, str):
|
|
208
|
+
raise TypeError("Failed to resolve deployment ID: Vercel CLI returned an invalid response.")
|
|
209
|
+
return url
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _inspect_deployment_id(deployment_url: str, scope: str | None = None) -> str:
|
|
213
|
+
args = ["inspect", deployment_url, "--format", "json", "--non-interactive"]
|
|
214
|
+
if scope is not None:
|
|
215
|
+
args.extend(["--scope", scope])
|
|
216
|
+
data = _run_vercel_json(args)
|
|
217
|
+
deployment_id = data.get("id") if isinstance(data, dict) else None
|
|
218
|
+
if not isinstance(deployment_id, str):
|
|
219
|
+
raise TypeError("Failed to resolve deployment ID: Vercel CLI returned an invalid response.")
|
|
220
|
+
return deployment_id
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_vercel_json(args: Sequence[str]) -> object:
|
|
224
|
+
command = ["vc", *args]
|
|
225
|
+
try:
|
|
226
|
+
result = subprocess.run( # noqa: S603 - command is fixed and args are internal constants.
|
|
227
|
+
command,
|
|
228
|
+
check=False,
|
|
229
|
+
capture_output=True,
|
|
230
|
+
text=True,
|
|
231
|
+
timeout=30,
|
|
232
|
+
)
|
|
233
|
+
except FileNotFoundError as exc:
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
"Failed to resolve deployment ID: install the Vercel CLI, log in, or pass --deployment."
|
|
236
|
+
) from exc
|
|
237
|
+
except subprocess.SubprocessError as exc:
|
|
238
|
+
raise RuntimeError(f"Failed to resolve deployment ID: Vercel CLI failed: {exc}") from exc
|
|
239
|
+
if result.returncode != 0:
|
|
240
|
+
message = (result.stderr or result.stdout).strip()
|
|
241
|
+
detail = f": {message}" if message else ""
|
|
242
|
+
raise RuntimeError(
|
|
243
|
+
"Failed to resolve deployment ID: Vercel CLI failed"
|
|
244
|
+
f"{detail}. Log in with the Vercel CLI or pass --deployment."
|
|
245
|
+
)
|
|
246
|
+
return _parse_vercel_json_output(result.stdout)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _parse_vercel_json_output(output: str) -> object:
|
|
250
|
+
start = output.find("{")
|
|
251
|
+
if start == -1:
|
|
252
|
+
raise TypeError("Failed to resolve deployment ID: Vercel CLI returned an invalid response.")
|
|
253
|
+
try:
|
|
254
|
+
return json.loads(output[start:])
|
|
255
|
+
except json.JSONDecodeError as exc:
|
|
256
|
+
raise TypeError(
|
|
257
|
+
"Failed to resolve deployment ID: Vercel CLI returned an invalid response."
|
|
258
|
+
) from exc
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _load_dotenv_local(path: Path | None = None) -> None:
|
|
262
|
+
env_path = path or Path.cwd() / ".env.local"
|
|
263
|
+
try:
|
|
264
|
+
lines = env_path.read_text(encoding="utf-8").splitlines()
|
|
265
|
+
except FileNotFoundError:
|
|
266
|
+
return
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
raise RuntimeError(f"failed to read {env_path}: {exc}") from exc
|
|
269
|
+
|
|
270
|
+
for line in lines:
|
|
271
|
+
parsed = _parse_dotenv_line(line)
|
|
272
|
+
if parsed is None:
|
|
273
|
+
continue
|
|
274
|
+
key, value = parsed
|
|
275
|
+
if key == "VERCEL_DEPLOYMENT_ID":
|
|
276
|
+
continue
|
|
277
|
+
os.environ.setdefault(key, value)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _parse_dotenv_line(line: str) -> tuple[str, str] | None:
|
|
281
|
+
stripped = line.strip()
|
|
282
|
+
if not stripped or stripped.startswith("#"):
|
|
283
|
+
return None
|
|
284
|
+
if stripped.startswith("export "):
|
|
285
|
+
stripped = stripped.removeprefix("export ").lstrip()
|
|
286
|
+
key, separator, value = stripped.partition("=")
|
|
287
|
+
key = key.strip()
|
|
288
|
+
if separator != "=" or not key:
|
|
289
|
+
return None
|
|
290
|
+
return key, _parse_dotenv_value(value.strip())
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _parse_dotenv_value(value: str) -> str:
|
|
294
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
295
|
+
return value[1:-1]
|
|
296
|
+
if " #" in value:
|
|
297
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
298
|
+
return value
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _write_output(message_id: str | None, deployment: str, output_format: str) -> None:
|
|
302
|
+
if output_format == "json":
|
|
303
|
+
sys.stdout.write(
|
|
304
|
+
json.dumps(
|
|
305
|
+
{"message_id": message_id, "deployment_id": deployment},
|
|
306
|
+
separators=(",", ":"),
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
sys.stdout.write("\n")
|
|
310
|
+
return
|
|
311
|
+
if message_id is not None:
|
|
312
|
+
sys.stdout.write(f"{message_id}\n")
|
|
313
|
+
sys.stdout.write(f"deployment: {deployment}\n")
|