calkit-python 0.50.2__py3-none-any.whl → 0.50.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calkit/cli/check.py +7 -1
- calkit/cli/core.py +32 -0
- calkit/cli/export.py +139 -0
- calkit/cli/latex.py +909 -42
- calkit/cli/main/core.py +147 -5
- calkit/config.py +266 -57
- calkit/core.py +23 -4
- calkit/docker.py +2 -0
- calkit/docx.py +616 -2
- calkit/export.py +169 -0
- calkit/latex.py +439 -10
- calkit/models/pipeline.py +183 -11
- calkit/pipeline.py +45 -14
- calkit/provenance.py +278 -6
- calkit/reproducibility.py +63 -1
- calkit/tests/cli/main/test_core.py +184 -0
- calkit/tests/cli/test_core.py +33 -0
- calkit/tests/cli/test_export.py +152 -0
- calkit/tests/cli/test_latex.py +63 -0
- calkit/tests/models/test_pipeline.py +76 -0
- calkit/tests/test_config.py +238 -48
- calkit/tests/test_core.py +24 -0
- calkit/tests/test_docker.py +6 -0
- calkit/tests/test_docx.py +336 -8
- calkit/tests/test_export.py +85 -0
- calkit/tests/test_latex.py +184 -0
- calkit/tests/test_pipeline.py +91 -0
- calkit/tests/test_provenance.py +186 -0
- calkit/tests/test_reproducibility.py +88 -4
- calkit/tests/test_upgrade.py +9 -1
- calkit/tests/test_windows.py +56 -0
- calkit/upgrade.py +7 -0
- calkit/windows.py +59 -0
- {calkit_python-0.50.2.dist-info → calkit_python-0.50.4.dist-info}/METADATA +56 -1
- {calkit_python-0.50.2.dist-info → calkit_python-0.50.4.dist-info}/RECORD +51 -45
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.50.2.data → calkit_python-0.50.4.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.50.2.dist-info → calkit_python-0.50.4.dist-info}/WHEEL +0 -0
- {calkit_python-0.50.2.dist-info → calkit_python-0.50.4.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.50.2.dist-info → calkit_python-0.50.4.dist-info}/licenses/LICENSE +0 -0
calkit/cli/check.py
CHANGED
|
@@ -16,7 +16,7 @@ import dotenv
|
|
|
16
16
|
import typer
|
|
17
17
|
|
|
18
18
|
import calkit
|
|
19
|
-
from calkit.cli import AliasGroup, raise_error, warn
|
|
19
|
+
from calkit.cli import AliasGroup, raise_error, warn, warn_app_control
|
|
20
20
|
from calkit.core import get_md5
|
|
21
21
|
|
|
22
22
|
|
|
@@ -605,6 +605,7 @@ def check_environment(
|
|
|
605
605
|
try:
|
|
606
606
|
subprocess.check_call(cmd)
|
|
607
607
|
except subprocess.CalledProcessError:
|
|
608
|
+
warn_app_control(maybe=True)
|
|
608
609
|
raise_error("Failed to check pixi environment")
|
|
609
610
|
elif env["kind"] == "uv":
|
|
610
611
|
cmd = ["uv", "sync"]
|
|
@@ -616,6 +617,7 @@ def check_environment(
|
|
|
616
617
|
try:
|
|
617
618
|
subprocess.check_call(cmd)
|
|
618
619
|
except subprocess.CalledProcessError:
|
|
620
|
+
warn_app_control(maybe=True)
|
|
619
621
|
raise_error("Failed to check uv environment")
|
|
620
622
|
elif (kind := env["kind"]) in ["uv-venv", "venv"]:
|
|
621
623
|
if "path" not in env:
|
|
@@ -1835,6 +1837,7 @@ def check_conda_env(
|
|
|
1835
1837
|
verbose=not quiet,
|
|
1836
1838
|
)
|
|
1837
1839
|
except Exception as e:
|
|
1840
|
+
warn_app_control(output=str(e), maybe=True)
|
|
1838
1841
|
raise_error(f"Failed to check conda environment: {e}")
|
|
1839
1842
|
|
|
1840
1843
|
|
|
@@ -1917,6 +1920,7 @@ def check_venv(
|
|
|
1917
1920
|
try:
|
|
1918
1921
|
subprocess.check_call(create_cmd + [prefix], cwd=wdir)
|
|
1919
1922
|
except subprocess.CalledProcessError:
|
|
1923
|
+
warn_app_control(maybe=True)
|
|
1920
1924
|
raise_error(f"Failed to create {kind} at {prefix}")
|
|
1921
1925
|
# Put a gitignore file in the env dir if one doesn't exist
|
|
1922
1926
|
gitignore_fpath = os.path.join(prefix_full_path, ".gitignore")
|
|
@@ -2117,6 +2121,7 @@ def check_venv(
|
|
|
2117
2121
|
pip_install_and_freeze(dep_file_txt)
|
|
2118
2122
|
except (subprocess.CalledProcessError, OSError):
|
|
2119
2123
|
if lock_is_current:
|
|
2124
|
+
warn_app_control(maybe=True)
|
|
2120
2125
|
raise_error(
|
|
2121
2126
|
f"Failed to create {kind} at {prefix} from its lock file "
|
|
2122
2127
|
f"({lock_fpath}), which matches the spec ({path}). "
|
|
@@ -2132,6 +2137,7 @@ def check_venv(
|
|
|
2132
2137
|
try:
|
|
2133
2138
|
pip_install_and_freeze(f"-r {path}")
|
|
2134
2139
|
except subprocess.CalledProcessError:
|
|
2140
|
+
warn_app_control(maybe=True)
|
|
2135
2141
|
raise_error(f"Failed to check {kind} from input file {path}")
|
|
2136
2142
|
|
|
2137
2143
|
|
calkit/cli/core.py
CHANGED
|
@@ -184,3 +184,35 @@ def warn(txt: str, prefix: str = "Warning: ", err: bool = False):
|
|
|
184
184
|
text = typer.style(prefix + encode_safe(str(txt), err=err), fg="yellow")
|
|
185
185
|
if not _echo_above_spinner(text, err=err):
|
|
186
186
|
typer.echo(text, err=err)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def warn_app_control(
|
|
190
|
+
output: str = "", returncode: int | None = None, maybe: bool = False
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Explain a failure Windows App Control may have caused.
|
|
193
|
+
|
|
194
|
+
Smart App Control blocks unsigned executables and DLLs it doesn't trust,
|
|
195
|
+
which environments are full of, and the failure reads as unrelated.
|
|
196
|
+
Pass ``maybe`` for a failure whose output went straight to the terminal,
|
|
197
|
+
to warn whenever Smart App Control is on.
|
|
198
|
+
"""
|
|
199
|
+
from calkit.windows import (
|
|
200
|
+
SMART_APP_CONTROL_DOCS_URL,
|
|
201
|
+
app_control_blocked,
|
|
202
|
+
smart_app_control_on,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if app_control_blocked(output=output, returncode=returncode):
|
|
206
|
+
warn(
|
|
207
|
+
"Windows blocked a file from running, which Smart App Control "
|
|
208
|
+
"does to unsigned files it doesn't trust; see "
|
|
209
|
+
f"{SMART_APP_CONTROL_DOCS_URL}",
|
|
210
|
+
err=True,
|
|
211
|
+
)
|
|
212
|
+
elif maybe and smart_app_control_on():
|
|
213
|
+
warn(
|
|
214
|
+
"Smart App Control is on, which can block files in environments "
|
|
215
|
+
"from running; if the output above says a file was blocked, see "
|
|
216
|
+
f"{SMART_APP_CONTROL_DOCS_URL}",
|
|
217
|
+
err=True,
|
|
218
|
+
)
|
calkit/cli/export.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""CLI for exporting data and artifacts from various apps, e.g., a PNG
|
|
2
|
+
from an Inkscape SVG.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from typing_extensions import Annotated
|
|
12
|
+
|
|
13
|
+
from calkit.cli import raise_error
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from calkit.export import Exporter
|
|
17
|
+
|
|
18
|
+
export_app = typer.Typer(no_args_is_help=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _prepare(exporter: Exporter, source: str, output: str) -> str:
|
|
22
|
+
"""Get ready to export, returning the executable to run."""
|
|
23
|
+
exporter.check(source, output)
|
|
24
|
+
if not os.path.isfile(source):
|
|
25
|
+
raise ValueError(f"'{source}' doesn't exist")
|
|
26
|
+
os.makedirs(os.path.dirname(output) or ".", exist_ok=True)
|
|
27
|
+
# What was there is from before, so it can't pass for a new export
|
|
28
|
+
if os.path.exists(output):
|
|
29
|
+
os.remove(output)
|
|
30
|
+
exe = exporter.find_executable()
|
|
31
|
+
if exe is None:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Exporting '{source}' needs {exporter.app} installed"
|
|
34
|
+
)
|
|
35
|
+
return exe
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def inkscape(source: str, output: str, dpi: int | None = None) -> None:
|
|
39
|
+
"""Export a drawing to the format ``output``'s extension names,
|
|
40
|
+
raising ``ValueError`` if it can't be.
|
|
41
|
+
"""
|
|
42
|
+
import subprocess
|
|
43
|
+
|
|
44
|
+
from calkit.export import INKSCAPE
|
|
45
|
+
|
|
46
|
+
exe = _prepare(INKSCAPE, source, output)
|
|
47
|
+
cmd = [exe, source, f"--export-filename={output}"]
|
|
48
|
+
if dpi is not None:
|
|
49
|
+
cmd.append(f"--export-dpi={dpi}")
|
|
50
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
51
|
+
if not os.path.isfile(output):
|
|
52
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Inkscape didn't export '{output}' from '{source}'"
|
|
55
|
+
+ (f": {detail.splitlines()[-1]}" if detail else "")
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def gimp(source: str, output: str) -> None:
|
|
60
|
+
"""Flatten a GIMP document into the format ``output``'s extension
|
|
61
|
+
names, raising ``ValueError`` if it can't be.
|
|
62
|
+
|
|
63
|
+
Runs GIMP 3's batch dialect, then GIMP 2's, which differ in their file
|
|
64
|
+
procedures. Neither reports a failed script in its exit code, so the
|
|
65
|
+
written file is what says it worked.
|
|
66
|
+
"""
|
|
67
|
+
import subprocess
|
|
68
|
+
|
|
69
|
+
from calkit.export import GIMP
|
|
70
|
+
|
|
71
|
+
exe = _prepare(GIMP, source, output)
|
|
72
|
+
src = os.path.abspath(source).replace("\\", "\\\\").replace('"', '\\"')
|
|
73
|
+
dst = os.path.abspath(output).replace("\\", "\\\\").replace('"', '\\"')
|
|
74
|
+
v3 = (
|
|
75
|
+
f'(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "{src}"))))'
|
|
76
|
+
" (gimp-image-flatten image)"
|
|
77
|
+
f' (gimp-file-save RUN-NONINTERACTIVE image "{dst}" "{dst}")'
|
|
78
|
+
" (gimp-image-delete image)"
|
|
79
|
+
" (gimp-quit 0))"
|
|
80
|
+
)
|
|
81
|
+
v2 = (
|
|
82
|
+
"(let* ((image (car (gimp-file-load RUN-NONINTERACTIVE "
|
|
83
|
+
f'"{src}" "{src}")))'
|
|
84
|
+
" (drawable (car (gimp-image-flatten image))))"
|
|
85
|
+
f' (gimp-file-save RUN-NONINTERACTIVE image drawable "{dst}" '
|
|
86
|
+
f'"{dst}")'
|
|
87
|
+
" (gimp-image-delete image)"
|
|
88
|
+
" (gimp-quit 0))"
|
|
89
|
+
)
|
|
90
|
+
detail = ""
|
|
91
|
+
for cmd in [
|
|
92
|
+
[exe, "-i", "--batch-interpreter=plug-in-script-fu-eval", "-b", v3],
|
|
93
|
+
[exe, "-i", "-b", v2],
|
|
94
|
+
]:
|
|
95
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
96
|
+
if os.path.isfile(output):
|
|
97
|
+
return
|
|
98
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"GIMP didn't export '{output}' from '{source}'"
|
|
101
|
+
+ (f": {detail.splitlines()[-1]}" if detail else "")
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@export_app.command(name="inkscape", help="Export a figure from an SVG.")
|
|
106
|
+
def export_inkscape(
|
|
107
|
+
source: Annotated[str, typer.Argument(help="SVG to export from.")],
|
|
108
|
+
output: Annotated[
|
|
109
|
+
str,
|
|
110
|
+
typer.Option(
|
|
111
|
+
"--output", "-o", help="File to export to, e.g., a PNG or PDF."
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
dpi: Annotated[
|
|
115
|
+
int | None, typer.Option("--dpi", help="Resolution of a PNG.")
|
|
116
|
+
] = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
try:
|
|
119
|
+
inkscape(source, output, dpi=dpi)
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
raise_error(str(e))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@export_app.command(
|
|
125
|
+
name="gimp", help="Export a flattened figure from an XCF file."
|
|
126
|
+
)
|
|
127
|
+
def export_gimp(
|
|
128
|
+
source: Annotated[str, typer.Argument(help="XCF file to export from.")],
|
|
129
|
+
output: Annotated[
|
|
130
|
+
str,
|
|
131
|
+
typer.Option(
|
|
132
|
+
"--output", "-o", help="File to export to, e.g., a PNG or TIFF."
|
|
133
|
+
),
|
|
134
|
+
],
|
|
135
|
+
) -> None:
|
|
136
|
+
try:
|
|
137
|
+
gimp(source, output)
|
|
138
|
+
except ValueError as e:
|
|
139
|
+
raise_error(str(e))
|