urun-cli 0.6.1__tar.gz → 0.6.2__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.1 → urun_cli-0.6.2}/.gitignore +3 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/PKG-INFO +10 -10
- {urun_cli-0.6.1 → urun_cli-0.6.2}/README.md +9 -9
- {urun_cli-0.6.1 → urun_cli-0.6.2}/pyproject.toml +1 -1
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/catalog.py +99 -3
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/cli.py +173 -182
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/client_tokens.py +34 -20
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/dev.py +22 -10
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/frontend_launch.py +68 -71
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/launch_auth.py +10 -13
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/scratch.py +48 -6
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/serve.py +49 -8
- {urun_cli-0.6.1 → urun_cli-0.6.2}/CHANGELOG.md +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/LICENSE +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/SECURITY.md +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/hatch_build.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/__init__.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/_bin/README.md +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/api.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/buildlogs.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/config.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/deps.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/devconsole.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/devstream.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/discovery.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/errors.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/fleet.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/manifest.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/nodes.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/openai_proxy_bin.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/repo_deps.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/retry.py +0 -0
- {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/runtime_health.py +0 -0
|
@@ -21,3 +21,6 @@ build/
|
|
|
21
21
|
# Vendored urun-openai proxy binary (scripts/vendor_openai_proxy.py puts it here
|
|
22
22
|
# at release-build time; hatch `artifacts` ships it in the wheel despite this ignore)
|
|
23
23
|
src/urun/_bin/urun-openai
|
|
24
|
+
|
|
25
|
+
# Agent tooling (Serena MCP) — never ships with the CLI.
|
|
26
|
+
.serena/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urun-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -98,14 +98,14 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
98
98
|
`deploy` remains available as the lower-level command while the full
|
|
99
99
|
deploy/run/monitor workflow is being built.
|
|
100
100
|
|
|
101
|
-
###
|
|
101
|
+
### Attach an official browser frontend
|
|
102
102
|
|
|
103
103
|
For official `urun-examples` apps, the shortest browser stream path is:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
cd ~/workspace/urun-examples
|
|
107
107
|
urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
|
|
108
|
-
urun
|
|
108
|
+
urun dev --attach prompt-canary --examples-root ~/workspace/urun-examples
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
The positional argument is the **deployed app slug** (as shown by `urun app
|
|
@@ -118,7 +118,7 @@ list`). Nothing is declared in a manifest — every value is discovered:
|
|
|
118
118
|
| local port | the `--port N` in that package's `scripts.dev`, else 3000 |
|
|
119
119
|
| function | read from the deployed app itself |
|
|
120
120
|
|
|
121
|
-
`urun
|
|
121
|
+
`urun dev --attach` uses your resolved API key server-side to request a short-lived,
|
|
122
122
|
function-scoped, origin-pinned browser JWT, sets the frontend's
|
|
123
123
|
`NEXT_PUBLIC_*` environment variables, starts the local Next.js frontend, and
|
|
124
124
|
opens it in your browser. The API key itself never reaches the page.
|
|
@@ -129,15 +129,15 @@ the right directory yourself with `--frontend-dir` (relative to the examples
|
|
|
129
129
|
root):
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
urun
|
|
132
|
+
urun dev --attach test-matrix-game --frontend-dir matrix-game-3/frontend
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
#### There is no curated list of
|
|
135
|
+
#### There is no curated list of attachable examples
|
|
136
136
|
|
|
137
|
-
`urun
|
|
138
|
-
not know which examples actually work. Earlier releases read a
|
|
139
|
-
carried a `supported` flag with a reason, and refused the
|
|
140
|
-
unsupported; that allowlist is gone along with the manifest.
|
|
137
|
+
`urun dev --attach` will launch **any** `<example>/frontend` it can match, and
|
|
138
|
+
it does not know which examples actually work. Earlier releases read a
|
|
139
|
+
manifest that carried a `supported` flag with a reason, and refused the
|
|
140
|
+
examples marked unsupported; that allowlist is gone along with the manifest.
|
|
141
141
|
|
|
142
142
|
The practical consequence: a frontend that is a stub, half-built, or known
|
|
143
143
|
broken now starts a dev server and opens a browser instead of failing up
|
|
@@ -76,14 +76,14 @@ In this release, `urun run` uses the same deploy pipeline as `urun deploy`.
|
|
|
76
76
|
`deploy` remains available as the lower-level command while the full
|
|
77
77
|
deploy/run/monitor workflow is being built.
|
|
78
78
|
|
|
79
|
-
###
|
|
79
|
+
### Attach an official browser frontend
|
|
80
80
|
|
|
81
81
|
For official `urun-examples` apps, the shortest browser stream path is:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
84
|
cd ~/workspace/urun-examples
|
|
85
85
|
urun deploy prompt_canary/backend/app.py --name prompt-canary --timeout 900
|
|
86
|
-
urun
|
|
86
|
+
urun dev --attach prompt-canary --examples-root ~/workspace/urun-examples
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
The positional argument is the **deployed app slug** (as shown by `urun app
|
|
@@ -96,7 +96,7 @@ list`). Nothing is declared in a manifest — every value is discovered:
|
|
|
96
96
|
| local port | the `--port N` in that package's `scripts.dev`, else 3000 |
|
|
97
97
|
| function | read from the deployed app itself |
|
|
98
98
|
|
|
99
|
-
`urun
|
|
99
|
+
`urun dev --attach` uses your resolved API key server-side to request a short-lived,
|
|
100
100
|
function-scoped, origin-pinned browser JWT, sets the frontend's
|
|
101
101
|
`NEXT_PUBLIC_*` environment variables, starts the local Next.js frontend, and
|
|
102
102
|
opens it in your browser. The API key itself never reaches the page.
|
|
@@ -107,15 +107,15 @@ the right directory yourself with `--frontend-dir` (relative to the examples
|
|
|
107
107
|
root):
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
urun
|
|
110
|
+
urun dev --attach test-matrix-game --frontend-dir matrix-game-3/frontend
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
#### There is no curated list of
|
|
113
|
+
#### There is no curated list of attachable examples
|
|
114
114
|
|
|
115
|
-
`urun
|
|
116
|
-
not know which examples actually work. Earlier releases read a
|
|
117
|
-
carried a `supported` flag with a reason, and refused the
|
|
118
|
-
unsupported; that allowlist is gone along with the manifest.
|
|
115
|
+
`urun dev --attach` will launch **any** `<example>/frontend` it can match, and
|
|
116
|
+
it does not know which examples actually work. Earlier releases read a
|
|
117
|
+
manifest that carried a `supported` flag with a reason, and refused the
|
|
118
|
+
examples marked unsupported; that allowlist is gone along with the manifest.
|
|
119
119
|
|
|
120
120
|
The practical consequence: a frontend that is a stub, half-built, or known
|
|
121
121
|
broken now starts a dev server and opens a browser instead of failing up
|
|
@@ -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 for the three post-v0.6.0 fixes.
|
|
23
|
-
version = "0.6.
|
|
23
|
+
version = "0.6.2"
|
|
24
24
|
description = "End-user CLI for deploying apps to urun"
|
|
25
25
|
readme = "README.md"
|
|
26
26
|
requires-python = ">=3.11"
|
|
@@ -673,13 +673,21 @@ def _published_cuda_for_torch(minor: tuple[int, int]) -> tuple[int, ...] | None:
|
|
|
673
673
|
return None
|
|
674
674
|
|
|
675
675
|
|
|
676
|
-
def torch_backend_for_serve(
|
|
676
|
+
def torch_backend_for_serve(
|
|
677
|
+
engine: str | None,
|
|
678
|
+
gpu_spec: str,
|
|
679
|
+
*,
|
|
680
|
+
engine_deps: list[str] | None = None,
|
|
681
|
+
) -> str:
|
|
677
682
|
"""The ``deps.torch_backend`` value to bake for a catalog serve.
|
|
678
683
|
|
|
679
684
|
ARCH-ANCHORED mirror of urun-python ``resolve_torch_backend`` (KEEP IN
|
|
680
685
|
SYNC): the GPU-arch map value (:func:`torch_backend_for_gpu`) encodes the
|
|
681
686
|
runtime base image's CUDA ABI and is kept byte-identical whenever it
|
|
682
|
-
publishes the
|
|
687
|
+
publishes the pinned torch. ``engine_deps`` (a MODEL-level override) is
|
|
688
|
+
the pin SOURCE when provided — the SAME list the shim's deps literal and
|
|
689
|
+
``URUN_SERVE_DEPS`` carry, so one derivation anchors the backend; None
|
|
690
|
+
falls back to the ENGINE's group (from :data:`ENGINE_DEPS`). When the
|
|
683
691
|
arch lane provably cannot publish the pin, repair to a lane that does AND
|
|
684
692
|
meets the GPU's kernel floor — preferring the ABI-compatible direction:
|
|
685
693
|
DOWN (newest below the anchor) for the pre-cxx11 torch<=2.6 era, the
|
|
@@ -715,7 +723,8 @@ def torch_backend_for_serve(engine: str | None, gpu_spec: str) -> str:
|
|
|
715
723
|
"verdict. Deploy a plain @app.function on gh200 instead."
|
|
716
724
|
)
|
|
717
725
|
arch_backend = torch_backend_for_gpu(gpu_spec)
|
|
718
|
-
|
|
726
|
+
dep_list = engine_deps if engine_deps is not None else serve_deps_for_engine(engine)
|
|
727
|
+
minor = _torch_minor(pinned_torch_version(dep_list))
|
|
719
728
|
if minor is None:
|
|
720
729
|
return arch_backend
|
|
721
730
|
published = _published_cuda_for_torch(minor)
|
|
@@ -738,6 +747,24 @@ def torch_backend_for_serve(engine: str | None, gpu_spec: str) -> str:
|
|
|
738
747
|
return f"cu{chosen}"
|
|
739
748
|
|
|
740
749
|
|
|
750
|
+
def serve_deps_for_row(row: CatalogRow) -> list[str]:
|
|
751
|
+
"""The python dependency list for THIS row's serve venv.
|
|
752
|
+
|
|
753
|
+
MODEL-level selection: an explicit ``row.engine_deps`` is the COMPLETE
|
|
754
|
+
list (requirement strings — including pinned wheel URLs — pass through
|
|
755
|
+
unchanged); ``None`` falls back to the ENGINE-level source of truth
|
|
756
|
+
(:func:`serve_deps_for_engine`, ``strict=True`` — an engine the map does
|
|
757
|
+
not know fails the deploy, never a silently empty venv).
|
|
758
|
+
"""
|
|
759
|
+
# The engine itself is validated strictly on BOTH paths: an unknown engine
|
|
760
|
+
# with an override would otherwise reach shim generation and the
|
|
761
|
+
# function-engines sidecar, moving a clear pre-deploy failure into the pod.
|
|
762
|
+
default = serve_deps_for_engine(row.engine, strict=True)
|
|
763
|
+
if row.engine_deps is not None:
|
|
764
|
+
return list(row.engine_deps)
|
|
765
|
+
return default
|
|
766
|
+
|
|
767
|
+
|
|
741
768
|
@dataclass(frozen=True)
|
|
742
769
|
class CatalogRow:
|
|
743
770
|
"""One flattened ``(model_id, variant, gpu_spec)`` catalog placement.
|
|
@@ -767,6 +794,10 @@ class CatalogRow:
|
|
|
767
794
|
default_gpu: str | None = None
|
|
768
795
|
min_vram_gb: int | None = None
|
|
769
796
|
min_host_ram_gb: int | None = None
|
|
797
|
+
# MODEL-level python dependency override (additive — older catalogs predate
|
|
798
|
+
# it): null = the ENGINE-level dep group, verbatim; an explicit list is the
|
|
799
|
+
# COMPLETE replacement (never merged with the engine's group).
|
|
800
|
+
engine_deps: list[str] | None = None
|
|
770
801
|
|
|
771
802
|
@property
|
|
772
803
|
def ref(self) -> str:
|
|
@@ -793,6 +824,40 @@ class CatalogRow:
|
|
|
793
824
|
return self.gpu_count == 1 and not self.disagg
|
|
794
825
|
|
|
795
826
|
|
|
827
|
+
# PEP 508 requirement SHAPE (stdlib-only; this module deliberately carries no
|
|
828
|
+
# `packaging` dependency): ``name[extras] (@ url | version clauses)? (; marker)?``
|
|
829
|
+
# with a real name and a version after every operator — enough to refuse the
|
|
830
|
+
# garbage a hand-typed catalog row can carry (``vllm===``, ``==``, ``torch 2``)
|
|
831
|
+
# at the gate instead of minutes later inside the venv build.
|
|
832
|
+
_REQ_NAME = r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?"
|
|
833
|
+
_REQ_VERSION_CLAUSE = r"(?:===|==|!=|<=|>=|~=|<|>)\s*[A-Za-z0-9][A-Za-z0-9.*+!_-]*"
|
|
834
|
+
_REQUIREMENT_SHAPE_RE = re.compile(
|
|
835
|
+
rf"^{_REQ_NAME}\s*(?:\[[A-Za-z0-9._,\s-]*\])?\s*"
|
|
836
|
+
rf"(?:@\s*\S+|{_REQ_VERSION_CLAUSE}(?:\s*,\s*{_REQ_VERSION_CLAUSE})*)?"
|
|
837
|
+
rf"\s*(?:;.+)?$"
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _is_requirement_shaped(dep: str) -> bool:
|
|
842
|
+
"""Whether ``dep`` has the shape of a PEP 508 requirement string."""
|
|
843
|
+
return _REQUIREMENT_SHAPE_RE.match(dep.strip()) is not None
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _requirement_url_carries_credentials(dep: str) -> bool:
|
|
847
|
+
"""Whether a direct-URL requirement (``name @ url``) embeds userinfo.
|
|
848
|
+
|
|
849
|
+
The catalog is public reference data (anon-readable) and the resolved list
|
|
850
|
+
is echoed into deploy artifacts, so a ``https://token@host/...`` URL would
|
|
851
|
+
publish a credential. Private indexes go through a secret-managed auth
|
|
852
|
+
path, never through the row.
|
|
853
|
+
"""
|
|
854
|
+
_name, sep, url = dep.partition("@")
|
|
855
|
+
if not sep:
|
|
856
|
+
return False
|
|
857
|
+
parts = urllib.parse.urlsplit(url.strip())
|
|
858
|
+
return bool(parts.username or parts.password)
|
|
859
|
+
|
|
860
|
+
|
|
796
861
|
def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
797
862
|
"""Build a :class:`CatalogRow` from one PostgREST JSON object.
|
|
798
863
|
|
|
@@ -828,6 +893,36 @@ def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
|
828
893
|
value = raw.get(key)
|
|
829
894
|
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
830
895
|
|
|
896
|
+
# Additive MODEL-level dep override: ABSENT (older catalogs) and SQL NULL
|
|
897
|
+
# both mean "the engine's own dep group". An explicit value REPLACES that
|
|
898
|
+
# group verbatim, so anything malformed fails LOUDLY here — never a silent
|
|
899
|
+
# fall-back to the default (old) engine stack.
|
|
900
|
+
engine_deps = raw.get("engine_deps")
|
|
901
|
+
if engine_deps is not None:
|
|
902
|
+
if (
|
|
903
|
+
not isinstance(engine_deps, list)
|
|
904
|
+
or not engine_deps
|
|
905
|
+
or not all(isinstance(dep, str) and dep.strip() for dep in engine_deps)
|
|
906
|
+
):
|
|
907
|
+
raise UrunError(
|
|
908
|
+
"catalog row 'engine_deps' must be null or a non-empty list of "
|
|
909
|
+
f"non-blank requirement strings, got {engine_deps!r}"
|
|
910
|
+
)
|
|
911
|
+
bad = [dep for dep in engine_deps if not _is_requirement_shaped(dep)]
|
|
912
|
+
if bad:
|
|
913
|
+
raise UrunError(
|
|
914
|
+
"catalog row 'engine_deps' entries must be PEP 508 requirement "
|
|
915
|
+
f"strings (name[extras] @ url | version clauses ; marker), got {bad!r}"
|
|
916
|
+
)
|
|
917
|
+
for dep in engine_deps:
|
|
918
|
+
if _requirement_url_carries_credentials(dep):
|
|
919
|
+
# Name only: never echo the credential-bearing URL.
|
|
920
|
+
raise UrunError(
|
|
921
|
+
"catalog row 'engine_deps' entry for "
|
|
922
|
+
f"{dep.partition('@')[0].strip()!r} embeds credentials in its URL; "
|
|
923
|
+
"the catalog is public reference data — use a secret-managed index"
|
|
924
|
+
)
|
|
925
|
+
|
|
831
926
|
return CatalogRow(
|
|
832
927
|
model_id=req_str("model_id"),
|
|
833
928
|
variant=req_str("variant"),
|
|
@@ -845,6 +940,7 @@ def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
|
845
940
|
default_gpu=default_gpu,
|
|
846
941
|
min_vram_gb=opt_int("min_vram_gb"),
|
|
847
942
|
min_host_ram_gb=opt_int("min_host_ram_gb"),
|
|
943
|
+
engine_deps=list(engine_deps) if engine_deps is not None else None,
|
|
848
944
|
)
|
|
849
945
|
|
|
850
946
|
|