synth-ai 0.2.8.dev12__py3-none-any.whl → 0.2.9.dev0__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.
- synth_ai/api/train/__init__.py +5 -0
- synth_ai/api/train/builders.py +165 -0
- synth_ai/api/train/cli.py +450 -0
- synth_ai/api/train/config_finder.py +168 -0
- synth_ai/api/train/env_resolver.py +302 -0
- synth_ai/api/train/pollers.py +66 -0
- synth_ai/api/train/task_app.py +193 -0
- synth_ai/api/train/utils.py +232 -0
- synth_ai/cli/__init__.py +23 -0
- synth_ai/cli/rl_demo.py +18 -6
- synth_ai/cli/root.py +38 -6
- synth_ai/cli/task_apps.py +1107 -0
- synth_ai/demo_registry.py +258 -0
- synth_ai/demos/core/cli.py +147 -111
- synth_ai/demos/demo_task_apps/__init__.py +7 -1
- synth_ai/demos/demo_task_apps/math/config.toml +55 -110
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +157 -21
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +39 -0
- synth_ai/task/__init__.py +94 -1
- synth_ai/task/apps/__init__.py +88 -0
- synth_ai/task/apps/grpo_crafter.py +438 -0
- synth_ai/task/apps/math_single_step.py +852 -0
- synth_ai/task/auth.py +153 -0
- synth_ai/task/client.py +165 -0
- synth_ai/task/contracts.py +29 -14
- synth_ai/task/datasets.py +105 -0
- synth_ai/task/errors.py +49 -0
- synth_ai/task/json.py +77 -0
- synth_ai/task/proxy.py +258 -0
- synth_ai/task/rubrics.py +212 -0
- synth_ai/task/server.py +398 -0
- synth_ai/task/tracing_utils.py +79 -0
- synth_ai/task/vendors.py +61 -0
- synth_ai/tracing_v3/session_tracer.py +13 -5
- synth_ai/tracing_v3/storage/base.py +10 -12
- synth_ai/tracing_v3/turso/manager.py +20 -6
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/METADATA +3 -2
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/RECORD +42 -18
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.8.dev12.dist-info → synth_ai-0.2.9.dev0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Registry of demo task app templates for `uvx synth-ai demo init`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Iterable
|
|
9
|
+
|
|
10
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CopySpec:
|
|
15
|
+
"""File copy specification from repo-relative source to template-relative destination."""
|
|
16
|
+
|
|
17
|
+
source: str
|
|
18
|
+
destination: str
|
|
19
|
+
make_executable: bool = False
|
|
20
|
+
|
|
21
|
+
def absolute_source(self) -> Path:
|
|
22
|
+
return (REPO_ROOT / self.source).resolve()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class DemoTemplate:
|
|
27
|
+
"""Describes a demo task app template that can be materialised into the CWD."""
|
|
28
|
+
|
|
29
|
+
template_id: str
|
|
30
|
+
name: str
|
|
31
|
+
description: str
|
|
32
|
+
copy_specs: tuple[CopySpec, ...]
|
|
33
|
+
default_subdir: str | None = None
|
|
34
|
+
env_lines: tuple[str, ...] = ()
|
|
35
|
+
config_source: str | None = None
|
|
36
|
+
config_destination: str = "demo_config.toml"
|
|
37
|
+
requires_modal: bool = False
|
|
38
|
+
post_copy: Callable[[Path], None] | None = None
|
|
39
|
+
|
|
40
|
+
def iter_copy_specs(self) -> Iterable[CopySpec]:
|
|
41
|
+
return self.copy_specs
|
|
42
|
+
|
|
43
|
+
def config_source_path(self) -> Path | None:
|
|
44
|
+
if not self.config_source:
|
|
45
|
+
return None
|
|
46
|
+
return (REPO_ROOT / self.config_source).resolve()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
DEMO_TEMPLATES: tuple[DemoTemplate, ...] = (
|
|
50
|
+
DemoTemplate(
|
|
51
|
+
template_id="math-modal",
|
|
52
|
+
name="Math Single-Step (Modal deployment)",
|
|
53
|
+
description="Packaged modal task app matching examples/rl math environment.",
|
|
54
|
+
copy_specs=(
|
|
55
|
+
CopySpec(
|
|
56
|
+
"synth_ai/demos/demo_task_apps/math/modal_task_app.py",
|
|
57
|
+
"task_app.py",
|
|
58
|
+
),
|
|
59
|
+
CopySpec(
|
|
60
|
+
"synth_ai/demos/demo_task_apps/math/deploy_task_app.sh",
|
|
61
|
+
"deploy_task_app.sh",
|
|
62
|
+
make_executable=True,
|
|
63
|
+
),
|
|
64
|
+
CopySpec(
|
|
65
|
+
"examples/rl/configs/rl_from_base_qwen17.toml",
|
|
66
|
+
"configs/rl_from_base_qwen17.toml",
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
default_subdir="math_demo",
|
|
70
|
+
env_lines=(
|
|
71
|
+
"# Required for task app auth to environment service",
|
|
72
|
+
"ENVIRONMENT_API_KEY=",
|
|
73
|
+
"",
|
|
74
|
+
"# Optional: for CLI job submission and proxying OpenAI models",
|
|
75
|
+
"SYNTH_API_KEY=",
|
|
76
|
+
"OPENAI_API_KEY=",
|
|
77
|
+
"",
|
|
78
|
+
"# Optional: set to 'prod' to use production names",
|
|
79
|
+
"ENVIRONMENT=",
|
|
80
|
+
),
|
|
81
|
+
config_source="examples/rl/configs/rl_from_base_qwen17.toml",
|
|
82
|
+
requires_modal=True,
|
|
83
|
+
post_copy=lambda root: _postprocess_math_modal(root),
|
|
84
|
+
),
|
|
85
|
+
DemoTemplate(
|
|
86
|
+
template_id="crafter-local",
|
|
87
|
+
name="Crafter GRPO (local FastAPI)",
|
|
88
|
+
description="Lightweight wrapper around synth_ai.task.apps.grpo_crafter for local experimentation.",
|
|
89
|
+
copy_specs=(
|
|
90
|
+
CopySpec(
|
|
91
|
+
"examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py",
|
|
92
|
+
"task_app.py",
|
|
93
|
+
),
|
|
94
|
+
CopySpec(
|
|
95
|
+
"examples/warming_up_to_rl/task_app/README.md",
|
|
96
|
+
"README.md",
|
|
97
|
+
),
|
|
98
|
+
CopySpec(
|
|
99
|
+
"examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml",
|
|
100
|
+
"configs/rl_from_base_qwen4b.toml",
|
|
101
|
+
),
|
|
102
|
+
CopySpec(
|
|
103
|
+
"examples/warming_up_to_rl/configs/crafter_fft_4b.toml",
|
|
104
|
+
"configs/crafter_fft_4b.toml",
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
default_subdir="crafter_demo",
|
|
108
|
+
env_lines=(
|
|
109
|
+
"ENVIRONMENT_API_KEY=",
|
|
110
|
+
"SYNTH_API_KEY=",
|
|
111
|
+
"",
|
|
112
|
+
"# Optional: URL for existing Crafter task app",
|
|
113
|
+
"TASK_APP_BASE_URL=",
|
|
114
|
+
),
|
|
115
|
+
config_source="examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml",
|
|
116
|
+
config_destination="demo_config.toml",
|
|
117
|
+
requires_modal=False,
|
|
118
|
+
post_copy=lambda root: _postprocess_crafter_local(root),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_demo_template(template_id: str) -> DemoTemplate | None:
|
|
124
|
+
for template in DEMO_TEMPLATES:
|
|
125
|
+
if template.template_id == template_id:
|
|
126
|
+
return template
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def list_demo_templates() -> tuple[DemoTemplate, ...]:
|
|
131
|
+
return DEMO_TEMPLATES
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _postprocess_math_modal(root: Path) -> None:
|
|
135
|
+
task_path = (root / "task_app.py").resolve()
|
|
136
|
+
if not task_path.exists():
|
|
137
|
+
return
|
|
138
|
+
text = task_path.read_text(encoding="utf-8")
|
|
139
|
+
text = text.replace('App("hendrycks-math-task-app")', 'App("hendrycks-math-task-app-demo")')
|
|
140
|
+
text = text.replace('DEFAULT_TASK_APP_SECRET_NAME = "hendrycks-math-task-app-secret"', 'DEFAULT_TASK_APP_SECRET_NAME = "hendrycks-math-task-app-demo-secret"')
|
|
141
|
+
task_path.write_text(text, encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
CRAFT_DEMO_TEMPLATE = textwrap.dedent(
|
|
145
|
+
'''
|
|
146
|
+
"""Demo-friendly wrapper for the GRPO Crafter task app."""
|
|
147
|
+
|
|
148
|
+
from __future__ import annotations
|
|
149
|
+
|
|
150
|
+
import argparse
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
|
|
153
|
+
from synth_ai.task.apps import ModalDeploymentConfig, registry
|
|
154
|
+
from synth_ai.task.apps.grpo_crafter import build_config
|
|
155
|
+
from synth_ai.task.server import TaskAppConfig, create_task_app, run_task_app
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
APP_ID = "grpo-crafter-demo"
|
|
159
|
+
BASE_APP_ID = "grpo-crafter"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_BASE_CONFIG = build_config()
|
|
163
|
+
TASK_APP_CONFIG = TaskAppConfig(
|
|
164
|
+
app_id="grpo-crafter-demo",
|
|
165
|
+
name=_BASE_CONFIG.name,
|
|
166
|
+
description=_BASE_CONFIG.description,
|
|
167
|
+
base_task_info=_BASE_CONFIG.base_task_info,
|
|
168
|
+
describe_taskset=_BASE_CONFIG.describe_taskset,
|
|
169
|
+
provide_task_instances=_BASE_CONFIG.provide_task_instances,
|
|
170
|
+
rollout=_BASE_CONFIG.rollout,
|
|
171
|
+
dataset_registry=_BASE_CONFIG.dataset_registry,
|
|
172
|
+
rubrics=_BASE_CONFIG.rubrics,
|
|
173
|
+
proxy=_BASE_CONFIG.proxy,
|
|
174
|
+
routers=_BASE_CONFIG.routers,
|
|
175
|
+
middleware=_BASE_CONFIG.middleware,
|
|
176
|
+
app_state=_BASE_CONFIG.app_state,
|
|
177
|
+
require_api_key=_BASE_CONFIG.require_api_key,
|
|
178
|
+
expose_debug_env=_BASE_CONFIG.expose_debug_env,
|
|
179
|
+
cors_origins=_BASE_CONFIG.cors_origins,
|
|
180
|
+
startup_hooks=_BASE_CONFIG.startup_hooks,
|
|
181
|
+
shutdown_hooks=_BASE_CONFIG.shutdown_hooks,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
_BASE_ENTRY = registry.get(BASE_APP_ID)
|
|
186
|
+
except Exception: # pragma: no cover - registry may be unavailable
|
|
187
|
+
MODAL_DEPLOYMENT: ModalDeploymentConfig | None = None
|
|
188
|
+
else:
|
|
189
|
+
base_modal = _BASE_ENTRY.modal
|
|
190
|
+
if base_modal is None:
|
|
191
|
+
MODAL_DEPLOYMENT = None
|
|
192
|
+
else:
|
|
193
|
+
modal_app_name = base_modal.app_name
|
|
194
|
+
if not modal_app_name.endswith("-demo"):
|
|
195
|
+
modal_app_name = f"{modal_app_name}-demo"
|
|
196
|
+
MODAL_DEPLOYMENT = ModalDeploymentConfig(
|
|
197
|
+
app_name=modal_app_name,
|
|
198
|
+
python_version=base_modal.python_version,
|
|
199
|
+
pip_packages=tuple(base_modal.pip_packages),
|
|
200
|
+
extra_local_dirs=tuple(base_modal.extra_local_dirs),
|
|
201
|
+
secret_names=tuple(base_modal.secret_names),
|
|
202
|
+
volume_mounts=tuple(base_modal.volume_mounts),
|
|
203
|
+
timeout=base_modal.timeout,
|
|
204
|
+
memory=base_modal.memory,
|
|
205
|
+
cpu=base_modal.cpu,
|
|
206
|
+
min_containers=base_modal.min_containers,
|
|
207
|
+
max_containers=base_modal.max_containers,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
ENV_FILES: tuple[str, ...] = ()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_task_app_config() -> TaskAppConfig:
|
|
214
|
+
"""Return a fresh TaskAppConfig for the demo wrapper."""
|
|
215
|
+
|
|
216
|
+
return TASK_APP_CONFIG.clone()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def fastapi_app():
|
|
220
|
+
"""Return the FastAPI application for Modal or other ASGI hosts."""
|
|
221
|
+
|
|
222
|
+
return create_task_app(build_task_app_config())
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
parser = argparse.ArgumentParser(description="Run the Crafter task app locally")
|
|
227
|
+
parser.add_argument("--host", default="0.0.0.0")
|
|
228
|
+
parser.add_argument("--port", type=int, default=8001)
|
|
229
|
+
parser.add_argument("--reload", action="store_true", help="Enable uvicorn autoreload")
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--env-file",
|
|
232
|
+
action="append",
|
|
233
|
+
default=[],
|
|
234
|
+
help="Additional .env files to load before startup",
|
|
235
|
+
)
|
|
236
|
+
args = parser.parse_args()
|
|
237
|
+
|
|
238
|
+
default_env = Path(__file__).resolve().parents[4] / "backend" / ".env.dev"
|
|
239
|
+
env_files = [str(default_env)] if default_env.exists() else []
|
|
240
|
+
env_files.extend(args.env_file or [])
|
|
241
|
+
|
|
242
|
+
run_task_app(
|
|
243
|
+
build_task_app_config,
|
|
244
|
+
host=args.host,
|
|
245
|
+
port=args.port,
|
|
246
|
+
reload=args.reload,
|
|
247
|
+
env_files=env_files,
|
|
248
|
+
)
|
|
249
|
+
'''
|
|
250
|
+
).strip() + "\n"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _postprocess_crafter_local(root: Path) -> None:
|
|
254
|
+
task_path = (root / "task_app.py").resolve()
|
|
255
|
+
if not task_path.exists():
|
|
256
|
+
return
|
|
257
|
+
task_path.write_text(CRAFT_DEMO_TEMPLATE, encoding="utf-8")
|
|
258
|
+
|
synth_ai/demos/core/cli.py
CHANGED
|
@@ -12,8 +12,14 @@ import stat
|
|
|
12
12
|
import textwrap
|
|
13
13
|
|
|
14
14
|
from synth_ai.demos.demo_task_apps import core as demo_core
|
|
15
|
-
from synth_ai.handshake import run_handshake, HandshakeError
|
|
16
15
|
from synth_ai.demos.demo_task_apps.core import DemoEnv, DEFAULT_TASK_APP_SECRET_NAME
|
|
16
|
+
from synth_ai.demo_registry import (
|
|
17
|
+
CopySpec,
|
|
18
|
+
DemoTemplate,
|
|
19
|
+
get_demo_template,
|
|
20
|
+
list_demo_templates,
|
|
21
|
+
)
|
|
22
|
+
from synth_ai.handshake import run_handshake, HandshakeError
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
def _key_preview(value: str, label: str) -> str:
|
|
@@ -912,125 +918,153 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
912
918
|
return 0
|
|
913
919
|
|
|
914
920
|
|
|
915
|
-
def
|
|
916
|
-
"""
|
|
921
|
+
def _ensure_modal_installed() -> None:
|
|
922
|
+
"""Install the modal package if it is not already available."""
|
|
917
923
|
|
|
918
|
-
Copies `examples/rl/task_app.py` and `examples/rl/deploy_task_app.sh` into CWD.
|
|
919
|
-
Creates a `.env` with placeholders if it does not exist.
|
|
920
|
-
"""
|
|
921
924
|
try:
|
|
922
|
-
|
|
923
|
-
def _has_modal() -> bool:
|
|
924
|
-
try:
|
|
925
|
-
import importlib.util as _iu
|
|
926
|
-
return _iu.find_spec("modal") is not None
|
|
927
|
-
except Exception:
|
|
928
|
-
return False
|
|
925
|
+
import importlib.util as _iu
|
|
929
926
|
|
|
930
|
-
if not
|
|
931
|
-
print("modal
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
print("Failed to install modal; continuing may fail.")
|
|
941
|
-
else:
|
|
942
|
-
print("modal installed successfully.")
|
|
943
|
-
except Exception as e:
|
|
944
|
-
print(f"modal install error: {e}")
|
|
945
|
-
# Re-check
|
|
946
|
-
if not _has_modal():
|
|
947
|
-
print("Warning: modal is still not importable after install attempt.")
|
|
927
|
+
if _iu.find_spec("modal") is not None:
|
|
928
|
+
print("modal package found")
|
|
929
|
+
return
|
|
930
|
+
except Exception:
|
|
931
|
+
pass
|
|
932
|
+
|
|
933
|
+
print("modal not found; installing…")
|
|
934
|
+
try:
|
|
935
|
+
if shutil.which("uv"):
|
|
936
|
+
code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
|
|
948
937
|
else:
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
print(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
938
|
+
code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
|
|
939
|
+
if code != 0:
|
|
940
|
+
print(out)
|
|
941
|
+
print("Failed to install modal; continuing may fail.")
|
|
942
|
+
else:
|
|
943
|
+
print("modal installed successfully.")
|
|
944
|
+
except Exception as exc:
|
|
945
|
+
print(f"modal install error: {exc}")
|
|
946
|
+
|
|
947
|
+
try:
|
|
948
|
+
import importlib.util as _iu
|
|
949
|
+
|
|
950
|
+
if _iu.find_spec("modal") is None:
|
|
951
|
+
print("Warning: modal is still not importable after install attempt.")
|
|
952
|
+
else:
|
|
953
|
+
print("modal package ready")
|
|
954
|
+
except Exception:
|
|
955
|
+
print("Warning: unable to verify modal installation.")
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
959
|
+
"""Materialise a demo task app template into the current directory."""
|
|
960
|
+
|
|
961
|
+
templates = list(list_demo_templates())
|
|
962
|
+
if not templates:
|
|
963
|
+
print("No demo templates registered. Update synth_ai/demo_registry.py to add entries.")
|
|
964
|
+
return 1
|
|
965
|
+
|
|
966
|
+
selected: DemoTemplate | None = None
|
|
967
|
+
if args.template:
|
|
968
|
+
selected = get_demo_template(args.template)
|
|
969
|
+
if selected is None:
|
|
970
|
+
available = ", ".join(t.template_id for t in templates)
|
|
971
|
+
print(f"Unknown template '{args.template}'. Available: {available}")
|
|
968
972
|
return 1
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
976
|
-
APP="$HERE/task_app.py"
|
|
977
|
-
if [ -f "$HERE/.env" ]; then
|
|
978
|
-
# shellcheck disable=SC2046
|
|
979
|
-
export $(grep -v '^#' "$HERE/.env" | xargs -I{} echo {})
|
|
980
|
-
fi
|
|
981
|
-
uv run modal deploy "$APP" | tee "$HERE/.last_deploy.log"
|
|
982
|
-
URL=$(grep -Eo 'https://[^ ]+\.modal\.run' "$HERE/.last_deploy.log" | tail -1 || true)
|
|
983
|
-
if [ -n "$URL" ]; then
|
|
984
|
-
if grep -q '^TASK_APP_BASE_URL=' "$HERE/.env" 2>/dev/null; then
|
|
985
|
-
sed -i.bak "s#^TASK_APP_BASE_URL=.*#TASK_APP_BASE_URL=$URL#" "$HERE/.env" || true
|
|
986
|
-
else
|
|
987
|
-
echo "TASK_APP_BASE_URL=$URL" >> "$HERE/.env"
|
|
988
|
-
fi
|
|
989
|
-
echo "Saved TASK_APP_BASE_URL to $HERE/.env"
|
|
990
|
-
fi
|
|
991
|
-
"""
|
|
992
|
-
_write_text(dst_deploy, deploy_text)
|
|
973
|
+
else:
|
|
974
|
+
print("Select a demo template:" + "\n")
|
|
975
|
+
for idx, template in enumerate(templates, start=1):
|
|
976
|
+
print(f" [{idx}] {template.name} ({template.template_id})")
|
|
977
|
+
print(f" {template.description}")
|
|
993
978
|
try:
|
|
994
|
-
|
|
995
|
-
os.chmod(dst_deploy, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
979
|
+
choice_raw = input(f"Enter choice [1-{len(templates)}] (default 1): ").strip() or "1"
|
|
996
980
|
except Exception:
|
|
997
|
-
|
|
981
|
+
choice_raw = "1"
|
|
982
|
+
if not choice_raw.isdigit():
|
|
983
|
+
print("Selection must be a number.")
|
|
984
|
+
return 1
|
|
985
|
+
choice_idx = int(choice_raw)
|
|
986
|
+
if not 1 <= choice_idx <= len(templates):
|
|
987
|
+
print("Selection out of range.")
|
|
988
|
+
return 1
|
|
989
|
+
selected = templates[choice_idx - 1]
|
|
998
990
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
shutil.copy2(packaged_cfg, dst_cfg)
|
|
1019
|
-
except Exception:
|
|
1020
|
-
pass
|
|
991
|
+
assert selected is not None
|
|
992
|
+
|
|
993
|
+
default_subdir = selected.default_subdir or selected.template_id
|
|
994
|
+
default_dest = Path(args.dest).expanduser().resolve() if args.dest else (Path.cwd() / default_subdir).resolve()
|
|
995
|
+
try:
|
|
996
|
+
dest_input = input(f"Destination directory [{default_dest}]: ").strip()
|
|
997
|
+
except Exception:
|
|
998
|
+
dest_input = ""
|
|
999
|
+
destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
|
|
1000
|
+
|
|
1001
|
+
if destination.exists():
|
|
1002
|
+
if destination.is_file():
|
|
1003
|
+
print(f"Destination {destination} is a file. Provide a directory path.")
|
|
1004
|
+
return 1
|
|
1005
|
+
if not args.force and any(destination.iterdir()):
|
|
1006
|
+
print(f"Destination {destination} is not empty. Use --force or choose another directory.")
|
|
1007
|
+
return 1
|
|
1008
|
+
else:
|
|
1009
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
1021
1010
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1011
|
+
if selected.requires_modal:
|
|
1012
|
+
_ensure_modal_installed()
|
|
1013
|
+
|
|
1014
|
+
try:
|
|
1015
|
+
for spec in selected.iter_copy_specs():
|
|
1016
|
+
src_path = spec.absolute_source()
|
|
1017
|
+
if not src_path.exists():
|
|
1018
|
+
print(f"Template source missing: {src_path}")
|
|
1019
|
+
return 1
|
|
1020
|
+
dest_path = (destination / spec.destination).resolve()
|
|
1021
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1022
|
+
if dest_path.exists() and not args.force:
|
|
1023
|
+
print(f"Refusing to overwrite existing file: {dest_path} (use --force)")
|
|
1024
|
+
return 1
|
|
1025
|
+
shutil.copy2(src_path, dest_path)
|
|
1026
|
+
if spec.make_executable:
|
|
1027
|
+
try:
|
|
1028
|
+
st = os.stat(dest_path)
|
|
1029
|
+
os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
1030
|
+
except Exception:
|
|
1031
|
+
pass
|
|
1032
|
+
|
|
1033
|
+
if selected.env_lines:
|
|
1034
|
+
env_path = destination / ".env"
|
|
1035
|
+
if not env_path.exists() or args.force:
|
|
1036
|
+
_write_text(env_path, "\n".join(selected.env_lines) + "\n")
|
|
1037
|
+
|
|
1038
|
+
config_src = selected.config_source_path()
|
|
1039
|
+
if config_src and config_src.exists():
|
|
1040
|
+
cfg_dst = (destination / selected.config_destination).resolve()
|
|
1041
|
+
if not cfg_dst.exists() or args.force:
|
|
1042
|
+
cfg_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
1043
|
+
shutil.copy2(config_src, cfg_dst)
|
|
1044
|
+
|
|
1045
|
+
if selected.post_copy is not None:
|
|
1046
|
+
try:
|
|
1047
|
+
selected.post_copy(destination)
|
|
1048
|
+
except Exception as post_exc:
|
|
1049
|
+
print(f"Post-processing failed: {post_exc}")
|
|
1050
|
+
return 1
|
|
1051
|
+
|
|
1052
|
+
print(f"Demo template '{selected.name}' materialised at {destination}.")
|
|
1053
|
+
print("Files created:")
|
|
1054
|
+
for spec in selected.iter_copy_specs():
|
|
1055
|
+
print(f" - {spec.destination}")
|
|
1056
|
+
if selected.env_lines:
|
|
1057
|
+
print(" - .env")
|
|
1058
|
+
if selected.config_source_path():
|
|
1059
|
+
print(f" - {selected.config_destination}")
|
|
1060
|
+
print("Review the files, edit .env, and run any provided deploy scripts when ready.")
|
|
1030
1061
|
return 0
|
|
1031
|
-
except
|
|
1032
|
-
print(
|
|
1033
|
-
return
|
|
1062
|
+
except KeyboardInterrupt:
|
|
1063
|
+
print("Aborted")
|
|
1064
|
+
return 1
|
|
1065
|
+
except Exception as exc:
|
|
1066
|
+
print(f"Init failed: {exc}")
|
|
1067
|
+
return 1
|
|
1034
1068
|
|
|
1035
1069
|
|
|
1036
1070
|
def _http(method: str, url: str, headers: Dict[str, str] | None = None, body: Dict[str, Any] | None = None) -> tuple[int, Dict[str, Any] | str]:
|
|
@@ -1358,7 +1392,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1358
1392
|
_add_parser(["rl_demo.setup", "demo.setup"], configure=lambda parser: parser.set_defaults(func=cmd_setup))
|
|
1359
1393
|
|
|
1360
1394
|
def _init_opts(parser):
|
|
1361
|
-
parser.add_argument("--
|
|
1395
|
+
parser.add_argument("--template", type=str, default=None, help="Template id to instantiate")
|
|
1396
|
+
parser.add_argument("--dest", type=str, default=None, help="Destination directory for files")
|
|
1397
|
+
parser.add_argument("--force", action="store_true", help="Overwrite existing files in destination")
|
|
1362
1398
|
parser.set_defaults(func=cmd_init)
|
|
1363
1399
|
|
|
1364
1400
|
_add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"""Namespace for demo task apps (math, etc.)."""
|
|
2
|
+
|
|
3
|
+
# Ensure registry entries are loaded for CLI discovery.
|
|
4
|
+
try: # pragma: no cover - optional on downstream installs
|
|
5
|
+
from .math import task_app_entry # noqa: F401
|
|
6
|
+
except Exception:
|
|
7
|
+
pass
|