urun-cli 0.6.0__tar.gz → 0.6.1__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.6.0 → urun_cli-0.6.1}/PKG-INFO +12 -1
- {urun_cli-0.6.0 → urun_cli-0.6.1}/README.md +11 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/pyproject.toml +5 -12
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/api.py +17 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/catalog.py +15 -12
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/cli.py +93 -9
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/frontend_launch.py +8 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/.gitignore +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/CHANGELOG.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/LICENSE +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/SECURITY.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/hatch_build.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/__init__.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/_bin/README.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/buildlogs.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/client_tokens.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/config.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/deps.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/dev.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/devconsole.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/devstream.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/discovery.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/errors.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/fleet.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/launch_auth.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/manifest.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/nodes.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/openai_proxy_bin.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/repo_deps.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/retry.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/runtime_health.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/scratch.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/serve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urun-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: End-user CLI for deploying apps to urun
|
|
5
5
|
Project-URL: Homepage, https://urun.sh
|
|
6
6
|
Project-URL: Repository, https://github.com/urun-sh/urun-cli
|
|
@@ -371,6 +371,17 @@ Enable a disabled app and bring it back online:
|
|
|
371
371
|
urun app enable lingbot-handle
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
+
If `urun deploy` or `urun dev --attach` reports that a function was paused
|
|
375
|
+
after repeated startup failures, inspect its runtime logs before overriding
|
|
376
|
+
the safety pause. Then explicitly resume every paused function:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
urun app enable rapid-clips --force
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`--force` may restart a known-crashing function; it does not change the
|
|
383
|
+
platform's automatic pause policy.
|
|
384
|
+
|
|
374
385
|
Stop a queued, starting, or live session:
|
|
375
386
|
|
|
376
387
|
```bash
|
|
@@ -349,6 +349,17 @@ Enable a disabled app and bring it back online:
|
|
|
349
349
|
urun app enable lingbot-handle
|
|
350
350
|
```
|
|
351
351
|
|
|
352
|
+
If `urun deploy` or `urun dev --attach` reports that a function was paused
|
|
353
|
+
after repeated startup failures, inspect its runtime logs before overriding
|
|
354
|
+
the safety pause. Then explicitly resume every paused function:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
urun app enable rapid-clips --force
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`--force` may restart a known-crashing function; it does not change the
|
|
361
|
+
platform's automatic pause policy.
|
|
362
|
+
|
|
352
363
|
Stop a queued, starting, or live session:
|
|
353
364
|
|
|
354
365
|
```bash
|
|
@@ -10,24 +10,17 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "urun-cli"
|
|
13
|
-
# Cut
|
|
13
|
+
# Cut the paused-function recovery fixes merged after v0.6.0.
|
|
14
14
|
#
|
|
15
15
|
# WHY THIS IS EDITED BY HAND. [tool.semantic_release] below is fully
|
|
16
16
|
# configured -- conventional commit parser, version_toml pointing here, tag
|
|
17
17
|
# format, changelog templates -- but NOTHING invokes it: `grep -rn
|
|
18
18
|
# semantic.release .github/workflows/` matches nothing. release.yaml only
|
|
19
19
|
# READS this value and skips publishing when the matching tag already exists,
|
|
20
|
-
# so
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
# 0.6.0 is semantic-release's OWN answer, not a guess:
|
|
26
|
-
# $ uvx --from python-semantic-release semantic-release --noop version --print
|
|
27
|
-
# 0.6.0
|
|
28
|
-
# (several feat: commits, and major_on_zero = false keeps 0.x on a minor bump)
|
|
29
|
-
# so choosing it leaves the numbering where automation would resume.
|
|
30
|
-
version = "0.6.0"
|
|
20
|
+
# so every merge remains on the already-published version until this value is
|
|
21
|
+
# advanced. Wiring semantic-release into CI remains separate work; this patch
|
|
22
|
+
# bump is the minimal release unblock for the three post-v0.6.0 fixes.
|
|
23
|
+
version = "0.6.1"
|
|
31
24
|
description = "End-user CLI for deploying apps to urun"
|
|
32
25
|
readme = "README.md"
|
|
33
26
|
requires-python = ">=3.11"
|
|
@@ -607,6 +607,23 @@ class ApiClient:
|
|
|
607
607
|
body: dict[str, Any] | None = {"environment": environment} if environment else None
|
|
608
608
|
return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/activate", body)
|
|
609
609
|
|
|
610
|
+
def set_function_paused(
|
|
611
|
+
self,
|
|
612
|
+
app: str,
|
|
613
|
+
function_name: str,
|
|
614
|
+
paused: bool,
|
|
615
|
+
environment: str | None = None,
|
|
616
|
+
) -> dict[str, Any]:
|
|
617
|
+
"""Pause or resume one function of an app.
|
|
618
|
+
|
|
619
|
+
Hits ``POST /app/<slug>/pause`` with the function name and explicit
|
|
620
|
+
pause state. The org is derived from the API key.
|
|
621
|
+
"""
|
|
622
|
+
body: dict[str, Any] = {"paused": paused, "function_name": function_name}
|
|
623
|
+
if environment:
|
|
624
|
+
body["environment"] = environment
|
|
625
|
+
return self._json("POST", f"/app/{urllib.parse.quote(app, safe='')}/pause", body)
|
|
626
|
+
|
|
610
627
|
def retire_app(self, app: str, environment: str | None = None) -> dict[str, Any]:
|
|
611
628
|
"""Retire an app through the CLI edge route (backs ``urun app delete``).
|
|
612
629
|
|
|
@@ -115,10 +115,11 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
115
115
|
"safetensors",
|
|
116
116
|
"huggingface_hub>=1.5.0,<2.0",
|
|
117
117
|
"opencv-python-headless==4.11.0.86",
|
|
118
|
-
"nvidia-cuda-nvcc==13.
|
|
119
|
-
"nvidia-cuda-crt==13.
|
|
120
|
-
"nvidia-cuda-cccl==13.
|
|
121
|
-
"nvidia-
|
|
118
|
+
"nvidia-cuda-nvcc==13.0.88",
|
|
119
|
+
"nvidia-cuda-crt==13.0.88",
|
|
120
|
+
"nvidia-cuda-cccl==13.0.85",
|
|
121
|
+
"nvidia-nvvm==13.0.88",
|
|
122
|
+
"nvidia-cuda-runtime==13.0.96",
|
|
122
123
|
"numpy<2",
|
|
123
124
|
"pillow>=10.0.0",
|
|
124
125
|
"orjson",
|
|
@@ -155,10 +156,11 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
155
156
|
"accelerate",
|
|
156
157
|
"safetensors",
|
|
157
158
|
"huggingface_hub>=1.5.0,<2.0",
|
|
158
|
-
"nvidia-cuda-nvcc==13.
|
|
159
|
-
"nvidia-cuda-crt==13.
|
|
160
|
-
"nvidia-cuda-cccl==13.
|
|
161
|
-
"nvidia-
|
|
159
|
+
"nvidia-cuda-nvcc==13.0.88",
|
|
160
|
+
"nvidia-cuda-crt==13.0.88",
|
|
161
|
+
"nvidia-cuda-cccl==13.0.85",
|
|
162
|
+
"nvidia-nvvm==13.0.88",
|
|
163
|
+
"nvidia-cuda-runtime==13.0.96",
|
|
162
164
|
"numpy<2",
|
|
163
165
|
"pillow>=10.0.0",
|
|
164
166
|
"orjson",
|
|
@@ -357,10 +359,11 @@ ENGINE_DEPS: dict[str, list[str]] = {
|
|
|
357
359
|
"safetensors",
|
|
358
360
|
"huggingface_hub>=1.5.0,<2.0",
|
|
359
361
|
"flashinfer_python[cu13]==0.6.14",
|
|
360
|
-
"nvidia-cuda-nvcc==13.
|
|
361
|
-
"nvidia-cuda-crt==13.
|
|
362
|
-
"nvidia-cuda-cccl==13.
|
|
363
|
-
"nvidia-
|
|
362
|
+
"nvidia-cuda-nvcc==13.0.88",
|
|
363
|
+
"nvidia-cuda-crt==13.0.88",
|
|
364
|
+
"nvidia-cuda-cccl==13.0.85",
|
|
365
|
+
"nvidia-nvvm==13.0.88",
|
|
366
|
+
"nvidia-cuda-runtime==13.0.96",
|
|
364
367
|
"numpy<2",
|
|
365
368
|
"pillow>=10.0.0",
|
|
366
369
|
"orjson",
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import getpass
|
|
5
|
+
import http.client
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
@@ -1349,10 +1350,20 @@ def add_app_parser(sub: argparse._SubParsersAction) -> None:
|
|
|
1349
1350
|
description=(
|
|
1350
1351
|
"Enable a previously disabled app (the inverse of `urun app "
|
|
1351
1352
|
"disable`): drive its deployment back to `active` so the control "
|
|
1352
|
-
"plane resumes recreating its runtime StatefulSet."
|
|
1353
|
+
"plane resumes recreating its runtime StatefulSet. Pass --force to "
|
|
1354
|
+
"also resume functions automatically paused after repeated startup "
|
|
1355
|
+
"failures."
|
|
1353
1356
|
),
|
|
1354
1357
|
)
|
|
1355
1358
|
enable_parser.add_argument("app", help="App slug (as shown by `urun app list`)")
|
|
1359
|
+
enable_parser.add_argument(
|
|
1360
|
+
"--force",
|
|
1361
|
+
action="store_true",
|
|
1362
|
+
help=(
|
|
1363
|
+
"Also resume every paused function. This may restart a function "
|
|
1364
|
+
"the platform paused because it repeatedly failed to start"
|
|
1365
|
+
),
|
|
1366
|
+
)
|
|
1356
1367
|
enable_parser.add_argument(
|
|
1357
1368
|
"--environment",
|
|
1358
1369
|
"--env",
|
|
@@ -2504,9 +2515,18 @@ def deploy_once(args: argparse.Namespace) -> DeployOutcome:
|
|
|
2504
2515
|
mark_phase("finalize")
|
|
2505
2516
|
if final.get("reused") is True:
|
|
2506
2517
|
# Same-manifest redeploy of an already-built release: the control
|
|
2507
|
-
# plane reuses the existing artifact
|
|
2508
|
-
#
|
|
2518
|
+
# plane reuses the existing artifact and its function rows. A function
|
|
2519
|
+
# the crash-loop guard paused therefore STAYS paused: unchanged source
|
|
2520
|
+
# cannot silently clear an operational safety decision. Surface that
|
|
2521
|
+
# state and its explicit recovery command before returning success.
|
|
2509
2522
|
print(f"unchanged — reusing release {mh[:12]}")
|
|
2523
|
+
pause_check_client = ApiClient(
|
|
2524
|
+
api_url,
|
|
2525
|
+
api_key,
|
|
2526
|
+
timeout=DEMO_PREFLIGHT_TIMEOUT_S,
|
|
2527
|
+
backoff=BackoffPolicy(max_attempts=1),
|
|
2528
|
+
)
|
|
2529
|
+
warn_paused_functions(pause_check_client, app_name, environment=environment)
|
|
2510
2530
|
print(format_phase_timing(phases))
|
|
2511
2531
|
return DeployOutcome(exit_code=0, manifest_hash=mh, deps=deps, status=dict(final))
|
|
2512
2532
|
print(f"Deployment {final.get('status', 'queued')}: {mh}")
|
|
@@ -4771,8 +4791,16 @@ def app_enable(args: argparse.Namespace) -> int:
|
|
|
4771
4791
|
environment = resolve_environment(args)
|
|
4772
4792
|
client = ApiClient(api_url, api_key)
|
|
4773
4793
|
response = client.activate_app(args.app, environment=environment)
|
|
4794
|
+
resumed: list[str] = []
|
|
4795
|
+
if getattr(args, "force", False):
|
|
4796
|
+
for function_name in paused_app_functions(client, args.app, environment=environment):
|
|
4797
|
+
client.set_function_paused(args.app, function_name, False, environment=environment)
|
|
4798
|
+
resumed.append(function_name)
|
|
4774
4799
|
if getattr(args, "json", False):
|
|
4775
|
-
|
|
4800
|
+
result = dict(response)
|
|
4801
|
+
if getattr(args, "force", False):
|
|
4802
|
+
result["functions_resumed"] = resumed
|
|
4803
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
4776
4804
|
return 0
|
|
4777
4805
|
app = _app_payload(response)
|
|
4778
4806
|
slug = _coalesce_str(app.get("app_slug"), args.app)
|
|
@@ -4780,19 +4808,75 @@ def app_enable(args: argparse.Namespace) -> int:
|
|
|
4780
4808
|
f"Enabled {slug} in {environment} "
|
|
4781
4809
|
f"(deployment status: {_coalesce_str(app.get('status'), 'active')})."
|
|
4782
4810
|
)
|
|
4811
|
+
if getattr(args, "force", False):
|
|
4812
|
+
if resumed:
|
|
4813
|
+
print(f"Resumed paused function(s): {', '.join(resumed)}.")
|
|
4814
|
+
else:
|
|
4815
|
+
print("No paused functions needed recovery.")
|
|
4783
4816
|
return 0
|
|
4784
4817
|
|
|
4785
4818
|
|
|
4819
|
+
def paused_app_functions(client: ApiClient, app_slug: str, *, environment: str) -> list[str]:
|
|
4820
|
+
"""Return paused current-release functions from the capacity API."""
|
|
4821
|
+
response = client.list_capacity({"app": app_slug, "environment": environment})
|
|
4822
|
+
rows = response.get("capacity") if isinstance(response, dict) else None
|
|
4823
|
+
if not isinstance(rows, list):
|
|
4824
|
+
raise UrunError("server returned invalid capacity payload")
|
|
4825
|
+
names: set[str] = set()
|
|
4826
|
+
for row in rows:
|
|
4827
|
+
if not isinstance(row, dict):
|
|
4828
|
+
raise UrunError("server returned invalid capacity payload")
|
|
4829
|
+
row_app = string_value(row.get("app_slug"))
|
|
4830
|
+
row_environment = string_value(row.get("environment")) or DEFAULT_ENVIRONMENT
|
|
4831
|
+
if row_app not in {None, app_slug} or row_environment != environment:
|
|
4832
|
+
continue
|
|
4833
|
+
name = string_value(row.get("function_name"))
|
|
4834
|
+
if row.get("paused") is True and name:
|
|
4835
|
+
names.add(name)
|
|
4836
|
+
return sorted(names)
|
|
4837
|
+
|
|
4838
|
+
|
|
4839
|
+
def warn_paused_functions(client: ApiClient, app_slug: str, *, environment: str) -> None:
|
|
4840
|
+
"""Warn when an unchanged deploy remains function-paused."""
|
|
4841
|
+
try:
|
|
4842
|
+
names = paused_app_functions(client, app_slug, environment=environment)
|
|
4843
|
+
except (UrunError, OSError, http.client.HTTPException) as exc:
|
|
4844
|
+
print(
|
|
4845
|
+
f"Warning: could not check whether {app_slug} has paused functions: {exc}",
|
|
4846
|
+
file=sys.stderr,
|
|
4847
|
+
)
|
|
4848
|
+
return
|
|
4849
|
+
if not names:
|
|
4850
|
+
return
|
|
4851
|
+
print_paused_function_warning(app_slug, names, environment=environment)
|
|
4852
|
+
|
|
4853
|
+
|
|
4854
|
+
def print_paused_function_warning(
|
|
4855
|
+
app_slug: str, function_names: list[str], *, environment: str
|
|
4856
|
+
) -> None:
|
|
4857
|
+
"""Print the shared paused-function explanation and recovery command."""
|
|
4858
|
+
command = shlex.join(["urun", "app", "enable", app_slug, "--force", "--env", environment])
|
|
4859
|
+
joined = ", ".join(function_names)
|
|
4860
|
+
print(
|
|
4861
|
+
f"Warning: {app_slug} has paused function(s): {joined}. The platform may "
|
|
4862
|
+
"have paused them after repeated startup failures. An unchanged deploy "
|
|
4863
|
+
"or frontend attach does not clear that safety state.",
|
|
4864
|
+
file=sys.stderr,
|
|
4865
|
+
)
|
|
4866
|
+
print(
|
|
4867
|
+
f"Review the function logs, then run `{command}` to resume them. This may "
|
|
4868
|
+
"restart a known-crashing function.",
|
|
4869
|
+
file=sys.stderr,
|
|
4870
|
+
)
|
|
4871
|
+
|
|
4872
|
+
|
|
4786
4873
|
def _confirm_disable(app: str, environment: str) -> bool:
|
|
4787
4874
|
if not sys.stdin.isatty():
|
|
4788
4875
|
raise UrunError(
|
|
4789
4876
|
f"refusing to disable {app} without confirmation; pass --yes (non-interactive)"
|
|
4790
4877
|
)
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
.strip()
|
|
4794
|
-
.lower()
|
|
4795
|
-
)
|
|
4878
|
+
prompt = f"Disable app '{app}' in environment '{environment}'? This stops its runtime. [y/N] "
|
|
4879
|
+
answer = input(prompt).strip().lower()
|
|
4796
4880
|
return answer in {"y", "yes"}
|
|
4797
4881
|
|
|
4798
4882
|
|
|
@@ -1446,6 +1446,7 @@ def preflight_demo_function(
|
|
|
1446
1446
|
_runtime_logs_action,
|
|
1447
1447
|
_sanitize_table_cell,
|
|
1448
1448
|
_truncate,
|
|
1449
|
+
print_paused_function_warning,
|
|
1449
1450
|
string_value,
|
|
1450
1451
|
)
|
|
1451
1452
|
|
|
@@ -1510,6 +1511,11 @@ def preflight_demo_function(
|
|
|
1510
1511
|
|
|
1511
1512
|
row = min(matching_rows, key=selection_key)
|
|
1512
1513
|
ready_gpu = row.get("ready_gpu_units")
|
|
1514
|
+
paused = row.get("paused") is True
|
|
1515
|
+
if paused:
|
|
1516
|
+
print_paused_function_warning(
|
|
1517
|
+
app_slug, [function_name], environment=selected_environment
|
|
1518
|
+
)
|
|
1513
1519
|
# ADMISSION BLOCKERS FIRST — they are fatal INDEPENDENTLY of capacity and
|
|
1514
1520
|
# health. A blocked org cannot start a session even with warm GPUs parked
|
|
1515
1521
|
# and a perfectly healthy runtime, so this must precede the positive-
|
|
@@ -1521,6 +1527,8 @@ def preflight_demo_function(
|
|
|
1521
1527
|
raise UrunError(
|
|
1522
1528
|
f"cannot demo {app_slug}/{function_name}: {_ready_reason_text(blocking_reason)}"
|
|
1523
1529
|
)
|
|
1530
|
+
if paused:
|
|
1531
|
+
return
|
|
1524
1532
|
health = normalize_runtime_health(row.get("runtime_health"))
|
|
1525
1533
|
if health is None and not isinstance(ready_gpu, int):
|
|
1526
1534
|
return
|
|
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
|
|
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
|