From 586640c89e31e93442ba5070b61355e7700a82b7 Mon Sep 17 00:00:00 2001 From: Julien Barbot Date: Thu, 13 Aug 2026 11:04:00 +0000 Subject: [PATCH 1/3] Add mas-ctl as a script This allows to have it installed when installed via 'uv tool install ./lab' Signed-off-by: Julien Barbot --- lab/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/lab/pyproject.toml b/lab/pyproject.toml index 47280fd9..f4ca0140 100644 --- a/lab/pyproject.toml +++ b/lab/pyproject.toml @@ -45,6 +45,7 @@ Issues = "https://github.com/outshift-open/mas-lab/issues" [project.scripts] mas-lab = "mas.lab.cli:app" +mas-ctl = "mas.ctl.cli.main:main" [tool.hatch.build.targets.wheel] packages = ["src/mas"] From 33eee6c17ac5a87a2d0dab57b9ac4dc754d19fdb Mon Sep 17 00:00:00 2001 From: Julien Barbot Date: Thu, 13 Aug 2026 13:20:28 +0200 Subject: [PATCH 2/3] Add missing stdlib dep for mas-ctl Signed-off-by: Julien Barbot --- ctl/pyproject.toml | 2 ++ library-standard/pyproject.toml | 3 +++ uv.lock | 40 +++++++++++++++++---------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/ctl/pyproject.toml b/ctl/pyproject.toml index 295099e1..0d7702fa 100644 --- a/ctl/pyproject.toml +++ b/ctl/pyproject.toml @@ -13,6 +13,7 @@ requires-python = ">=3.11" license = { text = "Apache-2.0" } dependencies = [ "mas-runtime", + "mas-library-standard", "click>=8.0", "pyyaml>=6.0", "python-dotenv>=1.0", @@ -46,3 +47,4 @@ exclude = ["src/mas/ctl/testing/**"] [tool.uv.sources] mas-runtime = { workspace = true } +mas-library-standard = { workspace = true } diff --git a/library-standard/pyproject.toml b/library-standard/pyproject.toml index d30b6a5f..34a1672e 100644 --- a/library-standard/pyproject.toml +++ b/library-standard/pyproject.toml @@ -35,5 +35,8 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/mas"] +[tool.hatch.build.targets.wheel.force-include] +"library.yaml" = "src/mas/library/standard/library.yaml" + [tool.uv.sources] mas-runtime = { workspace = true } diff --git a/uv.lock b/uv.lock index 5558df96..acee5e9a 100644 --- a/uv.lock +++ b/uv.lock @@ -3614,6 +3614,7 @@ dependencies = [ { name = "click" }, { name = "httpx" }, { name = "jsonschema" }, + { name = "mas-library-standard" }, { name = "mas-runtime" }, { name = "python-dotenv" }, { name = "pyyaml" }, @@ -3635,6 +3636,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27" }, { name = "import-linter", marker = "extra == 'dev'", specifier = ">=2.0" }, { name = "jsonschema", specifier = ">=4.0" }, + { name = "mas-library-standard", editable = "library-standard" }, { name = "mas-runtime", editable = "runtime" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "python-dotenv", specifier = ">=1.0" }, @@ -5012,7 +5014,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -6438,7 +6440,7 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -6523,7 +6525,7 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [ @@ -6574,8 +6576,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -6806,13 +6808,13 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform == 'emscripten'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'emscripten'" }, - { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, - { name = "packaging", marker = "sys_platform == 'emscripten'" }, - { name = "pandas", marker = "sys_platform == 'emscripten'" }, - { name = "patsy", marker = "sys_platform == 'emscripten'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform == 'emscripten'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/3b/963a015dd8ea17e10c7b0e2f14d7c4daec903baf60a017e756b57953a4bf/statsmodels-0.14.4.tar.gz", hash = "sha256:5d69e0f39060dc72c067f9bb6e8033b6dccdb0bae101d76a7ef0bcc94e898b67", size = 20354802, upload-time = "2024-10-03T16:15:36.273Z" } @@ -6833,13 +6835,13 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'emscripten'" }, - { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform != 'emscripten'" }, - { name = "packaging", marker = "sys_platform != 'emscripten'" }, - { name = "pandas", marker = "sys_platform != 'emscripten'" }, - { name = "patsy", marker = "sys_platform != 'emscripten'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'emscripten'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform != 'emscripten'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } wheels = [ From b7d604453c64ed668887b36ad03073e7fa6729d7 Mon Sep 17 00:00:00 2001 From: Julien Barbot Date: Fri, 14 Aug 2026 09:53:49 +0200 Subject: [PATCH 3/3] Fix schema/template paths for wheel installs mas-ctl and mas-lab found schemas and examples by walking up to repo checkout. Breaks when installed via 'uv tool install' (no checkout around). Now package those files into the wheels and fall back to them when no checkout is found. Signed-off-by: Julien Barbot --- ctl/pyproject.toml | 7 ++++++ ctl/src/mas/ctl/overlay/merge.py | 18 ++++++------- ctl/src/mas/ctl/validate/schemas.py | 25 +++++++++++++++++-- lab/components/core/pyproject.toml | 7 ++++++ .../core/src/mas/lab/schemas/paths.py | 20 ++++++++++++--- lab/pyproject.toml | 7 ++++++ lab/src/mas/lab/cli/commands/init.py | 16 ++++++++++-- 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/ctl/pyproject.toml b/ctl/pyproject.toml index 0d7702fa..0c205815 100644 --- a/ctl/pyproject.toml +++ b/ctl/pyproject.toml @@ -45,6 +45,13 @@ packages = ["src/mas"] # are excluded from the published wheel. exclude = ["src/mas/ctl/testing/**"] +# Package a copy of the canonical runtime/lab manifest schemas inside mas.ctl so +# overlay merging and manifest validation still work when installed as a wheel +# (e.g. via 'uv tool install'), where there is no repo checkout to read +# docs/schemas/ from. See mas.ctl.validate.schemas.schema_root(). +[tool.hatch.build.targets.wheel.force-include] +"../docs/schemas" = "src/mas/ctl/_schemas" + [tool.uv.sources] mas-runtime = { workspace = true } mas-library-standard = { workspace = true } diff --git a/ctl/src/mas/ctl/overlay/merge.py b/ctl/src/mas/ctl/overlay/merge.py index a23c7421..80f8d7bd 100644 --- a/ctl/src/mas/ctl/overlay/merge.py +++ b/ctl/src/mas/ctl/overlay/merge.py @@ -7,11 +7,12 @@ from functools import lru_cache import logging from copy import deepcopy -from pathlib import Path from typing import Any import yaml +from mas.ctl.validate.schemas import schema_root + logger = logging.getLogger(__name__) @@ -21,10 +22,10 @@ # single edit in a single file (docs/schemas/runtime/.schema.yaml), # not a second hand-maintained overlay-*-patch fragment kept in sync by hand. _OVERLAY_PATCH_SCHEMA_FILES: dict[str, str] = { - "Agent": "docs/schemas/runtime/agent.schema.yaml", - "MAS": "docs/schemas/runtime/mas.schema.yaml", - "Flavour": "docs/schemas/runtime/flavour.schema.yaml", - "Infra": "docs/schemas/runtime/infra.schema.yaml", + "Agent": "runtime/agent.schema.yaml", + "MAS": "runtime/mas.schema.yaml", + "Flavour": "runtime/flavour.schema.yaml", + "Infra": "runtime/infra.schema.yaml", } # Where to start reading "properties" from, per schema file, relative to that @@ -38,14 +39,9 @@ } -def _repo_root() -> Path: - # .../mas-lab/ctl/src/mas/ctl/overlay/merge.py -> parents[5] == repo root - return Path(__file__).resolve().parents[5] - - @lru_cache(maxsize=8) def _load_yaml_schema(rel_path: str) -> dict[str, Any]: - schema_path = _repo_root() / rel_path + schema_path = schema_root() / rel_path data = yaml.safe_load(schema_path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} diff --git a/ctl/src/mas/ctl/validate/schemas.py b/ctl/src/mas/ctl/validate/schemas.py index 68b1b437..5b83528a 100644 --- a/ctl/src/mas/ctl/validate/schemas.py +++ b/ctl/src/mas/ctl/validate/schemas.py @@ -1,16 +1,37 @@ # Copyright (c) 2026 Cisco Systems, Inc. and its affiliates # SPDX-License-Identifier: Apache-2.0 -"""Manifest JSON Schema loader — schemas live under docs/schemas/.""" +"""Manifest JSON Schema loader — schemas live under docs/schemas/. + +Two layouts are supported so this works both in a source checkout (dev / +editable install, where ``docs/schemas`` sits a few parents above this file) +and when installed as a built wheel (e.g. via ``uv tool install``, where +there is no repo root — the schemas are instead packaged inside ``mas.ctl`` +itself via ``force-include`` and resolved through ``importlib.resources``). +""" from __future__ import annotations from functools import lru_cache +from importlib import resources from pathlib import Path from typing import Any import yaml -_SCHEMA_ROOT = Path(__file__).resolve().parents[5] / "docs" / "schemas" + +def _repo_checkout_schema_root() -> Path | None: + # .../mas-lab/ctl/src/mas/ctl/validate/schemas.py -> parents[5] == repo root + candidate = Path(__file__).resolve().parents[5] / "docs" / "schemas" + return candidate if candidate.is_dir() else None + + +def _packaged_schema_root() -> Path: + # Falls back to the copy of docs/schemas packaged inside mas.ctl (see + # [tool.hatch.build.targets.wheel.force-include] in ctl/pyproject.toml). + return Path(str(resources.files("mas.ctl") / "_schemas")) + + +_SCHEMA_ROOT = _repo_checkout_schema_root() or _packaged_schema_root() _KIND_MAP: dict[str, str] = { "agent": "runtime/agent.schema.yaml", diff --git a/lab/components/core/pyproject.toml b/lab/components/core/pyproject.toml index 51be7961..1ca27582 100644 --- a/lab/components/core/pyproject.toml +++ b/lab/components/core/pyproject.toml @@ -19,5 +19,12 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = ["src/mas"] +# Package a copy of the lab manifest schemas inside mas.lab.schemas so +# mas-lab-core still works when installed as a wheel (e.g. via +# 'uv tool install'), where there is no repo checkout to read +# docs/schemas/lab from. See mas.lab.schemas.paths.lab_schema_dir(). +[tool.hatch.build.targets.wheel.force-include] +"../../../docs/schemas/lab" = "src/mas/lab/schemas/_schemas" + [project.entry-points."mas.lab.controller.plugins"] local-inproc = "mas.lab.controllers.local_inproc:LocalInprocController" diff --git a/lab/components/core/src/mas/lab/schemas/paths.py b/lab/components/core/src/mas/lab/schemas/paths.py index 4cda07eb..ec9935f1 100644 --- a/lab/components/core/src/mas/lab/schemas/paths.py +++ b/lab/components/core/src/mas/lab/schemas/paths.py @@ -3,6 +3,7 @@ """Resolve manifest schema files from installed package dependencies.""" from __future__ import annotations +from importlib import resources from pathlib import Path @@ -13,14 +14,25 @@ def runtime_schema_dir() -> Path: return _SCHEMA_ROOT / "runtime" -def _repo_root() -> Path: - """Repository root (contains docs/schemas/).""" - return Path(__file__).resolve().parents[7] +def _repo_checkout_root() -> Path | None: + """Repository root (contains docs/schemas/), if running from a checkout.""" + candidate = Path(__file__).resolve().parents[7] + return candidate if (candidate / "docs" / "schemas").is_dir() else None + + +def _packaged_lab_schema_dir() -> Path: + # Falls back to the copy of docs/schemas/lab packaged inside + # mas.lab.schemas (see [tool.hatch.build.targets.wheel.force-include] in + # lab/components/core/pyproject.toml). + return Path(str(resources.files("mas.lab.schemas") / "_schemas")) def lab_schema_dir() -> Path: """YAML/JSON schemas for mas-lab manifests (experiment, pipeline, dataset, lab-config).""" - return _repo_root() / "docs" / "schemas" / "lab" + repo_root = _repo_checkout_root() + if repo_root is not None: + return repo_root / "docs" / "schemas" / "lab" + return _packaged_lab_schema_dir() def lab_artefact_schema_dir() -> Path: diff --git a/lab/pyproject.toml b/lab/pyproject.toml index f4ca0140..a33b3498 100644 --- a/lab/pyproject.toml +++ b/lab/pyproject.toml @@ -49,3 +49,10 @@ mas-ctl = "mas.ctl.cli.main:main" [tool.hatch.build.targets.wheel] packages = ["src/mas"] + +# Package a copy of the top-level examples/ tree inside mas.lab so +# 'mas-lab init' still works when installed as a wheel (e.g. via +# 'uv tool install'), where there is no repo checkout to read examples/ +# from. See mas.lab.cli.commands.init._templates_root(). +[tool.hatch.build.targets.wheel.force-include] +"../examples" = "src/mas/lab/_examples" diff --git a/lab/src/mas/lab/cli/commands/init.py b/lab/src/mas/lab/cli/commands/init.py index c8a69562..721e8c2e 100644 --- a/lab/src/mas/lab/cli/commands/init.py +++ b/lab/src/mas/lab/cli/commands/init.py @@ -5,6 +5,7 @@ from __future__ import annotations import re +from importlib import resources from pathlib import Path from typing import Any @@ -21,9 +22,20 @@ _DEFAULT_TARGET_MODEL = "gpt-4o-mini" -def _templates_root() -> Path: +def _repo_checkout_examples_root() -> Path | None: # lab/src/mas/lab/cli/commands/init.py -> repo root/examples - return Path(__file__).resolve().parents[6] / "examples" + candidate = Path(__file__).resolve().parents[6] / "examples" + return candidate if candidate.is_dir() else None + + +def _packaged_examples_root() -> Path: + # Falls back to the copy of examples/ packaged inside mas.lab (see + # [tool.hatch.build.targets.wheel.force-include] in lab/pyproject.toml). + return Path(str(resources.files("mas.lab") / "_examples")) + + +def _templates_root() -> Path: + return _repo_checkout_examples_root() or _packaged_examples_root() def _load_template(rel_path: str) -> str: