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
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Container image building from Containerfiles.
|
|
2
|
+
|
|
3
|
+
Resolves container specs in astra.yaml — a single string that is either
|
|
4
|
+
a pre-built image name (e.g., ``python:3.9``) or a path to a Containerfile
|
|
5
|
+
(e.g., ``Containerfile``, ``containers/Dockerfile``). The runtime figures
|
|
6
|
+
out whether to pull or build by checking if the path exists as a file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import logging
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Files whose contents contribute to the image tag hash.
|
|
21
|
+
DEPENDENCY_FILES = (
|
|
22
|
+
"requirements.txt",
|
|
23
|
+
"requirements-dev.txt",
|
|
24
|
+
"requirements-test.txt",
|
|
25
|
+
"pyproject.toml",
|
|
26
|
+
"setup.py",
|
|
27
|
+
"setup.cfg",
|
|
28
|
+
"poetry.lock",
|
|
29
|
+
"Pipfile.lock",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_container_runtime() -> str | None:
|
|
34
|
+
"""Detect which container runtime is available locally.
|
|
35
|
+
|
|
36
|
+
Checks for Docker first, then Podman. Returns the binary name
|
|
37
|
+
(``"docker"`` or ``"podman"``) or ``None`` if neither is found.
|
|
38
|
+
|
|
39
|
+
This does **not** check for ``podman-hpc``, which is only relevant
|
|
40
|
+
for SLURM targets and handled separately.
|
|
41
|
+
"""
|
|
42
|
+
for runtime in ("docker", "podman"):
|
|
43
|
+
if shutil.which(runtime) is not None:
|
|
44
|
+
return runtime
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ContainerBuildError(Exception):
|
|
49
|
+
"""Raised when a container image build fails."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ContainerBuildResult:
|
|
54
|
+
"""Result of building a container image."""
|
|
55
|
+
|
|
56
|
+
tag: str
|
|
57
|
+
already_existed: bool
|
|
58
|
+
exit_code: int = 0
|
|
59
|
+
stdout: str = ""
|
|
60
|
+
stderr: str = ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_dependency_files(project_path: Path) -> list[Path]:
|
|
64
|
+
"""Return sorted list of dependency files found in *project_path*."""
|
|
65
|
+
found: list[Path] = []
|
|
66
|
+
for name in DEPENDENCY_FILES:
|
|
67
|
+
p = project_path / name
|
|
68
|
+
if p.is_file():
|
|
69
|
+
found.append(p)
|
|
70
|
+
return sorted(found)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def hash_file_contents(files: list[Path]) -> str:
|
|
74
|
+
"""Return a SHA-256 hex digest of the concatenated contents of *files*."""
|
|
75
|
+
h = hashlib.sha256()
|
|
76
|
+
for f in files:
|
|
77
|
+
h.update(f.read_bytes())
|
|
78
|
+
return h.hexdigest()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compute_image_tag(
|
|
82
|
+
project_name: str,
|
|
83
|
+
containerfile: Path,
|
|
84
|
+
project_path: Path,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Compute a content-addressed image tag.
|
|
87
|
+
|
|
88
|
+
The tag is ``lc-<project_name>-<12-char-sha256>``. The hash covers
|
|
89
|
+
the Containerfile contents plus any dependency files found in the
|
|
90
|
+
project root.
|
|
91
|
+
"""
|
|
92
|
+
digest = hash_file_contents([containerfile, *find_dependency_files(project_path)])[:12]
|
|
93
|
+
# Sanitise project name for use as a Docker tag component.
|
|
94
|
+
safe_name = project_name.lower().replace(" ", "-")
|
|
95
|
+
return f"lc-{safe_name}-{digest}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def image_exists_locally(tag: str, runtime: str = "docker") -> bool:
|
|
99
|
+
"""Check whether *tag* exists in the local container image store."""
|
|
100
|
+
try:
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
[runtime, "image", "inspect", tag],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
check=False,
|
|
105
|
+
)
|
|
106
|
+
return result.returncode == 0
|
|
107
|
+
except FileNotFoundError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_image(
|
|
112
|
+
tag: str,
|
|
113
|
+
containerfile: Path,
|
|
114
|
+
context: Path,
|
|
115
|
+
build_args: dict[str, str] | None = None,
|
|
116
|
+
runtime: str = "docker",
|
|
117
|
+
) -> ContainerBuildResult:
|
|
118
|
+
"""Build a container image with the specified runtime (Docker or Podman).
|
|
119
|
+
|
|
120
|
+
Note: *build_args* is a low-level parameter available when calling this
|
|
121
|
+
function directly. The high-level :func:`resolve_container_spec` API does
|
|
122
|
+
not expose build args — pass ``--build-arg`` values by pre-building the
|
|
123
|
+
image and referencing it by name in ``astra.yaml``.
|
|
124
|
+
|
|
125
|
+
Raises :class:`ContainerBuildError` on failure.
|
|
126
|
+
"""
|
|
127
|
+
cmd: list[str] = [
|
|
128
|
+
runtime, "build",
|
|
129
|
+
"-t", tag,
|
|
130
|
+
"-f", str(containerfile),
|
|
131
|
+
]
|
|
132
|
+
for key, value in (build_args or {}).items():
|
|
133
|
+
cmd += ["--build-arg", f"{key}={value}"]
|
|
134
|
+
cmd.append(str(context))
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
138
|
+
except FileNotFoundError:
|
|
139
|
+
raise ContainerBuildError(
|
|
140
|
+
f"{runtime} is not installed or not on PATH. "
|
|
141
|
+
f"Install {runtime} to build container images."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if proc.returncode != 0:
|
|
145
|
+
raise ContainerBuildError(
|
|
146
|
+
f"{runtime} build failed (exit code {proc.returncode}):\n{proc.stderr}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return ContainerBuildResult(
|
|
150
|
+
tag=tag,
|
|
151
|
+
already_existed=False,
|
|
152
|
+
exit_code=proc.returncode,
|
|
153
|
+
stdout=proc.stdout,
|
|
154
|
+
stderr=proc.stderr,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def is_containerfile(spec: str, project_path: Path) -> bool:
|
|
159
|
+
"""Return ``True`` if *spec* refers to an existing file (Containerfile)."""
|
|
160
|
+
return (project_path / spec).is_file()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def resolve_container_spec(
|
|
164
|
+
spec: str | None,
|
|
165
|
+
project_path: Path,
|
|
166
|
+
project_name: str,
|
|
167
|
+
*,
|
|
168
|
+
force: bool = False,
|
|
169
|
+
dry_run: bool = False,
|
|
170
|
+
runtime: str = "docker",
|
|
171
|
+
) -> str | None:
|
|
172
|
+
"""Resolve a container spec to an image tag string.
|
|
173
|
+
|
|
174
|
+
* ``None`` -> ``None``
|
|
175
|
+
* ``str`` pointing to an existing file -> build from Containerfile
|
|
176
|
+
* ``str`` otherwise -> returned as-is (pre-built image name)
|
|
177
|
+
|
|
178
|
+
If *dry_run* is ``True``, returns the tag that *would* be used without
|
|
179
|
+
actually building.
|
|
180
|
+
|
|
181
|
+
.. warning::
|
|
182
|
+
Any string that does not resolve to an existing file is treated as a
|
|
183
|
+
pre-built image name. A typo such as ``container: Containerfle``
|
|
184
|
+
will *not* raise an error here — the failure surfaces later at
|
|
185
|
+
execution time with a cryptic "image not found" message. Double-check
|
|
186
|
+
Containerfile paths with ``lc build --dry-run`` to catch mistakes
|
|
187
|
+
early.
|
|
188
|
+
"""
|
|
189
|
+
if spec is None:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
if not is_containerfile(spec, project_path):
|
|
193
|
+
# Pre-built image name — return as-is.
|
|
194
|
+
return spec
|
|
195
|
+
|
|
196
|
+
containerfile = project_path / spec
|
|
197
|
+
tag = compute_image_tag(project_name, containerfile, project_path)
|
|
198
|
+
|
|
199
|
+
if dry_run:
|
|
200
|
+
return tag
|
|
201
|
+
|
|
202
|
+
if not force and image_exists_locally(tag, runtime=runtime):
|
|
203
|
+
logger.info("Image %s already exists, skipping build.", tag)
|
|
204
|
+
return tag
|
|
205
|
+
|
|
206
|
+
logger.info("Building image %s from %s ...", tag, containerfile)
|
|
207
|
+
build_image(tag, containerfile, project_path, runtime=runtime)
|
|
208
|
+
return tag
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class ContainerStatus:
|
|
213
|
+
"""Status information for a container spec."""
|
|
214
|
+
|
|
215
|
+
type: str # "none", "prebuilt", "build"
|
|
216
|
+
image: str | None = None
|
|
217
|
+
exists: bool | None = None
|
|
218
|
+
containerfile: str | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# HPC container runtimes (podman-hpc)
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_image_podman_hpc(
|
|
227
|
+
tag: str,
|
|
228
|
+
containerfile: Path,
|
|
229
|
+
context: Path,
|
|
230
|
+
build_args: dict[str, str] | None = None,
|
|
231
|
+
) -> ContainerBuildResult:
|
|
232
|
+
"""Build a container image with ``podman-hpc build``.
|
|
233
|
+
|
|
234
|
+
Runs on NERSC login nodes. After building, the image is automatically
|
|
235
|
+
migrated so it is available on compute nodes.
|
|
236
|
+
|
|
237
|
+
Note: *build_args* is a low-level parameter available when calling this
|
|
238
|
+
function directly. The high-level :func:`resolve_container_for_slurm` API
|
|
239
|
+
does not expose build args — see :func:`build_image` for details.
|
|
240
|
+
|
|
241
|
+
Raises :class:`ContainerBuildError` on failure.
|
|
242
|
+
"""
|
|
243
|
+
cmd: list[str] = [
|
|
244
|
+
"podman-hpc", "build",
|
|
245
|
+
"-t", tag,
|
|
246
|
+
"-f", str(containerfile),
|
|
247
|
+
]
|
|
248
|
+
for key, value in (build_args or {}).items():
|
|
249
|
+
cmd += ["--build-arg", f"{key}={value}"]
|
|
250
|
+
cmd.append(str(context))
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
254
|
+
except FileNotFoundError:
|
|
255
|
+
raise ContainerBuildError(
|
|
256
|
+
"podman-hpc is not installed or not on PATH. "
|
|
257
|
+
"Are you running on a NERSC login node?"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if proc.returncode != 0:
|
|
261
|
+
raise ContainerBuildError(
|
|
262
|
+
f"podman-hpc build failed (exit code {proc.returncode}):\n{proc.stderr}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Migrate the image so compute nodes can access it.
|
|
266
|
+
_podman_hpc_migrate(tag)
|
|
267
|
+
|
|
268
|
+
return ContainerBuildResult(
|
|
269
|
+
tag=tag,
|
|
270
|
+
already_existed=False,
|
|
271
|
+
exit_code=proc.returncode,
|
|
272
|
+
stdout=proc.stdout,
|
|
273
|
+
stderr=proc.stderr,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _podman_hpc_migrate(tag: str) -> None:
|
|
278
|
+
"""Run ``podman-hpc migrate <tag>`` to make image available on compute nodes."""
|
|
279
|
+
try:
|
|
280
|
+
proc = subprocess.run(
|
|
281
|
+
["podman-hpc", "migrate", tag],
|
|
282
|
+
capture_output=True,
|
|
283
|
+
text=True,
|
|
284
|
+
check=False,
|
|
285
|
+
)
|
|
286
|
+
except FileNotFoundError:
|
|
287
|
+
raise ContainerBuildError("podman-hpc not found — cannot migrate image.")
|
|
288
|
+
if proc.returncode != 0:
|
|
289
|
+
raise ContainerBuildError(
|
|
290
|
+
f"podman-hpc migrate failed (exit code {proc.returncode}):\n{proc.stderr}"
|
|
291
|
+
)
|
|
292
|
+
logger.info("podman-hpc migrate %s succeeded.", tag)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def image_exists_podman_hpc(tag: str) -> bool:
|
|
296
|
+
"""Check whether *tag* exists in the local podman-hpc image store."""
|
|
297
|
+
try:
|
|
298
|
+
result = subprocess.run(
|
|
299
|
+
["podman-hpc", "image", "exists", tag],
|
|
300
|
+
capture_output=True,
|
|
301
|
+
check=False,
|
|
302
|
+
)
|
|
303
|
+
return result.returncode == 0
|
|
304
|
+
except FileNotFoundError:
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def resolve_container_for_slurm(
|
|
309
|
+
spec: str | None,
|
|
310
|
+
project_path: Path,
|
|
311
|
+
project_name: str,
|
|
312
|
+
container_runtime: str,
|
|
313
|
+
*,
|
|
314
|
+
force: bool = False,
|
|
315
|
+
) -> str | None:
|
|
316
|
+
"""Resolve a container spec for SLURM execution, building if needed.
|
|
317
|
+
|
|
318
|
+
Containerfile paths (strings pointing to existing files) are built with
|
|
319
|
+
``podman-hpc build`` and migrated automatically. Pre-built image names
|
|
320
|
+
are migrated if not already available.
|
|
321
|
+
|
|
322
|
+
Returns the image tag string to use, or ``None`` if no container.
|
|
323
|
+
"""
|
|
324
|
+
if spec is None:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
if not is_containerfile(spec, project_path):
|
|
328
|
+
# Pre-built image reference
|
|
329
|
+
if not force and image_exists_podman_hpc(spec):
|
|
330
|
+
logger.info("Image %s already available in podman-hpc, skipping migrate.", spec)
|
|
331
|
+
else:
|
|
332
|
+
logger.info("Migrating %s for podman-hpc compute nodes...", spec)
|
|
333
|
+
_podman_hpc_migrate(spec)
|
|
334
|
+
return spec
|
|
335
|
+
|
|
336
|
+
# Containerfile path — build from source.
|
|
337
|
+
containerfile = project_path / spec
|
|
338
|
+
tag = compute_image_tag(project_name, containerfile, project_path)
|
|
339
|
+
|
|
340
|
+
if not force and image_exists_podman_hpc(tag):
|
|
341
|
+
logger.info("Image %s already exists in podman-hpc, skipping build.", tag)
|
|
342
|
+
return tag
|
|
343
|
+
|
|
344
|
+
logger.info("Building image %s with podman-hpc from %s ...", tag, containerfile)
|
|
345
|
+
build_image_podman_hpc(tag, containerfile, project_path)
|
|
346
|
+
return tag
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_container_status(
|
|
350
|
+
spec: str | None,
|
|
351
|
+
project_path: Path,
|
|
352
|
+
project_name: str,
|
|
353
|
+
runtime: str = "docker",
|
|
354
|
+
) -> ContainerStatus:
|
|
355
|
+
"""Return status information for a container spec without building."""
|
|
356
|
+
if spec is None:
|
|
357
|
+
return ContainerStatus(type="none")
|
|
358
|
+
|
|
359
|
+
if not is_containerfile(spec, project_path):
|
|
360
|
+
return ContainerStatus(type="prebuilt", image=spec)
|
|
361
|
+
|
|
362
|
+
containerfile = project_path / spec
|
|
363
|
+
tag = compute_image_tag(project_name, containerfile, project_path)
|
|
364
|
+
exists = image_exists_locally(tag, runtime=runtime)
|
|
365
|
+
return ContainerStatus(
|
|
366
|
+
type="build",
|
|
367
|
+
image=tag,
|
|
368
|
+
exists=exists,
|
|
369
|
+
containerfile=spec,
|
|
370
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""ASTRA IO Manager for Dagster — maps (asset, universe) to filesystem paths."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ASTRAIOManager:
|
|
8
|
+
"""Maps ASTRA outputs to filesystem paths following ASTRA conventions.
|
|
9
|
+
|
|
10
|
+
Path convention: results/<universe_id>/<output_id>/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_root: str):
|
|
14
|
+
self.project_root = Path(project_root)
|
|
15
|
+
|
|
16
|
+
def get_output_path(self, output_id: str, universe_id: str) -> Path:
|
|
17
|
+
"""Get the filesystem path for an output in a given universe."""
|
|
18
|
+
return self.project_root / "results" / universe_id / output_id
|
|
19
|
+
|
|
20
|
+
def get_input_paths(
|
|
21
|
+
self, input_ids: list[str], universe_id: str
|
|
22
|
+
) -> dict[str, Path]:
|
|
23
|
+
"""Get filesystem paths for input dependencies."""
|
|
24
|
+
return {
|
|
25
|
+
inp_id: self.get_output_path(inp_id, universe_id)
|
|
26
|
+
for inp_id in input_ids
|
|
27
|
+
}
|