lightcone-cli 0.3.7__tar.gz → 0.4.1__tar.gz
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-0.3.7 → lightcone_cli-0.4.1}/.gitignore +0 -2
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/PKG-INFO +4 -3
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/pyproject.toml +15 -1
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/cli/commands.py +291 -58
- lightcone_cli-0.4.1/src/lightcone/engine/cloudbuild.py +415 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/container.py +138 -23
- lightcone_cli-0.4.1/src/lightcone/engine/dask_cluster.py +516 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/manifest.py +7 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/scratch.py +5 -9
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/site_registry.py +42 -4
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/snakefile.py +15 -6
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/status.py +10 -2
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/snakemake_executor_plugin_dask/__init__.py +8 -4
- lightcone_cli-0.4.1/src/snakemake_executor_plugin_dask/executor.py +229 -0
- lightcone_cli-0.3.7/src/lightcone/engine/dask_cluster.py +0 -252
- lightcone_cli-0.3.7/src/snakemake_executor_plugin_dask/executor.py +0 -173
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/LICENSE +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/README.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/agents/lc-extractor.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/hooks.json +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/scripts/activate-venv.sh +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/scripts/session-start.sh +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/scripts/validate-on-save.sh +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/README.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/astra/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/check-sentence-by-sentence/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/figure-comparison/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-cli/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-feedback/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-code/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/architect.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/compare.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/implement.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/literature.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/orient.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/review.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/run.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/references/specify.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/templates/CLAUDE.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-from-paper/templates/constitution.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/lc-new/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/paper-extraction/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/paper-extraction/references/arxiv-source.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/paper-extraction/references/pdf-fallback.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/paper-extraction/scripts/extract-paper-substrate.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/ralph/SKILL.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/ralph/references/constitution.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/ralph/references/crafting.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/skills/ralph/scripts/ralph +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/claude/lightcone/templates/CLAUDE.md +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/cli/__init__.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/cli/plugin.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/__init__.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/runner.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/tree.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/validation.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/verify.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/engine/wrroc.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/__init__.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/build.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/cli.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/graders.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/harness.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/models.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/report.py +0 -0
- {lightcone_cli-0.3.7 → lightcone_cli-0.4.1}/src/lightcone/eval/sandbox.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: lightcone-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Lightcone command-line toolchain
|
|
5
5
|
Author: Lightcone Research
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -20,8 +20,9 @@ Classifier: Topic :: Scientific/Engineering
|
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
|
-
Requires-Dist: astra-tools
|
|
23
|
+
Requires-Dist: astra-tools==0.2.11
|
|
24
24
|
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: dask-gateway>=2024.1
|
|
25
26
|
Requires-Dist: dask>=2024.1
|
|
26
27
|
Requires-Dist: distributed>=2024.1
|
|
27
28
|
Requires-Dist: jinja2>=3.0
|
|
@@ -26,7 +26,9 @@ classifiers = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
dependencies = [
|
|
29
|
-
|
|
29
|
+
# Pinned: astra-tools 0.2.14 made `astra init` idempotent, changing
|
|
30
|
+
# the callback signature this release's `lc init` delegates to.
|
|
31
|
+
"astra-tools==0.2.11",
|
|
30
32
|
"click>=8.0",
|
|
31
33
|
"pyyaml>=6.0",
|
|
32
34
|
"rich>=13.0",
|
|
@@ -36,6 +38,11 @@ dependencies = [
|
|
|
36
38
|
"snakemake-interface-common>=1.14",
|
|
37
39
|
"dask>=2024.1",
|
|
38
40
|
"distributed>=2024.1",
|
|
41
|
+
# Dask Gateway client for JupyterHub/Kubernetes deployments. A
|
|
42
|
+
# normal dependency (not an extra) so `lc run` works out of the box
|
|
43
|
+
# on a hub and scaffolded project images inherit it via their
|
|
44
|
+
# lightcone-cli pin.
|
|
45
|
+
"dask-gateway>=2024.1",
|
|
39
46
|
"rocrate>=0.11",
|
|
40
47
|
]
|
|
41
48
|
|
|
@@ -104,6 +111,13 @@ module = ["dask.*", "distributed.*"]
|
|
|
104
111
|
ignore_missing_imports = true
|
|
105
112
|
follow_untyped_imports = true
|
|
106
113
|
|
|
114
|
+
[[tool.mypy.overrides]]
|
|
115
|
+
# Untyped and without explicit re-exports; skip so `from dask_gateway
|
|
116
|
+
# import Gateway` doesn't trip strict no_implicit_reexport.
|
|
117
|
+
module = ["dask_gateway", "dask_gateway.*"]
|
|
118
|
+
ignore_missing_imports = true
|
|
119
|
+
follow_imports = "skip"
|
|
120
|
+
|
|
107
121
|
[[tool.mypy.overrides]]
|
|
108
122
|
module = ["rocrate.*"]
|
|
109
123
|
ignore_missing_imports = true
|
|
@@ -6,7 +6,8 @@ The redesigned CLI is a thin shim over Snakemake. Provenance integrity
|
|
|
6
6
|
``astra.yaml`` and shells out to ``snakemake``.
|
|
7
7
|
|
|
8
8
|
Commands:
|
|
9
|
-
- ``lc init`` — scaffold a project (CLAUDE.md, .claude/, venv, gitignore
|
|
9
|
+
- ``lc init`` — scaffold a project (CLAUDE.md, .claude/, venv, gitignore,
|
|
10
|
+
MyST report template).
|
|
10
11
|
- ``lc run`` — generate Snakefile and run snakemake.
|
|
11
12
|
- ``lc status`` — manifest-driven status walk (no Snakemake needed).
|
|
12
13
|
- ``lc verify`` — recompute hashes and validate the provenance chain.
|
|
@@ -172,7 +173,8 @@ def init(
|
|
|
172
173
|
base ``.gitignore``, ``src/``) to ``astra init``, then layers on the
|
|
173
174
|
lightcone-specific bits: ``Containerfile`` + ``requirements.txt``,
|
|
174
175
|
``.lightcone/`` project state, ``.claude/`` plugin bundle, ``CLAUDE.md``,
|
|
175
|
-
and an optional
|
|
176
|
+
a template MyST report (``myst.yml`` + ``index.md``), and an optional
|
|
177
|
+
Python venv.
|
|
176
178
|
"""
|
|
177
179
|
console.print(f"[cyan]{_LIGHTCONE}[/cyan]")
|
|
178
180
|
|
|
@@ -180,6 +182,7 @@ def init(
|
|
|
180
182
|
|
|
181
183
|
from lightcone.engine.site_registry import detect_current_site
|
|
182
184
|
|
|
185
|
+
site = detect_current_site()
|
|
183
186
|
directory = directory.resolve()
|
|
184
187
|
|
|
185
188
|
if (directory / "astra.yaml").exists():
|
|
@@ -204,8 +207,16 @@ def init(
|
|
|
204
207
|
"container: python:3.12-slim", "container: Containerfile", 1
|
|
205
208
|
)
|
|
206
209
|
)
|
|
210
|
+
# One scaffold everywhere — the Containerfile is agnostic to the
|
|
211
|
+
# execution environment. lightcone-cli in requirements.txt brings
|
|
212
|
+
# the whole execution stack (snakemake, dask, distributed,
|
|
213
|
+
# dask-gateway) so the same image can wrap recipes locally or run
|
|
214
|
+
# as a Dask Gateway worker pod on a hub; anything pod-specific
|
|
215
|
+
# (uid, mounts) is deployment configuration, not image content.
|
|
207
216
|
(directory / "Containerfile").write_text(_CONTAINERFILE)
|
|
208
|
-
(directory / "requirements.txt").write_text(
|
|
217
|
+
(directory / "requirements.txt").write_text(
|
|
218
|
+
_REQUIREMENTS + _lightcone_requirement()
|
|
219
|
+
)
|
|
209
220
|
|
|
210
221
|
# Append lightcone-specific entries to the .gitignore astra wrote.
|
|
211
222
|
gitignore_path = directory / ".gitignore"
|
|
@@ -231,6 +242,11 @@ def init(
|
|
|
231
242
|
# Project CLAUDE.md (a stub)
|
|
232
243
|
(directory / "CLAUDE.md").write_text(_PROJECT_CLAUDE_MD)
|
|
233
244
|
|
|
245
|
+
# Template MyST report. MyST support is a recommended add-on on top of
|
|
246
|
+
# the spec, not part of it — which is why the report scaffold lives here
|
|
247
|
+
# and not in `astra init`.
|
|
248
|
+
_create_report_template(directory)
|
|
249
|
+
|
|
234
250
|
# git init last so the initial commit captures every scaffolded file.
|
|
235
251
|
no_git = no_git or (directory / ".git").exists()
|
|
236
252
|
if not no_git:
|
|
@@ -287,7 +303,6 @@ def init(
|
|
|
287
303
|
# state (snakemake metadata, dask spill, cross-node locks). On NERSC
|
|
288
304
|
# this is critical: $HOME and CFS are mounted via DVS (no flock, slow
|
|
289
305
|
# small-file I/O), so lightcone keeps everything on $SCRATCH (Lustre).
|
|
290
|
-
site = detect_current_site()
|
|
291
306
|
if site:
|
|
292
307
|
scratch_expr = scratch_override or site.get("scratch_root")
|
|
293
308
|
if scratch_expr:
|
|
@@ -305,6 +320,10 @@ def init(
|
|
|
305
320
|
"[cyan]/lc-from-code[/cyan] to port existing code, "
|
|
306
321
|
"or [cyan]/lc-from-paper[/cyan] to reproduce a paper"
|
|
307
322
|
)
|
|
323
|
+
console.print(
|
|
324
|
+
" • Preview the report with [cyan]myst start[/cyan] "
|
|
325
|
+
"(requires the MyST CLI: [cyan]npm i -g mystmd[/cyan])"
|
|
326
|
+
)
|
|
308
327
|
|
|
309
328
|
|
|
310
329
|
_CONTAINERFILE = """\
|
|
@@ -313,16 +332,7 @@ FROM python:3.12-slim
|
|
|
313
332
|
WORKDIR /app
|
|
314
333
|
|
|
315
334
|
COPY requirements.txt .
|
|
316
|
-
|
|
317
|
-
# Install curl and certificates, then clean up to keep image size small
|
|
318
|
-
RUN apt-get update && \
|
|
319
|
-
apt-get install -y --no-install-recommends curl ca-certificates && \
|
|
320
|
-
rm -rf /var/lib/apt/lists/*
|
|
321
|
-
|
|
322
|
-
# Install uv
|
|
323
|
-
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
324
|
-
ENV PATH="/root/.local/bin:$PATH"
|
|
325
|
-
RUN uv pip install -r requirements.txt
|
|
335
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
326
336
|
|
|
327
337
|
COPY . .
|
|
328
338
|
"""
|
|
@@ -334,6 +344,31 @@ pandas
|
|
|
334
344
|
"""
|
|
335
345
|
|
|
336
346
|
|
|
347
|
+
def _lightcone_requirement() -> str:
|
|
348
|
+
"""Requirements lines pinning lightcone-cli into the project image.
|
|
349
|
+
|
|
350
|
+
The project image must be able to execute rules on any backend —
|
|
351
|
+
including as a Dask Gateway worker pod, where the dask worker and
|
|
352
|
+
the child snakemake run *inside* the image. lightcone-cli carries
|
|
353
|
+
that whole stack (snakemake, dask, distributed, dask-gateway) as
|
|
354
|
+
normal dependencies, so one line covers it. The pin mirrors the
|
|
355
|
+
version running ``lc init`` to keep driver and image in lockstep;
|
|
356
|
+
dev builds fall back to unpinned (their version isn't published).
|
|
357
|
+
"""
|
|
358
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
v = version("lightcone-cli")
|
|
362
|
+
except PackageNotFoundError:
|
|
363
|
+
v = ""
|
|
364
|
+
pin = f"lightcone-cli=={v}" if v and "dev" not in v else "lightcone-cli"
|
|
365
|
+
return (
|
|
366
|
+
"\n# Execution stack — lets this image run rules on any backend,\n"
|
|
367
|
+
"# including as a Dask Gateway worker pod.\n"
|
|
368
|
+
f"{pin}\n"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
337
372
|
_GITIGNORE_APPEND = """
|
|
338
373
|
# lightcone-cli
|
|
339
374
|
.lightcone/Snakefile
|
|
@@ -341,6 +376,68 @@ _GITIGNORE_APPEND = """
|
|
|
341
376
|
.snakemake/
|
|
342
377
|
.snakemake.legacy/
|
|
343
378
|
results/
|
|
379
|
+
|
|
380
|
+
# MyST build output
|
|
381
|
+
_build/
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _create_report_template(directory: Path) -> None:
|
|
386
|
+
"""Write ``myst.yml`` + ``index.md`` — a starter report for the analysis.
|
|
387
|
+
|
|
388
|
+
The report references the boilerplate ``astra.yaml`` elements by path via
|
|
389
|
+
the MySTRA plugin, so the ids used below must track the ``astra init``
|
|
390
|
+
boilerplate (``example_method``, ``main_result``).
|
|
391
|
+
"""
|
|
392
|
+
name = directory.name or "My Analysis"
|
|
393
|
+
(directory / "myst.yml").write_text(_MYST_YML)
|
|
394
|
+
(directory / "index.md").write_text(f"# {name}\n" + _INDEX_MD_BODY)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
_MYST_YML = """\
|
|
398
|
+
# MyST configuration for the analysis report (https://mystmd.org/).
|
|
399
|
+
# The MySTRA plugin resolves {astra}`...` references against astra.yaml.
|
|
400
|
+
# `latest` always tracks the newest MySTRA release; for a reproducible
|
|
401
|
+
# build, pin a tag instead, e.g. .../releases/download/v0.0.1/mystra.mjs
|
|
402
|
+
version: 1
|
|
403
|
+
project:
|
|
404
|
+
plugins:
|
|
405
|
+
- https://github.com/LightconeResearch/MySTRA/releases/latest/download/mystra.mjs
|
|
406
|
+
toc:
|
|
407
|
+
- file: index.md
|
|
408
|
+
site:
|
|
409
|
+
template: book-theme
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
_INDEX_MD_BODY = """
|
|
414
|
+
> **TODO:** this report was scaffolded by `lc init`. It references the
|
|
415
|
+
> analysis elements declared in `astra.yaml` *by path* — figures, decisions,
|
|
416
|
+
> and numbers stay single-sourced in the analysis, so never hard-type a
|
|
417
|
+
> measured value here. Preview with `myst start` (requires the MyST CLI).
|
|
418
|
+
|
|
419
|
+
## Introduction
|
|
420
|
+
|
|
421
|
+
TODO: the research question, its context, and why it matters.
|
|
422
|
+
|
|
423
|
+
## Methods
|
|
424
|
+
|
|
425
|
+
TODO: describe the approach. Reference the decisions the analysis exposes
|
|
426
|
+
rather than restating them — for example, we adopt the
|
|
427
|
+
{astra}`decisions.example_method` for this analysis:
|
|
428
|
+
|
|
429
|
+
:::{astra} decisions.example_method
|
|
430
|
+
:::
|
|
431
|
+
|
|
432
|
+
## Results
|
|
433
|
+
|
|
434
|
+
TODO: present the outputs. Once `lc run` has materialized results, pull
|
|
435
|
+
numbers in live, e.g.:
|
|
436
|
+
|
|
437
|
+
% The analysis yields {astra:value}`outputs.main_result`.
|
|
438
|
+
|
|
439
|
+
:::{astra} outputs
|
|
440
|
+
:::
|
|
344
441
|
"""
|
|
345
442
|
|
|
346
443
|
_PROJECT_CLAUDE_MD = """# Project Notes for Claude
|
|
@@ -367,6 +464,14 @@ lc verify # validate the provenance chain
|
|
|
367
464
|
Outputs land in `results/<universe>/<output_id>/` along with a sidecar
|
|
368
465
|
`.lightcone-manifest.json` recording the recipe, container, decisions,
|
|
369
466
|
input hashes, and output hash.
|
|
467
|
+
|
|
468
|
+
## Report
|
|
469
|
+
|
|
470
|
+
`index.md` + `myst.yml` are a template MyST report wired to the MySTRA
|
|
471
|
+
plugin. The report references analysis elements by path — inline mentions
|
|
472
|
+
with the `{astra}` role, block embeds with the `{astra}` directive, live
|
|
473
|
+
numbers with `{astra:value}` — so never hard-type a measured value in the
|
|
474
|
+
prose. Preview with `myst start` (requires the MyST CLI, `npm i -g mystmd`).
|
|
370
475
|
"""
|
|
371
476
|
|
|
372
477
|
|
|
@@ -456,13 +561,14 @@ def run(
|
|
|
456
561
|
"""Materialize outputs declared in astra.yaml.
|
|
457
562
|
|
|
458
563
|
Always dispatches through a Dask cluster: a ``LocalCluster`` on a
|
|
459
|
-
workstation, srun-launched workers inside a SLURM allocation,
|
|
564
|
+
workstation, srun-launched workers inside a SLURM allocation, a
|
|
565
|
+
run-scoped Dask Gateway cluster on a JupyterHub deployment, or an
|
|
460
566
|
existing scheduler if ``DASK_SCHEDULER_ADDRESS`` is set.
|
|
461
567
|
"""
|
|
462
568
|
_abort_on_perlmutter_login()
|
|
463
569
|
|
|
464
570
|
from lightcone.engine.container import load_runtime
|
|
465
|
-
from lightcone.engine.dask_cluster import cluster_for_run
|
|
571
|
+
from lightcone.engine.dask_cluster import cluster_for_run, gateway_branch_active
|
|
466
572
|
from lightcone.engine.scratch import (
|
|
467
573
|
RunLockBusyError,
|
|
468
574
|
acquire_run_lock,
|
|
@@ -487,11 +593,26 @@ def run(
|
|
|
487
593
|
console.print(f"[dim]Scratch root:[/dim] {resolve_scratch_root(project)}")
|
|
488
594
|
|
|
489
595
|
choice = load_runtime(project_path=project)
|
|
490
|
-
_ensure_images(project, runtime=choice.runtime)
|
|
596
|
+
images = _ensure_images(project, runtime=choice.runtime)
|
|
491
597
|
snakefile_path, cfg_path = generate(
|
|
492
598
|
project, universes=universes, runtime=choice.runtime
|
|
493
599
|
)
|
|
494
600
|
|
|
601
|
+
# On the Gateway branch the cluster is created with one image — the
|
|
602
|
+
# worker pod is the container for every rule, so a spec declaring
|
|
603
|
+
# several distinct containers cannot be honoured per-rule.
|
|
604
|
+
worker_image: str | None = None
|
|
605
|
+
if gateway_branch_active():
|
|
606
|
+
if len(images) > 1:
|
|
607
|
+
raise click.ClickException(
|
|
608
|
+
"This deployment runs recipes natively in worker pods, "
|
|
609
|
+
"which supports one container image per run; astra.yaml "
|
|
610
|
+
"declares several: " + ", ".join(images) + ". "
|
|
611
|
+
"Consolidate on a single Containerfile (or one shared "
|
|
612
|
+
"prebuilt image)."
|
|
613
|
+
)
|
|
614
|
+
worker_image = images[0] if images else None
|
|
615
|
+
|
|
495
616
|
# Provenance guard: when ``runtime: auto`` silently fell back to
|
|
496
617
|
# ``none`` and the spec declares any containers, the recipe will run
|
|
497
618
|
# on the host while the manifest's ``container_image`` field still
|
|
@@ -557,57 +678,80 @@ def run(
|
|
|
557
678
|
raise click.ClickException(str(e))
|
|
558
679
|
|
|
559
680
|
with cluster_for_run(
|
|
560
|
-
verbose=verbose,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
# ``flock`` before forwarding a rule's lightcone output, so
|
|
567
|
-
# parallel rules' blocks never interleave at the line level
|
|
568
|
-
# — even across nodes. The lockfile sits under our scratch
|
|
569
|
-
# root specifically to avoid DVS on NERSC.
|
|
570
|
-
"LIGHTCONE_OUT_LOCK": str(rundirs.lock_path),
|
|
571
|
-
}
|
|
681
|
+
verbose=verbose,
|
|
682
|
+
local_directory=str(rundirs.dask_local),
|
|
683
|
+
worker_image=worker_image,
|
|
684
|
+
max_workers=int(n),
|
|
685
|
+
) as cluster_env:
|
|
686
|
+
env = {**os.environ, **cluster_env}
|
|
572
687
|
if verbose:
|
|
573
688
|
console.print(f"[dim]$ {' '.join(cmd)}[/dim]")
|
|
574
|
-
|
|
575
|
-
|
|
689
|
+
sys.exit(
|
|
690
|
+
_run_snakemake(
|
|
691
|
+
cmd, env=env, scratch_root=rundirs.root, verbose=verbose
|
|
692
|
+
)
|
|
693
|
+
)
|
|
576
694
|
|
|
577
695
|
|
|
578
|
-
def
|
|
696
|
+
def _run_snakemake(
|
|
579
697
|
cmd: list[str],
|
|
580
698
|
*,
|
|
581
699
|
env: dict[str, str],
|
|
582
700
|
scratch_root: Path,
|
|
701
|
+
verbose: bool,
|
|
583
702
|
) -> int:
|
|
584
|
-
"""Run snakemake
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
703
|
+
"""Run snakemake, forwarding the run's narrative output.
|
|
704
|
+
|
|
705
|
+
The executor plugin prints each finished rule's block of
|
|
706
|
+
sentinel-prefixed lines (see :data:`lightcone.engine.runner.SENTINEL`)
|
|
707
|
+
on the snakemake process's stdout. We forward those lines — prefix
|
|
708
|
+
stripped — to the terminal and drop everything else snakemake emits
|
|
709
|
+
(DAG chatter, job stats), so a run reads as a clean narrative.
|
|
710
|
+
Verbose mode forwards the noise too.
|
|
711
|
+
|
|
712
|
+
stderr is tailed into a bounded ring buffer so a workflow crash
|
|
713
|
+
leaves a real log behind without it being visible during a
|
|
714
|
+
successful run (verbose passes stderr straight through instead).
|
|
594
715
|
"""
|
|
595
716
|
from collections import deque
|
|
596
717
|
|
|
718
|
+
from lightcone.engine.runner import SENTINEL
|
|
719
|
+
|
|
597
720
|
proc = subprocess.Popen(
|
|
598
721
|
cmd,
|
|
599
722
|
env=env,
|
|
600
|
-
stdout=subprocess.
|
|
601
|
-
stderr=subprocess.PIPE,
|
|
723
|
+
stdout=subprocess.PIPE,
|
|
724
|
+
stderr=None if verbose else subprocess.PIPE,
|
|
602
725
|
text=True,
|
|
603
726
|
bufsize=1,
|
|
604
727
|
)
|
|
605
|
-
assert proc.
|
|
728
|
+
assert proc.stdout is not None
|
|
606
729
|
tail: deque[str] = deque(maxlen=400)
|
|
607
|
-
|
|
608
|
-
|
|
730
|
+
|
|
731
|
+
def _pump_stderr() -> None:
|
|
732
|
+
assert proc.stderr is not None
|
|
733
|
+
for line in proc.stderr:
|
|
734
|
+
tail.append(line)
|
|
735
|
+
|
|
736
|
+
import threading
|
|
737
|
+
|
|
738
|
+
stderr_thread: threading.Thread | None = None
|
|
739
|
+
if not verbose:
|
|
740
|
+
stderr_thread = threading.Thread(target=_pump_stderr, daemon=True)
|
|
741
|
+
stderr_thread.start()
|
|
742
|
+
|
|
743
|
+
for line in proc.stdout:
|
|
744
|
+
if line.startswith(SENTINEL):
|
|
745
|
+
sys.stdout.write(line[len(SENTINEL):])
|
|
746
|
+
sys.stdout.flush()
|
|
747
|
+
elif verbose:
|
|
748
|
+
sys.stdout.write(line)
|
|
749
|
+
sys.stdout.flush()
|
|
750
|
+
|
|
609
751
|
rc = proc.wait()
|
|
610
|
-
if
|
|
752
|
+
if stderr_thread is not None:
|
|
753
|
+
stderr_thread.join(timeout=5)
|
|
754
|
+
if rc != 0 and not verbose:
|
|
611
755
|
log = scratch_root / f"snakemake-stderr-{os.getpid()}.log"
|
|
612
756
|
try:
|
|
613
757
|
log.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -637,6 +781,18 @@ def _build_snakemake_cmd(
|
|
|
637
781
|
``--rerun-triggers`` uses ``nargs=+`` in snakemake's argparse, so without
|
|
638
782
|
an explicit ``--`` separator it greedily consumes the first positional
|
|
639
783
|
target path as an extra trigger value, causing an "invalid choice" error.
|
|
784
|
+
|
|
785
|
+
``--shared-fs-usage`` lists everything *except*
|
|
786
|
+
``software-deployment``. With it included (snakemake's default),
|
|
787
|
+
spawned job commands embed the *driver's* ``sys.executable`` — a
|
|
788
|
+
path that doesn't exist inside a Dask Gateway worker image. Without
|
|
789
|
+
it, workers invoke plain ``python`` from their own environment,
|
|
790
|
+
which is equally correct on the other backends: LocalCluster
|
|
791
|
+
threads and srun-launched SLURM workers inherit the driver's
|
|
792
|
+
activated environment (and SLURM setups already require it — see
|
|
793
|
+
the ``dask``-on-PATH check in the cluster module). One invocation
|
|
794
|
+
shape for every backend; everything else stays shared via the
|
|
795
|
+
common filesystem (persistence, inputs/outputs, sources).
|
|
640
796
|
"""
|
|
641
797
|
cmd: list[str] = [
|
|
642
798
|
"snakemake",
|
|
@@ -650,6 +806,12 @@ def _build_snakemake_cmd(
|
|
|
650
806
|
n,
|
|
651
807
|
"--executor",
|
|
652
808
|
"dask",
|
|
809
|
+
"--shared-fs-usage",
|
|
810
|
+
"persistence",
|
|
811
|
+
"input-output",
|
|
812
|
+
"sources",
|
|
813
|
+
"storage-local-copies",
|
|
814
|
+
"source-cache",
|
|
653
815
|
"--rerun-triggers",
|
|
654
816
|
*rerun_triggers.split(","),
|
|
655
817
|
]
|
|
@@ -809,7 +971,10 @@ def verify(universe: str | None) -> None:
|
|
|
809
971
|
@click.option(
|
|
810
972
|
"--runtime",
|
|
811
973
|
default=None,
|
|
812
|
-
help=
|
|
974
|
+
help=(
|
|
975
|
+
"docker | podman | podman-hpc | kubernetes "
|
|
976
|
+
"(overrides ~/.lightcone/config.yaml)"
|
|
977
|
+
),
|
|
813
978
|
)
|
|
814
979
|
def build(force: bool, runtime: str | None) -> None:
|
|
815
980
|
"""Build container images declared in astra.yaml.
|
|
@@ -820,6 +985,12 @@ def build(force: bool, runtime: str | None) -> None:
|
|
|
820
985
|
image store. Pre-built registry images (``python:3.12-slim``,
|
|
821
986
|
``ghcr.io/foo/bar:tag``) are skipped — the runtime pulls them at
|
|
822
987
|
``lc run`` time.
|
|
988
|
+
|
|
989
|
+
On a deployment without a local OCI runtime (the ``kubernetes``
|
|
990
|
+
runtime on a lightcone JupyterHub), the same command builds through
|
|
991
|
+
the deployment's GCP Cloud Build service instead and pushes
|
|
992
|
+
``<registry>/lc-<project>:<hash>`` — same content-addressed
|
|
993
|
+
identity, zero configuration.
|
|
823
994
|
"""
|
|
824
995
|
from lightcone.engine.container import ContainerBuildError, load_runtime
|
|
825
996
|
|
|
@@ -842,21 +1013,27 @@ def build(force: bool, runtime: str | None) -> None:
|
|
|
842
1013
|
console.print("[green]Done.[/green]")
|
|
843
1014
|
|
|
844
1015
|
|
|
845
|
-
def _ensure_images(project: Path, *, runtime: str, force: bool = False) ->
|
|
1016
|
+
def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> list[str]:
|
|
846
1017
|
"""Build/pull every container image referenced in astra.yaml.
|
|
847
1018
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1019
|
+
Returns the distinct resolved images, in declaration order (local
|
|
1020
|
+
tags or registry refs for Containerfile specs, prebuilt specs
|
|
1021
|
+
as-is). No-op (and empty) when *runtime* is ``"none"``.
|
|
1022
|
+
|
|
1023
|
+
Idempotent: skips images already present (local image store, or the
|
|
1024
|
+
deployment registry on the ``kubernetes`` runtime — where builds go
|
|
1025
|
+
through Cloud Build instead of a local OCI CLI). Used by ``lc build``
|
|
1026
|
+
(with ``--force`` exposed) and as a pre-flight by ``lc run`` so the
|
|
1027
|
+
first invocation after editing a Containerfile doesn't fail mid-DAG
|
|
1028
|
+
with a missing image.
|
|
853
1029
|
"""
|
|
854
1030
|
if runtime == "none":
|
|
855
|
-
return
|
|
1031
|
+
return []
|
|
856
1032
|
|
|
857
1033
|
from astra.helpers import load_yaml, resolve_analysis_tree
|
|
858
1034
|
|
|
859
1035
|
from lightcone.engine.container import (
|
|
1036
|
+
KUBERNETES,
|
|
860
1037
|
ContainerBuildError,
|
|
861
1038
|
build_image,
|
|
862
1039
|
compute_image_tag,
|
|
@@ -869,6 +1046,7 @@ def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> None:
|
|
|
869
1046
|
spec = resolve_analysis_tree(load_yaml(project / "astra.yaml"), project)
|
|
870
1047
|
project_name = (spec.get("name") or project.name).lower().replace(" ", "-")
|
|
871
1048
|
|
|
1049
|
+
images: list[str] = []
|
|
872
1050
|
seen: set[str] = set()
|
|
873
1051
|
for to in collect_tree_outputs(spec):
|
|
874
1052
|
recipe = to.output_def.get("recipe") or {}
|
|
@@ -881,8 +1059,13 @@ def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> None:
|
|
|
881
1059
|
continue
|
|
882
1060
|
seen.add(spec_str)
|
|
883
1061
|
if not is_containerfile(spec_str, project):
|
|
884
|
-
|
|
885
|
-
|
|
1062
|
+
images.append(spec_str)
|
|
1063
|
+
if runtime == KUBERNETES:
|
|
1064
|
+
# Nothing to materialize: worker pods pull registry
|
|
1065
|
+
# images straight from their source.
|
|
1066
|
+
continue
|
|
1067
|
+
# Pull so ``lc run`` can use ``--pull=never`` without
|
|
1068
|
+
# depending on the runtime's registry resolution.
|
|
886
1069
|
if image_exists_locally(spec_str, runtime=runtime) and not force:
|
|
887
1070
|
continue
|
|
888
1071
|
console.print(f"[cyan]Pulling[/cyan] {spec_str} [dim](via {runtime})[/dim]")
|
|
@@ -892,8 +1075,13 @@ def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> None:
|
|
|
892
1075
|
raise click.ClickException(str(e))
|
|
893
1076
|
continue
|
|
894
1077
|
|
|
1078
|
+
if runtime == KUBERNETES:
|
|
1079
|
+
images.append(_cloudbuild_image(project, spec_str, project_name, force))
|
|
1080
|
+
continue
|
|
1081
|
+
|
|
895
1082
|
containerfile = project / spec_str
|
|
896
1083
|
tag = compute_image_tag(project_name, containerfile, project)
|
|
1084
|
+
images.append(tag)
|
|
897
1085
|
if image_exists_locally(tag, runtime=runtime) and not force:
|
|
898
1086
|
continue
|
|
899
1087
|
console.print(
|
|
@@ -903,6 +1091,51 @@ def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> None:
|
|
|
903
1091
|
build_image(tag, containerfile, project, runtime=runtime)
|
|
904
1092
|
except ContainerBuildError as e:
|
|
905
1093
|
raise click.ClickException(str(e))
|
|
1094
|
+
return images
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _cloudbuild_image(
|
|
1098
|
+
project: Path, spec_str: str, project_name: str, force: bool
|
|
1099
|
+
) -> str:
|
|
1100
|
+
"""Ensure one Containerfile's image via GCP Cloud Build; return its ref."""
|
|
1101
|
+
from lightcone.engine.cloudbuild import (
|
|
1102
|
+
CloudBuildError,
|
|
1103
|
+
cloudbuild_available,
|
|
1104
|
+
ensure_image,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
if not cloudbuild_available():
|
|
1108
|
+
raise click.ClickException(
|
|
1109
|
+
"No image build backend on this host: the kubernetes runtime "
|
|
1110
|
+
"has no local OCI CLI and this environment does not provide "
|
|
1111
|
+
"the Cloud Build contract (LIGHTCONE_REGISTRY + "
|
|
1112
|
+
"LIGHTCONE_BUILD_BUCKET). On a lightcone JupyterHub these are "
|
|
1113
|
+
"injected into every user pod — ask the hub admin."
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
status = console.status(f"[cyan]Ensuring image for[/cyan] {spec_str} …")
|
|
1117
|
+
status.start()
|
|
1118
|
+
|
|
1119
|
+
def on_progress(phase: str, detail: str) -> None:
|
|
1120
|
+
note = f" [dim]{detail}[/dim]" if detail else ""
|
|
1121
|
+
status.update(
|
|
1122
|
+
f"[cyan]Ensuring image for[/cyan] {spec_str}: {phase}{note}"
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
try:
|
|
1126
|
+
ref = ensure_image(
|
|
1127
|
+
project,
|
|
1128
|
+
spec_str,
|
|
1129
|
+
project_name=project_name,
|
|
1130
|
+
force=force,
|
|
1131
|
+
on_progress=on_progress,
|
|
1132
|
+
)
|
|
1133
|
+
except CloudBuildError as e:
|
|
1134
|
+
raise click.ClickException(str(e))
|
|
1135
|
+
finally:
|
|
1136
|
+
status.stop()
|
|
1137
|
+
console.print(f"[green]✓[/green] Worker image: [cyan]{ref}[/cyan]")
|
|
1138
|
+
return ref
|
|
906
1139
|
|
|
907
1140
|
|
|
908
1141
|
# =============================================================================
|