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.
Files changed (33) hide show
  1. {urun_cli-0.6.1 → urun_cli-0.6.2}/.gitignore +3 -0
  2. {urun_cli-0.6.1 → urun_cli-0.6.2}/PKG-INFO +10 -10
  3. {urun_cli-0.6.1 → urun_cli-0.6.2}/README.md +9 -9
  4. {urun_cli-0.6.1 → urun_cli-0.6.2}/pyproject.toml +1 -1
  5. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/catalog.py +99 -3
  6. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/cli.py +173 -182
  7. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/client_tokens.py +34 -20
  8. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/dev.py +22 -10
  9. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/frontend_launch.py +68 -71
  10. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/launch_auth.py +10 -13
  11. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/scratch.py +48 -6
  12. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/serve.py +49 -8
  13. {urun_cli-0.6.1 → urun_cli-0.6.2}/CHANGELOG.md +0 -0
  14. {urun_cli-0.6.1 → urun_cli-0.6.2}/LICENSE +0 -0
  15. {urun_cli-0.6.1 → urun_cli-0.6.2}/SECURITY.md +0 -0
  16. {urun_cli-0.6.1 → urun_cli-0.6.2}/hatch_build.py +0 -0
  17. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/__init__.py +0 -0
  18. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/_bin/README.md +0 -0
  19. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/api.py +0 -0
  20. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/buildlogs.py +0 -0
  21. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/config.py +0 -0
  22. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/deps.py +0 -0
  23. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/devconsole.py +0 -0
  24. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/devstream.py +0 -0
  25. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/discovery.py +0 -0
  26. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/errors.py +0 -0
  27. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/fleet.py +0 -0
  28. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/manifest.py +0 -0
  29. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/nodes.py +0 -0
  30. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/openai_proxy_bin.py +0 -0
  31. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/repo_deps.py +0 -0
  32. {urun_cli-0.6.1 → urun_cli-0.6.2}/src/urun/retry.py +0 -0
  33. {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.1
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
- ### Launch an official browser demo
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 demo prompt-canary --examples-root ~/workspace/urun-examples
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 demo` uses your stored API key server-side to request a short-lived,
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 demo test-matrix-game --frontend-dir matrix-game-3/frontend
132
+ urun dev --attach test-matrix-game --frontend-dir matrix-game-3/frontend
133
133
  ```
134
134
 
135
- #### There is no curated list of demoable examples
135
+ #### There is no curated list of attachable examples
136
136
 
137
- `urun demo` will launch **any** `<example>/frontend` it can match, and it does
138
- not know which examples actually work. Earlier releases read a manifest that
139
- carried a `supported` flag with a reason, and refused the examples marked
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
- ### Launch an official browser demo
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 demo prompt-canary --examples-root ~/workspace/urun-examples
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 demo` uses your stored API key server-side to request a short-lived,
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 demo test-matrix-game --frontend-dir matrix-game-3/frontend
110
+ urun dev --attach test-matrix-game --frontend-dir matrix-game-3/frontend
111
111
  ```
112
112
 
113
- #### There is no curated list of demoable examples
113
+ #### There is no curated list of attachable examples
114
114
 
115
- `urun demo` will launch **any** `<example>/frontend` it can match, and it does
116
- not know which examples actually work. Earlier releases read a manifest that
117
- carried a `supported` flag with a reason, and refused the examples marked
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.1"
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(engine: str | None, gpu_spec: str) -> str:
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 ENGINE's pinned torch (from :data:`ENGINE_DEPS`). When 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
- minor = _torch_minor(pinned_torch_version(serve_deps_for_engine(engine)))
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