urun-cli 0.3.1__tar.gz → 0.4.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.
- {urun_cli-0.3.1 → urun_cli-0.4.0}/PKG-INFO +1 -1
- {urun_cli-0.3.1 → urun_cli-0.4.0}/pyproject.toml +1 -1
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/api.py +36 -3
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/cli.py +39 -7
- {urun_cli-0.3.1 → urun_cli-0.4.0}/.gitignore +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/CHANGELOG.md +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/LICENSE +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/README.md +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/SECURITY.md +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/__init__.py +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/config.py +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/deps.py +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/discovery.py +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/errors.py +0 -0
- {urun_cli-0.3.1 → urun_cli-0.4.0}/src/urun/manifest.py +0 -0
|
@@ -5,6 +5,7 @@ import time
|
|
|
5
5
|
import urllib.error
|
|
6
6
|
import urllib.parse
|
|
7
7
|
import urllib.request
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
@@ -171,13 +172,36 @@ def require_safe_url(raw_url: str, label: str) -> None:
|
|
|
171
172
|
raise UrunError(f"{label} must use HTTPS except for localhost development")
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
# Terminal build states reported by the control plane's deployment-status document.
|
|
176
|
+
# The build worker transitions a deployment queued -> building -> ready|failed and
|
|
177
|
+
# records the actual build stderr/reason in the `error` field on failure.
|
|
178
|
+
TERMINAL_STATES = {"ready", "failed"}
|
|
179
|
+
|
|
180
|
+
# Human-readable progress lines for non-terminal states, so the CLI streams what the
|
|
181
|
+
# builder is doing instead of blocking silently until ready/failed.
|
|
182
|
+
_STATE_PROGRESS = {
|
|
183
|
+
"queued": "queued (waiting for a build slot)",
|
|
184
|
+
"building": "building (installing deps + materializing app)",
|
|
185
|
+
"not_found": "waiting for the control plane to register the build",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
174
189
|
def poll_until_done(
|
|
175
190
|
client: ApiClient,
|
|
176
191
|
manifest_hash: str,
|
|
177
192
|
interval: float = 2.0,
|
|
178
193
|
timeout: float = 1800.0,
|
|
179
194
|
not_found_grace: float = 30.0,
|
|
195
|
+
on_state: Callable[[str], None] | None = None,
|
|
180
196
|
) -> dict[str, Any]:
|
|
197
|
+
"""Poll the control plane for the real build outcome of a deployment.
|
|
198
|
+
|
|
199
|
+
Streams each new build state via ``on_state`` (so callers can show
|
|
200
|
+
queued -> building -> ... progress) and returns the terminal status
|
|
201
|
+
document (status ``ready`` or ``failed``). The ``failed`` document carries
|
|
202
|
+
the actual build error under ``error`` for the caller to surface. Raises
|
|
203
|
+
``UrunError`` on timeout.
|
|
204
|
+
"""
|
|
181
205
|
deadline = time.time() + timeout
|
|
182
206
|
not_found_deadline = time.time() + min(not_found_grace, timeout)
|
|
183
207
|
last_state = "unknown"
|
|
@@ -186,16 +210,25 @@ def poll_until_done(
|
|
|
186
210
|
status = client.deployment_status(manifest_hash)
|
|
187
211
|
except ApiError as exc:
|
|
188
212
|
if exc.status == 404 and time.time() < not_found_deadline:
|
|
213
|
+
if last_state != "not_found" and on_state is not None:
|
|
214
|
+
on_state("not_found")
|
|
189
215
|
last_state = "not_found"
|
|
190
216
|
time.sleep(interval)
|
|
191
217
|
continue
|
|
192
218
|
raise
|
|
193
|
-
state = status.get("status")
|
|
194
|
-
last_state
|
|
195
|
-
|
|
219
|
+
state = str(status.get("status"))
|
|
220
|
+
if state != last_state and on_state is not None:
|
|
221
|
+
on_state(state)
|
|
222
|
+
last_state = state
|
|
223
|
+
if state in TERMINAL_STATES:
|
|
196
224
|
return status
|
|
197
225
|
if time.time() >= deadline:
|
|
198
226
|
raise UrunError(
|
|
199
227
|
f"timed out waiting for deployment {manifest_hash} (last status: {last_state})"
|
|
200
228
|
)
|
|
201
229
|
time.sleep(interval)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def progress_line(state: str) -> str:
|
|
233
|
+
"""Render a human-readable progress line for a build state."""
|
|
234
|
+
return _STATE_PROGRESS.get(state, state)
|
|
@@ -11,7 +11,13 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from . import __version__
|
|
14
|
-
from .api import
|
|
14
|
+
from .api import (
|
|
15
|
+
DEFAULT_API_URL,
|
|
16
|
+
ApiClient,
|
|
17
|
+
poll_until_done,
|
|
18
|
+
progress_line,
|
|
19
|
+
upload_missing_blobs,
|
|
20
|
+
)
|
|
15
21
|
from .config import load_credentials, save_credentials
|
|
16
22
|
from .deps import resolve_deps
|
|
17
23
|
from .discovery import derive_app_name, discover_main_files
|
|
@@ -326,17 +332,43 @@ def deploy(args: argparse.Namespace) -> int:
|
|
|
326
332
|
print(f"Status: {status_url}")
|
|
327
333
|
return 0
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
print("Waiting for build to complete (--no-wait to skip)")
|
|
336
|
+
status = poll_until_done(
|
|
337
|
+
client,
|
|
338
|
+
mh,
|
|
339
|
+
args.poll_interval,
|
|
340
|
+
args.timeout,
|
|
341
|
+
on_state=lambda state: print(f" build: {progress_line(state)}"),
|
|
342
|
+
)
|
|
330
343
|
if status.get("status") == "failed":
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
message = err.get("message")
|
|
334
|
-
detail = f"{code}: {message}" if code and message else (message or code or "unknown error")
|
|
335
|
-
raise UrunError(f"deployment failed: {detail}")
|
|
344
|
+
print_build_failure(status)
|
|
345
|
+
return 1
|
|
336
346
|
print_ready(status)
|
|
337
347
|
return 0
|
|
338
348
|
|
|
339
349
|
|
|
350
|
+
def print_build_failure(status: dict[str, Any]) -> None:
|
|
351
|
+
"""Surface the REAL build failure (e.g. the uv/git/torch error) to stderr.
|
|
352
|
+
|
|
353
|
+
The control plane records the build worker's terminal error under
|
|
354
|
+
``error`` as ``{code, message}`` where ``message`` is the actual build
|
|
355
|
+
stderr/reason. We print the full message verbatim so callers no longer have
|
|
356
|
+
to kubectl into the cluster to learn why a build failed.
|
|
357
|
+
"""
|
|
358
|
+
err = status.get("error") or {}
|
|
359
|
+
code = string_value(err.get("code"))
|
|
360
|
+
message = string_value(err.get("message"))
|
|
361
|
+
print("Deployment failed: build did not succeed", file=sys.stderr)
|
|
362
|
+
if code:
|
|
363
|
+
print(f" reason: {code}", file=sys.stderr)
|
|
364
|
+
if message:
|
|
365
|
+
print(" build error:", file=sys.stderr)
|
|
366
|
+
for line in message.rstrip().splitlines() or [message]:
|
|
367
|
+
print(f" {line}", file=sys.stderr)
|
|
368
|
+
if not code and not message:
|
|
369
|
+
print(" (no error detail recorded by the build worker)", file=sys.stderr)
|
|
370
|
+
|
|
371
|
+
|
|
340
372
|
def resolve_api_credentials(args: argparse.Namespace) -> tuple[str, str]:
|
|
341
373
|
api_key = args.api_key or os.getenv("URUN_API_KEY")
|
|
342
374
|
api_url = args.api_url or os.getenv("URUN_API_URL")
|
|
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
|