urun-cli 0.6.0__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.0 → urun_cli-0.6.2}/.gitignore +3 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/PKG-INFO +21 -10
- {urun_cli-0.6.0 → urun_cli-0.6.2}/README.md +20 -9
- {urun_cli-0.6.0 → urun_cli-0.6.2}/pyproject.toml +5 -12
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/api.py +17 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/catalog.py +114 -15
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/cli.py +266 -191
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/client_tokens.py +34 -20
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/dev.py +22 -10
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/frontend_launch.py +76 -71
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/launch_auth.py +10 -13
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/scratch.py +48 -6
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/serve.py +49 -8
- {urun_cli-0.6.0 → urun_cli-0.6.2}/CHANGELOG.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/LICENSE +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/SECURITY.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/hatch_build.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/__init__.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/_bin/README.md +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/buildlogs.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/config.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/deps.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/devconsole.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/devstream.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/discovery.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/errors.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/fleet.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/manifest.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/nodes.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/openai_proxy_bin.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/repo_deps.py +0 -0
- {urun_cli-0.6.0 → urun_cli-0.6.2}/src/urun/retry.py +0 -0
- {urun_cli-0.6.0 → 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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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.2"
|
|
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",
|
|
@@ -670,13 +673,21 @@ def _published_cuda_for_torch(minor: tuple[int, int]) -> tuple[int, ...] | None:
|
|
|
670
673
|
return None
|
|
671
674
|
|
|
672
675
|
|
|
673
|
-
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:
|
|
674
682
|
"""The ``deps.torch_backend`` value to bake for a catalog serve.
|
|
675
683
|
|
|
676
684
|
ARCH-ANCHORED mirror of urun-python ``resolve_torch_backend`` (KEEP IN
|
|
677
685
|
SYNC): the GPU-arch map value (:func:`torch_backend_for_gpu`) encodes the
|
|
678
686
|
runtime base image's CUDA ABI and is kept byte-identical whenever it
|
|
679
|
-
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
|
|
680
691
|
arch lane provably cannot publish the pin, repair to a lane that does AND
|
|
681
692
|
meets the GPU's kernel floor — preferring the ABI-compatible direction:
|
|
682
693
|
DOWN (newest below the anchor) for the pre-cxx11 torch<=2.6 era, the
|
|
@@ -712,7 +723,8 @@ def torch_backend_for_serve(engine: str | None, gpu_spec: str) -> str:
|
|
|
712
723
|
"verdict. Deploy a plain @app.function on gh200 instead."
|
|
713
724
|
)
|
|
714
725
|
arch_backend = torch_backend_for_gpu(gpu_spec)
|
|
715
|
-
|
|
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))
|
|
716
728
|
if minor is None:
|
|
717
729
|
return arch_backend
|
|
718
730
|
published = _published_cuda_for_torch(minor)
|
|
@@ -735,6 +747,24 @@ def torch_backend_for_serve(engine: str | None, gpu_spec: str) -> str:
|
|
|
735
747
|
return f"cu{chosen}"
|
|
736
748
|
|
|
737
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
|
+
|
|
738
768
|
@dataclass(frozen=True)
|
|
739
769
|
class CatalogRow:
|
|
740
770
|
"""One flattened ``(model_id, variant, gpu_spec)`` catalog placement.
|
|
@@ -764,6 +794,10 @@ class CatalogRow:
|
|
|
764
794
|
default_gpu: str | None = None
|
|
765
795
|
min_vram_gb: int | None = None
|
|
766
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
|
|
767
801
|
|
|
768
802
|
@property
|
|
769
803
|
def ref(self) -> str:
|
|
@@ -790,6 +824,40 @@ class CatalogRow:
|
|
|
790
824
|
return self.gpu_count == 1 and not self.disagg
|
|
791
825
|
|
|
792
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
|
+
|
|
793
861
|
def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
794
862
|
"""Build a :class:`CatalogRow` from one PostgREST JSON object.
|
|
795
863
|
|
|
@@ -825,6 +893,36 @@ def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
|
825
893
|
value = raw.get(key)
|
|
826
894
|
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
827
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
|
+
|
|
828
926
|
return CatalogRow(
|
|
829
927
|
model_id=req_str("model_id"),
|
|
830
928
|
variant=req_str("variant"),
|
|
@@ -842,6 +940,7 @@ def parse_row(raw: dict[str, Any]) -> CatalogRow:
|
|
|
842
940
|
default_gpu=default_gpu,
|
|
843
941
|
min_vram_gb=opt_int("min_vram_gb"),
|
|
844
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,
|
|
845
944
|
)
|
|
846
945
|
|
|
847
946
|
|