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.
Files changed (33) hide show
  1. {urun_cli-0.6.0 → urun_cli-0.6.1}/PKG-INFO +12 -1
  2. {urun_cli-0.6.0 → urun_cli-0.6.1}/README.md +11 -0
  3. {urun_cli-0.6.0 → urun_cli-0.6.1}/pyproject.toml +5 -12
  4. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/api.py +17 -0
  5. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/catalog.py +15 -12
  6. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/cli.py +93 -9
  7. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/frontend_launch.py +8 -0
  8. {urun_cli-0.6.0 → urun_cli-0.6.1}/.gitignore +0 -0
  9. {urun_cli-0.6.0 → urun_cli-0.6.1}/CHANGELOG.md +0 -0
  10. {urun_cli-0.6.0 → urun_cli-0.6.1}/LICENSE +0 -0
  11. {urun_cli-0.6.0 → urun_cli-0.6.1}/SECURITY.md +0 -0
  12. {urun_cli-0.6.0 → urun_cli-0.6.1}/hatch_build.py +0 -0
  13. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/__init__.py +0 -0
  14. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/_bin/README.md +0 -0
  15. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/buildlogs.py +0 -0
  16. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/client_tokens.py +0 -0
  17. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/config.py +0 -0
  18. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/deps.py +0 -0
  19. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/dev.py +0 -0
  20. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/devconsole.py +0 -0
  21. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/devstream.py +0 -0
  22. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/discovery.py +0 -0
  23. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/errors.py +0 -0
  24. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/fleet.py +0 -0
  25. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/launch_auth.py +0 -0
  26. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/manifest.py +0 -0
  27. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/nodes.py +0 -0
  28. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/openai_proxy_bin.py +0 -0
  29. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/repo_deps.py +0 -0
  30. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/retry.py +0 -0
  31. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/runtime_health.py +0 -0
  32. {urun_cli-0.6.0 → urun_cli-0.6.1}/src/urun/scratch.py +0 -0
  33. {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.0
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 a release for the 11 commits stranded since v0.5.32 (2026-08-20).
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 the version has been frozen since v0.5.32 was tagged and every merge
21
- # after it published nothing. CHANGELOG.md stopping at 0.5.4 is the same gap
22
- # seen from the other end. Wiring semantic-release into CI is the real fix and
23
- # is deliberately NOT bundled here; this bump is the reversible unblock.
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.3.73",
119
- "nvidia-cuda-crt==13.3.73",
120
- "nvidia-cuda-cccl==13.3.3.4.1",
121
- "nvidia-cuda-runtime==13.3.29",
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.3.73",
159
- "nvidia-cuda-crt==13.3.73",
160
- "nvidia-cuda-cccl==13.3.3.4.1",
161
- "nvidia-cuda-runtime==13.3.29",
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.2.78",
361
- "nvidia-cuda-crt==13.2.78",
362
- "nvidia-cuda-cccl==13.2.75",
363
- "nvidia-cuda-runtime==13.2.86",
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 (nothing to build, nothing to
2508
- # wait for) and repoints the app onto it.
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
- print(json.dumps(response, indent=2, sort_keys=True))
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
- answer = (
4792
- input(f"Disable app '{app}' in environment '{environment}'? This stops its runtime. [y/N] ")
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