urun-cli 0.7.2__tar.gz → 0.7.3__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 (34) hide show
  1. {urun_cli-0.7.2 → urun_cli-0.7.3}/CHANGELOG.md +11 -0
  2. {urun_cli-0.7.2 → urun_cli-0.7.3}/PKG-INFO +1 -1
  3. {urun_cli-0.7.2 → urun_cli-0.7.3}/pyproject.toml +1 -1
  4. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/catalog.py +2 -2
  5. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/scratch.py +66 -3
  6. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/serve.py +86 -5
  7. {urun_cli-0.7.2 → urun_cli-0.7.3}/.gitignore +0 -0
  8. {urun_cli-0.7.2 → urun_cli-0.7.3}/LICENSE +0 -0
  9. {urun_cli-0.7.2 → urun_cli-0.7.3}/README.md +0 -0
  10. {urun_cli-0.7.2 → urun_cli-0.7.3}/SECURITY.md +0 -0
  11. {urun_cli-0.7.2 → urun_cli-0.7.3}/hatch_build.py +0 -0
  12. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/__init__.py +0 -0
  13. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/_bin/README.md +0 -0
  14. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/api.py +0 -0
  15. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/buildlogs.py +0 -0
  16. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/cli.py +0 -0
  17. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/client_tokens.py +0 -0
  18. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/config.py +0 -0
  19. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/deps.py +0 -0
  20. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/dev.py +0 -0
  21. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/devconsole.py +0 -0
  22. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/devstream.py +0 -0
  23. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/discovery.py +0 -0
  24. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/errors.py +0 -0
  25. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/fleet.py +0 -0
  26. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/frontend_launch.py +0 -0
  27. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/launch_auth.py +0 -0
  28. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/manifest.py +0 -0
  29. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/nodes.py +0 -0
  30. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/openai_proxy_bin.py +0 -0
  31. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/repo_deps.py +0 -0
  32. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/retry.py +0 -0
  33. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/runtime_health.py +0 -0
  34. {urun_cli-0.7.2 → urun_cli-0.7.3}/src/urun/rust.py +0 -0
@@ -1,3 +1,14 @@
1
+ ## 0.7.3
2
+
3
+ - `urun serve --urun-wheel <version>` — pin a deploy to a specific urun wheel
4
+ version (e.g. `2.0.0+1497.g8439ef6b3b77`). The pin renders
5
+ `URUN_WHEEL_VERSION` into the generated serve shim beside the `--warm` pin,
6
+ BEFORE the bundled-app import, and is content-bearing: it changes the
7
+ manifest hash, so a wheel roll is a NEW RELEASE (the shared reconciler's
8
+ wheel-roll path) — never a re-register of an immutable release, which the
9
+ control plane's deploy-register refuses with a 409. Omitted = byte-identical
10
+ shim (no pin line).
11
+
1
12
  ## 0.7.2
2
13
 
3
14
  - Fix the generated serve shim: the `--warm N` pin assigns `URUN_SERVE_WARM`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urun-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
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
@@ -20,7 +20,7 @@ name = "urun-cli"
20
20
  # so every merge remains on the already-published version until this value is
21
21
  # advanced. Wiring semantic-release into CI remains separate work; this patch
22
22
  # bump is the minimal release unblock (post-v0.6.3 fixes + the Rust feature minor).
23
- version = "0.7.2"
23
+ version = "0.7.3"
24
24
  description = "End-user CLI for deploying apps to urun"
25
25
  readme = "README.md"
26
26
  requires-python = ">=3.11"
@@ -384,7 +384,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
384
384
  "pillow>=10.0.0",
385
385
  "openimageio",
386
386
  "cloudpickle>=3.1",
387
- "huggingface_hub[hf_transfer]>=0.30.0,<1.0.0",
387
+ "huggingface_hub>=1.5.0,<2.0",
388
388
  "sentencepiece",
389
389
  ],
390
390
  "t2av": [
@@ -435,7 +435,7 @@ ENGINE_DEPS: dict[str, list[str]] = {
435
435
  "pillow>=10.0.0",
436
436
  "openimageio",
437
437
  "cloudpickle>=3.1",
438
- "huggingface_hub[hf_transfer]>=0.30.0,<1.0.0",
438
+ "huggingface_hub>=1.5.0,<2.0",
439
439
  "sentencepiece",
440
440
  "nvidia-cuda-nvcc==13.0.88",
441
441
  "nvidia-cuda-crt==13.0.88",
@@ -326,14 +326,33 @@ def capacity_verdict(
326
326
  # ---------------------------------------------------------------------------
327
327
 
328
328
 
329
- def _scratch_labels(name: str, source_name: str) -> dict[str, str]:
330
- return {
329
+ #: THE WEIGHTS-TIER IDENTITY a clone MUST keep. The vineyard fleet controller
330
+ #: (control-plane ``vineyard_fleet.py``) spawns the per-(node, org) vineyardd
331
+ #: for pods matching ``urun.sh/gpu-type,urun.sh/org-id`` and reads the org
332
+ #: from ``urun.sh/org-id-verbatim``; the runtime's armed boot contract then
333
+ #: waits 120 s for that daemon's socket and fails LOUD without it. A clone
334
+ #: that dropped these booted only by luck - when it landed on a node where
335
+ #: its org's daemon was already up for a real pod (dev-usw2 2026-09-10: the
336
+ #: scratch scheduled onto the node hosting another org's daemon and died 7x
337
+ #: on "vineyard daemon socket did not appear"). These label the weights
338
+ #: tier the pod needs, not the materializer / capacity / routing attachment
339
+ #: the clone is detached from - those stay stripped.
340
+ WEIGHTS_TIER_LABELS = ("urun.sh/gpu-type", "urun.sh/org-id", "urun.sh/org-id-verbatim")
341
+
342
+
343
+ def _scratch_labels(name: str, source_name: str, source_labels: dict[str, str]) -> dict[str, str]:
344
+ labels = {
331
345
  "app.kubernetes.io/component": SCRATCH_COMPONENT,
332
346
  "app.kubernetes.io/managed-by": SCRATCH_MANAGED_BY,
333
347
  "app.kubernetes.io/name": name,
334
348
  SCRATCH_LABEL: "true",
335
349
  SCRATCH_SOURCE_LABEL: source_name,
336
350
  }
351
+ for key in WEIGHTS_TIER_LABELS:
352
+ value = source_labels.get(key)
353
+ if value:
354
+ labels[key] = value
355
+ return labels
337
356
 
338
357
 
339
358
  def _clean_annotations(annotations: dict[str, str]) -> dict[str, str]:
@@ -529,6 +548,25 @@ def _invalidate_wheel_metadata() -> None:
529
548
  )
530
549
 
531
550
 
551
+ def _pip_accepts_break_system_packages() -> bool:
552
+ """Does the copied venv's OWN pip understand ``--break-system-packages``?
553
+
554
+ The option (and PEP 668 enforcement itself) landed in pip 23.0: on pip
555
+ 22.x the flag is an unknown option and ``python -m pip install`` exits
556
+ BEFORE resolving ("no such option"), breaking previously-working
557
+ overlays. ``derive_scratch_sts`` neither constrains nor upgrades the
558
+ source venv's pip, so the flag is passed ONLY when this interpreter's
559
+ pip supports it — where it is needed, it is pip's own PEP 668 opt-out.
560
+ """
561
+ try:
562
+ import pip
563
+
564
+ major, minor = (int(part) for part in str(pip.__version__).split(".")[:2])
565
+ except (ImportError, ValueError, TypeError):
566
+ return False
567
+ return (major, minor) >= (23, 0)
568
+
569
+
532
570
  def _install_and_resolve() -> dict:
533
571
  """Install the candidate ONCE and read the EXACT artifact from pip's report.
534
572
 
@@ -553,6 +591,12 @@ def _install_and_resolve() -> dict:
553
591
  "install",
554
592
  "--no-deps",
555
593
  "--force-reinstall",
594
+ # The copied deps venv is a uv-managed interpreter carrying the
595
+ # PEP 668 EXTERNALLY-MANAGED marker; the overlay is a private
596
+ # per-pod copy, so pip's own opt-out applies (without it: rc=1,
597
+ # no readiness). Only offered to pips that HAVE the option —
598
+ # pip 22.x rejects it before resolving ("no such option").
599
+ *(("--break-system-packages",) if _pip_accepts_break_system_packages() else ()),
556
600
  "--disable-pip-version-check",
557
601
  "--report",
558
602
  str(REPORT),
@@ -825,7 +869,26 @@ def derive_scratch_sts(
825
869
  )
826
870
 
827
871
  expires_at = (now.astimezone(UTC) + timedelta(hours=ttl_hours)).isoformat(timespec="seconds")
828
- labels = _scratch_labels(name, source_name)
872
+ source_pod_labels = dict(
873
+ (sts.get("spec", {}).get("template", {}).get("metadata", {}) or {}).get("labels") or {}
874
+ )
875
+ # Fail-closed BEFORE any create: the org identity is what the vineyard
876
+ # fleet controller routes the per-(node, org) daemon by (it reads the
877
+ # org from ``urun.sh/org-id-verbatim``) and the runtime's armed boot
878
+ # contract fails loud 120 s in without its socket. ``gpu-type`` stays
879
+ # optional (a CPU source carries none and needs no vineyard daemon);
880
+ # a template missing the org pair is NOT a cloneable source — a clone
881
+ # that silently omits it boots only by luck, then dies.
882
+ missing_identity = [
883
+ key for key in (ORG_ID_LABEL, "urun.sh/org-id-verbatim") if not source_pod_labels.get(key)
884
+ ]
885
+ if missing_identity:
886
+ raise UrunError(
887
+ "the source pod template is missing required org identity "
888
+ "label(s) — the clone would not be routed to its org's "
889
+ "vineyard daemon: " + ", ".join(missing_identity)
890
+ )
891
+ labels = _scratch_labels(name, source_name, source_pod_labels)
829
892
 
830
893
  meta = sts["metadata"]
831
894
  for key in _SERVER_METADATA_KEYS:
@@ -63,6 +63,22 @@ CATALOG_URL_ENV = "URUN_CATALOG_URL"
63
63
  ANON_KEY_ENVS = ("URUN_CATALOG_ANON_KEY", "URUN_SUPABASE_ANON_KEY")
64
64
 
65
65
 
66
+ def _urun_wheel_version_type(value: str) -> str:
67
+ """argparse type for ``--urun-wheel``: a non-empty string, no whitespace.
68
+
69
+ The wheel version is embedded verbatim in the generated shim (and compared
70
+ by the control plane against the recorded release pin), so anything blank
71
+ or whitespace-carrying is a typo, not a version. Raises argparse's
72
+ ArgumentTypeError so `urun serve --urun-wheel ""` fails at parse time with
73
+ a usable message.
74
+ """
75
+ if not value or value.split() != [value]:
76
+ raise argparse.ArgumentTypeError(
77
+ f"--urun-wheel must be a non-empty version string with no whitespace, got {value!r}"
78
+ )
79
+ return value
80
+
81
+
66
82
  def add_serve_parser(sub: argparse._SubParsersAction) -> None:
67
83
  serve_parser = sub.add_parser(
68
84
  "serve",
@@ -133,6 +149,20 @@ def _add_serve_model_args(parser: argparse.ArgumentParser) -> None:
133
149
  "deploys pass the catalog row's warm (default 0)."
134
150
  ),
135
151
  )
152
+ parser.add_argument(
153
+ "--urun-wheel",
154
+ dest="urun_wheel",
155
+ type=_urun_wheel_version_type,
156
+ default=None,
157
+ metavar="VERSION",
158
+ help=(
159
+ "Pin THIS deploy to a specific urun wheel version (e.g. "
160
+ "2.0.0+1497.g8439ef6b3b77). Content-bearing: it changes the deploy's "
161
+ "manifest hash, so a wheel roll is a NEW RELEASE, not a rebuild. "
162
+ "Shared-endpoint deploys pass the platform's current wheel version. "
163
+ "Omit = the platform's default wheel resolution."
164
+ ),
165
+ )
136
166
  parser.add_argument(
137
167
  "--entrypoint",
138
168
  help=(
@@ -316,6 +346,23 @@ def _validated_exo_pin(args: argparse.Namespace) -> tuple[str | None, str | None
316
346
  return exo_class, exo_image
317
347
 
318
348
 
349
+ def _validated_urun_wheel(value: str | None) -> str | None:
350
+ """Re-validate the ``--urun-wheel`` pin for Namespace-built call paths.
351
+
352
+ argparse already refuses empty/whitespace values (``_urun_wheel_version_type``);
353
+ a Namespace built in code (tests, future callers) skips argparse, so the pin
354
+ is checked again here with the SAME rule. The wheel version is CONTENT in
355
+ the generated shim — a blank value is a config error the deployer must see,
356
+ never a silently unpinned manifest that fails to change the release hash.
357
+ """
358
+ if value is None:
359
+ return None
360
+ try:
361
+ return _urun_wheel_version_type(value)
362
+ except argparse.ArgumentTypeError as exc:
363
+ raise UrunError(str(exc)) from None
364
+
365
+
319
366
  def _build_deploy_args(args: argparse.Namespace, row: CatalogRow) -> argparse.Namespace:
320
367
  """Prepare the deploy() Namespace + project root for the resolved row.
321
368
 
@@ -330,9 +377,13 @@ def _build_deploy_args(args: argparse.Namespace, row: CatalogRow) -> argparse.Na
330
377
  ``urun.serve.app:app``. When ``--warm`` is given, the idle replica floor rides
331
378
  the same channel (``URUN_SERVE_WARM``, JSON int) so the bundled app's
332
379
  ``@app.function(warm=...)`` declares it at import; an omitted ``--warm``
333
- renders nothing (byte-identical shim, legacy always-on NULL). Those land
334
- scoped to THIS deploy artifact (NOT a shared ``URUN_SERVE_CONFIG``
335
- materializer env, the retired coupling bug). The bundled app resolves engine
380
+ renders nothing (byte-identical shim, legacy always-on NULL). When
381
+ ``--urun-wheel`` is given, the wheel version rides ``URUN_WHEEL_VERSION`` the
382
+ same way — content-bearing, so a wheel roll produces a new manifest hash (a
383
+ NEW release, never a re-register of an immutable one); an omitted
384
+ ``--urun-wheel`` renders nothing. Those land scoped to THIS deploy artifact
385
+ (NOT a shared ``URUN_SERVE_CONFIG`` materializer env, the retired coupling
386
+ bug). The bundled app resolves engine
336
387
  ARGS by-string at boot; the engine NAME is pinned here only to select the
337
388
  build-time dependency group (vllm / llama-cpp-python / ...) so the deps venv
338
389
  carries the engine the adapter imports at load.
@@ -351,6 +402,16 @@ def _build_deploy_args(args: argparse.Namespace, row: CatalogRow) -> argparse.Na
351
402
  "--warm only applies to the bundled serve app (it is pinned in the "
352
403
  "generated shim); it cannot be combined with --entrypoint."
353
404
  )
405
+ # --urun-wheel re-validation (argparse already refuses empty/whitespace):
406
+ # a Namespace built in code skips argparse, so the pin is checked again
407
+ # here. The wheel version rides the shim env, so --entrypoint (the user's
408
+ # own app) + --urun-wheel cannot combine either.
409
+ urun_wheel = _validated_urun_wheel(getattr(args, "urun_wheel", None))
410
+ if urun_wheel is not None and getattr(args, "entrypoint", None):
411
+ raise UrunError(
412
+ "--urun-wheel only applies to the bundled serve app (it is pinned in "
413
+ "the generated shim); it cannot be combined with --entrypoint."
414
+ )
354
415
 
355
416
  entrypoint = getattr(args, "entrypoint", None)
356
417
  if entrypoint:
@@ -363,7 +424,7 @@ def _build_deploy_args(args: argparse.Namespace, row: CatalogRow) -> argparse.Na
363
424
  shim_root = None
364
425
  else:
365
426
  project_root, rel_entrypoint = _write_serve_shim(
366
- row, exo_class=exo_class, exo_image=exo_image, warm=warm
427
+ row, exo_class=exo_class, exo_image=exo_image, warm=warm, urun_wheel=urun_wheel
367
428
  )
368
429
  # PLATFORM RELEASE SCOPE (catalog prebuilt instant serve). The shim is
369
430
  # deterministic given the resolved row + gpu, so its manifest hash is an
@@ -429,13 +490,20 @@ def _write_serve_shim(
429
490
  exo_class: str | None = None,
430
491
  exo_image: str | None = None,
431
492
  warm: int | None = None,
493
+ urun_wheel: str | None = None,
432
494
  ) -> tuple[Path, str]:
433
495
  """Write the bundled-app shim ``app.py`` pinned to ``row`` into a temp root."""
434
496
  project_root = Path(tempfile.mkdtemp(prefix="urun-serve-"))
435
497
  try:
436
498
  app_path = project_root / "app.py"
437
499
  app_path.write_text(
438
- render_serve_shim_py(row, exo_class=exo_class, exo_image=exo_image, warm=warm),
500
+ render_serve_shim_py(
501
+ row,
502
+ exo_class=exo_class,
503
+ exo_image=exo_image,
504
+ warm=warm,
505
+ urun_wheel=urun_wheel,
506
+ ),
439
507
  encoding="utf-8",
440
508
  )
441
509
  # Function-engine map for the launcher's PRE-IMPORT topology decision
@@ -554,6 +622,7 @@ def render_serve_shim_py(
554
622
  exo_class: str | None = None,
555
623
  exo_image: str | None = None,
556
624
  warm: int | None = None,
625
+ urun_wheel: str | None = None,
557
626
  ) -> str:
558
627
  """Render the tiny shim that deploys the BUNDLED urun-python serve app.
559
628
 
@@ -575,6 +644,11 @@ def render_serve_shim_py(
575
644
  the bundled app's ``@app.function(warm=...)`` declares the idle replica
576
645
  floor (app_functions.warm_replicas) at import. Unset renders NOTHING —
577
646
  byte-identical shim, legacy always-on.
647
+ * When ``urun_wheel`` is not None, ``URUN_WHEEL_VERSION`` is set before the
648
+ import so the platform's wheel resolver pins THAT urun wheel. The pin is
649
+ CONTENT-BEARING: it changes the shim (hence the manifest hash), so a
650
+ wheel roll is a NEW RELEASE, never a re-register of the immutable one.
651
+ Unset renders NOTHING — byte-identical shim.
578
652
 
579
653
  Values are embedded as JSON (NOT interpolated Python) so a weird model string,
580
654
  gpu spec, or engine name can never break the module. The catalog row's
@@ -641,6 +715,7 @@ os.environ["URUN_SERVE_GPUS"] = %(gpus)s
641
715
  os.environ["URUN_SERVE_ENGINE"] = %(engine)s
642
716
  %(exo_env)s
643
717
  %(warm_env)s
718
+ %(urun_wheel_env)s
644
719
  %(serve_deps_env)s
645
720
 
646
721
  from urun.serve.app import app # noqa: E402 (bundled urun-python serve app)
@@ -667,6 +742,11 @@ __all__ = ["app"]
667
742
  if warm is not None
668
743
  else ""
669
744
  )
745
+ urun_wheel_env = (
746
+ 'os.environ["URUN_WHEEL_VERSION"] = ' + json.dumps(urun_wheel) + "\n"
747
+ if urun_wheel is not None
748
+ else ""
749
+ )
670
750
  return template % {
671
751
  "entrypoint": BUNDLED_SERVE_ENTRYPOINT,
672
752
  "model": model,
@@ -676,6 +756,7 @@ __all__ = ["app"]
676
756
  "serve_deps_env": serve_deps_env,
677
757
  "exo_env": exo_env,
678
758
  "warm_env": warm_env,
759
+ "urun_wheel_env": urun_wheel_env,
679
760
  }
680
761
 
681
762
 
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