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.
Files changed (46) hide show
  1. lightcone/cli/__init__.py +16 -0
  2. lightcone/cli/claude/lightcone/agents/lc-extractor.md +114 -0
  3. lightcone/cli/claude/lightcone/guides/astra-reference.md +290 -0
  4. lightcone/cli/claude/lightcone/guides/lightcone-cli-reference.md +75 -0
  5. lightcone/cli/claude/lightcone/guides/ui-brand.md +86 -0
  6. lightcone/cli/claude/lightcone/hooks/langfuse_git_commit_hook.py +303 -0
  7. lightcone/cli/claude/lightcone/hooks/langfuse_hook.py +894 -0
  8. lightcone/cli/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +142 -0
  9. lightcone/cli/claude/lightcone/hooks/langfuse_session_init_hook.py +83 -0
  10. lightcone/cli/claude/lightcone/hooks/langfuse_utils.py +457 -0
  11. lightcone/cli/claude/lightcone/scripts/activate-venv.sh +44 -0
  12. lightcone/cli/claude/lightcone/scripts/check-lc-run.sh +140 -0
  13. lightcone/cli/claude/lightcone/scripts/session-start.sh +140 -0
  14. lightcone/cli/claude/lightcone/scripts/validate-on-save.sh +77 -0
  15. lightcone/cli/claude/lightcone/skills/lc-build/SKILL.md +92 -0
  16. lightcone/cli/claude/lightcone/skills/lc-build/assets/loop-prompt.md +92 -0
  17. lightcone/cli/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +240 -0
  18. lightcone/cli/claude/lightcone/skills/lc-feedback/SKILL.md +94 -0
  19. lightcone/cli/claude/lightcone/skills/lc-migrate/SKILL.md +98 -0
  20. lightcone/cli/claude/lightcone/skills/lc-new/SKILL.md +183 -0
  21. lightcone/cli/claude/lightcone/skills/lc-verify/SKILL.md +53 -0
  22. lightcone/cli/claude/lightcone/templates/CLAUDE.md +32 -0
  23. lightcone/cli/commands.py +2327 -0
  24. lightcone/cli/plugin.py +34 -0
  25. lightcone/engine/__init__.py +42 -0
  26. lightcone/engine/assets.py +418 -0
  27. lightcone/engine/container.py +370 -0
  28. lightcone/engine/io_manager.py +27 -0
  29. lightcone/engine/runner.py +1017 -0
  30. lightcone/engine/site_registry.py +142 -0
  31. lightcone/engine/status.py +135 -0
  32. lightcone/engine/targets.py +68 -0
  33. lightcone/engine/tree.py +245 -0
  34. lightcone/eval/__init__.py +25 -0
  35. lightcone/eval/build.py +148 -0
  36. lightcone/eval/cli.py +176 -0
  37. lightcone/eval/graders.py +192 -0
  38. lightcone/eval/harness.py +265 -0
  39. lightcone/eval/models.py +117 -0
  40. lightcone/eval/report.py +214 -0
  41. lightcone/eval/sandbox.py +394 -0
  42. lightcone_cli-0.2.0.dist-info/METADATA +16 -0
  43. lightcone_cli-0.2.0.dist-info/RECORD +46 -0
  44. lightcone_cli-0.2.0.dist-info/WHEEL +4 -0
  45. lightcone_cli-0.2.0.dist-info/entry_points.txt +2 -0
  46. 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
+ }