calkit-python 0.16.2__tar.gz → 0.17.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.16.2 → calkit_python-0.17.0}/.gitignore +1 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/PKG-INFO +2 -1
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/__init__.py +1 -1
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/check.py +30 -4
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/main.py +133 -11
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/conda.py +12 -3
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/models.py +15 -1
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/cli/test_main.py +27 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/environments.md +73 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/examples.md +11 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/pyproject.toml +1 -0
- calkit_python-0.17.0/uv.lock +3735 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/.github/FUNDING.yml +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/.github/workflows/docs.yml +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/LICENSE +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/README.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/calc.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/check.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/config.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/core.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/import_.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/list.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/new.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/office.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cli/update.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/cloud.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/config.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/core.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/datasets.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/docker.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/dvc.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/git.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/gui.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/jupyter.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/magics.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/office.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/ops.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/server.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/core.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/cli/test_new.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_calc.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_conda.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/CNAME +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/apps.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/calculations.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/calkit-yaml.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/cli-reference.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/cloud-integration.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/help.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/img/c-to-the-k-white.svg +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/index.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/installation.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/pipeline/index.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/pipeline/manual-steps.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/references.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/first-project.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/create-project.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/excel-chart.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/excel-data.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/needs-clone.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/new-stage.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/untracked-data.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/updated-publication.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/office/workflow-page.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/clone.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/latex-codespaces.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/matlab.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/office.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/openfoam.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/docs/version-control.md +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/mkdocs.yml +0 -0
- {calkit_python-0.16.2 → calkit_python-0.17.0}/test/pipeline.ipynb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.0
|
|
4
4
|
Summary: Reproducibility simplified.
|
|
5
5
|
Project-URL: Homepage, https://calkit.org
|
|
6
6
|
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
@@ -12,6 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Requires-Dist: arithmeval
|
|
15
|
+
Requires-Dist: checksumdir
|
|
15
16
|
Requires-Dist: docx2pdf
|
|
16
17
|
Requires-Dist: dvc
|
|
17
18
|
Requires-Dist: eval-type-backport; python_version < '3.10'
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import subprocess
|
|
10
10
|
from typing import Annotated
|
|
11
11
|
|
|
12
|
+
import checksumdir
|
|
12
13
|
import typer
|
|
13
14
|
|
|
14
15
|
import calkit
|
|
@@ -65,6 +66,14 @@ def check_docker_env(
|
|
|
65
66
|
platform: Annotated[
|
|
66
67
|
str, typer.Option("--platform", help="Which platform(s) to build for.")
|
|
67
68
|
] = None,
|
|
69
|
+
deps: Annotated[
|
|
70
|
+
list[str],
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--dep",
|
|
73
|
+
"-d",
|
|
74
|
+
help="Declare an explicit dependency for this Docker image.",
|
|
75
|
+
),
|
|
76
|
+
] = [],
|
|
68
77
|
quiet: Annotated[
|
|
69
78
|
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
70
79
|
] = False,
|
|
@@ -81,6 +90,14 @@ def check_docker_env(
|
|
|
81
90
|
_ = out[0].pop("DockerVersion")
|
|
82
91
|
return out
|
|
83
92
|
|
|
93
|
+
def get_md5(path: str, exclude_files: list[str] | None = None) -> str:
|
|
94
|
+
if os.path.isdir(path):
|
|
95
|
+
return checksumdir.dirhash(dep, excluded_files=exclude_files)
|
|
96
|
+
else:
|
|
97
|
+
with open(path) as f:
|
|
98
|
+
content = f.read()
|
|
99
|
+
return hashlib.md5(content.encode()).hexdigest()
|
|
100
|
+
|
|
84
101
|
outfile = open(os.devnull, "w") if quiet else None
|
|
85
102
|
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
86
103
|
# First call Docker inspect
|
|
@@ -90,10 +107,12 @@ def check_docker_env(
|
|
|
90
107
|
typer.echo(f"No image with tag {tag} found locally", file=outfile)
|
|
91
108
|
inspect = []
|
|
92
109
|
typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
|
|
93
|
-
|
|
94
|
-
dockerfile = f.read()
|
|
95
|
-
dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
|
|
110
|
+
dockerfile_md5 = get_md5(fpath)
|
|
96
111
|
lock_fpath = fpath + "-lock.json"
|
|
112
|
+
# Compute MD5s of any dependencies
|
|
113
|
+
deps_md5s = {}
|
|
114
|
+
for dep in deps:
|
|
115
|
+
deps_md5s[dep] = get_md5(dep, exclude_files=lock_fpath)
|
|
97
116
|
rebuild = True
|
|
98
117
|
if os.path.isfile(lock_fpath):
|
|
99
118
|
typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
|
|
@@ -109,6 +128,12 @@ def check_docker_env(
|
|
|
109
128
|
rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
|
|
110
129
|
"Layers"
|
|
111
130
|
] or dockerfile_md5 != lock[0].get("DockerfileMD5")
|
|
131
|
+
if not rebuild:
|
|
132
|
+
for dep, md5 in deps_md5s.items():
|
|
133
|
+
if md5 != lock[0].get("DepsMD5s", {}).get(dep):
|
|
134
|
+
typer.echo(f"Found modified dependency: {dep}")
|
|
135
|
+
rebuild = True
|
|
136
|
+
break
|
|
112
137
|
if rebuild:
|
|
113
138
|
wdir, fname = os.path.split(fpath)
|
|
114
139
|
if not wdir:
|
|
@@ -117,10 +142,11 @@ def check_docker_env(
|
|
|
117
142
|
if platform is not None:
|
|
118
143
|
cmd += ["--platform", platform]
|
|
119
144
|
cmd.append(".")
|
|
120
|
-
subprocess.
|
|
145
|
+
subprocess.check_output(cmd, cwd=wdir)
|
|
121
146
|
# Write the lock file
|
|
122
147
|
inspect = get_docker_inspect()
|
|
123
148
|
inspect[0]["DockerfileMD5"] = dockerfile_md5
|
|
149
|
+
inspect[0]["DepsMD5s"] = deps_md5s
|
|
124
150
|
with open(lock_fpath, "w") as f:
|
|
125
151
|
json.dump(inspect, f, indent=4)
|
|
126
152
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
|
+
import glob
|
|
6
7
|
import os
|
|
7
8
|
import platform as _platform
|
|
8
9
|
import subprocess
|
|
@@ -57,7 +58,8 @@ def _to_shell_cmd(cmd: list[str]) -> str:
|
|
|
57
58
|
"""
|
|
58
59
|
quoted_cmd = []
|
|
59
60
|
for part in cmd:
|
|
60
|
-
|
|
61
|
+
# Find quotes within quotes and escape them
|
|
62
|
+
if " " in part or '"' in part[1:-1] or "'" in part[1:-1]:
|
|
61
63
|
part = part.replace('"', r"\"")
|
|
62
64
|
quoted_cmd.append(f'"{part}"')
|
|
63
65
|
else:
|
|
@@ -708,12 +710,11 @@ def run_in_env(
|
|
|
708
710
|
if env_name not in envs:
|
|
709
711
|
raise_error(f"Environment '{env_name}' does not exist")
|
|
710
712
|
env = envs[env_name]
|
|
711
|
-
if wdir is not None:
|
|
712
|
-
cwd = os.path.abspath(wdir)
|
|
713
|
-
else:
|
|
714
|
-
cwd = os.getcwd()
|
|
715
713
|
image_name = env.get("image", env_name)
|
|
716
714
|
docker_wdir = env.get("wdir", "/work")
|
|
715
|
+
docker_wdir_mount = docker_wdir
|
|
716
|
+
if wdir is not None:
|
|
717
|
+
docker_wdir = os.path.join(docker_wdir, wdir)
|
|
717
718
|
shell = env.get("shell", "sh")
|
|
718
719
|
platform = env.get("platform")
|
|
719
720
|
if env["kind"] == "docker":
|
|
@@ -724,6 +725,7 @@ def run_in_env(
|
|
|
724
725
|
tag=env["image"],
|
|
725
726
|
fpath=env["path"],
|
|
726
727
|
platform=env.get("platform"),
|
|
728
|
+
deps=env.get("deps", []),
|
|
727
729
|
quiet=True,
|
|
728
730
|
)
|
|
729
731
|
shell_cmd = _to_shell_cmd(cmd)
|
|
@@ -733,13 +735,14 @@ def run_in_env(
|
|
|
733
735
|
]
|
|
734
736
|
if platform:
|
|
735
737
|
docker_cmd += ["--platform", platform]
|
|
738
|
+
docker_cmd += env.get("args", [])
|
|
736
739
|
docker_cmd += [
|
|
737
740
|
"-it" if sys.stdin.isatty() else "-i",
|
|
738
741
|
"--rm",
|
|
739
742
|
"-w",
|
|
740
743
|
docker_wdir,
|
|
741
744
|
"-v",
|
|
742
|
-
f"{
|
|
745
|
+
f"{os.getcwd()}:{docker_wdir_mount}",
|
|
743
746
|
image_name,
|
|
744
747
|
shell,
|
|
745
748
|
"-c",
|
|
@@ -839,6 +842,117 @@ def run_in_env(
|
|
|
839
842
|
subprocess.check_call(cmd, shell=True, cwd=wdir)
|
|
840
843
|
except subprocess.CalledProcessError:
|
|
841
844
|
raise_error(f"Failed to run in {kind}")
|
|
845
|
+
elif env["kind"] == "ssh":
|
|
846
|
+
try:
|
|
847
|
+
host = os.path.expandvars(env["host"])
|
|
848
|
+
user = os.path.expandvars(env["user"])
|
|
849
|
+
remote_wdir = env["wdir"]
|
|
850
|
+
except KeyError:
|
|
851
|
+
raise_error(
|
|
852
|
+
"Host, user, and wdir must be defined for ssh environments"
|
|
853
|
+
)
|
|
854
|
+
send_paths = env.get("send_paths")
|
|
855
|
+
get_paths = env.get("get_paths")
|
|
856
|
+
key = env.get("key")
|
|
857
|
+
if key is not None:
|
|
858
|
+
key = os.path.expanduser(os.path.expandvars(key))
|
|
859
|
+
remote_shell_cmd = _to_shell_cmd(cmd)
|
|
860
|
+
# Run with nohup so we can disconnect
|
|
861
|
+
# TODO: Should we collect output instead of send to /dev/null?
|
|
862
|
+
remote_cmd = (
|
|
863
|
+
f"cd '{remote_wdir}' ; nohup {remote_shell_cmd} "
|
|
864
|
+
"> /dev/null 2>&1 & echo $! "
|
|
865
|
+
)
|
|
866
|
+
key_cmd = ["-i", key] if key is not None else []
|
|
867
|
+
# Check to see if we've already submitted a job with this command
|
|
868
|
+
jobs_fpath = ".calkit/jobs.yaml"
|
|
869
|
+
job_key = f"{env_name}::{remote_shell_cmd}"
|
|
870
|
+
remote_pid = None
|
|
871
|
+
if os.path.isfile(jobs_fpath):
|
|
872
|
+
with open(jobs_fpath) as f:
|
|
873
|
+
jobs = calkit.ryaml.load(f)
|
|
874
|
+
if jobs is None:
|
|
875
|
+
jobs = {}
|
|
876
|
+
else:
|
|
877
|
+
jobs = {}
|
|
878
|
+
job = jobs.get(job_key, {})
|
|
879
|
+
remote_pid = job.get("remote_pid")
|
|
880
|
+
if remote_pid is None:
|
|
881
|
+
# First make sure the remote working dir exists
|
|
882
|
+
typer.echo("Ensuring remote working directory exists")
|
|
883
|
+
subprocess.check_call(
|
|
884
|
+
["ssh"]
|
|
885
|
+
+ key_cmd
|
|
886
|
+
+ [f"{user}@{host}", f"mkdir -p {remote_wdir}"]
|
|
887
|
+
)
|
|
888
|
+
# Now send any necessary files
|
|
889
|
+
if send_paths:
|
|
890
|
+
typer.echo("Sending to remote directory")
|
|
891
|
+
# Accept glob patterns
|
|
892
|
+
paths = []
|
|
893
|
+
for p in send_paths:
|
|
894
|
+
paths += glob.glob(p)
|
|
895
|
+
scp_cmd = (
|
|
896
|
+
["scp", "-r"]
|
|
897
|
+
+ key_cmd
|
|
898
|
+
+ paths
|
|
899
|
+
+ [f"{user}@{host}:{remote_wdir}/"]
|
|
900
|
+
)
|
|
901
|
+
if verbose:
|
|
902
|
+
typer.echo(f"scp cmd: {scp_cmd}")
|
|
903
|
+
subprocess.check_call(scp_cmd)
|
|
904
|
+
# Now run the command
|
|
905
|
+
typer.echo(f"Running remote command: {remote_shell_cmd}")
|
|
906
|
+
if verbose:
|
|
907
|
+
typer.echo(f"Full command: {remote_cmd}")
|
|
908
|
+
remote_pid = (
|
|
909
|
+
subprocess.check_output(
|
|
910
|
+
["ssh"] + key_cmd + [f"{user}@{host}", remote_cmd]
|
|
911
|
+
)
|
|
912
|
+
.decode()
|
|
913
|
+
.strip()
|
|
914
|
+
)
|
|
915
|
+
typer.echo(f"Running with remote PID: {remote_pid}")
|
|
916
|
+
# Save PID to jobs database so we can resume waiting
|
|
917
|
+
typer.echo("Updating jobs database")
|
|
918
|
+
os.makedirs(".calkit", exist_ok=True)
|
|
919
|
+
job["remote_pid"] = remote_pid
|
|
920
|
+
job["submitted"] = time.time()
|
|
921
|
+
job["finished"] = None
|
|
922
|
+
jobs[job_key] = job
|
|
923
|
+
with open(jobs_fpath, "w") as f:
|
|
924
|
+
calkit.ryaml.dump(jobs, f)
|
|
925
|
+
# Now wait for the job to complete
|
|
926
|
+
typer.echo(f"Waiting for remote PID {remote_pid} to finish")
|
|
927
|
+
ps_cmd = ["ssh"] + key_cmd + [f"{user}@{host}", "ps", "-p", remote_pid]
|
|
928
|
+
finished = False
|
|
929
|
+
while not finished:
|
|
930
|
+
try:
|
|
931
|
+
subprocess.check_output(ps_cmd)
|
|
932
|
+
finished = False
|
|
933
|
+
time.sleep(2)
|
|
934
|
+
except subprocess.CalledProcessError:
|
|
935
|
+
finished = True
|
|
936
|
+
typer.echo("Remote process finished")
|
|
937
|
+
# Now sync the files back
|
|
938
|
+
# TODO: Figure out how to do this in one command
|
|
939
|
+
# Getting the syntax right is troublesome since it appears to work
|
|
940
|
+
# differently on different platforms
|
|
941
|
+
if get_paths:
|
|
942
|
+
typer.echo("Copying files back from remote directory")
|
|
943
|
+
for src_path in get_paths:
|
|
944
|
+
src_path = remote_wdir + "/" + src_path
|
|
945
|
+
src = f"{user}@{host}:{src_path}"
|
|
946
|
+
scp_cmd = ["scp", "-r"] + key_cmd + [src, "."]
|
|
947
|
+
subprocess.check_call(scp_cmd)
|
|
948
|
+
# Now delete the remote PID from the jobs file
|
|
949
|
+
typer.echo("Updating jobs database")
|
|
950
|
+
os.makedirs(".calkit", exist_ok=True)
|
|
951
|
+
job["remote_pid"] = None
|
|
952
|
+
job["finished"] = time.time()
|
|
953
|
+
jobs[job_key] = job
|
|
954
|
+
with open(jobs_fpath, "w") as f:
|
|
955
|
+
calkit.ryaml.dump(jobs, f)
|
|
842
956
|
else:
|
|
843
957
|
raise_error("Environment kind not supported")
|
|
844
958
|
|
|
@@ -1041,9 +1155,17 @@ def set_env_var(
|
|
|
1041
1155
|
@app.command(name="upgrade")
|
|
1042
1156
|
def upgrade():
|
|
1043
1157
|
"""Upgrade Calkit."""
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1158
|
+
if calkit.check_dep_exists("pipx"):
|
|
1159
|
+
cmd = ["pipx", "upgrade", "calkit-python"]
|
|
1160
|
+
elif calkit.check_dep_exists("uv"):
|
|
1161
|
+
cmd = [
|
|
1162
|
+
"uv",
|
|
1163
|
+
"pip",
|
|
1164
|
+
"install",
|
|
1165
|
+
"--system",
|
|
1166
|
+
"--upgrade",
|
|
1167
|
+
"calkit-python",
|
|
1168
|
+
]
|
|
1047
1169
|
else:
|
|
1048
|
-
cmd = ["pip", "install"]
|
|
1049
|
-
subprocess.run(cmd
|
|
1170
|
+
cmd = ["pip", "install", "--upgrade", "calkit-python"]
|
|
1171
|
+
subprocess.run(cmd)
|
|
@@ -4,9 +4,10 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import subprocess
|
|
7
|
+
import warnings
|
|
7
8
|
|
|
8
9
|
from packaging.specifiers import SpecifierSet
|
|
9
|
-
from packaging.version import Version
|
|
10
|
+
from packaging.version import InvalidVersion, Version
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
import calkit
|
|
@@ -20,14 +21,22 @@ def _check_single(req: str, actual: str, conda: bool = False) -> bool:
|
|
|
20
21
|
if conda and req_spec.startswith("="):
|
|
21
22
|
req_spec = "=" + req_spec
|
|
22
23
|
if not req_spec.endswith(".*"):
|
|
23
|
-
req_spec
|
|
24
|
+
if len(req_spec.split(".")) < 3:
|
|
25
|
+
req_spec += ".*"
|
|
24
26
|
actual_name, actual_vers = re.split("[=<>]+", actual, maxsplit=1)
|
|
25
27
|
if actual_name != req_name:
|
|
26
28
|
return False
|
|
27
29
|
actual_spec = actual.removeprefix(actual_name)
|
|
28
30
|
if conda and actual_spec.startswith("="):
|
|
29
31
|
actual_spec = "=" + actual_spec
|
|
30
|
-
|
|
32
|
+
try:
|
|
33
|
+
version = Version(actual_vers)
|
|
34
|
+
except InvalidVersion:
|
|
35
|
+
warnings.warn(
|
|
36
|
+
f"Cannot properly check {actual_name} version {actual_vers}"
|
|
37
|
+
)
|
|
38
|
+
# TODO: Check exact version only
|
|
39
|
+
return True
|
|
31
40
|
spec = SpecifierSet(req_spec)
|
|
32
41
|
return spec.contains(version)
|
|
33
42
|
|
|
@@ -109,6 +109,16 @@ class REnvironment(Environment):
|
|
|
109
109
|
prefix: str
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class SSHEnvironment(BaseModel):
|
|
113
|
+
kind: Literal["ssh"]
|
|
114
|
+
host: str
|
|
115
|
+
user: str
|
|
116
|
+
wdir: str
|
|
117
|
+
key: str | None = None
|
|
118
|
+
send_paths: list[str] = ["./*"]
|
|
119
|
+
get_paths: list[str] = ["*"]
|
|
120
|
+
|
|
121
|
+
|
|
112
122
|
class Software(BaseModel):
|
|
113
123
|
title: str
|
|
114
124
|
path: str
|
|
@@ -248,7 +258,11 @@ class ProjectInfo(BaseModel):
|
|
|
248
258
|
references: list[ReferenceCollection] = []
|
|
249
259
|
environments: dict[
|
|
250
260
|
str,
|
|
251
|
-
Environment
|
|
261
|
+
Environment
|
|
262
|
+
| DockerEnvironment
|
|
263
|
+
| VenvEnvironment
|
|
264
|
+
| UvVenvEnvironment
|
|
265
|
+
| SSHEnvironment,
|
|
252
266
|
] = {}
|
|
253
267
|
software: list[Software] = []
|
|
254
268
|
notebooks: list[Notebook] = []
|
|
@@ -85,6 +85,26 @@ def test_run_in_env(tmp_dir):
|
|
|
85
85
|
ck_info = calkit.load_calkit_info()
|
|
86
86
|
env = ck_info["environments"]["py3.10"]
|
|
87
87
|
assert env.get("path") is None
|
|
88
|
+
# Test that we can run a command that changes directory first
|
|
89
|
+
os.makedirs("my-new-dir/another", exist_ok=True)
|
|
90
|
+
out = (
|
|
91
|
+
subprocess.check_output(
|
|
92
|
+
"calkit xenv -n py3.10 --wdir my-new-dir -- ls",
|
|
93
|
+
shell=True,
|
|
94
|
+
)
|
|
95
|
+
.decode()
|
|
96
|
+
.strip()
|
|
97
|
+
)
|
|
98
|
+
assert out == "another"
|
|
99
|
+
out = (
|
|
100
|
+
subprocess.check_output(
|
|
101
|
+
"calkit xenv -n py3.10 --wdir my-new-dir -- ls ..",
|
|
102
|
+
shell=True,
|
|
103
|
+
)
|
|
104
|
+
.decode()
|
|
105
|
+
.strip()
|
|
106
|
+
)
|
|
107
|
+
assert "my-new-dir" in out.split("\n")
|
|
88
108
|
|
|
89
109
|
|
|
90
110
|
def test_run_in_venv(tmp_dir):
|
|
@@ -206,6 +226,13 @@ def test_to_shell_cmd():
|
|
|
206
226
|
shell_cmd = _to_shell_cmd(cmd)
|
|
207
227
|
assert shell_cmd == 'python -c "print(\\"hello world\\")"'
|
|
208
228
|
subprocess.check_call(shell_cmd, shell=True)
|
|
229
|
+
cmd = [
|
|
230
|
+
"sh",
|
|
231
|
+
"-c",
|
|
232
|
+
"cd dir1 && ls",
|
|
233
|
+
]
|
|
234
|
+
good_shell_cmd = 'sh -c "cd dir1 && ls"'
|
|
235
|
+
assert _to_shell_cmd(cmd) == good_shell_cmd
|
|
209
236
|
|
|
210
237
|
|
|
211
238
|
def test_add(tmp_dir):
|
|
@@ -23,6 +23,7 @@ Calkit supports defining and running code in these environment types:
|
|
|
23
23
|
- [`uv`](https://docs.astral.sh/uv/) (both `venv` and project-based)
|
|
24
24
|
- [Pixi](https://github.com/prefix-dev/pixi)
|
|
25
25
|
- [`renv`](https://rstudio.github.io/renv/index.html)
|
|
26
|
+
- `ssh`
|
|
26
27
|
|
|
27
28
|
Environment definitions live in the project's `calkit.yaml` file
|
|
28
29
|
in the `environments` section.
|
|
@@ -154,6 +155,21 @@ and another call to `calkit xenv -n foam2` will kick off a rebuild
|
|
|
154
155
|
automatically,
|
|
155
156
|
since the lock file will no longer match the Dockerfile.
|
|
156
157
|
|
|
158
|
+
If you're copying local files into the Docker image,
|
|
159
|
+
you can declare these
|
|
160
|
+
dependencies in the environment definition so the content of those will be
|
|
161
|
+
tracked as well:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
# In calkit.yaml
|
|
165
|
+
environments:
|
|
166
|
+
foam2:
|
|
167
|
+
kind: docker
|
|
168
|
+
image: foam2
|
|
169
|
+
deps:
|
|
170
|
+
- src/mySolver.C
|
|
171
|
+
```
|
|
172
|
+
|
|
157
173
|
This highlights Calkit's declarative design philosophy.
|
|
158
174
|
Simply declare the environment and use it in a pipeline stage
|
|
159
175
|
and Calkit will ensure it is built and up to date.
|
|
@@ -238,3 +254,60 @@ and an updated `environment-lock.yml` file will be created.
|
|
|
238
254
|
Again this highlights Calkit's declarative design philosophy.
|
|
239
255
|
Declare the environment and what command should be executed inside,
|
|
240
256
|
and Calkit will handle the rest.
|
|
257
|
+
|
|
258
|
+
### SSH
|
|
259
|
+
|
|
260
|
+
It's possible to define a remote environment that uses `ssh` to connect
|
|
261
|
+
and run commands,
|
|
262
|
+
and `scp` to copy files back and forth.
|
|
263
|
+
This could be useful, e.g.,
|
|
264
|
+
for running one or more pipeline stages on a high performance computing (HPC)
|
|
265
|
+
cluster,
|
|
266
|
+
or simply offloading some work to a virtual machine in the cloud
|
|
267
|
+
with specialized hardware like a more powerful GPU.
|
|
268
|
+
|
|
269
|
+
It is assumed that dependencies on the remote machine are managed separately.
|
|
270
|
+
|
|
271
|
+
An SSH environment defined in `calkit.yaml` looks like:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
environments:
|
|
275
|
+
cluster:
|
|
276
|
+
kind: ssh
|
|
277
|
+
host: "10.225.22.25"
|
|
278
|
+
user: my-user-name
|
|
279
|
+
wdir: /home/my-user-name/calkit/example-ssh
|
|
280
|
+
key: ~/.ssh/id_ed25519
|
|
281
|
+
send_paths:
|
|
282
|
+
- script.sh
|
|
283
|
+
get_paths:
|
|
284
|
+
- results
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
In the example above, we define an environment called `cluster`,
|
|
288
|
+
where we specify the host IP address, our username on that machine,
|
|
289
|
+
the working directory, the path to an SSH key on our local machine
|
|
290
|
+
(so we can connect without a password),
|
|
291
|
+
which paths we want to send before executing commands,
|
|
292
|
+
and which we want to copy back after they finish.
|
|
293
|
+
Wildcards in paths are supported, so the entire directory could be copied
|
|
294
|
+
if desired by specifying `*`.
|
|
295
|
+
|
|
296
|
+
To register an SSH key with the host, use `ssh-copy-id`. For example:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
ssh-copy-id -i ~/.ssh/id_ed25519 my-user-name@10.225.22.25
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
To execute a command in this environment, we can add a stage like this
|
|
303
|
+
to our DVC pipeline in `dvc.yaml`:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
stages:
|
|
307
|
+
run-simulation:
|
|
308
|
+
cmd: calkit xenv -n cluster bash script.sh
|
|
309
|
+
deps:
|
|
310
|
+
- script.sh
|
|
311
|
+
outs:
|
|
312
|
+
- results
|
|
313
|
+
```
|
|
@@ -52,6 +52,7 @@ Features:
|
|
|
52
52
|
- Environmental variable dependencies
|
|
53
53
|
- A pipeline designed to be run periodically to accumulate new data
|
|
54
54
|
- A project showcase with interactive Plotly figures
|
|
55
|
+
- A uv project-based environment and dedicated Python package
|
|
55
56
|
|
|
56
57
|
## OpenFOAM RANS boundary later validation
|
|
57
58
|
|
|
@@ -64,3 +65,13 @@ Features:
|
|
|
64
65
|
- A LaTeX document built with a Docker container
|
|
65
66
|
- A direct numerical simulation dataset for validation imported from a
|
|
66
67
|
different project, derived from the Johns Hopkins Turbulence Database
|
|
68
|
+
|
|
69
|
+
## SSH
|
|
70
|
+
|
|
71
|
+
[Project page](https://calkit.io/calkit/example-ssh) |
|
|
72
|
+
[GitHub repo](https://github.com/calkit/example-ssh)
|
|
73
|
+
|
|
74
|
+
Features:
|
|
75
|
+
|
|
76
|
+
- An SSH environment for running a remote command over SSH and copying back
|
|
77
|
+
results to the local machine
|