calkit-python 0.29.2__tar.gz → 0.30.0__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.
- {calkit_python-0.29.2 → calkit_python-0.30.0}/PKG-INFO +1 -1
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/__init__.py +1 -1
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/check.py +2 -17
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/main.py +32 -6
- calkit_python-0.30.0/calkit/cli/slurm.py +344 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/config.py +10 -5
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/core.py +14 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/models/pipeline.py +63 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/models/test_pipeline.py +37 -4
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_pipeline.py +37 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/environments.md +17 -2
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/pipeline/index.md +34 -1
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/pipeline/running-and-logging.md +1 -0
- calkit_python-0.30.0/docs/pipeline/slurm.md +63 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/mkdocs.yml +1 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/FUNDING.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/workflows/docs.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/workflows/format.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.github/workflows/test.yml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.gitignore +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.pre-commit-config.yaml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/.python-version +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/CITATION.cff +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/CONTRIBUTING.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/LICENSE +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/Makefile +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/README.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/__main__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/calc.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/check.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/cloud.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/config.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/core.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/describe.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/import_.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/list.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/new.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/office.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/overleaf.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cli/update.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/cloud.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/conda.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/datasets.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/docker.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/dvc.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/environments.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/git.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/github.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/gui.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/jupyter.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/magics.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/matlab.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/models/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/models/core.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/models/io.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/models/iteration.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/notebooks.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/office.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/ops.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/pipeline.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/releases.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/server.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/core.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_check.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_config.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_main.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_new.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/cli/test_notebooks.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/models/__init__.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/models/test_iteration.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_calc.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_conda.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_notebooks.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/calkit/zenodo.py +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/CNAME +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/apps.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/calculations.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/calkit-yaml.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/cli-reference.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/cloud-integration.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/datasets.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/dependencies.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/examples.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/help.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/img/c-to-the-k-white.svg +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/img/connect-zenodo.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/img/jupyterlab-params.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/img/vscode-nb-params.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/index.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/installation.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/local-server.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/notebooks.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/overleaf.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/pipeline/manual-steps.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/quickstart.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/references.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/releases.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/existing-project.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/first-project.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/github-actions.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/actions-repo-secrets.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/create-project.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/excel-chart.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/excel-data.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/needs-clone.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/new-stage.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/untracked-data.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/updated-publication.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/office/workflow-page.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/clone.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/quick-actions.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/index.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/latex-codespaces.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/matlab.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/office.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/openfoam.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/docs/version-control.md +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/pyproject.toml +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/scripts/install.ps1 +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/scripts/install.sh +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/test/nb-params.ipynb +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/test/nb-subdir.ipynb +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/test/pipeline.ipynb +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/test/test-log.log +0 -0
- {calkit_python-0.29.2 → calkit_python-0.30.0}/uv.lock +0 -0
|
@@ -3,21 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import functools
|
|
6
|
-
import hashlib
|
|
7
6
|
import json
|
|
8
7
|
import os
|
|
9
8
|
import platform as _platform
|
|
10
9
|
import subprocess
|
|
11
|
-
import warnings
|
|
12
10
|
from typing import Annotated
|
|
13
11
|
|
|
14
|
-
from calkit.environments import get_env_lock_fpath
|
|
15
|
-
|
|
16
|
-
# See https://github.com/calkit/calkit/issues/346
|
|
17
|
-
with warnings.catch_warnings():
|
|
18
|
-
warnings.filterwarnings("ignore", category=UserWarning)
|
|
19
|
-
import checksumdir
|
|
20
|
-
|
|
21
12
|
import dotenv
|
|
22
13
|
import git
|
|
23
14
|
import typer
|
|
@@ -27,6 +18,8 @@ import calkit.matlab
|
|
|
27
18
|
import calkit.pipeline
|
|
28
19
|
from calkit.check import check_reproducibility
|
|
29
20
|
from calkit.cli import raise_error, warn
|
|
21
|
+
from calkit.core import get_md5
|
|
22
|
+
from calkit.environments import get_env_lock_fpath
|
|
30
23
|
|
|
31
24
|
check_app = typer.Typer(no_args_is_help=True)
|
|
32
25
|
|
|
@@ -288,14 +281,6 @@ def check_docker_env(
|
|
|
288
281
|
resp[key] = out[0].get(key)
|
|
289
282
|
return resp
|
|
290
283
|
|
|
291
|
-
def get_md5(path: str, exclude_files: list[str] | None = None) -> str:
|
|
292
|
-
if os.path.isdir(path):
|
|
293
|
-
return checksumdir.dirhash(dep, excluded_files=exclude_files)
|
|
294
|
-
else:
|
|
295
|
-
with open(path) as f:
|
|
296
|
-
content = f.read()
|
|
297
|
-
return hashlib.md5(content.encode()).hexdigest()
|
|
298
|
-
|
|
299
284
|
outfile = open(os.devnull, "w") if quiet else None
|
|
300
285
|
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
301
286
|
# First call Docker inspect
|
|
@@ -51,6 +51,7 @@ from calkit.cli.new import new_app
|
|
|
51
51
|
from calkit.cli.notebooks import notebooks_app
|
|
52
52
|
from calkit.cli.office import office_app
|
|
53
53
|
from calkit.cli.overleaf import overleaf_app
|
|
54
|
+
from calkit.cli.slurm import slurm_app
|
|
54
55
|
from calkit.cli.update import update_app
|
|
55
56
|
from calkit.environments import get_env_lock_fpath
|
|
56
57
|
from calkit.models import Procedure
|
|
@@ -77,6 +78,7 @@ app.add_typer(update_app, name="update", help="Update objects.")
|
|
|
77
78
|
app.add_typer(check_app, name="check", help="Check things.")
|
|
78
79
|
app.add_typer(overleaf_app, name="overleaf", help="Interact with Overleaf.")
|
|
79
80
|
app.add_typer(cloud_app, name="cloud", help="Interact with a Calkit Cloud.")
|
|
81
|
+
app.add_typer(slurm_app, name="slurm", help="Work with SLURM.")
|
|
80
82
|
|
|
81
83
|
# Constants for version control auto-ignore
|
|
82
84
|
AUTO_IGNORE_SUFFIXES = [".DS_Store", ".env", ".pyc", ".synctex.gz"]
|
|
@@ -187,8 +189,11 @@ def clone(
|
|
|
187
189
|
no_dvc_pull: Annotated[
|
|
188
190
|
bool, typer.Option("--no-dvc-pull", help="Do not pull DVC objects.")
|
|
189
191
|
] = False,
|
|
190
|
-
|
|
191
|
-
bool,
|
|
192
|
+
non_recursive: Annotated[
|
|
193
|
+
bool,
|
|
194
|
+
typer.Option(
|
|
195
|
+
"--no-recursive", help="Do not recursively clone submodules."
|
|
196
|
+
),
|
|
192
197
|
] = False,
|
|
193
198
|
):
|
|
194
199
|
"""Clone a Git repo and by default configure and pull from the DVC
|
|
@@ -219,7 +224,7 @@ def clone(
|
|
|
219
224
|
url = url.replace("https://github.com/", "git@github.com:")
|
|
220
225
|
# Git clone
|
|
221
226
|
cmd = ["git", "clone", url]
|
|
222
|
-
if
|
|
227
|
+
if not non_recursive:
|
|
223
228
|
cmd.append("--recursive")
|
|
224
229
|
if location is not None:
|
|
225
230
|
cmd.append(location)
|
|
@@ -552,6 +557,9 @@ def save(
|
|
|
552
557
|
help="Additional DVC args to pass when pushing.",
|
|
553
558
|
),
|
|
554
559
|
] = [],
|
|
560
|
+
no_recursive: Annotated[
|
|
561
|
+
bool, typer.Option("--no-recursive", help="Do not push to submodules.")
|
|
562
|
+
] = False,
|
|
555
563
|
verbose: Annotated[
|
|
556
564
|
bool, typer.Option("--verbose", "-v", help="Print verbose output.")
|
|
557
565
|
] = False,
|
|
@@ -606,7 +614,10 @@ def save(
|
|
|
606
614
|
if verbose and not any_dvc:
|
|
607
615
|
typer.echo("Not pushing to DVC since no DVC files were staged")
|
|
608
616
|
push(
|
|
609
|
-
no_dvc=not any_dvc,
|
|
617
|
+
no_dvc=not any_dvc,
|
|
618
|
+
git_args=git_push_args,
|
|
619
|
+
dvc_args=dvc_push_args,
|
|
620
|
+
no_recursive=no_recursive,
|
|
610
621
|
)
|
|
611
622
|
|
|
612
623
|
|
|
@@ -629,6 +640,12 @@ def pull(
|
|
|
629
640
|
help="Force pull, potentially overwriting local changes.",
|
|
630
641
|
),
|
|
631
642
|
] = False,
|
|
643
|
+
no_recursive: Annotated[
|
|
644
|
+
bool,
|
|
645
|
+
typer.Option(
|
|
646
|
+
"--no-recursive", help="Do not recursively pull from submodules."
|
|
647
|
+
),
|
|
648
|
+
] = False,
|
|
632
649
|
):
|
|
633
650
|
"""Pull with both Git and DVC."""
|
|
634
651
|
typer.echo("Git pulling")
|
|
@@ -638,7 +655,10 @@ def pull(
|
|
|
638
655
|
if "-f" not in dvc_args and "--force" not in dvc_args:
|
|
639
656
|
dvc_args.append("-f")
|
|
640
657
|
try:
|
|
641
|
-
|
|
658
|
+
git_cmd = ["git", "pull"]
|
|
659
|
+
if not no_recursive and "--recurse-submodules" not in git_args:
|
|
660
|
+
git_cmd.append("--recurse-submodules")
|
|
661
|
+
subprocess.check_call(git_cmd + git_args)
|
|
642
662
|
except subprocess.CalledProcessError:
|
|
643
663
|
raise_error("Git pull failed")
|
|
644
664
|
typer.echo("DVC pulling")
|
|
@@ -667,11 +687,17 @@ def push(
|
|
|
667
687
|
list[str],
|
|
668
688
|
typer.Option("--dvc-arg", help="Additional DVC args."),
|
|
669
689
|
] = [],
|
|
690
|
+
no_recursive: Annotated[
|
|
691
|
+
bool, typer.Option("--no-recursive", help="Do not push to submodules.")
|
|
692
|
+
] = False,
|
|
670
693
|
):
|
|
671
694
|
"""Push with both Git and DVC."""
|
|
672
695
|
typer.echo("Pushing to Git remote")
|
|
673
696
|
try:
|
|
674
|
-
|
|
697
|
+
git_cmd = ["git", "push"]
|
|
698
|
+
if not no_recursive and "--recurse-submodules" not in git_args:
|
|
699
|
+
git_cmd.append("--recurse-submodules=on-demand")
|
|
700
|
+
subprocess.check_call(git_cmd + git_args)
|
|
675
701
|
except subprocess.CalledProcessError:
|
|
676
702
|
raise_error("Git push failed")
|
|
677
703
|
if not no_dvc:
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""CLI for working with SLURM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from typing_extensions import Annotated
|
|
14
|
+
|
|
15
|
+
import calkit
|
|
16
|
+
from calkit.cli import raise_error
|
|
17
|
+
|
|
18
|
+
slurm_app = typer.Typer(no_args_is_help=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@slurm_app.command(name="batch")
|
|
22
|
+
def run_sbatch(
|
|
23
|
+
name: Annotated[
|
|
24
|
+
str,
|
|
25
|
+
typer.Option("--name", "-n", help="Job name."),
|
|
26
|
+
],
|
|
27
|
+
script: Annotated[
|
|
28
|
+
str,
|
|
29
|
+
typer.Argument(help="Path to the SLURM script to run."),
|
|
30
|
+
],
|
|
31
|
+
environment: Annotated[
|
|
32
|
+
str,
|
|
33
|
+
typer.Option(
|
|
34
|
+
"--environment",
|
|
35
|
+
"-e",
|
|
36
|
+
help="Calkit (slurm) environment to use for the job.",
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
args: Annotated[
|
|
40
|
+
list[str] | None,
|
|
41
|
+
typer.Argument(
|
|
42
|
+
help=(
|
|
43
|
+
"Arguments for sbatch, the first of which should be the "
|
|
44
|
+
"script."
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
] = None,
|
|
48
|
+
deps: Annotated[
|
|
49
|
+
list[str],
|
|
50
|
+
typer.Option(
|
|
51
|
+
"--dep",
|
|
52
|
+
"-d",
|
|
53
|
+
help=(
|
|
54
|
+
"Additional dependencies to track, which if changed signify"
|
|
55
|
+
" a job is invalid."
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
] = [],
|
|
59
|
+
outs: Annotated[
|
|
60
|
+
list[str],
|
|
61
|
+
typer.Option(
|
|
62
|
+
"--out",
|
|
63
|
+
"-o",
|
|
64
|
+
help=(
|
|
65
|
+
"Non-persistent output files or directories produced by the "
|
|
66
|
+
"job, which will be deleted before submitting a new job."
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
] = [],
|
|
70
|
+
sbatch_opts: Annotated[
|
|
71
|
+
list[str],
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--sbatch-option",
|
|
74
|
+
"-s",
|
|
75
|
+
help="Additional options to pass to sbatch (no spaces allowed).",
|
|
76
|
+
),
|
|
77
|
+
] = [],
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Submit a SLURM batch job for the project.
|
|
80
|
+
|
|
81
|
+
Duplicates are not allowed, so if one is already running or queued with
|
|
82
|
+
the same name, we'll wait for it to finish. The only exception is if the
|
|
83
|
+
dependencies have changed, in which case any queued or running jobs will
|
|
84
|
+
be cancelled and a new one submitted.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def check_job_running_or_queued(job_id: str) -> bool:
|
|
88
|
+
p = subprocess.run(
|
|
89
|
+
["squeue", "--job", job_id], capture_output=True, text=True
|
|
90
|
+
)
|
|
91
|
+
if p.returncode != 0:
|
|
92
|
+
return False
|
|
93
|
+
return len(p.stdout.strip().split("\n")) > 1
|
|
94
|
+
|
|
95
|
+
def cancel_job(job_id: str, reason: str) -> None:
|
|
96
|
+
typer.echo(f"{reason}; canceling existing job ID {job_id}")
|
|
97
|
+
p = subprocess.run(
|
|
98
|
+
["scancel", job_id], capture_output=True, text=True, check=False
|
|
99
|
+
)
|
|
100
|
+
if p.returncode != 0:
|
|
101
|
+
raise_error(
|
|
102
|
+
f"Failed to cancel existing job ID {job_id}: {p.stderr}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if args is None:
|
|
106
|
+
args = []
|
|
107
|
+
cmd = (
|
|
108
|
+
[
|
|
109
|
+
"sbatch",
|
|
110
|
+
"--parsable",
|
|
111
|
+
"--job-name",
|
|
112
|
+
name,
|
|
113
|
+
"-o",
|
|
114
|
+
".calkit/slurm/logs/%j.out",
|
|
115
|
+
]
|
|
116
|
+
+ sbatch_opts
|
|
117
|
+
+ [script]
|
|
118
|
+
+ args
|
|
119
|
+
)
|
|
120
|
+
if not os.path.isfile(script):
|
|
121
|
+
raise_error(f"SLURM script '{script}' does not exist")
|
|
122
|
+
if environment != "_system":
|
|
123
|
+
ck_info = calkit.load_calkit_info()
|
|
124
|
+
env = ck_info.get("environments", {}).get(environment, {})
|
|
125
|
+
env_kind = env.get("kind")
|
|
126
|
+
if env_kind != "slurm":
|
|
127
|
+
raise_error(
|
|
128
|
+
f"Environment '{environment}' is not a slurm environment"
|
|
129
|
+
)
|
|
130
|
+
# Check host matches
|
|
131
|
+
env_host = env.get("host", "localhost")
|
|
132
|
+
if env_host != "localhost" and env_host != socket.gethostname():
|
|
133
|
+
raise_error(
|
|
134
|
+
f"Environment '{environment}' is for host '{env_host}', but "
|
|
135
|
+
f"this is '{socket.gethostname()}'"
|
|
136
|
+
)
|
|
137
|
+
deps = [script] + deps
|
|
138
|
+
slurm_dir = os.path.join(".calkit", "slurm")
|
|
139
|
+
logs_dir = os.path.join(slurm_dir, "logs")
|
|
140
|
+
os.makedirs(logs_dir, exist_ok=True)
|
|
141
|
+
jobs_path = os.path.join(slurm_dir, "jobs.json")
|
|
142
|
+
if os.path.isfile(jobs_path):
|
|
143
|
+
with open(jobs_path, "r") as f:
|
|
144
|
+
jobs = json.load(f)
|
|
145
|
+
else:
|
|
146
|
+
jobs = {}
|
|
147
|
+
typer.echo("Computing MD5s for dependencies")
|
|
148
|
+
current_dep_md5s = {}
|
|
149
|
+
for dep in deps:
|
|
150
|
+
if not os.path.exists(dep):
|
|
151
|
+
raise_error(f"Dependency path '{dep}' does not exist.")
|
|
152
|
+
current_dep_md5s[dep] = calkit.get_md5(dep)
|
|
153
|
+
# See if there is a job with this name
|
|
154
|
+
if name in jobs:
|
|
155
|
+
job_info = jobs[name]
|
|
156
|
+
job_id = job_info["job_id"]
|
|
157
|
+
job_deps = job_info["deps"]
|
|
158
|
+
job_args = job_info.get("args", [])
|
|
159
|
+
running_or_queued = check_job_running_or_queued(job_id)
|
|
160
|
+
should_wait = True
|
|
161
|
+
if running_or_queued:
|
|
162
|
+
typer.echo(
|
|
163
|
+
f"Job '{name}' with is already running or queued with ID "
|
|
164
|
+
f"{job_id}"
|
|
165
|
+
)
|
|
166
|
+
# Check if args have changed
|
|
167
|
+
if job_args != args:
|
|
168
|
+
should_wait = False
|
|
169
|
+
cancel_job(
|
|
170
|
+
job_id=job_id,
|
|
171
|
+
reason=f"Arguments for job '{name}' have changed",
|
|
172
|
+
)
|
|
173
|
+
# Check if dependency paths have changed
|
|
174
|
+
if set(job_deps) != set(deps):
|
|
175
|
+
should_wait = False
|
|
176
|
+
cancel_job(
|
|
177
|
+
job_id=job_id,
|
|
178
|
+
reason=f"Dependencies for job '{name}' have changed",
|
|
179
|
+
)
|
|
180
|
+
# Check dependency hashes
|
|
181
|
+
job_dep_md5s = job_info.get("dep_md5s", {})
|
|
182
|
+
for dep_path, md5 in current_dep_md5s.items():
|
|
183
|
+
job_md5 = job_dep_md5s.get(dep_path)
|
|
184
|
+
if md5 != job_md5:
|
|
185
|
+
should_wait = False
|
|
186
|
+
cancel_job(
|
|
187
|
+
job_id=job_id,
|
|
188
|
+
reason=(
|
|
189
|
+
f"Dependency '{dep_path}' for job '{name}' has "
|
|
190
|
+
"changed"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
break
|
|
194
|
+
# Wait for the job to finish if it's running or queued and valid
|
|
195
|
+
if should_wait:
|
|
196
|
+
typer.echo("Waiting for job to finish")
|
|
197
|
+
while running_or_queued and should_wait:
|
|
198
|
+
running_or_queued = check_job_running_or_queued(job_id)
|
|
199
|
+
time.sleep(1)
|
|
200
|
+
if should_wait:
|
|
201
|
+
raise typer.Exit(0)
|
|
202
|
+
# Job is not running or queued, so we can submit
|
|
203
|
+
# But first, delete any non-persistent outputs
|
|
204
|
+
for out in outs:
|
|
205
|
+
if os.path.exists(out):
|
|
206
|
+
typer.echo(f"Deleting output path '{out}'")
|
|
207
|
+
try:
|
|
208
|
+
if os.path.isfile(out):
|
|
209
|
+
os.remove(out)
|
|
210
|
+
else:
|
|
211
|
+
shutil.rmtree(out)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise_error(f"Error deleting '{out}': {e}")
|
|
214
|
+
p = subprocess.run(cmd, capture_output=True, check=False, text=True)
|
|
215
|
+
if p.returncode != 0:
|
|
216
|
+
raise_error(f"Failed to submit new job: {p.stderr}")
|
|
217
|
+
job_id = p.stdout.strip()
|
|
218
|
+
typer.echo(f"Submitted job with ID: {job_id}")
|
|
219
|
+
new_job = {
|
|
220
|
+
"job_id": job_id,
|
|
221
|
+
"deps": deps,
|
|
222
|
+
"args": args,
|
|
223
|
+
"dep_md5s": current_dep_md5s,
|
|
224
|
+
}
|
|
225
|
+
jobs[name] = new_job
|
|
226
|
+
with open(jobs_path, "w") as f:
|
|
227
|
+
json.dump(jobs, f, indent=4)
|
|
228
|
+
# Now wait for job to finish
|
|
229
|
+
typer.echo("Waiting for job to finish")
|
|
230
|
+
running_or_queued = True
|
|
231
|
+
while running_or_queued:
|
|
232
|
+
running_or_queued = check_job_running_or_queued(job_id)
|
|
233
|
+
time.sleep(1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@slurm_app.command(name="queue")
|
|
237
|
+
def get_queue() -> None:
|
|
238
|
+
"""List SLURM jobs submitted via Calkit."""
|
|
239
|
+
slurm_dir = os.path.join(".calkit", "slurm")
|
|
240
|
+
jobs_path = os.path.join(slurm_dir, "jobs.json")
|
|
241
|
+
if os.path.isfile(jobs_path):
|
|
242
|
+
with open(jobs_path, "r") as f:
|
|
243
|
+
jobs = json.load(f)
|
|
244
|
+
else:
|
|
245
|
+
jobs = {}
|
|
246
|
+
if len(jobs) == 0:
|
|
247
|
+
typer.echo("No jobs found for this project")
|
|
248
|
+
raise typer.Exit(0)
|
|
249
|
+
job_ids = [j["job_id"] for j in jobs.values()]
|
|
250
|
+
subprocess.run(
|
|
251
|
+
["squeue", "-j", ",".join(job_ids)],
|
|
252
|
+
capture_output=False,
|
|
253
|
+
text=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@slurm_app.command(name="cancel")
|
|
258
|
+
def cancel_jobs(
|
|
259
|
+
names: Annotated[
|
|
260
|
+
list[str],
|
|
261
|
+
typer.Argument(help="Names of jobs to cancel."),
|
|
262
|
+
],
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Cancel SLURM jobs by their name in the project."""
|
|
265
|
+
slurm_dir = os.path.join(".calkit", "slurm")
|
|
266
|
+
jobs_path = os.path.join(slurm_dir, "jobs.json")
|
|
267
|
+
if os.path.isfile(jobs_path):
|
|
268
|
+
with open(jobs_path, "r") as f:
|
|
269
|
+
jobs = json.load(f)
|
|
270
|
+
else:
|
|
271
|
+
jobs = {}
|
|
272
|
+
if len(jobs) == 0:
|
|
273
|
+
typer.echo("No jobs found for this project")
|
|
274
|
+
raise typer.Exit(0)
|
|
275
|
+
# Get any job IDs that are actually running or queued
|
|
276
|
+
job_ids = [j["job_id"] for j in jobs.values()]
|
|
277
|
+
p = subprocess.run(
|
|
278
|
+
["squeue", "-h", "-o", "%A", "-j", ",".join(job_ids)],
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
)
|
|
282
|
+
running_or_queued_ids = p.stdout.strip().split("\n")
|
|
283
|
+
running_or_queued_ids = [j for j in running_or_queued_ids if j]
|
|
284
|
+
for name in names:
|
|
285
|
+
if name not in jobs:
|
|
286
|
+
typer.echo(f"No job named '{name}' found for this project")
|
|
287
|
+
continue
|
|
288
|
+
job_info = jobs[name]
|
|
289
|
+
job_id = job_info["job_id"]
|
|
290
|
+
if job_id not in running_or_queued_ids:
|
|
291
|
+
typer.echo(
|
|
292
|
+
f"Job '{name}' (last submitted ID: {job_id}) is not "
|
|
293
|
+
"running or queued"
|
|
294
|
+
)
|
|
295
|
+
continue
|
|
296
|
+
p = subprocess.run(
|
|
297
|
+
["scancel", job_id], capture_output=True, text=True, check=False
|
|
298
|
+
)
|
|
299
|
+
if p.returncode != 0:
|
|
300
|
+
raise_error(f"Failed to cancel job ID {job_id}: {p.stderr}")
|
|
301
|
+
typer.echo(f"Cancelled job '{name}' with ID {job_id}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@slurm_app.command(name="logs")
|
|
305
|
+
def get_logs(
|
|
306
|
+
name: Annotated[
|
|
307
|
+
str,
|
|
308
|
+
typer.Argument(help="Name of the job to get logs for."),
|
|
309
|
+
],
|
|
310
|
+
follow: Annotated[
|
|
311
|
+
bool,
|
|
312
|
+
typer.Option(
|
|
313
|
+
"--follow", "-f", help="Follow the log output like tail -f."
|
|
314
|
+
),
|
|
315
|
+
] = False,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Get the logs for a SLURM job by its name in the project."""
|
|
318
|
+
slurm_dir = os.path.join(".calkit", "slurm")
|
|
319
|
+
jobs_path = os.path.join(slurm_dir, "jobs.json")
|
|
320
|
+
if os.path.isfile(jobs_path):
|
|
321
|
+
with open(jobs_path, "r") as f:
|
|
322
|
+
jobs = json.load(f)
|
|
323
|
+
else:
|
|
324
|
+
jobs = {}
|
|
325
|
+
if len(jobs) == 0:
|
|
326
|
+
typer.echo("No jobs found for this project")
|
|
327
|
+
raise typer.Exit(0)
|
|
328
|
+
if name not in jobs:
|
|
329
|
+
raise_error(f"No job named '{name}' found for this project")
|
|
330
|
+
job_info = jobs[name]
|
|
331
|
+
job_id = job_info["job_id"]
|
|
332
|
+
log_path = os.path.join(slurm_dir, "logs", f"{job_id}.out")
|
|
333
|
+
if not os.path.isfile(log_path):
|
|
334
|
+
raise_error(f"No log file found for job '{name}' with ID {job_id}")
|
|
335
|
+
if follow:
|
|
336
|
+
p = subprocess.Popen(["tail", "-f", log_path])
|
|
337
|
+
try:
|
|
338
|
+
p.wait()
|
|
339
|
+
except KeyboardInterrupt:
|
|
340
|
+
p.terminate()
|
|
341
|
+
raise typer.Exit(0)
|
|
342
|
+
else:
|
|
343
|
+
with open(log_path, "r") as f:
|
|
344
|
+
typer.echo(f.read())
|
|
@@ -56,8 +56,11 @@ def supports_keyring() -> bool:
|
|
|
56
56
|
KEYRING_SUPPORTED = supports_keyring()
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def get_env() -> Literal["local", "staging", "production"]:
|
|
60
|
-
|
|
59
|
+
def get_env() -> Literal["test", "local", "staging", "production"]:
|
|
60
|
+
env = os.getenv("CALKIT_ENV", "production")
|
|
61
|
+
if env not in ["test", "local", "staging", "production"]:
|
|
62
|
+
raise ValueError(f"{env} is not a valid environment name")
|
|
63
|
+
return env # type: ignore
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
def set_env(name: Literal["local", "staging", "production"]) -> None:
|
|
@@ -184,10 +187,10 @@ class Settings(BaseSettings):
|
|
|
184
187
|
dotenv_settings,
|
|
185
188
|
YamlConfigSettingsSource(settings_cls),
|
|
186
189
|
KeyringSecretsSource(settings_cls),
|
|
187
|
-
)
|
|
190
|
+
) # type: ignore
|
|
188
191
|
|
|
189
192
|
def write(self) -> None:
|
|
190
|
-
base_dir = os.path.dirname(self.model_config["yaml_file"])
|
|
193
|
+
base_dir = os.path.dirname(self.model_config["yaml_file"]) # type: ignore
|
|
191
194
|
os.makedirs(base_dir, exist_ok=True)
|
|
192
195
|
cfg = self.model_dump()
|
|
193
196
|
# Remove anything that should be in the keyring
|
|
@@ -205,8 +208,10 @@ class Settings(BaseSettings):
|
|
|
205
208
|
except keyring.errors.KeyringError:
|
|
206
209
|
# Ignore errors when deleting secrets
|
|
207
210
|
pass
|
|
208
|
-
with open(self.model_config["yaml_file"], "w") as f:
|
|
211
|
+
with open(self.model_config["yaml_file"], "w") as f: # type: ignore
|
|
209
212
|
yaml.safe_dump(cfg, f)
|
|
213
|
+
# Ensure permissions are user read/write only
|
|
214
|
+
os.chmod(self.model_config["yaml_file"], 0o600) # type: ignore
|
|
210
215
|
|
|
211
216
|
|
|
212
217
|
def read() -> Settings:
|
|
@@ -15,7 +15,12 @@ import re
|
|
|
15
15
|
import socket
|
|
16
16
|
import subprocess
|
|
17
17
|
import uuid
|
|
18
|
+
import warnings
|
|
18
19
|
|
|
20
|
+
# See https://github.com/calkit/calkit/issues/346
|
|
21
|
+
with warnings.catch_warnings():
|
|
22
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
23
|
+
import checksumdir
|
|
19
24
|
import psutil
|
|
20
25
|
import requests
|
|
21
26
|
|
|
@@ -572,3 +577,12 @@ def get_system_info() -> dict:
|
|
|
572
577
|
system_info_str = json.dumps(system_info, sort_keys=True).encode()
|
|
573
578
|
system_info["id"] = hashlib.sha1(system_info_str).hexdigest()
|
|
574
579
|
return system_info
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def get_md5(path: str, exclude_files: list[str] | None = None) -> str:
|
|
583
|
+
if os.path.isdir(path):
|
|
584
|
+
return checksumdir.dirhash(path, excluded_files=exclude_files)
|
|
585
|
+
else:
|
|
586
|
+
with open(path) as f:
|
|
587
|
+
content = f.read()
|
|
588
|
+
return hashlib.md5(content.encode()).hexdigest()
|
|
@@ -13,6 +13,7 @@ from pydantic import (
|
|
|
13
13
|
Discriminator,
|
|
14
14
|
ValidationError,
|
|
15
15
|
field_validator,
|
|
16
|
+
model_validator,
|
|
16
17
|
)
|
|
17
18
|
from typing_extensions import Annotated
|
|
18
19
|
|
|
@@ -109,6 +110,7 @@ class StageIteration(BaseModel):
|
|
|
109
110
|
class Stage(BaseModel):
|
|
110
111
|
"""A stage in the pipeline."""
|
|
111
112
|
|
|
113
|
+
name: str | None = None
|
|
112
114
|
kind: Literal[
|
|
113
115
|
"python-script",
|
|
114
116
|
"latex",
|
|
@@ -366,6 +368,54 @@ class JuliaCommandStage(Stage):
|
|
|
366
368
|
return cmd
|
|
367
369
|
|
|
368
370
|
|
|
371
|
+
class SBatchStage(Stage):
|
|
372
|
+
kind: Literal["sbatch"] = "sbatch"
|
|
373
|
+
script_path: str
|
|
374
|
+
args: list[str] = []
|
|
375
|
+
sbatch_options: list[str] = []
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def dvc_deps(self) -> list[str]:
|
|
379
|
+
return [self.script_path] + super().dvc_deps
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def dvc_outs(self) -> list[str | dict]:
|
|
383
|
+
# All outputs must be persistent, since ``calkit slurm batch``
|
|
384
|
+
# handles deletion
|
|
385
|
+
outs = super().dvc_outs
|
|
386
|
+
final_outs = []
|
|
387
|
+
for out in outs:
|
|
388
|
+
if isinstance(out, str):
|
|
389
|
+
final_outs.append({out: {"persist": True}})
|
|
390
|
+
elif isinstance(out, dict):
|
|
391
|
+
k = list(out.keys())[0]
|
|
392
|
+
v = out[k]
|
|
393
|
+
v["persist"] = True
|
|
394
|
+
final_outs.append({k: v})
|
|
395
|
+
return final_outs
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def dvc_cmd(self) -> str:
|
|
399
|
+
cmd = f"calkit slurm batch --name {self.name}"
|
|
400
|
+
if self.environment != "_system":
|
|
401
|
+
cmd += f" --environment {self.environment}"
|
|
402
|
+
for dep in self.dvc_deps:
|
|
403
|
+
if dep != self.script_path:
|
|
404
|
+
cmd += f" --dep {dep}"
|
|
405
|
+
for out in self.outputs:
|
|
406
|
+
# Determine if this is a non-persistent output
|
|
407
|
+
if isinstance(out, str):
|
|
408
|
+
cmd += f" --out {out}"
|
|
409
|
+
elif isinstance(out, PathOutput) and out.delete_before_run:
|
|
410
|
+
cmd += f" --out {out.path}"
|
|
411
|
+
for opt in self.sbatch_options:
|
|
412
|
+
cmd += f" -s {opt}"
|
|
413
|
+
cmd += f" -- {self.script_path}"
|
|
414
|
+
for arg in self.args:
|
|
415
|
+
cmd += f" {arg}"
|
|
416
|
+
return cmd
|
|
417
|
+
|
|
418
|
+
|
|
369
419
|
class JupyterNotebookStage(Stage):
|
|
370
420
|
"""A stage that runs a Jupyter notebook.
|
|
371
421
|
|
|
@@ -565,9 +615,22 @@ class Pipeline(BaseModel):
|
|
|
565
615
|
| JupyterNotebookStage
|
|
566
616
|
| JuliaScriptStage
|
|
567
617
|
| JuliaCommandStage
|
|
618
|
+
| SBatchStage
|
|
568
619
|
),
|
|
569
620
|
Discriminator("kind"),
|
|
570
621
|
],
|
|
571
622
|
]
|
|
572
623
|
# Do not allow extra keys
|
|
573
624
|
model_config = ConfigDict(extra="forbid")
|
|
625
|
+
|
|
626
|
+
@model_validator(mode="after")
|
|
627
|
+
def set_stage_names(self):
|
|
628
|
+
"""Set the name field of each stage to match its key in the dict."""
|
|
629
|
+
for stage_name, stage in self.stages.items():
|
|
630
|
+
if stage.name is not None and stage.name != stage_name:
|
|
631
|
+
raise ValueError(
|
|
632
|
+
f"Stage name '{stage.name}' does not match key "
|
|
633
|
+
f"'{stage_name}'"
|
|
634
|
+
)
|
|
635
|
+
stage.name = stage_name
|
|
636
|
+
return self
|