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.
Files changed (43) hide show
  1. vercel/queue/__init__.py +8 -0
  2. vercel/queue/__main__.py +10 -0
  3. vercel/queue/_internal/__init__.py +1 -0
  4. vercel/queue/_internal/api_async.py +219 -0
  5. vercel/queue/_internal/api_common.py +104 -0
  6. vercel/queue/_internal/api_sync.py +137 -0
  7. vercel/queue/_internal/asgi.py +145 -0
  8. vercel/queue/_internal/asynctools.py +40 -0
  9. vercel/queue/_internal/cli.py +313 -0
  10. vercel/queue/_internal/client.py +1109 -0
  11. vercel/queue/_internal/client_sync.py +432 -0
  12. vercel/queue/_internal/config.py +160 -0
  13. vercel/queue/_internal/constants.py +50 -0
  14. vercel/queue/_internal/devserver.py +205 -0
  15. vercel/queue/_internal/embedded.py +1900 -0
  16. vercel/queue/_internal/errors.py +262 -0
  17. vercel/queue/_internal/http.py +634 -0
  18. vercel/queue/_internal/lease.py +1007 -0
  19. vercel/queue/_internal/log.py +143 -0
  20. vercel/queue/_internal/messages.py +122 -0
  21. vercel/queue/_internal/multipart.py +255 -0
  22. vercel/queue/_internal/names.py +101 -0
  23. vercel/queue/_internal/polling.py +200 -0
  24. vercel/queue/_internal/push.py +246 -0
  25. vercel/queue/_internal/response.py +111 -0
  26. vercel/queue/_internal/retry.py +86 -0
  27. vercel/queue/_internal/streams.py +313 -0
  28. vercel/queue/_internal/subscribers.py +1025 -0
  29. vercel/queue/_internal/transports.py +363 -0
  30. vercel/queue/_internal/types.py +300 -0
  31. vercel/queue/_internal/typeutils.py +203 -0
  32. vercel/queue/devserver.py +24 -0
  33. vercel/queue/embedded.py +50 -0
  34. vercel/queue/py.typed +1 -0
  35. vercel/queue/sync.py +8 -0
  36. vercel/queue/testing/__init__.py +14 -0
  37. vercel/queue/testing/pytest.py +42 -0
  38. vercel/queue/testing/state.py +32 -0
  39. vercel/queue/version.py +3 -0
  40. vercel_queue-0.7.0.dist-info/METADATA +681 -0
  41. vercel_queue-0.7.0.dist-info/RECORD +43 -0
  42. vercel_queue-0.7.0.dist-info/WHEEL +4 -0
  43. 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")