lightcone-cli 0.2.0__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.
- lightcone/cli/__init__.py +16 -0
- lightcone/cli/claude/lightcone/agents/lc-extractor.md +114 -0
- lightcone/cli/claude/lightcone/guides/astra-reference.md +290 -0
- lightcone/cli/claude/lightcone/guides/lightcone-cli-reference.md +75 -0
- lightcone/cli/claude/lightcone/guides/ui-brand.md +86 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_git_commit_hook.py +303 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_hook.py +894 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +142 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_session_init_hook.py +83 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_utils.py +457 -0
- lightcone/cli/claude/lightcone/scripts/activate-venv.sh +44 -0
- lightcone/cli/claude/lightcone/scripts/check-lc-run.sh +140 -0
- lightcone/cli/claude/lightcone/scripts/session-start.sh +140 -0
- lightcone/cli/claude/lightcone/scripts/validate-on-save.sh +77 -0
- lightcone/cli/claude/lightcone/skills/lc-build/SKILL.md +92 -0
- lightcone/cli/claude/lightcone/skills/lc-build/assets/loop-prompt.md +92 -0
- lightcone/cli/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +240 -0
- lightcone/cli/claude/lightcone/skills/lc-feedback/SKILL.md +94 -0
- lightcone/cli/claude/lightcone/skills/lc-migrate/SKILL.md +98 -0
- lightcone/cli/claude/lightcone/skills/lc-new/SKILL.md +183 -0
- lightcone/cli/claude/lightcone/skills/lc-verify/SKILL.md +53 -0
- lightcone/cli/claude/lightcone/templates/CLAUDE.md +32 -0
- lightcone/cli/commands.py +2327 -0
- lightcone/cli/plugin.py +34 -0
- lightcone/engine/__init__.py +42 -0
- lightcone/engine/assets.py +418 -0
- lightcone/engine/container.py +370 -0
- lightcone/engine/io_manager.py +27 -0
- lightcone/engine/runner.py +1017 -0
- lightcone/engine/site_registry.py +142 -0
- lightcone/engine/status.py +135 -0
- lightcone/engine/targets.py +68 -0
- lightcone/engine/tree.py +245 -0
- lightcone/eval/__init__.py +25 -0
- lightcone/eval/build.py +148 -0
- lightcone/eval/cli.py +176 -0
- lightcone/eval/graders.py +192 -0
- lightcone/eval/harness.py +265 -0
- lightcone/eval/models.py +117 -0
- lightcone/eval/report.py +214 -0
- lightcone/eval/sandbox.py +394 -0
- lightcone_cli-0.2.0.dist-info/METADATA +16 -0
- lightcone_cli-0.2.0.dist-info/RECORD +46 -0
- lightcone_cli-0.2.0.dist-info/WHEEL +4 -0
- lightcone_cli-0.2.0.dist-info/entry_points.txt +2 -0
- lightcone_cli-0.2.0.dist-info/licenses/LICENSE +29 -0
lightcone/cli/plugin.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Plugin bundle discovery — finds the Claude Code skills/hooks shipped with lightcone-cli.
|
|
2
|
+
|
|
3
|
+
Kept deliberately leaf (no imports from :mod:`lightcone.cli.commands` or :mod:`lightcone.eval`)
|
|
4
|
+
so it can be used by both the CLI and the eval harness without introducing an import cycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_plugin_source_dir() -> Path | None:
|
|
13
|
+
"""Find the lightcone Claude plugin source directory.
|
|
14
|
+
|
|
15
|
+
Looks for the plugin files in:
|
|
16
|
+
|
|
17
|
+
1. Bundled location (installed package): ``lightcone/cli/claude/lightcone/``
|
|
18
|
+
2. Development location (repo): ``claude/lightcone/`` relative to repo root
|
|
19
|
+
"""
|
|
20
|
+
import lightcone.cli
|
|
21
|
+
|
|
22
|
+
package_dir = Path(lightcone.cli.__file__).parent
|
|
23
|
+
bundled_plugin = package_dir / "claude" / "lightcone"
|
|
24
|
+
if bundled_plugin.exists():
|
|
25
|
+
return bundled_plugin
|
|
26
|
+
|
|
27
|
+
# Try development location (running from repo)
|
|
28
|
+
# package_dir == <repo>/src/lightcone/cli → parents[2] == <repo>
|
|
29
|
+
repo_root = package_dir.parents[2]
|
|
30
|
+
dev_plugin = repo_root / "claude" / "lightcone"
|
|
31
|
+
if dev_plugin.exists():
|
|
32
|
+
return dev_plugin
|
|
33
|
+
|
|
34
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Lightcone execution engine — Dagster assets, runners, and HPC targets.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- build_definitions(): Generate Dagster Definitions from astra.yaml
|
|
5
|
+
- ASTRAContainerRunner: Execute recipes in Docker/SLURM containers
|
|
6
|
+
- ASTRAIOManager: Map (asset, universe) to filesystem paths
|
|
7
|
+
- get_output_status(): Query materialization status
|
|
8
|
+
"""
|
|
9
|
+
from lightcone.engine.io_manager import ASTRAIOManager
|
|
10
|
+
from lightcone.engine.runner import (
|
|
11
|
+
ASTRAContainerRunner,
|
|
12
|
+
generate_sbatch_script,
|
|
13
|
+
translate_resources_to_slurm_directives,
|
|
14
|
+
)
|
|
15
|
+
from lightcone.engine.status import get_all_universe_status, get_output_status
|
|
16
|
+
from lightcone.engine.targets import list_targets, load_target, save_target
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ASTRAContainerRunner",
|
|
20
|
+
"ASTRAIOManager",
|
|
21
|
+
"build_asset_definitions",
|
|
22
|
+
"build_definitions",
|
|
23
|
+
"generate_sbatch_script",
|
|
24
|
+
"get_output_status",
|
|
25
|
+
"get_all_universe_status",
|
|
26
|
+
"list_targets",
|
|
27
|
+
"load_target",
|
|
28
|
+
"save_target",
|
|
29
|
+
"translate_resources_to_slurm_directives",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_definitions(*args, **kwargs):
|
|
34
|
+
"""Build Dagster Definitions from astra.yaml. Requires dagster to be installed."""
|
|
35
|
+
from lightcone.engine.assets import build_definitions as _build
|
|
36
|
+
return _build(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_asset_definitions(*args, **kwargs):
|
|
40
|
+
"""Build asset definitions from astra.yaml. Requires dagster to be installed."""
|
|
41
|
+
from lightcone.engine.assets import build_asset_definitions as _build
|
|
42
|
+
return _build(*args, **kwargs)
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Asset factory — generates Dagster assets from astra.yaml output recipes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import dagster as dg
|
|
9
|
+
from astra.helpers import get_inputs, load_yaml, resolve_analysis_tree
|
|
10
|
+
|
|
11
|
+
from lightcone.engine.container import resolve_container_for_slurm, resolve_container_spec
|
|
12
|
+
from lightcone.engine.runner import ASTRAContainerRunner
|
|
13
|
+
from lightcone.engine.tree import (
|
|
14
|
+
TreeOutput,
|
|
15
|
+
collect_tree_outputs,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_external_inputs(spec: dict[str, Any]) -> dict[str, str]:
|
|
22
|
+
"""Return {input_id: source_path} for inputs with a filesystem source."""
|
|
23
|
+
result = {}
|
|
24
|
+
for inp in get_inputs(spec):
|
|
25
|
+
source = inp.get("source")
|
|
26
|
+
if source and isinstance(source, str) and source.startswith("/"):
|
|
27
|
+
result[inp["id"]] = source
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_container(
|
|
32
|
+
spec: str | None,
|
|
33
|
+
project_path: Path,
|
|
34
|
+
project_name: str,
|
|
35
|
+
container_runtime: str | None = None,
|
|
36
|
+
local_runtime: str | None = None,
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""Resolve a container spec, dispatching to the right builder.
|
|
39
|
+
|
|
40
|
+
When *container_runtime* is set (i.e. we are targeting SLURM), uses
|
|
41
|
+
``resolve_container_for_slurm`` which handles podman-hpc build/migrate
|
|
42
|
+
and podman-hpc migrate automatically. When *local_runtime* is set
|
|
43
|
+
(Docker or Podman detected locally), uses ``resolve_container_spec``
|
|
44
|
+
with that runtime. Returns ``None`` if no runtime is available.
|
|
45
|
+
"""
|
|
46
|
+
if container_runtime:
|
|
47
|
+
return resolve_container_for_slurm(
|
|
48
|
+
spec, project_path, project_name, container_runtime,
|
|
49
|
+
)
|
|
50
|
+
if local_runtime:
|
|
51
|
+
return resolve_container_spec(
|
|
52
|
+
spec, project_path, project_name, runtime=local_runtime,
|
|
53
|
+
)
|
|
54
|
+
# No container runtime available — skip building
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_asset_definitions(
|
|
59
|
+
spec: dict[str, Any],
|
|
60
|
+
runner: ASTRAContainerRunner | None = None,
|
|
61
|
+
universe_id: str = "baseline",
|
|
62
|
+
project_path: Path | None = None,
|
|
63
|
+
project_name: str | None = None,
|
|
64
|
+
no_build: bool = False,
|
|
65
|
+
container_runtime: str | None = None,
|
|
66
|
+
local_runtime: str | None = None,
|
|
67
|
+
) -> list[dg.AssetsDefinition | dg.AssetSpec]:
|
|
68
|
+
"""Generate one @asset per output with a recipe.
|
|
69
|
+
|
|
70
|
+
Walks the full analysis tree (including sub-analyses) to create assets
|
|
71
|
+
with hierarchical keys like ``[universe, sub_analysis_id, output_id]``.
|
|
72
|
+
"""
|
|
73
|
+
# Resolve analysis-level container spec once.
|
|
74
|
+
raw_default = spec.get("container")
|
|
75
|
+
if raw_default is not None and not no_build and (container_runtime or local_runtime):
|
|
76
|
+
_name = project_name or spec.get("name") or "project"
|
|
77
|
+
_path = project_path or Path.cwd()
|
|
78
|
+
default_container = _resolve_container(
|
|
79
|
+
raw_default, _path, _name, container_runtime, local_runtime,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
default_container = raw_default
|
|
83
|
+
|
|
84
|
+
# Collect external inputs (inputs with filesystem source paths)
|
|
85
|
+
external = get_external_inputs(spec)
|
|
86
|
+
asset_specs = [
|
|
87
|
+
dg.AssetSpec(
|
|
88
|
+
key=dg.AssetKey([universe_id, inp_id]),
|
|
89
|
+
metadata={"source": source, "external": True},
|
|
90
|
+
)
|
|
91
|
+
for inp_id, source in external.items()
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
assets: list[dg.AssetsDefinition | dg.AssetSpec] = list(asset_specs)
|
|
95
|
+
|
|
96
|
+
# Collect outputs from the full tree (root + sub-analyses)
|
|
97
|
+
tree_outputs = collect_tree_outputs(spec)
|
|
98
|
+
|
|
99
|
+
for tree_out in tree_outputs:
|
|
100
|
+
output_id = tree_out.output_id
|
|
101
|
+
output_def = tree_out.output_def
|
|
102
|
+
recipe = output_def.get("recipe")
|
|
103
|
+
|
|
104
|
+
if not output_id or not recipe:
|
|
105
|
+
# Root-level alias outputs (from: sub.output) become AssetSpecs
|
|
106
|
+
from_ref = output_def.get("from")
|
|
107
|
+
if output_id and from_ref and tree_out.analysis_id is None:
|
|
108
|
+
# Alias: root output referencing a sub-analysis output
|
|
109
|
+
if "." in from_ref:
|
|
110
|
+
sub_id, sub_out = from_ref.split(".", 1)
|
|
111
|
+
assets.append(dg.AssetSpec(
|
|
112
|
+
key=dg.AssetKey([universe_id, output_id]),
|
|
113
|
+
deps=[dg.AssetKey([universe_id, sub_id, sub_out])],
|
|
114
|
+
metadata={"alias_for": from_ref},
|
|
115
|
+
))
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Determine asset key prefix based on sub-analysis membership
|
|
119
|
+
if tree_out.analysis_id:
|
|
120
|
+
key_prefix = [universe_id, tree_out.analysis_id]
|
|
121
|
+
group_name = tree_out.analysis_id
|
|
122
|
+
else:
|
|
123
|
+
key_prefix = [universe_id]
|
|
124
|
+
group_name = None
|
|
125
|
+
|
|
126
|
+
# Resolve container for this sub-analysis (inheritance chain)
|
|
127
|
+
sub_container = _resolve_sub_container(
|
|
128
|
+
tree_out, spec, default_container, project_path,
|
|
129
|
+
project_name, no_build, container_runtime, local_runtime,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Resolve dependencies: recipe.inputs may reference cross-sub-analysis outputs
|
|
133
|
+
dep_keys = _resolve_recipe_deps(
|
|
134
|
+
recipe, tree_out, spec, universe_id,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Build the external inputs relevant to this sub-analysis
|
|
138
|
+
recipe_input_ids = recipe.get("inputs") or []
|
|
139
|
+
sub_external = {
|
|
140
|
+
k: v for k, v in external.items() if k in recipe_input_ids
|
|
141
|
+
} or None
|
|
142
|
+
|
|
143
|
+
assets.append(
|
|
144
|
+
_build_single_asset(
|
|
145
|
+
output_id, recipe, runner, universe_id, project_path,
|
|
146
|
+
project_name=project_name, default_container=sub_container,
|
|
147
|
+
no_build=no_build, container_runtime=container_runtime,
|
|
148
|
+
local_runtime=local_runtime, external_inputs=sub_external,
|
|
149
|
+
key_prefix=key_prefix, group_name=group_name,
|
|
150
|
+
dep_keys=dep_keys, tree_output=tree_out, spec=spec,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return assets
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_sub_container(
|
|
158
|
+
tree_out: TreeOutput,
|
|
159
|
+
spec: dict[str, Any],
|
|
160
|
+
default_container: str | None,
|
|
161
|
+
project_path: Path | None,
|
|
162
|
+
project_name: str | None,
|
|
163
|
+
no_build: bool,
|
|
164
|
+
container_runtime: str | None,
|
|
165
|
+
local_runtime: str | None,
|
|
166
|
+
) -> str | None:
|
|
167
|
+
"""Resolve container for a tree output with inheritance.
|
|
168
|
+
|
|
169
|
+
Order: recipe-level > sub-analysis-level > root-level (default_container).
|
|
170
|
+
"""
|
|
171
|
+
# Check sub-analysis level container
|
|
172
|
+
if tree_out.analysis_id:
|
|
173
|
+
sub_raw = tree_out.analysis_spec.get("container")
|
|
174
|
+
if sub_raw is not None and not no_build and (container_runtime or local_runtime):
|
|
175
|
+
_name = project_name or "project"
|
|
176
|
+
_path = project_path or Path.cwd()
|
|
177
|
+
return _resolve_container(
|
|
178
|
+
sub_raw, _path, _name, container_runtime, local_runtime,
|
|
179
|
+
)
|
|
180
|
+
elif sub_raw is not None:
|
|
181
|
+
return sub_raw
|
|
182
|
+
|
|
183
|
+
return default_container
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _resolve_recipe_deps(
|
|
187
|
+
recipe: dict[str, Any],
|
|
188
|
+
tree_out: TreeOutput,
|
|
189
|
+
spec: dict[str, Any],
|
|
190
|
+
universe_id: str,
|
|
191
|
+
) -> list[dg.AssetKey] | None:
|
|
192
|
+
"""Resolve recipe input dependencies to Dagster asset keys.
|
|
193
|
+
|
|
194
|
+
Handles:
|
|
195
|
+
- Simple IDs within the same analysis scope
|
|
196
|
+
- ``from:`` references on sub-analysis inputs that point to siblings
|
|
197
|
+
"""
|
|
198
|
+
input_ids = recipe.get("inputs") or []
|
|
199
|
+
if not input_ids:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
deps: list[dg.AssetKey] = []
|
|
203
|
+
analysis_inputs = {
|
|
204
|
+
inp.get("id"): inp
|
|
205
|
+
for inp in get_inputs(tree_out.analysis_spec)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for inp_id in input_ids:
|
|
209
|
+
inp_def = analysis_inputs.get(inp_id)
|
|
210
|
+
if inp_def and inp_def.get("from"):
|
|
211
|
+
from_ref = inp_def["from"]
|
|
212
|
+
# ../sibling.output_id -> [universe, sibling, output_id]
|
|
213
|
+
ref = from_ref.removeprefix("../").removeprefix("/")
|
|
214
|
+
if "." in ref:
|
|
215
|
+
sub_id, sub_out = ref.split(".", 1)
|
|
216
|
+
deps.append(dg.AssetKey([universe_id, sub_id, sub_out]))
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# Dot-notation cross-analysis reference (e.g. hod_fitting.galaxy_mesh)
|
|
220
|
+
if "." in inp_id:
|
|
221
|
+
sub_id, sub_out = inp_id.split(".", 1)
|
|
222
|
+
deps.append(dg.AssetKey([universe_id, sub_id, sub_out]))
|
|
223
|
+
elif tree_out.analysis_id:
|
|
224
|
+
deps.append(dg.AssetKey([universe_id, tree_out.analysis_id, inp_id]))
|
|
225
|
+
else:
|
|
226
|
+
deps.append(dg.AssetKey([universe_id, inp_id]))
|
|
227
|
+
|
|
228
|
+
return deps
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _load_universe_params(
|
|
232
|
+
project_path: Path | None, universe_id: str
|
|
233
|
+
) -> dict[str, Any]:
|
|
234
|
+
"""Load universe decisions as params dict."""
|
|
235
|
+
if project_path is None:
|
|
236
|
+
return {}
|
|
237
|
+
universe_file = project_path / "universes" / f"{universe_id}.yaml"
|
|
238
|
+
if not universe_file.exists():
|
|
239
|
+
return {}
|
|
240
|
+
universe_data = load_yaml(universe_file)
|
|
241
|
+
return universe_data.get("decisions", {})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _build_single_asset(
|
|
245
|
+
output_id: str,
|
|
246
|
+
recipe: dict[str, Any],
|
|
247
|
+
runner: ASTRAContainerRunner | None = None,
|
|
248
|
+
universe_id: str = "baseline",
|
|
249
|
+
project_path: Path | None = None,
|
|
250
|
+
project_name: str | None = None,
|
|
251
|
+
default_container: str | None = None,
|
|
252
|
+
no_build: bool = False,
|
|
253
|
+
container_runtime: str | None = None,
|
|
254
|
+
local_runtime: str | None = None,
|
|
255
|
+
external_inputs: dict[str, str] | None = None,
|
|
256
|
+
key_prefix: list[str] | None = None,
|
|
257
|
+
group_name: str | None = None,
|
|
258
|
+
dep_keys: list[dg.AssetKey] | None = None,
|
|
259
|
+
tree_output: TreeOutput | None = None,
|
|
260
|
+
spec: dict[str, Any] | None = None,
|
|
261
|
+
) -> dg.AssetsDefinition:
|
|
262
|
+
"""Build a single Dagster asset from an output recipe."""
|
|
263
|
+
input_ids = recipe.get("inputs") or []
|
|
264
|
+
command = recipe["command"]
|
|
265
|
+
# Filter external inputs to those referenced by this recipe
|
|
266
|
+
recipe_external = {
|
|
267
|
+
k: v for k, v in (external_inputs or {}).items() if k in input_ids
|
|
268
|
+
} or None
|
|
269
|
+
raw_container = recipe.get("container")
|
|
270
|
+
# Resolve per-recipe container spec; fall back to analysis-level default.
|
|
271
|
+
if raw_container is not None and not no_build and (container_runtime or local_runtime):
|
|
272
|
+
_name = project_name or "project"
|
|
273
|
+
_path = project_path or Path.cwd()
|
|
274
|
+
container = _resolve_container(
|
|
275
|
+
raw_container, _path, _name, container_runtime, local_runtime,
|
|
276
|
+
)
|
|
277
|
+
elif raw_container is not None:
|
|
278
|
+
container = raw_container
|
|
279
|
+
else:
|
|
280
|
+
container = default_container
|
|
281
|
+
resources = recipe.get("resources") or {}
|
|
282
|
+
|
|
283
|
+
# Use provided key_prefix or default to [universe_id]
|
|
284
|
+
effective_prefix = key_prefix or [universe_id]
|
|
285
|
+
# Use provided dep_keys or build from input_ids
|
|
286
|
+
effective_deps = dep_keys or [dg.AssetKey([universe_id, i]) for i in input_ids]
|
|
287
|
+
|
|
288
|
+
asset_kwargs: dict[str, Any] = {
|
|
289
|
+
"name": output_id,
|
|
290
|
+
"key_prefix": effective_prefix,
|
|
291
|
+
"deps": effective_deps,
|
|
292
|
+
"metadata": {
|
|
293
|
+
"command": command,
|
|
294
|
+
"container": container or "default",
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
if group_name:
|
|
298
|
+
asset_kwargs["group_name"] = group_name
|
|
299
|
+
|
|
300
|
+
@dg.asset(**asset_kwargs)
|
|
301
|
+
def _asset(context) -> dg.MaterializeResult:
|
|
302
|
+
params = _load_universe_params(project_path, universe_id)
|
|
303
|
+
|
|
304
|
+
# Determine working directory for sub-analysis recipes
|
|
305
|
+
cwd_override = None
|
|
306
|
+
if tree_output and tree_output.analysis_path and project_path:
|
|
307
|
+
cwd_override = str(
|
|
308
|
+
(project_path / tree_output.analysis_path).resolve()
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
result = runner.execute(
|
|
312
|
+
command=command,
|
|
313
|
+
container=container,
|
|
314
|
+
inputs=input_ids,
|
|
315
|
+
output_id=output_id,
|
|
316
|
+
universe_id=universe_id,
|
|
317
|
+
resources=resources,
|
|
318
|
+
params=params,
|
|
319
|
+
external_inputs=recipe_external,
|
|
320
|
+
cwd_override=cwd_override,
|
|
321
|
+
)
|
|
322
|
+
if result.metadata.get("stdout"):
|
|
323
|
+
context.log.info(result.metadata["stdout"])
|
|
324
|
+
if result.exit_code != 0:
|
|
325
|
+
stderr = result.metadata.get("stderr", "")
|
|
326
|
+
raise RuntimeError(
|
|
327
|
+
f"Recipe for '{output_id}' failed (exit code {result.exit_code})"
|
|
328
|
+
f"{': ' + stderr if stderr else ''}"
|
|
329
|
+
)
|
|
330
|
+
return dg.MaterializeResult(
|
|
331
|
+
metadata={
|
|
332
|
+
"exit_code": result.exit_code,
|
|
333
|
+
"output_path": str(result.output_path),
|
|
334
|
+
"backend": result.metadata.get("backend", "unknown"),
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return _asset
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def build_definitions(
|
|
342
|
+
project_path: Path,
|
|
343
|
+
target_config: dict[str, Any] | None = None,
|
|
344
|
+
universe_id: str = "baseline",
|
|
345
|
+
no_build: bool = False,
|
|
346
|
+
) -> dg.Definitions:
|
|
347
|
+
"""Build complete Dagster Definitions from an ASTRA project.
|
|
348
|
+
|
|
349
|
+
This is the main entry point for the Dagster integration. When a SLURM
|
|
350
|
+
target is provided, container images are automatically built (podman-hpc)
|
|
351
|
+
or pulled before asset definitions are constructed.
|
|
352
|
+
"""
|
|
353
|
+
spec = load_yaml(project_path / "astra.yaml")
|
|
354
|
+
# Resolve sub-analysis tree: expand path: references
|
|
355
|
+
spec = resolve_analysis_tree(spec, project_path)
|
|
356
|
+
project_name = spec.get("name") or project_path.name
|
|
357
|
+
|
|
358
|
+
# Build runner config from target
|
|
359
|
+
runner_config = None
|
|
360
|
+
container_runtime: str | None = None
|
|
361
|
+
local_runtime: str | None = None
|
|
362
|
+
backend = "docker"
|
|
363
|
+
|
|
364
|
+
if target_config:
|
|
365
|
+
backend = target_config.get("backend", "docker")
|
|
366
|
+
container_runtime = target_config.get("container_runtime")
|
|
367
|
+
# Transform flat target_config into the shape the runner expects
|
|
368
|
+
runner_config = {"connection": target_config.get("connection", {})}
|
|
369
|
+
scheduler = {}
|
|
370
|
+
for key in ("site", "account", "qos", "constraint", "node_type",
|
|
371
|
+
"container_runtime", "container_flags",
|
|
372
|
+
"nodes", "time_limit", "extra_slurm_args"):
|
|
373
|
+
if target_config.get(key) is not None:
|
|
374
|
+
scheduler[key] = target_config[key]
|
|
375
|
+
if scheduler:
|
|
376
|
+
runner_config["scheduler"] = scheduler
|
|
377
|
+
else:
|
|
378
|
+
# No target config → local target. Detect available container runtime.
|
|
379
|
+
from lightcone.engine.container import detect_container_runtime
|
|
380
|
+
local_runtime = detect_container_runtime()
|
|
381
|
+
backend = "docker" if local_runtime else "venv"
|
|
382
|
+
|
|
383
|
+
# Resolve analysis-level container spec to a string for the runner.
|
|
384
|
+
# For SLURM targets this triggers podman-hpc build/migrate or
|
|
385
|
+
# podman-hpc migrate automatically. Skipped entirely when no
|
|
386
|
+
# container runtime is available.
|
|
387
|
+
raw_container = spec.get("container")
|
|
388
|
+
if not no_build and (container_runtime or local_runtime):
|
|
389
|
+
default_container = _resolve_container(
|
|
390
|
+
raw_container, project_path, project_name,
|
|
391
|
+
container_runtime, local_runtime,
|
|
392
|
+
)
|
|
393
|
+
else:
|
|
394
|
+
default_container = raw_container
|
|
395
|
+
|
|
396
|
+
# Build runner from target config
|
|
397
|
+
if runner_config:
|
|
398
|
+
runner = ASTRAContainerRunner(
|
|
399
|
+
project_root=str(project_path),
|
|
400
|
+
backend=backend,
|
|
401
|
+
default_container=default_container,
|
|
402
|
+
target_config=runner_config,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
runner = ASTRAContainerRunner(
|
|
406
|
+
project_root=str(project_path),
|
|
407
|
+
backend=backend,
|
|
408
|
+
default_container=default_container,
|
|
409
|
+
container_runtime=local_runtime,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
assets = build_asset_definitions(
|
|
413
|
+
spec, runner=runner, universe_id=universe_id, project_path=project_path,
|
|
414
|
+
project_name=project_name, no_build=no_build,
|
|
415
|
+
container_runtime=container_runtime, local_runtime=local_runtime,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return dg.Definitions(assets=assets)
|