maestro-install 0.1.6__py3-none-any.whl

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.
@@ -0,0 +1,10 @@
1
+ """maestro-install — bootstrap a CARE workspace from a single `uvx` command.
2
+
3
+ The package ships the existing top-level `wizard.py`, `prepare.py`,
4
+ `services.py`, `repos.yaml`, and `bin/` tree (re-rooted under
5
+ `care_install/` at build time — see `[tool.hatch.build.targets.wheel.force-include]`
6
+ in pyproject.toml). The `maestro-install` console script entry point lives in
7
+ `care_install.wizard:main`.
8
+ """
9
+
10
+ __version__ = "0.1.1"
care_install/bin/c-dev ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # c-dev: dispatcher for the CARE workspace docker stacks.
3
+ # Usage:
4
+ # c-dev <service> <action> [extra args forwarded to docker compose]
5
+ # service: all | mem | plat
6
+ # action: up | down | ps | logs | restart
7
+ #
8
+ # Environment:
9
+ # CARE_WORKSPACE prepared workspace dir
10
+ # (default: ~/Development/care-workspace)
11
+ #
12
+ # The wrapper scripts in the same directory (c-up, c-mem-logs, ...)
13
+ # all delegate here.
14
+
15
+ set -euo pipefail
16
+
17
+ # Resolve the real script dir, following any symlinks the user installed
18
+ # under ~/.local/bin or similar.
19
+ src="${BASH_SOURCE[0]}"
20
+ while [ -L "$src" ]; do
21
+ dir="$(cd -P "$(dirname "$src")" >/dev/null && pwd)"
22
+ src="$(readlink "$src")"
23
+ [[ $src != /* ]] && src="$dir/$src"
24
+ done
25
+ SCRIPT_DIR="$(cd -P "$(dirname "$src")" >/dev/null && pwd)"
26
+ REPO_DIR="$(dirname "$SCRIPT_DIR")"
27
+
28
+ WORKSPACE="${CARE_WORKSPACE:-$HOME/Development/care-workspace}"
29
+
30
+ usage() {
31
+ cat >&2 <<'EOF'
32
+ usage: c-dev <service> <action> [args...]
33
+ service: all | mem | plat
34
+ action: up | down | ps | logs | restart
35
+
36
+ env:
37
+ CARE_WORKSPACE path to the prepared workspace
38
+ (default: ~/Development/care-workspace)
39
+ EOF
40
+ exit 2
41
+ }
42
+
43
+ [ $# -ge 2 ] || usage
44
+ service="$1"
45
+ action="$2"
46
+ shift 2
47
+
48
+ case "$service" in
49
+ all) only=() ;;
50
+ mem) only=(--only gigaevo-memory) ;;
51
+ plat) only=(--only gigaevo-platform) ;;
52
+ *) echo "c-dev: unknown service '$service'" >&2; usage ;;
53
+ esac
54
+
55
+ case "$action" in
56
+ up|down|ps|logs|restart) ;;
57
+ *) echo "c-dev: unknown action '$action'" >&2; usage ;;
58
+ esac
59
+
60
+ exec uv run "$REPO_DIR/services.py" "$WORKSPACE" "$action" "${only[@]}" "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" all down "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" all logs "$@"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # c-mage: print MAGE workspace status.
3
+ # MAGE (carl-mage) is an in-process Python library, not a docker stack,
4
+ # so there are no up/down/logs commands for it — CARE imports it directly.
5
+ # Use this script to jump-check the repo state.
6
+
7
+ set -euo pipefail
8
+
9
+ WORKSPACE="${CARE_WORKSPACE:-$HOME/Development/care-workspace}"
10
+ MAGE_DIR="$WORKSPACE/carl-mage"
11
+
12
+ if [ ! -d "$MAGE_DIR" ]; then
13
+ echo "carl-mage not found at $MAGE_DIR" >&2
14
+ echo "Run \`make prepare DIR=$WORKSPACE\` first." >&2
15
+ exit 1
16
+ fi
17
+
18
+ echo "MAGE: $MAGE_DIR"
19
+ echo "Note: MAGE runs in-process inside CARE. No docker stack."
20
+ echo
21
+ git -C "$MAGE_DIR" status --short --branch
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" mem down "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" mem logs "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" mem ps "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" mem restart "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" mem up "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" plat down "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" plat logs "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" plat ps "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" plat restart "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" plat up "$@"
care_install/bin/c-ps ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" all ps "$@"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" all restart "$@"
care_install/bin/c-up ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ exec "$(dirname "$0")/c-dev" all up "$@"
care_install/bin/c-ws ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # c-ws: print the resolved CARE workspace path.
3
+ # Useful for `cd "$(c-ws)"` from any directory.
4
+ echo "${CARE_WORKSPACE:-$HOME/Development/care-workspace}"
care_install/bin/care ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+ # `care` system shim — installed by care-install/wizard.py.
3
+ #
4
+ # Reads ~/.config/care-install/state to find out:
5
+ # CARE_WORKSPACE workspace dir (holds .env)
6
+ # CARE_MODE "uvx" or "local"
7
+ # CARE_LOCAL_PATH local clone of the care repo (only when MODE=local)
8
+ #
9
+ # Then it cd's into the workspace so `care`'s dotenv loader picks up
10
+ # `<workspace>/.env`, and dispatches to either `uvx maestro-care` or
11
+ # `uv run --project <care-clone> care`.
12
+
13
+ set -euo pipefail
14
+
15
+ STATE_FILE="${CARE_INSTALL_STATE:-$HOME/.config/care-install/state}"
16
+
17
+ if [[ ! -f "$STATE_FILE" ]]; then
18
+ echo "care: $STATE_FILE not found — run the wizard:" >&2
19
+ echo " uv run /path/to/care-install/wizard.py" >&2
20
+ exit 1
21
+ fi
22
+
23
+ # shellcheck disable=SC1090
24
+ source "$STATE_FILE"
25
+
26
+ : "${CARE_WORKSPACE:?CARE_WORKSPACE missing from $STATE_FILE}"
27
+ : "${CARE_MODE:?CARE_MODE missing from $STATE_FILE}"
28
+
29
+ cd "$CARE_WORKSPACE"
30
+
31
+ case "$CARE_MODE" in
32
+ uvx)
33
+ exec uvx --from maestro-care care "$@"
34
+ ;;
35
+ local)
36
+ : "${CARE_LOCAL_PATH:?CARE_LOCAL_PATH missing from $STATE_FILE}"
37
+ exec uv run --project "$CARE_LOCAL_PATH" care "$@"
38
+ ;;
39
+ *)
40
+ echo "care: unknown CARE_MODE='$CARE_MODE' in $STATE_FILE" >&2
41
+ exit 1
42
+ ;;
43
+ esac
@@ -0,0 +1,161 @@
1
+ """Build a slim variant of gigaevo-platform's runner-api image.
2
+
3
+ The upstream runner Dockerfile pulls ~2 GB of ML deps (lightautoml, catboost,
4
+ bert_score, evaluate, statsmodels) and their transitive CUDA wheels. The
5
+ chain-experiment path that CARE drives only needs ROUGE-L scoring +
6
+ mmar-carl + httpx, so the heavy classical-ML / NLP-eval deps add 15+ minutes
7
+ to a cold build that CARE-driven workflows never pay back.
8
+
9
+ This script generates ``runner_api/Dockerfile.slim`` next to the upstream
10
+ Dockerfile (without touching the upstream file) and invokes
11
+ ``docker build --file Dockerfile.slim``. It mirrors
12
+ ``scripts/build_runner_image.sh`` so the rest of the pre_compose pipeline
13
+ (generate_runner_pool_compose.py, RUNNER_IMAGE_NAME wiring) is unchanged.
14
+
15
+ Invoked from ``care-install/repos.yaml`` pre_compose. Slim is the default
16
+ build; ``CARE_RUNNER_VARIANT=full`` (wizard.py's ``--full`` flag) opts back
17
+ into the heavy upstream build instead.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import os
24
+ import re
25
+ import subprocess
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ # Deps the chain-experiment path actually uses. Anything else from the
30
+ # upstream pip-install block is dropped in slim mode.
31
+ SLIM_KEEP = {"gigaevo-memory", "scikit-learn", "rouge_score", "mmar-carl", "httpx"}
32
+
33
+ # Deps to skip entirely (upstream lists them on continuation lines).
34
+ SLIM_DROP = {"lightautoml", "catboost", "evaluate", "bert_score"}
35
+
36
+
37
+ def _patch_dockerfile(src: Path) -> str:
38
+ """Return upstream Dockerfile text with the heavy pip install stripped.
39
+
40
+ The replacement keeps the same RUN structure (single layer) so
41
+ BuildKit reuses earlier layers unchanged. We rewrite only the
42
+ ``uv pip install ... lightautoml catboost ...`` block and drop the
43
+ follow-up ``statsmodels`` install.
44
+ """
45
+ text = src.read_text(encoding="utf-8")
46
+
47
+ # Replace the heavy multi-package install + statsmodels follow-up
48
+ # + pip-check as a single span so the resulting RUN ends cleanly
49
+ # (no orphan trailing ``\`` that would silently merge with the
50
+ # next RUN). Anchor: the second ``uv pip install`` line (the one
51
+ # that lists lightautoml/catboost/…) through the ``pip … check ||
52
+ # true`` line. We don't touch the first ``uv pip install`` block
53
+ # that installs the local gigaevo-core editable package.
54
+ pattern = re.compile(
55
+ r"uv pip install --python /opt/gigaevo-core/\.venv/bin/python \\\s*\n"
56
+ r"(?:[ \t]+[^\n]*\n)+?"
57
+ r"[ \t]+\"httpx>=0\.25\.0\";[ \t]*\\?\s*\n"
58
+ r"(?:[ \t]*PY=/opt/gigaevo-core/\.venv/bin/python;[ \t]*\\\s*\n"
59
+ r"[ \t]*uv pip install --python \"\$PY\" \"statsmodels==[\d.]+\";[ \t]*\\\s*\n"
60
+ r"[ \t]*\"\$PY\" -m pip --disable-pip-version-check check \|\| true\s*\n)?",
61
+ re.MULTILINE,
62
+ )
63
+ slim_block = (
64
+ "uv pip install --python /opt/gigaevo-core/.venv/bin/python \\\n"
65
+ " gigaevo-memory \\\n"
66
+ " scikit-learn \\\n"
67
+ " rouge_score \\\n"
68
+ " \"mmar-carl>=0.1.0\" \\\n"
69
+ " \"httpx>=0.25.0\"\n"
70
+ )
71
+ new_text, n = pattern.subn(slim_block, text, count=1)
72
+ if n != 1:
73
+ raise RuntimeError(
74
+ "build_runner_slim: couldn't locate upstream pip-install block "
75
+ "in Dockerfile — upstream layout changed. Inspect "
76
+ f"{src} and update the regex."
77
+ )
78
+
79
+ banner = (
80
+ "# Auto-generated by care-install/care_install/build_runner_slim.py.\n"
81
+ "# Slim variant: drops lightautoml, catboost, bert_score, evaluate,\n"
82
+ "# statsmodels (and their CUDA wheel closure). Do not edit by hand —\n"
83
+ "# re-run `wizard.py --action up` (slim is the default).\n"
84
+ )
85
+ return banner + new_text
86
+
87
+
88
+ def _source_env_file(env_file: Path) -> dict[str, str]:
89
+ """Parse the .env file the upstream build script sources. We only
90
+ need RUNNER_IMAGE_NAME and the optional GIGAEVO_CORE_* knobs."""
91
+ env: dict[str, str] = {}
92
+ if not env_file.exists():
93
+ return env
94
+ for raw in env_file.read_text(encoding="utf-8").splitlines():
95
+ line = raw.strip()
96
+ if not line or line.startswith("#") or "=" not in line:
97
+ continue
98
+ key, _, value = line.partition("=")
99
+ value = value.strip().strip('"').strip("'")
100
+ env[key.strip()] = value
101
+ return env
102
+
103
+
104
+ def main(argv: list[str] | None = None) -> int:
105
+ parser = argparse.ArgumentParser(description=__doc__)
106
+ parser.add_argument(
107
+ "--platform-root",
108
+ type=Path,
109
+ required=True,
110
+ help="Path to the gigaevo-platform repo in the workspace.",
111
+ )
112
+ parser.add_argument(
113
+ "--mode",
114
+ choices=("dev", "prod"),
115
+ default="dev",
116
+ help="Build target (dev/prod), matches upstream build_runner_image.sh.",
117
+ )
118
+ args = parser.parse_args(argv)
119
+
120
+ platform_root = args.platform_root.resolve()
121
+ src = platform_root / "runner_api" / "Dockerfile"
122
+ if not src.is_file():
123
+ print(f"build_runner_slim: missing {src}", file=sys.stderr)
124
+ return 2
125
+
126
+ slim_path = src.parent / "Dockerfile.slim"
127
+ slim_path.write_text(_patch_dockerfile(src), encoding="utf-8")
128
+ print(f"[build-runner-slim] wrote {slim_path}")
129
+
130
+ env = _source_env_file(platform_root / ".env")
131
+ image_name = env.get("RUNNER_IMAGE_NAME") or os.environ.get("RUNNER_IMAGE_NAME")
132
+ if not image_name:
133
+ # Mirror upstream's fallback so wizard-driven flows still work
134
+ # before the .env has been seeded with COMPOSE_PROJECT_NAME.
135
+ project = env.get("COMPOSE_PROJECT_NAME") or "gigaevo-platform"
136
+ image_name = f"{project}-runner-api"
137
+
138
+ docker_args = [
139
+ "docker", "build",
140
+ "--file", str(slim_path),
141
+ "--target", args.mode,
142
+ "--tag", image_name,
143
+ "--build-arg", f"GIGAEVO_CORE_REF={env.get('GIGAEVO_CORE_REF', 'main')}",
144
+ "--build-arg",
145
+ "GIGAEVO_CORE_REPO_URL=" + env.get(
146
+ "GIGAEVO_CORE_REPO_URL",
147
+ "https://github.com/FusionBrainLab/gigaevo-core",
148
+ ),
149
+ str(platform_root),
150
+ ]
151
+ if os.environ.get("GITHUB_PAT"):
152
+ docker_args[2:2] = ["--secret", "id=github_pat,env=GITHUB_PAT"]
153
+
154
+ print(f"[build-runner-slim] building {image_name} (mode={args.mode})")
155
+ env_for_build = {**os.environ, "DOCKER_BUILDKIT": "1"}
156
+ result = subprocess.run(docker_args, env=env_for_build, check=False)
157
+ return result.returncode
158
+
159
+
160
+ if __name__ == "__main__":
161
+ raise SystemExit(main())
care_install/i18n.py ADDED
@@ -0,0 +1,94 @@
1
+ """Key-based UI localization for the maestro-install wizard (Russian default).
2
+
3
+ Mirrors the approach used in the CARE TUI (``care/runtime/i18n.py``): strings
4
+ live in per-language JSON catalogs under ``locales/`` and are looked up by a
5
+ dotted key — the same shape as JS i18n libraries::
6
+
7
+ from i18n import t
8
+ print(t("banner.title"))
9
+ print(t("action.detected", path=workspace))
10
+
11
+ The catalog is nested JSON; keys address it with dots
12
+ (``"service.howProvisioned"``). Values may carry ``str.format`` placeholders
13
+ filled from keyword args. Lookup order: active language → English fallback →
14
+ the key itself (so a missing translation is visible, not a crash).
15
+
16
+ Works both as a standalone script import (``import i18n`` — the wizard runs via
17
+ ``uv run --script`` with its own directory on ``sys.path``) and as part of the
18
+ installed package (``from . import i18n`` → ``care_install.i18n``). Catalogs
19
+ are read by file path relative to *this* module, so no package-resource magic
20
+ is needed in either layout.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ _LOCALES_DIR = Path(__file__).resolve().parent / "locales"
31
+
32
+ #: Languages we ship. The first is the default; the last is the fallback the
33
+ #: lookup walks to when a key is missing in the active catalog.
34
+ SUPPORTED: tuple[str, ...] = ("ru", "en")
35
+ _FALLBACK_LANGUAGE = "en"
36
+
37
+ # Process-global active language. Defaults to Russian; set once near wizard
38
+ # start via :func:`set_language`. Read on every :func:`t` call.
39
+ _language = "ru"
40
+
41
+
42
+ def set_language(language: str | None) -> None:
43
+ """Set the active language. Unknown / ``None`` falls back to Russian."""
44
+ global _language
45
+ lang = str(language or "").strip().lower()
46
+ _language = lang if lang in SUPPORTED else "ru"
47
+
48
+
49
+ def get_language() -> str:
50
+ """Return the active language code (``"ru"`` or ``"en"``)."""
51
+ return _language
52
+
53
+
54
+ def _flatten(node: dict[str, Any], prefix: str = "") -> dict[str, str]:
55
+ """Flatten a nested catalog into ``{"a.b.c": "text"}`` pairs."""
56
+ flat: dict[str, str] = {}
57
+ for key, value in node.items():
58
+ dotted = f"{prefix}.{key}" if prefix else key
59
+ if isinstance(value, dict):
60
+ flat.update(_flatten(value, dotted))
61
+ else:
62
+ flat[dotted] = value
63
+ return flat
64
+
65
+
66
+ @lru_cache(maxsize=None)
67
+ def _catalog(language: str) -> dict[str, str]:
68
+ """Load + flatten a language catalog (cached for the process lifetime)."""
69
+ path = _LOCALES_DIR / f"{language}.json"
70
+ try:
71
+ raw = path.read_text(encoding="utf-8")
72
+ except OSError:
73
+ return {}
74
+ return _flatten(json.loads(raw))
75
+
76
+
77
+ def t(key: str, /, **params: Any) -> str:
78
+ """Translate *key* into the active language.
79
+
80
+ Falls back to the English catalog when the active language lacks the key,
81
+ then to the raw key so a typo is visible rather than silent. ``params``
82
+ fill ``str.format`` placeholders; a missing arg leaves the template intact.
83
+ """
84
+ text = _catalog(_language).get(key)
85
+ if text is None and _language != _FALLBACK_LANGUAGE:
86
+ text = _catalog(_FALLBACK_LANGUAGE).get(key)
87
+ if text is None:
88
+ text = key
89
+ if params:
90
+ try:
91
+ return text.format(**params)
92
+ except (KeyError, IndexError, ValueError):
93
+ return text
94
+ return text