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.
- care_install/__init__.py +10 -0
- care_install/bin/c-dev +60 -0
- care_install/bin/c-down +2 -0
- care_install/bin/c-logs +2 -0
- care_install/bin/c-mage +21 -0
- care_install/bin/c-mem-down +2 -0
- care_install/bin/c-mem-logs +2 -0
- care_install/bin/c-mem-ps +2 -0
- care_install/bin/c-mem-restart +2 -0
- care_install/bin/c-mem-up +2 -0
- care_install/bin/c-plat-down +2 -0
- care_install/bin/c-plat-logs +2 -0
- care_install/bin/c-plat-ps +2 -0
- care_install/bin/c-plat-restart +2 -0
- care_install/bin/c-plat-up +2 -0
- care_install/bin/c-ps +2 -0
- care_install/bin/c-restart +2 -0
- care_install/bin/c-up +2 -0
- care_install/bin/c-ws +4 -0
- care_install/bin/care +43 -0
- care_install/build_runner_slim.py +161 -0
- care_install/i18n.py +94 -0
- care_install/locales/en.json +212 -0
- care_install/locales/ru.json +212 -0
- care_install/prepare.py +207 -0
- care_install/repos.yaml +143 -0
- care_install/services.py +550 -0
- care_install/wizard.py +2191 -0
- maestro_install-0.1.6.dist-info/METADATA +271 -0
- maestro_install-0.1.6.dist-info/RECORD +32 -0
- maestro_install-0.1.6.dist-info/WHEEL +4 -0
- maestro_install-0.1.6.dist-info/entry_points.txt +2 -0
care_install/__init__.py
ADDED
|
@@ -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[@]}" "$@"
|
care_install/bin/c-down
ADDED
care_install/bin/c-logs
ADDED
care_install/bin/c-mage
ADDED
|
@@ -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
|
care_install/bin/c-ps
ADDED
care_install/bin/c-up
ADDED
care_install/bin/c-ws
ADDED
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
|