calkit-python 0.41.9__py3-none-any.whl → 0.41.11__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 +90 -131
- calkit/cli/describe.py +45 -0
- calkit/cli/list.py +18 -2
- calkit/cli/main/core.py +28 -5
- calkit/cli/new.py +36 -16
- calkit/cli/notebooks.py +2 -2
- calkit/cli/scheduler.py +7 -8
- calkit/cli/update.py +558 -4
- calkit/core.py +12 -0
- calkit/dvc/core.py +19 -0
- calkit/environments.py +62 -6
- calkit/julia.py +36 -3
- calkit/models/core.py +12 -5
- calkit/pipeline.py +28 -10
- calkit/tests/cli/main/test_core.py +49 -1
- calkit/tests/cli/test_list.py +19 -2
- calkit/tests/cli/test_new.py +2 -1
- calkit/tests/cli/test_scheduler.py +31 -0
- calkit/tests/cli/test_update.py +136 -0
- calkit/tests/dvc/test_core.py +13 -0
- calkit/tests/test_environments.py +63 -3
- calkit/tests/test_julia.py +34 -0
- calkit/tests/test_pipeline.py +35 -0
- {calkit_python-0.41.9.dist-info → calkit_python-0.41.11.dist-info}/METADATA +1 -1
- {calkit_python-0.41.9.dist-info → calkit_python-0.41.11.dist-info}/RECORD +43 -43
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.41.9.data → calkit_python-0.41.11.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.41.9.dist-info → calkit_python-0.41.11.dist-info}/WHEEL +0 -0
- {calkit_python-0.41.9.dist-info → calkit_python-0.41.11.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.41.9.dist-info → calkit_python-0.41.11.dist-info}/licenses/LICENSE +0 -0
calkit/cli/check.py
CHANGED
|
@@ -163,7 +163,7 @@ def _check_julia_env(
|
|
|
163
163
|
deps_to_add = []
|
|
164
164
|
if deps_to_add:
|
|
165
165
|
pkg_list = ", ".join(f'"{dep}"' for dep in deps_to_add)
|
|
166
|
-
cmd = [
|
|
166
|
+
cmd = [calkit.julia.get_julia_exe()]
|
|
167
167
|
if julia_version:
|
|
168
168
|
cmd.append(f"+{julia_version}")
|
|
169
169
|
cmd += [
|
|
@@ -190,7 +190,7 @@ def _check_julia_env(
|
|
|
190
190
|
success=False,
|
|
191
191
|
)
|
|
192
192
|
raise_error("Failed to add Julia dependencies")
|
|
193
|
-
cmd = [
|
|
193
|
+
cmd = [calkit.julia.get_julia_exe()]
|
|
194
194
|
if julia_version:
|
|
195
195
|
cmd.append(f"+{julia_version}")
|
|
196
196
|
cmd += [
|
|
@@ -318,6 +318,7 @@ def check_environment(
|
|
|
318
318
|
get_all_conda_lock_fpaths,
|
|
319
319
|
get_all_docker_lock_fpaths,
|
|
320
320
|
get_all_venv_lock_fpaths,
|
|
321
|
+
get_default_venv_prefix,
|
|
321
322
|
get_env_lock_fpath,
|
|
322
323
|
write_scheduler_env_lock,
|
|
323
324
|
)
|
|
@@ -403,12 +404,14 @@ def check_environment(
|
|
|
403
404
|
except subprocess.CalledProcessError:
|
|
404
405
|
raise_error("Failed to check uv environment")
|
|
405
406
|
elif (kind := env["kind"]) in ["uv-venv", "venv"]:
|
|
406
|
-
if "prefix" not in env:
|
|
407
|
-
raise_error("venv environments require a prefix")
|
|
408
407
|
if "path" not in env:
|
|
409
408
|
raise_error("venv environments require a path")
|
|
410
|
-
prefix = os.path.expandvars(env["prefix"])
|
|
411
409
|
path = os.path.expandvars(env["path"])
|
|
410
|
+
# Resolve the prefix on the fly if it isn't pinned in calkit.yaml
|
|
411
|
+
prefix = env.get("prefix")
|
|
412
|
+
if prefix is None:
|
|
413
|
+
prefix = get_default_venv_prefix(envs, path, env_name)
|
|
414
|
+
prefix = os.path.expandvars(prefix)
|
|
412
415
|
lock_fpath = get_env_lock_fpath(
|
|
413
416
|
env=env, env_name=env_name, as_posix=False
|
|
414
417
|
)
|
|
@@ -543,6 +546,41 @@ def check_environments(
|
|
|
543
546
|
)
|
|
544
547
|
|
|
545
548
|
|
|
549
|
+
def _renv_snapshot_from_description(env_dir: str, verbose: bool) -> None:
|
|
550
|
+
"""Install packages from DESCRIPTION and (re)write renv.lock.
|
|
551
|
+
|
|
552
|
+
Used to create the initial lock, to add newly declared packages, and as a
|
|
553
|
+
fallback when an existing lock can't be restored (e.g. its versions don't
|
|
554
|
+
build against the installed R version).
|
|
555
|
+
"""
|
|
556
|
+
hydrate_cmd = [
|
|
557
|
+
"Rscript",
|
|
558
|
+
"--vanilla",
|
|
559
|
+
"-e",
|
|
560
|
+
"renv::load(); renv::hydrate()",
|
|
561
|
+
]
|
|
562
|
+
if verbose:
|
|
563
|
+
typer.echo(f"Running: {' '.join(hydrate_cmd)}")
|
|
564
|
+
try:
|
|
565
|
+
subprocess.check_call(hydrate_cmd, cwd=env_dir)
|
|
566
|
+
except subprocess.CalledProcessError:
|
|
567
|
+
# Hydrate may fail if some packages aren't available; snapshot anyway
|
|
568
|
+
if verbose:
|
|
569
|
+
typer.echo("Warning: hydrate had issues, continuing to snapshot")
|
|
570
|
+
snapshot_cmd = [
|
|
571
|
+
"Rscript",
|
|
572
|
+
"--vanilla",
|
|
573
|
+
"-e",
|
|
574
|
+
"renv::load(); renv::snapshot(type='explicit', prompt=FALSE)",
|
|
575
|
+
]
|
|
576
|
+
if verbose:
|
|
577
|
+
typer.echo(f"Running: {' '.join(snapshot_cmd)}")
|
|
578
|
+
try:
|
|
579
|
+
subprocess.check_call(snapshot_cmd, cwd=env_dir)
|
|
580
|
+
except subprocess.CalledProcessError:
|
|
581
|
+
raise_error(f"Failed to snapshot renv in {env_dir}")
|
|
582
|
+
|
|
583
|
+
|
|
546
584
|
@check_app.command(name="renv")
|
|
547
585
|
def check_renv(
|
|
548
586
|
env_path: Annotated[
|
|
@@ -606,153 +644,74 @@ def check_renv(
|
|
|
606
644
|
subprocess.check_call(init_cmd, cwd=env_dir)
|
|
607
645
|
except subprocess.CalledProcessError:
|
|
608
646
|
raise_error(f"Failed to initialize renv in {env_dir}")
|
|
609
|
-
#
|
|
647
|
+
# Install packages from DESCRIPTION and write the lock file
|
|
610
648
|
if verbose:
|
|
611
649
|
typer.echo("Setting up environment from DESCRIPTION")
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
]
|
|
618
|
-
if verbose:
|
|
619
|
-
typer.echo(f"Running: {' '.join(hydrate_cmd)}")
|
|
620
|
-
try:
|
|
621
|
-
subprocess.check_call(hydrate_cmd, cwd=env_dir)
|
|
622
|
-
except subprocess.CalledProcessError:
|
|
623
|
-
# Hydrate might fail if packages aren't available, continue anyway
|
|
624
|
-
if verbose:
|
|
625
|
-
typer.echo(
|
|
626
|
-
"Warning: hydrate had issues, continuing to snapshot"
|
|
627
|
-
)
|
|
628
|
-
# Always snapshot after hydrate to create lock file from DESCRIPTION
|
|
650
|
+
_renv_snapshot_from_description(env_dir, verbose=verbose)
|
|
651
|
+
else:
|
|
652
|
+
# A lock file exists, so treat it as the source of truth: restore the
|
|
653
|
+
# library to the exact recorded versions rather than re-resolving from
|
|
654
|
+
# DESCRIPTION (which would bump packages and overwrite the lock).
|
|
629
655
|
if verbose:
|
|
630
|
-
typer.echo("
|
|
631
|
-
|
|
656
|
+
typer.echo("Restoring library from lockfile")
|
|
657
|
+
restore_cmd = [
|
|
632
658
|
"Rscript",
|
|
633
659
|
"--vanilla",
|
|
634
660
|
"-e",
|
|
635
|
-
"renv::load(); renv::
|
|
661
|
+
"renv::load(); renv::restore(prompt=FALSE)",
|
|
636
662
|
]
|
|
637
663
|
if verbose:
|
|
638
|
-
typer.echo(f"Running: {' '.join(
|
|
664
|
+
typer.echo(f"Running: {' '.join(restore_cmd)}")
|
|
639
665
|
try:
|
|
640
|
-
subprocess.check_call(
|
|
666
|
+
subprocess.check_call(restore_cmd, cwd=env_dir)
|
|
667
|
+
restored = True
|
|
641
668
|
except subprocess.CalledProcessError:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
"renv::load(); status <- renv::status(); cat(status$synchronized)",
|
|
653
|
-
]
|
|
654
|
-
try:
|
|
655
|
-
result = subprocess.run(
|
|
656
|
-
status_cmd,
|
|
657
|
-
cwd=env_dir,
|
|
658
|
-
capture_output=True,
|
|
659
|
-
text=True,
|
|
660
|
-
check=True,
|
|
669
|
+
restored = False
|
|
670
|
+
if not restored:
|
|
671
|
+
# The locked versions couldn't be installed (e.g. they don't build
|
|
672
|
+
# against the installed R version). Fall back to re-resolving from
|
|
673
|
+
# DESCRIPTION, which updates renv.lock to a working set.
|
|
674
|
+
warn(
|
|
675
|
+
f"Could not restore renv environment in {env_dir} from "
|
|
676
|
+
"renv.lock; the locked versions may be incompatible with the "
|
|
677
|
+
"installed R version. Re-resolving from DESCRIPTION and "
|
|
678
|
+
"updating renv.lock."
|
|
661
679
|
)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
680
|
+
_renv_snapshot_from_description(env_dir, verbose=verbose)
|
|
681
|
+
else:
|
|
682
|
+
# Only update the lock if DESCRIPTION declares dependencies the
|
|
683
|
+
# lock doesn't cover (e.g. the user added a package). After the
|
|
684
|
+
# restore the library matches the lock, so status is unsynchronized
|
|
685
|
+
# only when DESCRIPTION and the lock genuinely disagree---not merely
|
|
686
|
+
# because packages were missing on a fresh checkout.
|
|
669
687
|
if verbose:
|
|
670
|
-
typer.echo("
|
|
671
|
-
|
|
672
|
-
hydrate_cmd = [
|
|
688
|
+
typer.echo("Checking if DESCRIPTION matches lockfile")
|
|
689
|
+
status_cmd = [
|
|
673
690
|
"Rscript",
|
|
674
691
|
"--vanilla",
|
|
675
692
|
"-e",
|
|
676
|
-
"renv::load(); renv::
|
|
693
|
+
"renv::load(); cat(renv::status()$synchronized)",
|
|
677
694
|
]
|
|
678
|
-
if verbose:
|
|
679
|
-
typer.echo(f"Running: {' '.join(hydrate_cmd)}")
|
|
680
695
|
try:
|
|
681
|
-
subprocess.
|
|
696
|
+
result = subprocess.run(
|
|
697
|
+
status_cmd,
|
|
698
|
+
cwd=env_dir,
|
|
699
|
+
capture_output=True,
|
|
700
|
+
text=True,
|
|
701
|
+
check=True,
|
|
702
|
+
)
|
|
703
|
+
in_sync = "TRUE" in result.stdout
|
|
682
704
|
except subprocess.CalledProcessError:
|
|
705
|
+
# If status fails, keep the lock rather than risk clobbering it
|
|
706
|
+
in_sync = True
|
|
683
707
|
if verbose:
|
|
684
|
-
typer.echo(
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
# Snapshot to update lock
|
|
688
|
-
snapshot_cmd = [
|
|
689
|
-
"Rscript",
|
|
690
|
-
"--vanilla",
|
|
691
|
-
"-e",
|
|
692
|
-
"renv::load(); renv::snapshot(type='explicit', prompt=FALSE)",
|
|
693
|
-
]
|
|
694
|
-
if verbose:
|
|
695
|
-
typer.echo(f"Running: {' '.join(snapshot_cmd)}")
|
|
696
|
-
try:
|
|
697
|
-
subprocess.check_call(snapshot_cmd, cwd=env_dir)
|
|
698
|
-
except subprocess.CalledProcessError:
|
|
708
|
+
typer.echo("Warning: status check failed, keeping lock")
|
|
709
|
+
if not in_sync:
|
|
699
710
|
if verbose:
|
|
700
|
-
typer.echo("
|
|
701
|
-
|
|
702
|
-
|
|
711
|
+
typer.echo("DESCRIPTION changed; updating lockfile")
|
|
712
|
+
_renv_snapshot_from_description(env_dir, verbose=verbose)
|
|
713
|
+
elif verbose:
|
|
703
714
|
typer.echo("Lockfile is already in sync with DESCRIPTION")
|
|
704
|
-
# Check if library needs restoring
|
|
705
|
-
if verbose:
|
|
706
|
-
typer.echo("Checking if library is in sync with lockfile")
|
|
707
|
-
lib_status_cmd = [
|
|
708
|
-
"Rscript",
|
|
709
|
-
"--vanilla",
|
|
710
|
-
"-e",
|
|
711
|
-
(
|
|
712
|
-
"renv::load(); "
|
|
713
|
-
"status <- tryCatch({"
|
|
714
|
-
" renv::status();"
|
|
715
|
-
" cat('synchronized');"
|
|
716
|
-
"}, error = function(e) {"
|
|
717
|
-
" cat('needs_restore');"
|
|
718
|
-
"})"
|
|
719
|
-
),
|
|
720
|
-
]
|
|
721
|
-
try:
|
|
722
|
-
result = subprocess.run(
|
|
723
|
-
lib_status_cmd,
|
|
724
|
-
cwd=env_dir,
|
|
725
|
-
capture_output=True,
|
|
726
|
-
text=True,
|
|
727
|
-
check=True,
|
|
728
|
-
)
|
|
729
|
-
needs_restore = "needs_restore" in result.stdout or (
|
|
730
|
-
"synchronized" not in result.stdout
|
|
731
|
-
)
|
|
732
|
-
except subprocess.CalledProcessError:
|
|
733
|
-
# If check fails, restore to be safe
|
|
734
|
-
needs_restore = True
|
|
735
|
-
if verbose:
|
|
736
|
-
typer.echo("Warning: library status check failed, will restore")
|
|
737
|
-
|
|
738
|
-
if needs_restore:
|
|
739
|
-
if verbose:
|
|
740
|
-
typer.echo("Restoring library from lockfile")
|
|
741
|
-
restore_cmd = [
|
|
742
|
-
"Rscript",
|
|
743
|
-
"--vanilla",
|
|
744
|
-
"-e",
|
|
745
|
-
"renv::load(); renv::restore(prompt=FALSE)",
|
|
746
|
-
]
|
|
747
|
-
if verbose:
|
|
748
|
-
typer.echo(f"Running: {' '.join(restore_cmd)}")
|
|
749
|
-
try:
|
|
750
|
-
subprocess.check_call(restore_cmd, cwd=env_dir)
|
|
751
|
-
except subprocess.CalledProcessError:
|
|
752
|
-
raise_error(f"Failed to restore renv in {env_dir}")
|
|
753
|
-
else:
|
|
754
|
-
if verbose:
|
|
755
|
-
typer.echo("Library is already in sync with lockfile")
|
|
756
715
|
|
|
757
716
|
|
|
758
717
|
@check_app.command(name="docker-env")
|
calkit/cli/describe.py
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
from typing import Annotated
|
|
6
7
|
|
|
7
8
|
import typer
|
|
8
9
|
|
|
9
10
|
import calkit
|
|
11
|
+
from calkit.cli import raise_error
|
|
12
|
+
from calkit.environments import get_env_lock_fpath
|
|
10
13
|
|
|
11
14
|
describe_app = typer.Typer(no_args_is_help=True)
|
|
12
15
|
|
|
@@ -16,3 +19,45 @@ def describe_system():
|
|
|
16
19
|
"""Describe the system."""
|
|
17
20
|
system_info = calkit.get_system_info()
|
|
18
21
|
typer.echo(json.dumps(system_info, indent=2))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@describe_app.command(name="env")
|
|
25
|
+
def describe_env(
|
|
26
|
+
name: Annotated[
|
|
27
|
+
str,
|
|
28
|
+
typer.Option("--name", "-n", help="Environment name."),
|
|
29
|
+
],
|
|
30
|
+
):
|
|
31
|
+
"""Describe a single environment, including spec and lock file paths."""
|
|
32
|
+
ck_info = calkit.load_calkit_info()
|
|
33
|
+
envs: dict = ck_info.get("environments", {})
|
|
34
|
+
if name not in envs:
|
|
35
|
+
raise_error(f"Environment '{name}' not found.")
|
|
36
|
+
env = envs[name]
|
|
37
|
+
lock_path = get_env_lock_fpath(env=env, env_name=name)
|
|
38
|
+
result = {
|
|
39
|
+
"kind": env.get("kind"),
|
|
40
|
+
"spec_path": env.get("path"),
|
|
41
|
+
"lock_path": lock_path,
|
|
42
|
+
"prefix": env.get("prefix"),
|
|
43
|
+
"python": env.get("python"),
|
|
44
|
+
}
|
|
45
|
+
typer.echo(json.dumps(result, indent=2))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@describe_app.command(name="envs")
|
|
49
|
+
def describe_envs():
|
|
50
|
+
"""Describe all environments, including spec and lock file paths."""
|
|
51
|
+
ck_info = calkit.load_calkit_info()
|
|
52
|
+
envs: dict = ck_info.get("environments", {})
|
|
53
|
+
result = {}
|
|
54
|
+
for env_name, env in envs.items():
|
|
55
|
+
lock_path = get_env_lock_fpath(env=env, env_name=env_name)
|
|
56
|
+
result[env_name] = {
|
|
57
|
+
"kind": env.get("kind"),
|
|
58
|
+
"spec_path": env.get("path"),
|
|
59
|
+
"lock_path": lock_path,
|
|
60
|
+
"prefix": env.get("prefix"),
|
|
61
|
+
"python": env.get("python"),
|
|
62
|
+
}
|
|
63
|
+
typer.echo(json.dumps(result, indent=2))
|
calkit/cli/list.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Annotated, Literal
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
import calkit
|
|
10
|
-
from calkit.cli import AliasGroup, warn
|
|
10
|
+
from calkit.cli import AliasGroup, raise_error, warn
|
|
11
11
|
|
|
12
12
|
list_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
|
|
13
13
|
|
|
@@ -167,12 +167,28 @@ def list_stages(
|
|
|
167
167
|
list[str] | None,
|
|
168
168
|
typer.Option("--kind", "-k", help="Filter stages by kind."),
|
|
169
169
|
] = None,
|
|
170
|
+
stale_only: Annotated[
|
|
171
|
+
bool, typer.Option("--stale", help="Show only stale stages.")
|
|
172
|
+
] = False,
|
|
170
173
|
):
|
|
171
174
|
"""List pipeline stages."""
|
|
172
|
-
|
|
175
|
+
ck_info = calkit.load_calkit_info()
|
|
176
|
+
stages = ck_info.get("pipeline", {}).get("stages", {})
|
|
177
|
+
# If we only want stale stages, we need to get the status first.
|
|
178
|
+
# This compiles the pipeline, cleans notebooks, and checks environments,
|
|
179
|
+
# all of which can affect whether a stage is stale, so we don't skip them.
|
|
180
|
+
if stale_only:
|
|
181
|
+
status = calkit.pipeline.get_status(ck_info=ck_info)
|
|
182
|
+
if status.errors:
|
|
183
|
+
raise_error(
|
|
184
|
+
"Failed to determine stale stages: " + "; ".join(status.errors)
|
|
185
|
+
)
|
|
186
|
+
stale_stage_names = status.stale_stage_names
|
|
173
187
|
for name, stage in stages.items():
|
|
174
188
|
if kinds is not None and stage.get("kind") not in kinds:
|
|
175
189
|
continue
|
|
190
|
+
if stale_only and name not in stale_stage_names:
|
|
191
|
+
continue
|
|
176
192
|
typer.echo(name)
|
|
177
193
|
|
|
178
194
|
|
calkit/cli/main/core.py
CHANGED
|
@@ -196,7 +196,12 @@ def init(
|
|
|
196
196
|
repo = calkit.git.get_repo()
|
|
197
197
|
repo.git.add(".dvc")
|
|
198
198
|
repo.git.commit("-m", "Initialize DVC")
|
|
199
|
-
#
|
|
199
|
+
# Create an empty calkit.yaml if one doesn't already exist
|
|
200
|
+
if not os.path.isfile("calkit.yaml"):
|
|
201
|
+
with open("calkit.yaml", "w"):
|
|
202
|
+
pass
|
|
203
|
+
repo.git.add("calkit.yaml")
|
|
204
|
+
repo.git.commit("-m", "Initialize Calkit")
|
|
200
205
|
# TODO: Initialize `dvc.yaml`
|
|
201
206
|
# TODO: Add a sane .gitignore file
|
|
202
207
|
# TODO: Add a sane LICENSE file?
|
|
@@ -1762,6 +1767,17 @@ def run(
|
|
|
1762
1767
|
"--no-push", help="Do not push to Git and DVC after saving."
|
|
1763
1768
|
),
|
|
1764
1769
|
] = False,
|
|
1770
|
+
mock_scheduler: Annotated[
|
|
1771
|
+
bool,
|
|
1772
|
+
typer.Option(
|
|
1773
|
+
"--mock-scheduler",
|
|
1774
|
+
"-K",
|
|
1775
|
+
help=(
|
|
1776
|
+
"Run job-scheduler (SLURM/PBS) stages locally instead of "
|
|
1777
|
+
"submitting them to a real scheduler."
|
|
1778
|
+
),
|
|
1779
|
+
),
|
|
1780
|
+
] = False,
|
|
1765
1781
|
) -> dict:
|
|
1766
1782
|
"""Check dependencies and run the pipeline."""
|
|
1767
1783
|
import dvc.log
|
|
@@ -1779,6 +1795,10 @@ def run(
|
|
|
1779
1795
|
if (target_inputs or target_outputs) and targets:
|
|
1780
1796
|
raise_error("Cannot specify both targets and inputs")
|
|
1781
1797
|
os.environ["CALKIT_PIPELINE_RUNNING"] = "1"
|
|
1798
|
+
# Mock the scheduler for this run (and any subprocesses) so SLURM/PBS
|
|
1799
|
+
# stages execute locally; child processes inherit it via os.environ
|
|
1800
|
+
if mock_scheduler:
|
|
1801
|
+
os.environ[calkit.cli.scheduler.MOCK_ENV_VAR] = "1"
|
|
1782
1802
|
dotenv.load_dotenv(dotenv_path=".env", verbose=verbose)
|
|
1783
1803
|
ck_info = calkit.load_calkit_info()
|
|
1784
1804
|
# Ensure Git is initialized so DVC can be used.
|
|
@@ -2463,12 +2483,15 @@ def run_in_env(
|
|
|
2463
2483
|
except subprocess.CalledProcessError:
|
|
2464
2484
|
raise_error("Failed to run in uv environment")
|
|
2465
2485
|
elif (kind := env["kind"]) in ["uv-venv", "venv"]:
|
|
2466
|
-
if "prefix" not in env:
|
|
2467
|
-
raise_error("venv environments require a prefix")
|
|
2468
2486
|
if "path" not in env:
|
|
2469
2487
|
raise_error("venv environments require a path")
|
|
2470
|
-
prefix = env["prefix"]
|
|
2471
2488
|
path = env["path"]
|
|
2489
|
+
# Resolve the prefix on the fly if it isn't pinned in calkit.yaml
|
|
2490
|
+
prefix = env.get("prefix")
|
|
2491
|
+
if prefix is None:
|
|
2492
|
+
prefix = calkit.environments.get_default_venv_prefix(
|
|
2493
|
+
envs, path, env_name
|
|
2494
|
+
)
|
|
2472
2495
|
shell_cmd = _to_shell_cmd(cmd)
|
|
2473
2496
|
if _platform.system() == "Windows":
|
|
2474
2497
|
activate_cmd = f"{prefix}\\Scripts\\activate"
|
|
@@ -2525,7 +2548,7 @@ def run_in_env(
|
|
|
2525
2548
|
# specifying the project
|
|
2526
2549
|
cmd = [arg for arg in cmd if not arg.startswith("--project=")]
|
|
2527
2550
|
julia_cmd = [
|
|
2528
|
-
|
|
2551
|
+
calkit.julia.get_julia_exe(),
|
|
2529
2552
|
f"+{julia_version}",
|
|
2530
2553
|
"--project=" + env_dir,
|
|
2531
2554
|
] + cmd
|
calkit/cli/new.py
CHANGED
|
@@ -1604,8 +1604,15 @@ def new_uv_venv(
|
|
|
1604
1604
|
str, typer.Option("--path", help="Path for requirements file.")
|
|
1605
1605
|
] = "requirements.txt",
|
|
1606
1606
|
prefix: Annotated[
|
|
1607
|
-
str
|
|
1608
|
-
|
|
1607
|
+
str | None,
|
|
1608
|
+
typer.Option(
|
|
1609
|
+
"--prefix",
|
|
1610
|
+
help=(
|
|
1611
|
+
"Prefix for environment location (defaults to .venv, or "
|
|
1612
|
+
".calkit/envs/<name>/.venv if .venv is already taken)."
|
|
1613
|
+
),
|
|
1614
|
+
),
|
|
1615
|
+
] = None,
|
|
1609
1616
|
python_version: Annotated[
|
|
1610
1617
|
str | None, typer.Option("--python", "-p", help="Python version.")
|
|
1611
1618
|
] = None,
|
|
@@ -1654,12 +1661,13 @@ def new_uv_venv(
|
|
|
1654
1661
|
f"Environment with name {name} already exists "
|
|
1655
1662
|
"(use -f to overwrite)"
|
|
1656
1663
|
)
|
|
1657
|
-
#
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1664
|
+
# Only pin a prefix if one was explicitly given; otherwise it is resolved
|
|
1665
|
+
# on the fly (defaulting to .venv) at check/run time
|
|
1666
|
+
if prefix is not None and not overwrite:
|
|
1667
|
+
for other_name, other_env in envs.items():
|
|
1668
|
+
if other_env.get("prefix") == prefix:
|
|
1661
1669
|
raise_error(
|
|
1662
|
-
f"Environment '{
|
|
1670
|
+
f"Environment '{other_name}' already exists with "
|
|
1663
1671
|
f"prefix '{prefix}'"
|
|
1664
1672
|
)
|
|
1665
1673
|
if packages is not None:
|
|
@@ -1669,7 +1677,9 @@ def new_uv_venv(
|
|
|
1669
1677
|
with open(path, "w") as f:
|
|
1670
1678
|
f.write(packages_txt)
|
|
1671
1679
|
typer.echo("Adding environment to calkit.yaml")
|
|
1672
|
-
env = dict(path=path, kind="uv-venv"
|
|
1680
|
+
env = dict(path=path, kind="uv-venv")
|
|
1681
|
+
if prefix is not None:
|
|
1682
|
+
env["prefix"] = prefix
|
|
1673
1683
|
if python_version is not None:
|
|
1674
1684
|
env["python"] = python_version
|
|
1675
1685
|
if description is not None:
|
|
@@ -1703,8 +1713,15 @@ def new_venv(
|
|
|
1703
1713
|
str, typer.Option("--path", help="Path for requirements file.")
|
|
1704
1714
|
] = "requirements.txt",
|
|
1705
1715
|
prefix: Annotated[
|
|
1706
|
-
str
|
|
1707
|
-
|
|
1716
|
+
str | None,
|
|
1717
|
+
typer.Option(
|
|
1718
|
+
"--prefix",
|
|
1719
|
+
help=(
|
|
1720
|
+
"Prefix for environment location (defaults to .venv, or "
|
|
1721
|
+
".calkit/envs/<name>/.venv if .venv is already taken)."
|
|
1722
|
+
),
|
|
1723
|
+
),
|
|
1724
|
+
] = None,
|
|
1708
1725
|
description: Annotated[
|
|
1709
1726
|
str | None, typer.Option("--description", help="Description.")
|
|
1710
1727
|
] = None,
|
|
@@ -1745,12 +1762,13 @@ def new_venv(
|
|
|
1745
1762
|
f"Environment with name {name} already exists "
|
|
1746
1763
|
"(use -f to overwrite)"
|
|
1747
1764
|
)
|
|
1748
|
-
#
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1765
|
+
# Only pin a prefix if one was explicitly given; otherwise it is resolved
|
|
1766
|
+
# on the fly (defaulting to .venv) at check/run time
|
|
1767
|
+
if prefix is not None and not overwrite:
|
|
1768
|
+
for other_name, other_env in envs.items():
|
|
1769
|
+
if other_env.get("prefix") == prefix:
|
|
1752
1770
|
raise_error(
|
|
1753
|
-
f"Environment '{
|
|
1771
|
+
f"Environment '{other_name}' already exists with "
|
|
1754
1772
|
f"prefix '{prefix}'"
|
|
1755
1773
|
)
|
|
1756
1774
|
packages_txt = "\n".join(packages)
|
|
@@ -1760,7 +1778,9 @@ def new_venv(
|
|
|
1760
1778
|
f.write(packages_txt)
|
|
1761
1779
|
repo.git.add(path)
|
|
1762
1780
|
typer.echo("Adding environment to calkit.yaml")
|
|
1763
|
-
env = dict(path=path, kind="venv"
|
|
1781
|
+
env = dict(path=path, kind="venv")
|
|
1782
|
+
if prefix is not None:
|
|
1783
|
+
env["prefix"] = prefix
|
|
1764
1784
|
if description is not None:
|
|
1765
1785
|
env["description"] = description
|
|
1766
1786
|
envs[name] = env
|
calkit/cli/notebooks.py
CHANGED
|
@@ -90,7 +90,7 @@ def _check_ijulia_available(
|
|
|
90
90
|
env_dir: str,
|
|
91
91
|
) -> bool:
|
|
92
92
|
ijulia_check_cmd = [
|
|
93
|
-
|
|
93
|
+
calkit.julia.get_julia_exe(),
|
|
94
94
|
f"+{julia_version}",
|
|
95
95
|
"--project=" + env_dir,
|
|
96
96
|
"-e",
|
|
@@ -322,7 +322,7 @@ def check_env_kernel(
|
|
|
322
322
|
"println(kp);"
|
|
323
323
|
)
|
|
324
324
|
cmd = [
|
|
325
|
-
|
|
325
|
+
calkit.julia.get_julia_exe(),
|
|
326
326
|
f"+{julia_version}",
|
|
327
327
|
"--project=" + env_dir,
|
|
328
328
|
"-e",
|
calkit/cli/scheduler.py
CHANGED
|
@@ -251,20 +251,19 @@ def _cancel(kind: str, job_id: str) -> tuple[bool, str]:
|
|
|
251
251
|
|
|
252
252
|
|
|
253
253
|
def _wait_until_done(kind: str, job_id: str, name: str) -> None:
|
|
254
|
-
"""Poll until the job finishes,
|
|
254
|
+
"""Poll until the job finishes, canceling it on Ctrl+C.
|
|
255
255
|
|
|
256
|
-
The job is
|
|
257
|
-
|
|
258
|
-
resumes from here.
|
|
256
|
+
The job is submitted and tracked, so interrupting the local wait cancels
|
|
257
|
+
the scheduler job before exiting rather than leaving it orphaned.
|
|
259
258
|
"""
|
|
260
259
|
try:
|
|
261
260
|
while _is_active(kind, job_id):
|
|
262
261
|
time.sleep(1)
|
|
263
262
|
except KeyboardInterrupt:
|
|
264
|
-
typer.echo(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
typer.echo(f"Interrupted; canceling job '{name}' ({job_id})")
|
|
264
|
+
ok, stderr = _cancel(kind, job_id)
|
|
265
|
+
if not ok:
|
|
266
|
+
typer.echo(f"Failed to cancel job '{name}' ({job_id}): {stderr}")
|
|
268
267
|
raise typer.Exit(130)
|
|
269
268
|
|
|
270
269
|
|