calkit-python 0.16.1__tar.gz → 0.16.3__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.1 → calkit_python-0.16.3}/.gitignore +1 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/PKG-INFO +2 -1
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/__init__.py +1 -1
- calkit_python-0.16.3/calkit/cli/check.py +196 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/main.py +9 -127
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/new.py +1 -37
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/conda.py +12 -3
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/cli/test_main.py +28 -11
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/environments.md +15 -0
- calkit_python-0.16.3/docs/tutorials/conda-envs.md +57 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/pyproject.toml +1 -0
- calkit_python-0.16.3/uv.lock +3735 -0
- calkit_python-0.16.1/calkit/cli/check.py +0 -48
- calkit_python-0.16.1/docs/tutorials/conda-envs.md +0 -85
- {calkit_python-0.16.1 → calkit_python-0.16.3}/.github/FUNDING.yml +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/.github/workflows/docs.yml +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/LICENSE +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/README.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/calc.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/check.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/config.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/core.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/import_.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/list.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/office.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cli/update.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/cloud.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/config.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/core.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/datasets.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/docker.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/dvc.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/git.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/gui.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/jupyter.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/magics.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/models.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/office.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/ops.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/server.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/core.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/cli/test_new.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_calc.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_conda.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/CNAME +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/apps.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/calculations.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/calkit-yaml.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/cli-reference.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/cloud-integration.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/examples.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/help.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/img/c-to-the-k-white.svg +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/index.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/installation.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/pipeline/index.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/pipeline/manual-steps.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/references.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/first-project.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/push.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/chart-more-rows.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/create-project.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/excel-chart.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/excel-data.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/needs-clone.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/new-stage.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/status-more-rows.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/untracked-data.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/updated-publication.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/office/workflow-page.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/clone.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/create-project.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/new-token.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/reclone.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/latex-codespaces.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/matlab.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/office.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/openfoam.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/docs/version-control.md +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/mkdocs.yml +0 -0
- {calkit_python-0.16.1 → calkit_python-0.16.3}/test/pipeline.ipynb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.3
|
|
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'
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""CLI for checking things."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import checksumdir
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
import calkit
|
|
16
|
+
from calkit.check import check_reproducibility
|
|
17
|
+
from calkit.cli import raise_error
|
|
18
|
+
|
|
19
|
+
check_app = typer.Typer(no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@check_app.command(name="repro")
|
|
23
|
+
def check_repro(
|
|
24
|
+
wdir: Annotated[
|
|
25
|
+
str, typer.Option("--wdir", help="Project working directory.")
|
|
26
|
+
] = ".",
|
|
27
|
+
):
|
|
28
|
+
"""Check the reproducibility of a project."""
|
|
29
|
+
res = check_reproducibility(wdir=wdir, log_func=typer.echo)
|
|
30
|
+
typer.echo(res.to_pretty().encode("utf-8", errors="replace"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@check_app.command(name="call")
|
|
34
|
+
def check_call(
|
|
35
|
+
cmd: Annotated[str, typer.Argument(help="Command to check.")],
|
|
36
|
+
if_error: Annotated[
|
|
37
|
+
str,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--if-error", help="Command to run if there is an error."
|
|
40
|
+
),
|
|
41
|
+
],
|
|
42
|
+
):
|
|
43
|
+
"""Check that a command succeeds and run an alternate if not."""
|
|
44
|
+
try:
|
|
45
|
+
subprocess.check_call(cmd, shell=True)
|
|
46
|
+
typer.echo("Command succeeded")
|
|
47
|
+
except subprocess.CalledProcessError:
|
|
48
|
+
typer.echo("Command failed")
|
|
49
|
+
try:
|
|
50
|
+
typer.echo("Attempting fallback call")
|
|
51
|
+
subprocess.check_call(if_error, shell=True)
|
|
52
|
+
typer.echo("Fallback call succeeded")
|
|
53
|
+
except subprocess.CalledProcessError:
|
|
54
|
+
raise_error("Fallback call failed")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@check_app.command(
|
|
58
|
+
name="docker-env",
|
|
59
|
+
help="Check that Docker image is up-to-date.",
|
|
60
|
+
)
|
|
61
|
+
def check_docker_env(
|
|
62
|
+
tag: Annotated[str, typer.Argument(help="Image tag.")],
|
|
63
|
+
fpath: Annotated[
|
|
64
|
+
str, typer.Option("-i", "--input", help="Path to input Dockerfile.")
|
|
65
|
+
] = "Dockerfile",
|
|
66
|
+
platform: Annotated[
|
|
67
|
+
str, typer.Option("--platform", help="Which platform(s) to build for.")
|
|
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
|
+
] = [],
|
|
77
|
+
quiet: Annotated[
|
|
78
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
79
|
+
] = False,
|
|
80
|
+
):
|
|
81
|
+
def get_docker_inspect():
|
|
82
|
+
out = json.loads(
|
|
83
|
+
subprocess.check_output(["docker", "inspect", tag]).decode()
|
|
84
|
+
)
|
|
85
|
+
# Remove some keys that can change without the important aspects of
|
|
86
|
+
# the image changing
|
|
87
|
+
_ = out[0].pop("Id")
|
|
88
|
+
_ = out[0].pop("RepoDigests")
|
|
89
|
+
_ = out[0].pop("Metadata")
|
|
90
|
+
_ = out[0].pop("DockerVersion")
|
|
91
|
+
return out
|
|
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
|
+
|
|
101
|
+
outfile = open(os.devnull, "w") if quiet else None
|
|
102
|
+
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
103
|
+
# First call Docker inspect
|
|
104
|
+
try:
|
|
105
|
+
inspect = get_docker_inspect()
|
|
106
|
+
except subprocess.CalledProcessError:
|
|
107
|
+
typer.echo(f"No image with tag {tag} found locally", file=outfile)
|
|
108
|
+
inspect = []
|
|
109
|
+
typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
|
|
110
|
+
dockerfile_md5 = get_md5(fpath)
|
|
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)
|
|
116
|
+
rebuild = True
|
|
117
|
+
if os.path.isfile(lock_fpath):
|
|
118
|
+
typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
|
|
119
|
+
with open(lock_fpath) as f:
|
|
120
|
+
lock = json.load(f)
|
|
121
|
+
else:
|
|
122
|
+
typer.echo(f"Lock file ({lock_fpath}) does not exist", file=outfile)
|
|
123
|
+
lock = None
|
|
124
|
+
if inspect and lock:
|
|
125
|
+
typer.echo(
|
|
126
|
+
"Checking image and Dockerfile against lock file", file=outfile
|
|
127
|
+
)
|
|
128
|
+
rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
|
|
129
|
+
"Layers"
|
|
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
|
|
137
|
+
if rebuild:
|
|
138
|
+
wdir, fname = os.path.split(fpath)
|
|
139
|
+
if not wdir:
|
|
140
|
+
wdir = None
|
|
141
|
+
cmd = ["docker", "build", "-t", tag, "-f", fname]
|
|
142
|
+
if platform is not None:
|
|
143
|
+
cmd += ["--platform", platform]
|
|
144
|
+
cmd.append(".")
|
|
145
|
+
subprocess.check_output(cmd, cwd=wdir)
|
|
146
|
+
# Write the lock file
|
|
147
|
+
inspect = get_docker_inspect()
|
|
148
|
+
inspect[0]["DockerfileMD5"] = dockerfile_md5
|
|
149
|
+
inspect[0]["DepsMD5s"] = deps_md5s
|
|
150
|
+
with open(lock_fpath, "w") as f:
|
|
151
|
+
json.dump(inspect, f, indent=4)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@check_app.command(
|
|
155
|
+
name="conda-env",
|
|
156
|
+
help="Check a conda environment and rebuild if necessary.",
|
|
157
|
+
)
|
|
158
|
+
def check_conda_env(
|
|
159
|
+
env_fpath: Annotated[
|
|
160
|
+
str,
|
|
161
|
+
typer.Option(
|
|
162
|
+
"--file", "-f", help="Path to conda environment YAML file."
|
|
163
|
+
),
|
|
164
|
+
] = "environment.yml",
|
|
165
|
+
output_fpath: Annotated[
|
|
166
|
+
str,
|
|
167
|
+
typer.Option(
|
|
168
|
+
"--output",
|
|
169
|
+
"-o",
|
|
170
|
+
help=(
|
|
171
|
+
"Path to which existing environment should be exported. "
|
|
172
|
+
"If not specified, will have the same filename with '-lock' "
|
|
173
|
+
"appended to it, keeping the same extension."
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
] = None,
|
|
177
|
+
relaxed: Annotated[
|
|
178
|
+
bool,
|
|
179
|
+
typer.Option(
|
|
180
|
+
"--relaxed", help="Treat conda and pip dependencies as equivalent."
|
|
181
|
+
),
|
|
182
|
+
] = False,
|
|
183
|
+
quiet: Annotated[
|
|
184
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
185
|
+
] = False,
|
|
186
|
+
):
|
|
187
|
+
if quiet:
|
|
188
|
+
log_func = functools.partial(typer.echo, file=open(os.devnull, "w"))
|
|
189
|
+
else:
|
|
190
|
+
log_func = typer.echo
|
|
191
|
+
calkit.conda.check_env(
|
|
192
|
+
env_fpath=env_fpath,
|
|
193
|
+
output_fpath=output_fpath,
|
|
194
|
+
log_func=log_func,
|
|
195
|
+
relaxed=relaxed,
|
|
196
|
+
)
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
|
-
import functools
|
|
7
|
-
import hashlib
|
|
8
|
-
import json
|
|
9
6
|
import os
|
|
10
7
|
import platform as _platform
|
|
11
8
|
import subprocess
|
|
@@ -22,7 +19,7 @@ from typing_extensions import Annotated, Optional
|
|
|
22
19
|
|
|
23
20
|
import calkit
|
|
24
21
|
from calkit.cli import print_sep, raise_error, run_cmd, warn
|
|
25
|
-
from calkit.cli.check import check_app
|
|
22
|
+
from calkit.cli.check import check_app, check_conda_env, check_docker_env
|
|
26
23
|
from calkit.cli.config import config_app
|
|
27
24
|
from calkit.cli.import_ import import_app
|
|
28
25
|
from calkit.cli.list import list_app
|
|
@@ -60,7 +57,8 @@ def _to_shell_cmd(cmd: list[str]) -> str:
|
|
|
60
57
|
"""
|
|
61
58
|
quoted_cmd = []
|
|
62
59
|
for part in cmd:
|
|
63
|
-
|
|
60
|
+
# Find quotes within quotes and escape them
|
|
61
|
+
if " " in part or '"' in part[1:-1] or "'" in part[1:-1]:
|
|
64
62
|
part = part.replace('"', r"\"")
|
|
65
63
|
quoted_cmd.append(f'"{part}"')
|
|
66
64
|
else:
|
|
@@ -711,12 +709,11 @@ def run_in_env(
|
|
|
711
709
|
if env_name not in envs:
|
|
712
710
|
raise_error(f"Environment '{env_name}' does not exist")
|
|
713
711
|
env = envs[env_name]
|
|
714
|
-
if wdir is not None:
|
|
715
|
-
cwd = os.path.abspath(wdir)
|
|
716
|
-
else:
|
|
717
|
-
cwd = os.getcwd()
|
|
718
712
|
image_name = env.get("image", env_name)
|
|
719
713
|
docker_wdir = env.get("wdir", "/work")
|
|
714
|
+
docker_wdir_mount = docker_wdir
|
|
715
|
+
if wdir is not None:
|
|
716
|
+
docker_wdir = os.path.join(docker_wdir, wdir)
|
|
720
717
|
shell = env.get("shell", "sh")
|
|
721
718
|
platform = env.get("platform")
|
|
722
719
|
if env["kind"] == "docker":
|
|
@@ -727,6 +724,7 @@ def run_in_env(
|
|
|
727
724
|
tag=env["image"],
|
|
728
725
|
fpath=env["path"],
|
|
729
726
|
platform=env.get("platform"),
|
|
727
|
+
deps=env.get("deps", []),
|
|
730
728
|
quiet=True,
|
|
731
729
|
)
|
|
732
730
|
shell_cmd = _to_shell_cmd(cmd)
|
|
@@ -736,13 +734,14 @@ def run_in_env(
|
|
|
736
734
|
]
|
|
737
735
|
if platform:
|
|
738
736
|
docker_cmd += ["--platform", platform]
|
|
737
|
+
docker_cmd += env.get("args", [])
|
|
739
738
|
docker_cmd += [
|
|
740
739
|
"-it" if sys.stdin.isatty() else "-i",
|
|
741
740
|
"--rm",
|
|
742
741
|
"-w",
|
|
743
742
|
docker_wdir,
|
|
744
743
|
"-v",
|
|
745
|
-
f"{
|
|
744
|
+
f"{os.getcwd()}:{docker_wdir_mount}",
|
|
746
745
|
image_name,
|
|
747
746
|
shell,
|
|
748
747
|
"-c",
|
|
@@ -846,78 +845,6 @@ def run_in_env(
|
|
|
846
845
|
raise_error("Environment kind not supported")
|
|
847
846
|
|
|
848
847
|
|
|
849
|
-
@app.command(
|
|
850
|
-
name="build-docker",
|
|
851
|
-
help="Build Docker image if missing or different from lock file.",
|
|
852
|
-
)
|
|
853
|
-
def check_docker_env(
|
|
854
|
-
tag: Annotated[str, typer.Argument(help="Image tag.")],
|
|
855
|
-
fpath: Annotated[
|
|
856
|
-
str, typer.Option("-i", "--input", help="Path to input Dockerfile.")
|
|
857
|
-
] = "Dockerfile",
|
|
858
|
-
platform: Annotated[
|
|
859
|
-
str, typer.Option("--platform", help="Which platform(s) to build for.")
|
|
860
|
-
] = None,
|
|
861
|
-
quiet: Annotated[
|
|
862
|
-
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
863
|
-
] = False,
|
|
864
|
-
):
|
|
865
|
-
def get_docker_inspect():
|
|
866
|
-
out = json.loads(
|
|
867
|
-
subprocess.check_output(["docker", "inspect", tag]).decode()
|
|
868
|
-
)
|
|
869
|
-
# Remove some keys that can change without the important aspects of
|
|
870
|
-
# the image changing
|
|
871
|
-
_ = out[0].pop("Id")
|
|
872
|
-
_ = out[0].pop("RepoDigests")
|
|
873
|
-
_ = out[0].pop("Metadata")
|
|
874
|
-
_ = out[0].pop("DockerVersion")
|
|
875
|
-
return out
|
|
876
|
-
|
|
877
|
-
outfile = open(os.devnull, "w") if quiet else None
|
|
878
|
-
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
879
|
-
# First call Docker inspect
|
|
880
|
-
try:
|
|
881
|
-
inspect = get_docker_inspect()
|
|
882
|
-
except subprocess.CalledProcessError:
|
|
883
|
-
typer.echo(f"No image with tag {tag} found locally", file=outfile)
|
|
884
|
-
inspect = []
|
|
885
|
-
typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
|
|
886
|
-
with open(fpath) as f:
|
|
887
|
-
dockerfile = f.read()
|
|
888
|
-
dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
|
|
889
|
-
lock_fpath = fpath + "-lock.json"
|
|
890
|
-
rebuild = True
|
|
891
|
-
if os.path.isfile(lock_fpath):
|
|
892
|
-
typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
|
|
893
|
-
with open(lock_fpath) as f:
|
|
894
|
-
lock = json.load(f)
|
|
895
|
-
else:
|
|
896
|
-
typer.echo(f"Lock file ({lock_fpath}) does not exist", file=outfile)
|
|
897
|
-
lock = None
|
|
898
|
-
if inspect and lock:
|
|
899
|
-
typer.echo(
|
|
900
|
-
"Checking image and Dockerfile against lock file", file=outfile
|
|
901
|
-
)
|
|
902
|
-
rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
|
|
903
|
-
"Layers"
|
|
904
|
-
] or dockerfile_md5 != lock[0].get("DockerfileMD5")
|
|
905
|
-
if rebuild:
|
|
906
|
-
wdir, fname = os.path.split(fpath)
|
|
907
|
-
if not wdir:
|
|
908
|
-
wdir = None
|
|
909
|
-
cmd = ["docker", "build", "-t", tag, "-f", fname]
|
|
910
|
-
if platform is not None:
|
|
911
|
-
cmd += ["--platform", platform]
|
|
912
|
-
cmd.append(".")
|
|
913
|
-
subprocess.check_call(cmd, cwd=wdir)
|
|
914
|
-
# Write the lock file
|
|
915
|
-
inspect = get_docker_inspect()
|
|
916
|
-
inspect[0]["DockerfileMD5"] = dockerfile_md5
|
|
917
|
-
with open(lock_fpath, "w") as f:
|
|
918
|
-
json.dump(inspect, f, indent=4)
|
|
919
|
-
|
|
920
|
-
|
|
921
848
|
@app.command(name="runproc", help="Execute a procedure (alias for 'xproc').")
|
|
922
849
|
@app.command(name="xproc", help="Execute a procedure.")
|
|
923
850
|
def run_procedure(
|
|
@@ -1059,51 +986,6 @@ def run_procedure(
|
|
|
1059
986
|
wait(step.wait_after_s)
|
|
1060
987
|
|
|
1061
988
|
|
|
1062
|
-
@app.command(
|
|
1063
|
-
name="check-conda-env",
|
|
1064
|
-
help="Check a conda environment and rebuild if necessary.",
|
|
1065
|
-
)
|
|
1066
|
-
def check_conda_env(
|
|
1067
|
-
env_fpath: Annotated[
|
|
1068
|
-
str,
|
|
1069
|
-
typer.Option(
|
|
1070
|
-
"--file", "-f", help="Path to conda environment YAML file."
|
|
1071
|
-
),
|
|
1072
|
-
] = "environment.yml",
|
|
1073
|
-
output_fpath: Annotated[
|
|
1074
|
-
str,
|
|
1075
|
-
typer.Option(
|
|
1076
|
-
"--output",
|
|
1077
|
-
"-o",
|
|
1078
|
-
help=(
|
|
1079
|
-
"Path to which existing environment should be exported. "
|
|
1080
|
-
"If not specified, will have the same filename with '-lock' "
|
|
1081
|
-
"appended to it, keeping the same extension."
|
|
1082
|
-
),
|
|
1083
|
-
),
|
|
1084
|
-
] = None,
|
|
1085
|
-
relaxed: Annotated[
|
|
1086
|
-
bool,
|
|
1087
|
-
typer.Option(
|
|
1088
|
-
"--relaxed", help="Treat conda and pip dependencies as equivalent."
|
|
1089
|
-
),
|
|
1090
|
-
] = False,
|
|
1091
|
-
quiet: Annotated[
|
|
1092
|
-
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
1093
|
-
] = False,
|
|
1094
|
-
):
|
|
1095
|
-
if quiet:
|
|
1096
|
-
log_func = functools.partial(typer.echo, file=open(os.devnull, "w"))
|
|
1097
|
-
else:
|
|
1098
|
-
log_func = typer.echo
|
|
1099
|
-
calkit.conda.check_env(
|
|
1100
|
-
env_fpath=env_fpath,
|
|
1101
|
-
output_fpath=output_fpath,
|
|
1102
|
-
log_func=log_func,
|
|
1103
|
-
relaxed=relaxed,
|
|
1104
|
-
)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
989
|
@app.command(name="calc")
|
|
1108
990
|
def run_calculation(
|
|
1109
991
|
name: Annotated[str, typer.Argument(help="Calculation name.")],
|
|
@@ -469,10 +469,6 @@ def new_docker_env(
|
|
|
469
469
|
),
|
|
470
470
|
),
|
|
471
471
|
] = None,
|
|
472
|
-
stage: Annotated[
|
|
473
|
-
str,
|
|
474
|
-
typer.Option("--stage", help="DVC pipeline stage name, deprecated."),
|
|
475
|
-
] = None,
|
|
476
472
|
layers: Annotated[
|
|
477
473
|
list[str],
|
|
478
474
|
typer.Option(
|
|
@@ -505,8 +501,6 @@ def new_docker_env(
|
|
|
505
501
|
path = "Dockerfile"
|
|
506
502
|
if base and os.path.isfile(path) and not overwrite:
|
|
507
503
|
raise_error("Output path already exists (use -f to overwrite)")
|
|
508
|
-
if stage and not base:
|
|
509
|
-
raise_error("--from must be specified when creating a build stage")
|
|
510
504
|
if image_name is None:
|
|
511
505
|
typer.echo("No image name specified; using environment name")
|
|
512
506
|
image_name = name
|
|
@@ -543,8 +537,6 @@ def new_docker_env(
|
|
|
543
537
|
)
|
|
544
538
|
if base is not None or path is not None:
|
|
545
539
|
env["path"] = path
|
|
546
|
-
if stage is not None:
|
|
547
|
-
env["stage"] = stage
|
|
548
540
|
if description is not None:
|
|
549
541
|
env["description"] = description
|
|
550
542
|
if layers:
|
|
@@ -555,35 +547,7 @@ def new_docker_env(
|
|
|
555
547
|
ck_info["environments"] = envs
|
|
556
548
|
with open("calkit.yaml", "w") as f:
|
|
557
549
|
ryaml.dump(ck_info, f)
|
|
558
|
-
# If we're creating a stage, do so with DVC
|
|
559
|
-
if stage:
|
|
560
|
-
warn("--stage is deprecated since envs are checked at run time")
|
|
561
|
-
typer.echo(f"Creating DVC stage {stage}")
|
|
562
|
-
if not os.path.isfile(".dvc/config"):
|
|
563
|
-
typer.echo(f"Running dvc init")
|
|
564
|
-
subprocess.check_call(["dvc", "init"])
|
|
565
|
-
ck_cmd = f"calkit build-docker {image_name} -i {path}"
|
|
566
|
-
if platform:
|
|
567
|
-
ck_cmd += f" --platform {platform}"
|
|
568
|
-
subprocess.check_call(
|
|
569
|
-
[
|
|
570
|
-
"dvc",
|
|
571
|
-
"stage",
|
|
572
|
-
"add",
|
|
573
|
-
"-f",
|
|
574
|
-
"-n",
|
|
575
|
-
stage,
|
|
576
|
-
"--always-changed",
|
|
577
|
-
"-d",
|
|
578
|
-
path,
|
|
579
|
-
"--outs-persist-no-cache",
|
|
580
|
-
f"{path}-lock.json",
|
|
581
|
-
ck_cmd,
|
|
582
|
-
]
|
|
583
|
-
)
|
|
584
550
|
repo.git.add("calkit.yaml")
|
|
585
|
-
if stage:
|
|
586
|
-
repo.git.add("dvc.yaml")
|
|
587
551
|
if not no_commit and repo.git.diff("--staged"):
|
|
588
552
|
repo.git.commit(["-m", f"Add Docker environment {name}"])
|
|
589
553
|
|
|
@@ -1016,7 +980,7 @@ def new_conda_env(
|
|
|
1016
980
|
if stage:
|
|
1017
981
|
typer.echo(f"Creating DVC stage {stage}")
|
|
1018
982
|
if not os.path.isfile(".dvc/config"):
|
|
1019
|
-
typer.echo(
|
|
983
|
+
typer.echo("Running dvc init")
|
|
1020
984
|
subprocess.check_call(["dvc", "init"])
|
|
1021
985
|
ck_cmd = f"calkit check-conda-env -f {path}"
|
|
1022
986
|
fname, ext = os.path.splitext(path)
|
|
@@ -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
|
|
|
@@ -11,7 +11,6 @@ from git.exc import InvalidGitRepositoryError
|
|
|
11
11
|
|
|
12
12
|
import calkit
|
|
13
13
|
from calkit.cli.main import _to_shell_cmd
|
|
14
|
-
from calkit.core import ryaml
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
def test_run_in_env(tmp_dir):
|
|
@@ -21,13 +20,11 @@ def test_run_in_env(tmp_dir):
|
|
|
21
20
|
subprocess.check_call(
|
|
22
21
|
"calkit new docker-env "
|
|
23
22
|
"--name my-image "
|
|
24
|
-
"--stage build-image "
|
|
25
23
|
"--from ubuntu "
|
|
26
24
|
"--add-layer miniforge "
|
|
27
25
|
"--description 'This is a test image'",
|
|
28
26
|
shell=True,
|
|
29
27
|
)
|
|
30
|
-
subprocess.check_call("calkit run", shell=True)
|
|
31
28
|
out = (
|
|
32
29
|
subprocess.check_output("calkit xenv echo sup", shell=True)
|
|
33
30
|
.decode()
|
|
@@ -40,7 +37,6 @@ def test_run_in_env(tmp_dir):
|
|
|
40
37
|
"calkit new docker-env "
|
|
41
38
|
"-n env2 "
|
|
42
39
|
"--image my-image-2 "
|
|
43
|
-
"--stage build-image-2 "
|
|
44
40
|
"--path Dockerfile.2 "
|
|
45
41
|
"--from ubuntu "
|
|
46
42
|
"--add-layer miniforge "
|
|
@@ -48,12 +44,6 @@ def test_run_in_env(tmp_dir):
|
|
|
48
44
|
"--description 'This is a test image 2'",
|
|
49
45
|
shell=True,
|
|
50
46
|
)
|
|
51
|
-
with open("dvc.yaml") as f:
|
|
52
|
-
pipeline = ryaml.load(f)
|
|
53
|
-
stg = pipeline["stages"]["build-image-2"]
|
|
54
|
-
cmd = stg["cmd"]
|
|
55
|
-
assert "-i Dockerfile.2" in cmd
|
|
56
|
-
subprocess.check_call("calkit run", shell=True)
|
|
57
47
|
with pytest.raises(subprocess.CalledProcessError):
|
|
58
48
|
out = (
|
|
59
49
|
subprocess.check_output("calkit xenv echo sup", shell=True)
|
|
@@ -95,6 +85,26 @@ def test_run_in_env(tmp_dir):
|
|
|
95
85
|
ck_info = calkit.load_calkit_info()
|
|
96
86
|
env = ck_info["environments"]["py3.10"]
|
|
97
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")
|
|
98
108
|
|
|
99
109
|
|
|
100
110
|
def test_run_in_venv(tmp_dir):
|
|
@@ -178,7 +188,7 @@ def test_run_in_venv(tmp_dir):
|
|
|
178
188
|
)
|
|
179
189
|
ck_info = calkit.load_calkit_info(as_pydantic=True)
|
|
180
190
|
envs = ck_info.environments
|
|
181
|
-
|
|
191
|
+
assert "my-pixi" in envs
|
|
182
192
|
out = (
|
|
183
193
|
subprocess.check_output(
|
|
184
194
|
[
|
|
@@ -216,6 +226,13 @@ def test_to_shell_cmd():
|
|
|
216
226
|
shell_cmd = _to_shell_cmd(cmd)
|
|
217
227
|
assert shell_cmd == 'python -c "print(\\"hello world\\")"'
|
|
218
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
|
|
219
236
|
|
|
220
237
|
|
|
221
238
|
def test_add(tmp_dir):
|
|
@@ -154,6 +154,21 @@ and another call to `calkit xenv -n foam2` will kick off a rebuild
|
|
|
154
154
|
automatically,
|
|
155
155
|
since the lock file will no longer match the Dockerfile.
|
|
156
156
|
|
|
157
|
+
If you're copying local files into the Docker image,
|
|
158
|
+
you can declare these
|
|
159
|
+
dependencies in the environment definition so the content of those will be
|
|
160
|
+
tracked as well:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
# In calkit.yaml
|
|
164
|
+
environments:
|
|
165
|
+
foam2:
|
|
166
|
+
kind: docker
|
|
167
|
+
image: foam2
|
|
168
|
+
deps:
|
|
169
|
+
- src/mySolver.C
|
|
170
|
+
```
|
|
171
|
+
|
|
157
172
|
This highlights Calkit's declarative design philosophy.
|
|
158
173
|
Simply declare the environment and use it in a pipeline stage
|
|
159
174
|
and Calkit will ensure it is built and up to date.
|