calkit-python 0.37.2__py3-none-any.whl → 0.37.3__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 +247 -105
- calkit/cli/main/core.py +3 -0
- calkit/cli/new.py +16 -0
- calkit/cli/notebooks.py +7 -0
- calkit/cli/slurm.py +41 -9
- calkit/dvc/zip.py +9 -3
- calkit/environments.py +126 -13
- calkit/fs.py +53 -2
- calkit/git.py +1 -1
- calkit/julia.py +30 -2
- calkit/matlab.py +1 -1
- calkit/models/core.py +29 -0
- calkit/models/pipeline.py +18 -15
- calkit/pipeline.py +2 -2
- calkit/tests/cli/test_check.py +35 -0
- calkit/tests/models/test_pipeline.py +13 -1
- calkit/tests/test_environments.py +155 -0
- calkit/tests/test_fs.py +119 -1
- calkit/tests/test_git.py +5 -5
- calkit/tests/test_julia.py +67 -1
- calkit/tests/test_pipeline.py +38 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/package.json +1 -1
- calkit_python-0.37.2.data/data/share/jupyter/labextensions/calkit/static/909.651be47ca47390b78a92.js → calkit_python-0.37.3.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +1 -1
- calkit_python-0.37.2.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.c091821b3d7f2d287a67.js → calkit_python-0.37.3.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +1 -1
- {calkit_python-0.37.2.dist-info → calkit_python-0.37.3.dist-info}/METADATA +56 -45
- {calkit_python-0.37.2.dist-info → calkit_python-0.37.3.dist-info}/RECORD +41 -41
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.37.2.data → calkit_python-0.37.3.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.37.2.dist-info → calkit_python-0.37.3.dist-info}/WHEEL +0 -0
- {calkit_python-0.37.2.dist-info → calkit_python-0.37.3.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.37.2.dist-info → calkit_python-0.37.3.dist-info}/licenses/LICENSE +0 -0
calkit/cli/check.py
CHANGED
|
@@ -31,6 +31,217 @@ from calkit.environments import (
|
|
|
31
31
|
check_app = typer.Typer(no_args_is_help=True)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _juliaup_version_installed(julia_version: str) -> bool:
|
|
35
|
+
"""Return True if juliaup already has the given channel installed locally."""
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["juliaup", "status"],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
check=False,
|
|
42
|
+
timeout=10,
|
|
43
|
+
)
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
return False
|
|
46
|
+
except subprocess.TimeoutExpired:
|
|
47
|
+
warn("Timed out while running `juliaup status`.")
|
|
48
|
+
return False
|
|
49
|
+
if result.returncode != 0:
|
|
50
|
+
err_output = (result.stderr or result.stdout or "").strip()
|
|
51
|
+
if err_output:
|
|
52
|
+
warn(f"`juliaup status` failed: {err_output}")
|
|
53
|
+
else:
|
|
54
|
+
warn(
|
|
55
|
+
f"`juliaup status` failed with exit code {result.returncode}."
|
|
56
|
+
)
|
|
57
|
+
return False
|
|
58
|
+
for line in result.stdout.splitlines():
|
|
59
|
+
parts = line.split()
|
|
60
|
+
if not parts:
|
|
61
|
+
continue
|
|
62
|
+
# Columns: [Default?] Channel Version [Update?]
|
|
63
|
+
# Default column is either empty or "*", so channel is parts[0] or parts[1]
|
|
64
|
+
for col in parts[:2]:
|
|
65
|
+
if col == julia_version:
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_julia_env(
|
|
71
|
+
env_path: str,
|
|
72
|
+
julia_version: str | None = None,
|
|
73
|
+
verbose: bool = False,
|
|
74
|
+
cache_key: str | None = None,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Check a Julia environment and instantiate only when needed."""
|
|
77
|
+
abs_env_path = os.path.abspath(env_path)
|
|
78
|
+
env_fname = os.path.basename(abs_env_path)
|
|
79
|
+
if env_fname != "Project.toml":
|
|
80
|
+
raise_error(
|
|
81
|
+
"Julia environments require a path pointing to Project.toml"
|
|
82
|
+
)
|
|
83
|
+
env_dir = os.path.dirname(abs_env_path) or "."
|
|
84
|
+
env_path_for_cache = os.path.basename(abs_env_path)
|
|
85
|
+
env = {
|
|
86
|
+
"kind": "julia",
|
|
87
|
+
"path": env_path_for_cache,
|
|
88
|
+
"julia": julia_version or "",
|
|
89
|
+
}
|
|
90
|
+
cache_env_name = cache_key or (
|
|
91
|
+
f"julia::{abs_env_path}::{julia_version or ''}"
|
|
92
|
+
)
|
|
93
|
+
if calkit.environments.check_cache(
|
|
94
|
+
env_name=cache_env_name,
|
|
95
|
+
env=env,
|
|
96
|
+
wdir=env_dir,
|
|
97
|
+
respect_ttl=False,
|
|
98
|
+
):
|
|
99
|
+
if verbose:
|
|
100
|
+
typer.echo(
|
|
101
|
+
"Julia environment cache is valid; skipping Pkg.instantiate()"
|
|
102
|
+
)
|
|
103
|
+
lock_fpath = calkit.environments.get_env_lock_fpath(
|
|
104
|
+
env=env,
|
|
105
|
+
env_name=cache_env_name,
|
|
106
|
+
wdir=env_dir,
|
|
107
|
+
as_posix=False,
|
|
108
|
+
)
|
|
109
|
+
return lock_fpath or os.path.join(env_dir, "Manifest.toml")
|
|
110
|
+
if julia_version:
|
|
111
|
+
if shutil.which("juliaup") is not None:
|
|
112
|
+
if _juliaup_version_installed(julia_version):
|
|
113
|
+
if verbose:
|
|
114
|
+
typer.echo(
|
|
115
|
+
f"Julia {julia_version} is already installed; "
|
|
116
|
+
"skipping juliaup add"
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
cmd = ["juliaup", "add", julia_version]
|
|
120
|
+
if verbose:
|
|
121
|
+
typer.echo(f"Running command: {cmd}")
|
|
122
|
+
try:
|
|
123
|
+
subprocess.run(cmd, check=True)
|
|
124
|
+
except subprocess.CalledProcessError:
|
|
125
|
+
raise_error(
|
|
126
|
+
f"Failed to install Julia version {julia_version}"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
try:
|
|
130
|
+
compatible = calkit.julia.current_version_is_compatible(
|
|
131
|
+
julia_version
|
|
132
|
+
)
|
|
133
|
+
except ValueError as e:
|
|
134
|
+
raise_error(str(e))
|
|
135
|
+
if not compatible:
|
|
136
|
+
raise_error(
|
|
137
|
+
"Current Julia version is not compatible with required "
|
|
138
|
+
f"version ({julia_version}), and juliaup is not "
|
|
139
|
+
"available to install it"
|
|
140
|
+
)
|
|
141
|
+
deps_to_add: list[str] = []
|
|
142
|
+
try:
|
|
143
|
+
with open(abs_env_path, "r") as f:
|
|
144
|
+
content = f.read()
|
|
145
|
+
lines = [line.rstrip() for line in content.splitlines()]
|
|
146
|
+
deps_section = False
|
|
147
|
+
deps_found = False
|
|
148
|
+
for line in lines:
|
|
149
|
+
stripped = line.strip()
|
|
150
|
+
if stripped == "[deps]":
|
|
151
|
+
deps_section = True
|
|
152
|
+
continue
|
|
153
|
+
if deps_section:
|
|
154
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
155
|
+
break
|
|
156
|
+
if stripped and not stripped.startswith("#"):
|
|
157
|
+
if "=" in stripped:
|
|
158
|
+
deps_found = True
|
|
159
|
+
break
|
|
160
|
+
if not deps_found:
|
|
161
|
+
for idx, line in enumerate(lines):
|
|
162
|
+
marker = "# Dependencies (add with Julia's Pkg.add):"
|
|
163
|
+
if line.strip() == marker and idx + 1 < len(lines):
|
|
164
|
+
dep_line = lines[idx + 1].strip()
|
|
165
|
+
if dep_line.startswith("#"):
|
|
166
|
+
dep_line = dep_line.lstrip("#").strip()
|
|
167
|
+
deps_to_add = [
|
|
168
|
+
dep.strip()
|
|
169
|
+
for dep in dep_line.split(",")
|
|
170
|
+
if dep.strip()
|
|
171
|
+
]
|
|
172
|
+
break
|
|
173
|
+
except OSError:
|
|
174
|
+
deps_to_add = []
|
|
175
|
+
if deps_to_add:
|
|
176
|
+
pkg_list = ", ".join(f'"{dep}"' for dep in deps_to_add)
|
|
177
|
+
cmd = ["julia"]
|
|
178
|
+
if julia_version:
|
|
179
|
+
cmd.append(f"+{julia_version}")
|
|
180
|
+
cmd += [
|
|
181
|
+
f"--project={env_dir}",
|
|
182
|
+
"-e",
|
|
183
|
+
f"using Pkg; Pkg.add([{pkg_list}]);",
|
|
184
|
+
]
|
|
185
|
+
if julia_version:
|
|
186
|
+
try:
|
|
187
|
+
cmd = calkit.julia.check_version_in_command(cmd)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
raise_error(f"Failed to check Julia version: {e}")
|
|
190
|
+
cmd = calkit.julia.ensure_startup_file_disabled_in_command(cmd)
|
|
191
|
+
try:
|
|
192
|
+
subprocess.check_call(
|
|
193
|
+
cmd,
|
|
194
|
+
env=os.environ.copy() | {"JULIA_LOAD_PATH": "@:@stdlib"},
|
|
195
|
+
)
|
|
196
|
+
except subprocess.CalledProcessError:
|
|
197
|
+
calkit.environments.save_cache(
|
|
198
|
+
env_name=cache_env_name,
|
|
199
|
+
env=env,
|
|
200
|
+
wdir=env_dir,
|
|
201
|
+
success=False,
|
|
202
|
+
)
|
|
203
|
+
raise_error("Failed to add Julia dependencies")
|
|
204
|
+
cmd = ["julia"]
|
|
205
|
+
if julia_version:
|
|
206
|
+
cmd.append(f"+{julia_version}")
|
|
207
|
+
cmd += [
|
|
208
|
+
f"--project={env_dir}",
|
|
209
|
+
"-e",
|
|
210
|
+
"using Pkg; Pkg.instantiate();",
|
|
211
|
+
]
|
|
212
|
+
if julia_version:
|
|
213
|
+
try:
|
|
214
|
+
cmd = calkit.julia.check_version_in_command(cmd)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
raise_error(f"Failed to check Julia version: {e}")
|
|
217
|
+
cmd = calkit.julia.ensure_startup_file_disabled_in_command(cmd)
|
|
218
|
+
try:
|
|
219
|
+
subprocess.check_call(
|
|
220
|
+
cmd, env=os.environ.copy() | {"JULIA_LOAD_PATH": "@:@stdlib"}
|
|
221
|
+
)
|
|
222
|
+
except subprocess.CalledProcessError:
|
|
223
|
+
calkit.environments.save_cache(
|
|
224
|
+
env_name=cache_env_name,
|
|
225
|
+
env=env,
|
|
226
|
+
wdir=env_dir,
|
|
227
|
+
success=False,
|
|
228
|
+
)
|
|
229
|
+
raise_error("Failed to check julia environment")
|
|
230
|
+
calkit.environments.save_cache(
|
|
231
|
+
env_name=cache_env_name,
|
|
232
|
+
env=env,
|
|
233
|
+
wdir=env_dir,
|
|
234
|
+
success=True,
|
|
235
|
+
)
|
|
236
|
+
lock_fpath = calkit.environments.get_env_lock_fpath(
|
|
237
|
+
env=env,
|
|
238
|
+
env_name=cache_env_name,
|
|
239
|
+
wdir=env_dir,
|
|
240
|
+
as_posix=False,
|
|
241
|
+
)
|
|
242
|
+
return lock_fpath or os.path.join(env_dir, "Manifest.toml")
|
|
243
|
+
|
|
244
|
+
|
|
34
245
|
@check_app.command(name="repro")
|
|
35
246
|
def check_repro(
|
|
36
247
|
wdir: Annotated[
|
|
@@ -190,116 +401,47 @@ def check_environment(
|
|
|
190
401
|
julia_version = env.get("julia")
|
|
191
402
|
if julia_version is None:
|
|
192
403
|
raise_error("Julia environments require a Julia version")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if shutil.which("juliaup") is not None:
|
|
200
|
-
cmd = ["juliaup", "add", julia_version]
|
|
201
|
-
if verbose:
|
|
202
|
-
typer.echo(f"Running command: {cmd}")
|
|
203
|
-
try:
|
|
204
|
-
subprocess.run(cmd, check=True)
|
|
205
|
-
except subprocess.CalledProcessError:
|
|
206
|
-
raise_error(f"Failed to install Julia version {julia_version}")
|
|
207
|
-
else:
|
|
208
|
-
try:
|
|
209
|
-
compatible = calkit.julia.current_version_is_compatible(
|
|
210
|
-
julia_version
|
|
211
|
-
)
|
|
212
|
-
except ValueError as e:
|
|
213
|
-
raise_error(str(e))
|
|
214
|
-
if not compatible:
|
|
215
|
-
raise_error(
|
|
216
|
-
f"Current Julia version is not compatible with required "
|
|
217
|
-
f"version ({julia_version}), and juliaup is not available to "
|
|
218
|
-
"install it"
|
|
219
|
-
)
|
|
220
|
-
env_dir = os.path.dirname(env_path)
|
|
221
|
-
if not env_dir:
|
|
222
|
-
env_dir = "."
|
|
223
|
-
# If auto-detection couldn't resolve UUIDs, Project.toml includes a
|
|
224
|
-
# commented dependency list
|
|
225
|
-
# In that case, add those packages with
|
|
226
|
-
# Pkg.add before instantiating so the env is usable at run time
|
|
227
|
-
deps_to_add: list[str] = []
|
|
228
|
-
try:
|
|
229
|
-
with open(env_path, "r") as f:
|
|
230
|
-
content = f.read()
|
|
231
|
-
lines = [line.rstrip() for line in content.splitlines()]
|
|
232
|
-
deps_section = False
|
|
233
|
-
deps_found = False
|
|
234
|
-
for line in lines:
|
|
235
|
-
stripped = line.strip()
|
|
236
|
-
if stripped == "[deps]":
|
|
237
|
-
deps_section = True
|
|
238
|
-
continue
|
|
239
|
-
if deps_section:
|
|
240
|
-
if stripped.startswith("[") and stripped.endswith("]"):
|
|
241
|
-
break
|
|
242
|
-
if stripped and not stripped.startswith("#"):
|
|
243
|
-
if "=" in stripped:
|
|
244
|
-
deps_found = True
|
|
245
|
-
break
|
|
246
|
-
if not deps_found:
|
|
247
|
-
for idx, line in enumerate(lines):
|
|
248
|
-
marker = "# Dependencies (add with Julia's Pkg.add):"
|
|
249
|
-
if line.strip() == marker and idx + 1 < len(lines):
|
|
250
|
-
dep_line = lines[idx + 1].strip()
|
|
251
|
-
if dep_line.startswith("#"):
|
|
252
|
-
dep_line = dep_line.lstrip("#").strip()
|
|
253
|
-
deps_to_add = [
|
|
254
|
-
dep.strip()
|
|
255
|
-
for dep in dep_line.split(",")
|
|
256
|
-
if dep.strip()
|
|
257
|
-
]
|
|
258
|
-
break
|
|
259
|
-
except OSError:
|
|
260
|
-
deps_to_add = []
|
|
261
|
-
if deps_to_add:
|
|
262
|
-
pkg_list = ", ".join(f'"{dep}"' for dep in deps_to_add)
|
|
263
|
-
cmd = [
|
|
264
|
-
"julia",
|
|
265
|
-
f"+{julia_version}",
|
|
266
|
-
f"--project={env_dir}",
|
|
267
|
-
"-e",
|
|
268
|
-
f"using Pkg; Pkg.add([{pkg_list}]);",
|
|
269
|
-
]
|
|
270
|
-
try:
|
|
271
|
-
cmd = calkit.julia.check_version_in_command(cmd)
|
|
272
|
-
except Exception as e:
|
|
273
|
-
raise_error(f"Failed to check Julia version: {e}")
|
|
274
|
-
try:
|
|
275
|
-
subprocess.check_call(
|
|
276
|
-
cmd,
|
|
277
|
-
env=os.environ.copy() | {"JULIA_LOAD_PATH": "@:@stdlib"},
|
|
278
|
-
)
|
|
279
|
-
except subprocess.CalledProcessError:
|
|
280
|
-
raise_error("Failed to add Julia dependencies")
|
|
281
|
-
cmd = [
|
|
282
|
-
"julia",
|
|
283
|
-
f"+{julia_version}",
|
|
284
|
-
f"--project={env_dir}",
|
|
285
|
-
"-e",
|
|
286
|
-
"using Pkg; Pkg.instantiate();",
|
|
287
|
-
]
|
|
288
|
-
try:
|
|
289
|
-
cmd = calkit.julia.check_version_in_command(cmd)
|
|
290
|
-
except Exception as e:
|
|
291
|
-
raise_error(f"Failed to check Julia version: {e}")
|
|
292
|
-
try:
|
|
293
|
-
subprocess.check_call(
|
|
294
|
-
cmd, env=os.environ.copy() | {"JULIA_LOAD_PATH": "@:@stdlib"}
|
|
295
|
-
)
|
|
296
|
-
except subprocess.CalledProcessError:
|
|
297
|
-
raise_error("Failed to check julia environment")
|
|
404
|
+
_check_julia_env(
|
|
405
|
+
env_path=env_path,
|
|
406
|
+
julia_version=julia_version,
|
|
407
|
+
verbose=verbose,
|
|
408
|
+
cache_key=env_name,
|
|
409
|
+
)
|
|
298
410
|
else:
|
|
299
411
|
raise_error(f"Environment kind '{env['kind']}' not supported")
|
|
300
412
|
return get_env_lock_fpath(env=env, env_name=env_name, as_posix=False)
|
|
301
413
|
|
|
302
414
|
|
|
415
|
+
@check_app.command(
|
|
416
|
+
name="julia-env",
|
|
417
|
+
help=(
|
|
418
|
+
"Check a Julia environment and instantiate only when project, "
|
|
419
|
+
"manifest, and package cache state have changed."
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
def check_julia_env(
|
|
423
|
+
env_path: Annotated[
|
|
424
|
+
str,
|
|
425
|
+
typer.Argument(help="Path to Julia Project.toml file."),
|
|
426
|
+
] = "Project.toml",
|
|
427
|
+
julia_version: Annotated[
|
|
428
|
+
str | None,
|
|
429
|
+
typer.Option(
|
|
430
|
+
"--julia",
|
|
431
|
+
help="Julia version to enforce (e.g., 1.11).",
|
|
432
|
+
),
|
|
433
|
+
] = None,
|
|
434
|
+
verbose: Annotated[
|
|
435
|
+
bool, typer.Option("--verbose", help="Print verbose output.")
|
|
436
|
+
] = False,
|
|
437
|
+
) -> str:
|
|
438
|
+
return _check_julia_env(
|
|
439
|
+
env_path=env_path,
|
|
440
|
+
julia_version=julia_version,
|
|
441
|
+
verbose=verbose,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
303
445
|
@check_app.command(
|
|
304
446
|
name="envs",
|
|
305
447
|
help="Check that all environments are up-to-date.",
|
calkit/cli/main/core.py
CHANGED
|
@@ -1910,6 +1910,9 @@ def run_in_env(
|
|
|
1910
1910
|
julia_cmd = calkit.julia.check_version_in_command(julia_cmd)
|
|
1911
1911
|
except Exception as e:
|
|
1912
1912
|
raise_error(f"Failed to check Julia version: {e}")
|
|
1913
|
+
julia_cmd = calkit.julia.ensure_startup_file_disabled_in_command(
|
|
1914
|
+
julia_cmd
|
|
1915
|
+
)
|
|
1913
1916
|
if verbose:
|
|
1914
1917
|
typer.echo(f"Running command: {julia_cmd}")
|
|
1915
1918
|
try:
|
calkit/cli/new.py
CHANGED
|
@@ -1351,6 +1351,17 @@ def new_slurm_env(
|
|
|
1351
1351
|
),
|
|
1352
1352
|
),
|
|
1353
1353
|
] = [],
|
|
1354
|
+
default_setup: Annotated[
|
|
1355
|
+
list[str],
|
|
1356
|
+
typer.Option(
|
|
1357
|
+
"--default-setup",
|
|
1358
|
+
help=(
|
|
1359
|
+
"Default shell setup command to run before SLURM jobs "
|
|
1360
|
+
"(for example 'module load julia/1.11'). Repeat for "
|
|
1361
|
+
"multiple commands."
|
|
1362
|
+
),
|
|
1363
|
+
),
|
|
1364
|
+
] = [],
|
|
1354
1365
|
description: Annotated[
|
|
1355
1366
|
str | None, typer.Option("--description", help="Description.")
|
|
1356
1367
|
] = None,
|
|
@@ -1383,9 +1394,14 @@ def new_slurm_env(
|
|
|
1383
1394
|
normalized_default_options = [
|
|
1384
1395
|
opt.strip() for opt in default_options if opt.strip()
|
|
1385
1396
|
]
|
|
1397
|
+
normalized_default_setup = [
|
|
1398
|
+
cmd.strip() for cmd in default_setup if cmd.strip()
|
|
1399
|
+
]
|
|
1386
1400
|
env = {"kind": "slurm", "host": host}
|
|
1387
1401
|
if normalized_default_options:
|
|
1388
1402
|
env["default_options"] = normalized_default_options # type: ignore
|
|
1403
|
+
if normalized_default_setup:
|
|
1404
|
+
env["default_setup"] = normalized_default_setup # type: ignore
|
|
1389
1405
|
if description is not None:
|
|
1390
1406
|
env["description"] = description
|
|
1391
1407
|
envs[name] = env
|
calkit/cli/notebooks.py
CHANGED
|
@@ -109,6 +109,11 @@ def _check_ijulia_available(
|
|
|
109
109
|
except Exception as e:
|
|
110
110
|
raise_error(f"Failed to check Julia version: {e}")
|
|
111
111
|
return False
|
|
112
|
+
ijulia_check_cmd_checked = (
|
|
113
|
+
calkit.julia.ensure_startup_file_disabled_in_command(
|
|
114
|
+
ijulia_check_cmd_checked
|
|
115
|
+
)
|
|
116
|
+
)
|
|
112
117
|
res = subprocess.run(
|
|
113
118
|
ijulia_check_cmd_checked,
|
|
114
119
|
capture_output=True,
|
|
@@ -292,6 +297,7 @@ def check_env_kernel(
|
|
|
292
297
|
"kp=IJulia.installkernel("
|
|
293
298
|
f'"{display_name}",'
|
|
294
299
|
f'"--project={env_dir_abs}",'
|
|
300
|
+
'"--startup-file=no",'
|
|
295
301
|
'env=Dict("JULIA_LOAD_PATH" => "@:@stdlib")'
|
|
296
302
|
");"
|
|
297
303
|
"println(kp);"
|
|
@@ -307,6 +313,7 @@ def check_env_kernel(
|
|
|
307
313
|
cmd = calkit.julia.check_version_in_command(cmd)
|
|
308
314
|
except Exception as e:
|
|
309
315
|
raise_error(f"Failed to check Julia version: {e}")
|
|
316
|
+
cmd = calkit.julia.ensure_startup_file_disabled_in_command(cmd)
|
|
310
317
|
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
311
318
|
if res.returncode != 0:
|
|
312
319
|
raise_error(f"Failed to create kernel:\n{res.stdout}")
|
calkit/cli/slurm.py
CHANGED
|
@@ -81,6 +81,16 @@ def run_sbatch(
|
|
|
81
81
|
help="Additional options to pass to sbatch (no spaces allowed).",
|
|
82
82
|
),
|
|
83
83
|
] = [],
|
|
84
|
+
setup_cmds: Annotated[
|
|
85
|
+
list[str],
|
|
86
|
+
typer.Option(
|
|
87
|
+
"--setup",
|
|
88
|
+
help=(
|
|
89
|
+
"Shell setup command to run before launching the target "
|
|
90
|
+
"(repeat for multiple commands)."
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
] = [],
|
|
84
94
|
log_path: Annotated[
|
|
85
95
|
str | None, typer.Option("--log-path", help="Output log path.")
|
|
86
96
|
] = None,
|
|
@@ -97,7 +107,7 @@ def run_sbatch(
|
|
|
97
107
|
Duplicates are not allowed, so if one is already running or queued with
|
|
98
108
|
the same name, we'll wait for it to finish. The only exception is if the
|
|
99
109
|
dependencies have changed, in which case any queued or running jobs will
|
|
100
|
-
be
|
|
110
|
+
be canceled and a new one submitted.
|
|
101
111
|
"""
|
|
102
112
|
|
|
103
113
|
def check_job_running_or_queued(job_id: str) -> bool:
|
|
@@ -132,13 +142,6 @@ def run_sbatch(
|
|
|
132
142
|
] + sbatch_opts
|
|
133
143
|
if is_command is None:
|
|
134
144
|
is_command = not os.path.isfile(target)
|
|
135
|
-
if is_command:
|
|
136
|
-
# Use shlex.join to properly escape target and args for shell execution
|
|
137
|
-
cmd += ["--wrap", shlex.join([target] + args)]
|
|
138
|
-
else:
|
|
139
|
-
cmd += [target] + args
|
|
140
|
-
if target not in deps:
|
|
141
|
-
deps = [target] + deps
|
|
142
145
|
if environment != "_system":
|
|
143
146
|
ck_info = calkit.load_calkit_info()
|
|
144
147
|
env = ck_info.get("environments", {}).get(environment, {})
|
|
@@ -163,6 +166,26 @@ def run_sbatch(
|
|
|
163
166
|
f"Environment '{environment}' is for host '{env_host}', but "
|
|
164
167
|
f"this is '{current_host}'"
|
|
165
168
|
)
|
|
169
|
+
env_setup_cmds = env.get("default_setup", [])
|
|
170
|
+
if env_setup_cmds:
|
|
171
|
+
setup_cmds = [
|
|
172
|
+
s for s in [*env_setup_cmds, *setup_cmds] if s.strip()
|
|
173
|
+
]
|
|
174
|
+
if setup_cmds:
|
|
175
|
+
# Run setup and target in the same shell so setup side effects (like
|
|
176
|
+
# module loads) persist for the target command.
|
|
177
|
+
wrapped_target = shlex.join([target] + args)
|
|
178
|
+
setup_chain = " && ".join([*setup_cmds, wrapped_target])
|
|
179
|
+
cmd += ["--wrap", setup_chain]
|
|
180
|
+
if not is_command and target not in deps:
|
|
181
|
+
deps = [target] + deps
|
|
182
|
+
elif is_command:
|
|
183
|
+
# Use shlex.join to properly escape target and args for shell execution
|
|
184
|
+
cmd += ["--wrap", shlex.join([target] + args)]
|
|
185
|
+
else:
|
|
186
|
+
cmd += [target] + args
|
|
187
|
+
if target not in deps:
|
|
188
|
+
deps = [target] + deps
|
|
166
189
|
slurm_dir = os.path.join(".calkit", "slurm")
|
|
167
190
|
os.makedirs(slurm_dir, exist_ok=True)
|
|
168
191
|
logs_dir = os.path.dirname(log_path)
|
|
@@ -187,6 +210,7 @@ def run_sbatch(
|
|
|
187
210
|
job_deps = job_info["deps"]
|
|
188
211
|
job_target = job_info.get("target")
|
|
189
212
|
job_args = job_info.get("args", [])
|
|
213
|
+
job_setup = job_info.get("setup", [])
|
|
190
214
|
running_or_queued = check_job_running_or_queued(job_id)
|
|
191
215
|
should_wait = True
|
|
192
216
|
if running_or_queued:
|
|
@@ -207,6 +231,13 @@ def run_sbatch(
|
|
|
207
231
|
job_id=job_id,
|
|
208
232
|
reason=f"Arguments for job '{name}' have changed",
|
|
209
233
|
)
|
|
234
|
+
# Check if setup commands have changed
|
|
235
|
+
if job_setup != setup_cmds:
|
|
236
|
+
should_wait = False
|
|
237
|
+
cancel_job(
|
|
238
|
+
job_id=job_id,
|
|
239
|
+
reason=f"Setup commands for job '{name}' have changed",
|
|
240
|
+
)
|
|
210
241
|
# Check if dependency paths have changed
|
|
211
242
|
if set(job_deps) != set(deps):
|
|
212
243
|
should_wait = False
|
|
@@ -258,6 +289,7 @@ def run_sbatch(
|
|
|
258
289
|
"deps": deps,
|
|
259
290
|
"target": target,
|
|
260
291
|
"args": args,
|
|
292
|
+
"setup": setup_cmds,
|
|
261
293
|
"dep_md5s": current_dep_md5s,
|
|
262
294
|
}
|
|
263
295
|
jobs[name] = new_job
|
|
@@ -336,7 +368,7 @@ def cancel_jobs(
|
|
|
336
368
|
)
|
|
337
369
|
if p.returncode != 0:
|
|
338
370
|
raise_error(f"Failed to cancel job ID {job_id}: {p.stderr}")
|
|
339
|
-
typer.echo(f"
|
|
371
|
+
typer.echo(f"Canceled job '{name}' with ID {job_id}")
|
|
340
372
|
|
|
341
373
|
|
|
342
374
|
@slurm_app.command(name="logs")
|
calkit/dvc/zip.py
CHANGED
|
@@ -185,17 +185,23 @@ def hash_path(path: str, alg="md5") -> str:
|
|
|
185
185
|
def calc_dir_sig(path: str) -> str:
|
|
186
186
|
"""Calculate a fast signature for a directory to know if we should
|
|
187
187
|
rehash.
|
|
188
|
+
|
|
189
|
+
The signature includes file count, total size, and latest mtime in
|
|
190
|
+
nanoseconds over all files in the directory tree.
|
|
188
191
|
"""
|
|
189
192
|
if not os.path.isdir(path):
|
|
190
193
|
return ""
|
|
191
194
|
file_count = 0
|
|
192
195
|
total_size = 0
|
|
193
196
|
latest_mtime = 0
|
|
194
|
-
for foldername,
|
|
197
|
+
for foldername, _, filenames in os.walk(path):
|
|
195
198
|
for filename in filenames:
|
|
196
|
-
file_count += 1
|
|
197
199
|
fpath = os.path.join(foldername, filename)
|
|
198
|
-
|
|
200
|
+
try:
|
|
201
|
+
st = os.stat(fpath)
|
|
202
|
+
except OSError:
|
|
203
|
+
continue
|
|
204
|
+
file_count += 1
|
|
199
205
|
total_size += st.st_size
|
|
200
206
|
if st.st_mtime_ns > latest_mtime:
|
|
201
207
|
latest_mtime = st.st_mtime_ns
|