calkit-python 0.8.4__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {calkit_python-0.8.4 → calkit_python-0.9.0}/PKG-INFO +3 -2
- {calkit_python-0.8.4 → calkit_python-0.9.0}/README.md +2 -1
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/__init__.py +1 -1
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/main.py +123 -11
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/conda.py +37 -7
- calkit_python-0.9.0/calkit/tests/test_conda.py +126 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/FUNDING.yml +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/.gitignore +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/LICENSE +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/config.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/core.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/import_.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/list.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/new.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cli/office.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/cloud.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/config.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/core.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/data.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/docker.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/dvc.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/git.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/gui.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/jupyter.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/magics.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/models.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/office.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/server.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/core.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_main.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/cli/test_new.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/pyproject.toml +0 -0
- {calkit_python-0.8.4 → calkit_python-0.9.0}/test/pipeline.ipynb +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Reproducibility simplified.
|
|
5
5
|
Project-URL: Homepage, https://github.com/calkit/calkit
|
|
6
6
|
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
@@ -46,7 +46,8 @@ Our goal is to make reproducibility easier so it becomes more common.
|
|
|
46
46
|
To do this, we try to make it easy for users to follow two simple rules:
|
|
47
47
|
|
|
48
48
|
1. **Keep everything in version control.** This includes large files like
|
|
49
|
-
datasets, enabled by DVC.
|
|
49
|
+
datasets, enabled by DVC.
|
|
50
|
+
The [Calkit cloud](https://github.com/calkit/calkit-cloud)
|
|
50
51
|
serves as a simple default DVC remote storage location for those who do not
|
|
51
52
|
want to manage their own infrastructure.
|
|
52
53
|
2. **Generate all important artifacts with a single pipeline.** There should be
|
|
@@ -15,7 +15,8 @@ Our goal is to make reproducibility easier so it becomes more common.
|
|
|
15
15
|
To do this, we try to make it easy for users to follow two simple rules:
|
|
16
16
|
|
|
17
17
|
1. **Keep everything in version control.** This includes large files like
|
|
18
|
-
datasets, enabled by DVC.
|
|
18
|
+
datasets, enabled by DVC.
|
|
19
|
+
The [Calkit cloud](https://github.com/calkit/calkit-cloud)
|
|
19
20
|
serves as a simple default DVC remote storage location for those who do not
|
|
20
21
|
want to manage their own infrastructure.
|
|
21
22
|
2. **Generate all important artifacts with a single pipeline.** There should be
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
|
+
import functools
|
|
6
7
|
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
@@ -387,7 +388,10 @@ def run_dvc_repro(
|
|
|
387
388
|
args += ["--pipeline", pipeline]
|
|
388
389
|
if downstream is not None:
|
|
389
390
|
args += downstream
|
|
390
|
-
|
|
391
|
+
try:
|
|
392
|
+
subprocess.check_call(["dvc", "repro"] + args)
|
|
393
|
+
except subprocess.CalledProcessError:
|
|
394
|
+
raise_error("DVC pipeline failed")
|
|
391
395
|
# Now parse stage metadata for calkit objects
|
|
392
396
|
if not os.path.isfile("dvc.yaml"):
|
|
393
397
|
raise_error("No dvc.yaml file found")
|
|
@@ -504,6 +508,20 @@ def run_in_env(
|
|
|
504
508
|
),
|
|
505
509
|
),
|
|
506
510
|
] = None,
|
|
511
|
+
no_check: Annotated[
|
|
512
|
+
bool,
|
|
513
|
+
typer.Option(
|
|
514
|
+
"--no-check",
|
|
515
|
+
help="Don't check the environment is valid before running in it.",
|
|
516
|
+
),
|
|
517
|
+
] = False,
|
|
518
|
+
relaxed_check: Annotated[
|
|
519
|
+
bool,
|
|
520
|
+
typer.Option(
|
|
521
|
+
"--relaxed",
|
|
522
|
+
help="Check the environment in a relaxed way, if applicable.",
|
|
523
|
+
),
|
|
524
|
+
] = False,
|
|
507
525
|
verbose: Annotated[
|
|
508
526
|
bool, typer.Option("--verbose", "-v", help="Print verbose output.")
|
|
509
527
|
] = False,
|
|
@@ -529,6 +547,8 @@ def run_in_env(
|
|
|
529
547
|
env_name = default_env_name
|
|
530
548
|
if env_name is None:
|
|
531
549
|
raise_error("Environment must be specified if there are multiple")
|
|
550
|
+
if env_name not in envs:
|
|
551
|
+
raise_error(f"Environment '{env_name}' does not exist")
|
|
532
552
|
env = envs[env_name]
|
|
533
553
|
if wdir is not None:
|
|
534
554
|
cwd = os.path.abspath(wdir)
|
|
@@ -539,6 +559,15 @@ def run_in_env(
|
|
|
539
559
|
shell = env.get("shell", "sh")
|
|
540
560
|
platform = env.get("platform")
|
|
541
561
|
if env["kind"] == "docker":
|
|
562
|
+
if "image" not in env:
|
|
563
|
+
raise_error("Image must be defined for Docker environments")
|
|
564
|
+
if "path" in env and not no_check:
|
|
565
|
+
check_docker_env(
|
|
566
|
+
tag=env["image"],
|
|
567
|
+
fpath=env["path"],
|
|
568
|
+
platform=env.get("platform"),
|
|
569
|
+
quiet=True,
|
|
570
|
+
)
|
|
542
571
|
shell_cmd = " ".join(cmd)
|
|
543
572
|
docker_cmd = [
|
|
544
573
|
"docker",
|
|
@@ -560,14 +589,75 @@ def run_in_env(
|
|
|
560
589
|
]
|
|
561
590
|
if verbose:
|
|
562
591
|
typer.echo(f"Running command: {docker_cmd}")
|
|
563
|
-
|
|
592
|
+
try:
|
|
593
|
+
subprocess.check_call(docker_cmd, cwd=wdir)
|
|
594
|
+
except subprocess.CalledProcessError:
|
|
595
|
+
raise_error("Failed to run in Docker environment")
|
|
564
596
|
elif env["kind"] == "conda":
|
|
565
597
|
with open(env["path"]) as f:
|
|
566
598
|
conda_env = calkit.ryaml.load(f)
|
|
599
|
+
if not no_check:
|
|
600
|
+
check_conda_env(
|
|
601
|
+
env_fpath=env["path"], relaxed=relaxed_check, quiet=True
|
|
602
|
+
)
|
|
567
603
|
cmd = ["conda", "run", "-n", conda_env["name"]] + cmd
|
|
568
604
|
if verbose:
|
|
569
605
|
typer.echo(f"Running command: {cmd}")
|
|
570
|
-
|
|
606
|
+
try:
|
|
607
|
+
subprocess.check_call(cmd, cwd=wdir)
|
|
608
|
+
except subprocess.CalledProcessError:
|
|
609
|
+
raise_error("Failed to run in Conda environment")
|
|
610
|
+
elif env["kind"] in ["pixi", "uv"]:
|
|
611
|
+
env_cmd = []
|
|
612
|
+
if "name" in env:
|
|
613
|
+
env_cmd = ["--environment", env["name"]]
|
|
614
|
+
cmd = [env["kind"], "run"] + env_cmd + cmd
|
|
615
|
+
if verbose:
|
|
616
|
+
typer.echo(f"Running command: {cmd}")
|
|
617
|
+
try:
|
|
618
|
+
subprocess.check_call(cmd, cwd=wdir)
|
|
619
|
+
except subprocess.CalledProcessError:
|
|
620
|
+
raise_error(f"Failed to run in {env['kind']} environment")
|
|
621
|
+
elif env["kind"] == "uv-venv":
|
|
622
|
+
# TODO: This doesn't work on Windows
|
|
623
|
+
if "prefix" not in env:
|
|
624
|
+
raise_error("uv-venv environments require a prefix")
|
|
625
|
+
if "path" not in env:
|
|
626
|
+
raise_error("uv-venv environments require a path")
|
|
627
|
+
prefix = env["prefix"]
|
|
628
|
+
path = env["path"]
|
|
629
|
+
shell_cmd = " ".join(cmd)
|
|
630
|
+
# Check environment
|
|
631
|
+
if not no_check:
|
|
632
|
+
if not os.path.isdir(prefix):
|
|
633
|
+
if verbose:
|
|
634
|
+
typer.echo(f"Creating uv-venv at {prefix}")
|
|
635
|
+
try:
|
|
636
|
+
subprocess.check_call(["uv", "venv", prefix], cwd=wdir)
|
|
637
|
+
except subprocess.CalledProcessError:
|
|
638
|
+
raise_error(f"Failed to create uv-venv at {prefix}")
|
|
639
|
+
fname, ext = os.path.splitext(path)
|
|
640
|
+
lock_fpath = fname + "-lock" + ext
|
|
641
|
+
check_cmd = (
|
|
642
|
+
f". {prefix}/bin/activate "
|
|
643
|
+
f"&& uv pip install -q -r {path} "
|
|
644
|
+
f"&& uv pip freeze > {lock_fpath} "
|
|
645
|
+
"&& deactivate"
|
|
646
|
+
)
|
|
647
|
+
try:
|
|
648
|
+
if verbose:
|
|
649
|
+
typer.echo(f"Running command: {check_cmd}")
|
|
650
|
+
subprocess.check_output(check_cmd, shell=True, cwd=wdir)
|
|
651
|
+
except subprocess.CalledProcessError:
|
|
652
|
+
raise_error("Failed to check uv-venv")
|
|
653
|
+
# Now run the command
|
|
654
|
+
cmd = f". {prefix}/bin/activate && {shell_cmd} && deactivate"
|
|
655
|
+
if verbose:
|
|
656
|
+
typer.echo(f"Running command: {cmd}")
|
|
657
|
+
try:
|
|
658
|
+
subprocess.check_call(cmd, shell=True, cwd=wdir)
|
|
659
|
+
except subprocess.CalledProcessError:
|
|
660
|
+
raise_error("Failed to run in uv-venv")
|
|
571
661
|
else:
|
|
572
662
|
raise_error("Environment kind not supported")
|
|
573
663
|
|
|
@@ -605,7 +695,7 @@ def check_call(
|
|
|
605
695
|
name="build-docker",
|
|
606
696
|
help="Build Docker image if missing or different from lock file.",
|
|
607
697
|
)
|
|
608
|
-
def
|
|
698
|
+
def check_docker_env(
|
|
609
699
|
tag: Annotated[str, typer.Argument(help="Image tag.")],
|
|
610
700
|
fpath: Annotated[
|
|
611
701
|
str, typer.Option("-i", "--input", help="Path to input Dockerfile.")
|
|
@@ -613,6 +703,9 @@ def build_docker(
|
|
|
613
703
|
platform: Annotated[
|
|
614
704
|
str, typer.Option("--platform", help="Which platform(s) to build for.")
|
|
615
705
|
] = None,
|
|
706
|
+
quiet: Annotated[
|
|
707
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
708
|
+
] = False,
|
|
616
709
|
):
|
|
617
710
|
def get_docker_inspect():
|
|
618
711
|
out = json.loads(
|
|
@@ -626,28 +719,31 @@ def build_docker(
|
|
|
626
719
|
_ = out[0].pop("DockerVersion")
|
|
627
720
|
return out
|
|
628
721
|
|
|
629
|
-
|
|
722
|
+
outfile = open(os.devnull, "w") if quiet else None
|
|
723
|
+
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
630
724
|
# First call Docker inspect
|
|
631
725
|
try:
|
|
632
726
|
inspect = get_docker_inspect()
|
|
633
727
|
except subprocess.CalledProcessError:
|
|
634
|
-
typer.echo(f"No image with tag {tag} found locally")
|
|
728
|
+
typer.echo(f"No image with tag {tag} found locally", file=outfile)
|
|
635
729
|
inspect = []
|
|
636
|
-
typer.echo(f"Reading Dockerfile from {fpath}")
|
|
730
|
+
typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
|
|
637
731
|
with open(fpath) as f:
|
|
638
732
|
dockerfile = f.read()
|
|
639
733
|
dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
|
|
640
734
|
lock_fpath = fpath + "-lock.json"
|
|
641
735
|
rebuild = True
|
|
642
736
|
if os.path.isfile(lock_fpath):
|
|
643
|
-
typer.echo(f"Reading lock file: {lock_fpath}")
|
|
737
|
+
typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
|
|
644
738
|
with open(lock_fpath) as f:
|
|
645
739
|
lock = json.load(f)
|
|
646
740
|
else:
|
|
647
|
-
typer.echo(f"Lock file ({lock_fpath}) does not exist")
|
|
741
|
+
typer.echo(f"Lock file ({lock_fpath}) does not exist", file=outfile)
|
|
648
742
|
lock = None
|
|
649
743
|
if inspect and lock:
|
|
650
|
-
typer.echo(
|
|
744
|
+
typer.echo(
|
|
745
|
+
"Checking image and Dockerfile against lock file", file=outfile
|
|
746
|
+
)
|
|
651
747
|
rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
|
|
652
748
|
"Layers"
|
|
653
749
|
] or dockerfile_md5 != lock[0].get("DockerfileMD5")
|
|
@@ -827,7 +923,23 @@ def check_conda_env(
|
|
|
827
923
|
),
|
|
828
924
|
),
|
|
829
925
|
] = None,
|
|
926
|
+
relaxed: Annotated[
|
|
927
|
+
bool,
|
|
928
|
+
typer.Option(
|
|
929
|
+
"--relaxed", help="Treat conda and pip dependencies as equivalent."
|
|
930
|
+
),
|
|
931
|
+
] = False,
|
|
932
|
+
quiet: Annotated[
|
|
933
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
934
|
+
] = False,
|
|
830
935
|
):
|
|
936
|
+
if quiet:
|
|
937
|
+
log_func = functools.partial(typer.echo, file=open(os.devnull, "w"))
|
|
938
|
+
else:
|
|
939
|
+
log_func = typer.echo
|
|
831
940
|
calkit.conda.check_env(
|
|
832
|
-
env_fpath=env_fpath,
|
|
941
|
+
env_fpath=env_fpath,
|
|
942
|
+
output_fpath=output_fpath,
|
|
943
|
+
log_func=log_func,
|
|
944
|
+
relaxed=relaxed,
|
|
833
945
|
)
|
|
@@ -4,27 +4,40 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
6
|
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
7
9
|
import calkit
|
|
8
10
|
from calkit import ryaml
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
class EnvCheckResult(BaseModel):
|
|
14
|
+
env_exists: bool | None = None
|
|
15
|
+
env_needs_export: bool | None = None
|
|
16
|
+
env_needs_rebuild: bool | None = None
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
def check_env(
|
|
12
20
|
env_fpath: str = "environment.yml",
|
|
13
21
|
use_mamba=True,
|
|
14
22
|
log_func=None,
|
|
15
23
|
output_fpath: str = None,
|
|
16
|
-
|
|
24
|
+
relaxed: bool = False,
|
|
25
|
+
) -> EnvCheckResult:
|
|
17
26
|
"""Check that a conda environment matches its spec.
|
|
18
27
|
|
|
19
28
|
If it doesn't match, recreate it.
|
|
20
29
|
|
|
21
30
|
Note that this only works with exact or no version specification.
|
|
22
31
|
Using greater than and less than operators is not supported.
|
|
32
|
+
|
|
33
|
+
If ``relaxed`` is enabled, dependencies can exist in either the conda or
|
|
34
|
+
pip category.
|
|
23
35
|
"""
|
|
24
36
|
conda = "mamba" if use_mamba else "conda"
|
|
25
37
|
if log_func is None:
|
|
26
38
|
log_func = calkit.logger.info
|
|
27
39
|
log_func(f"Checking conda env defined in {env_fpath}")
|
|
40
|
+
res = EnvCheckResult()
|
|
28
41
|
envs = json.loads(
|
|
29
42
|
subprocess.check_output([conda, "env", "list", "--json"]).decode()
|
|
30
43
|
)["envs"]
|
|
@@ -44,11 +57,13 @@ def check_env(
|
|
|
44
57
|
# Check if env even exists
|
|
45
58
|
if env_name not in existing_env_names:
|
|
46
59
|
log_func(f"Environment {env_name} doesn't exist; creating")
|
|
60
|
+
res.env_exists = False
|
|
47
61
|
# Environment doesn't exist, so create it
|
|
48
62
|
subprocess.check_call([conda, "env", "create", "-y", "-f", env_fpath])
|
|
49
63
|
env_needs_rebuild = False
|
|
50
64
|
env_needs_export = True
|
|
51
65
|
else:
|
|
66
|
+
res.env_exists = True
|
|
52
67
|
env_needs_export = False
|
|
53
68
|
# Environment does exist, so check it
|
|
54
69
|
if os.path.isfile(env_check_fpath):
|
|
@@ -67,6 +82,7 @@ def check_env(
|
|
|
67
82
|
else:
|
|
68
83
|
env_needs_export = True
|
|
69
84
|
if env_needs_export:
|
|
85
|
+
res.env_needs_export = True
|
|
70
86
|
log_func(f"Exporting existing env to {env_check_fpath}")
|
|
71
87
|
env_check = json.loads(
|
|
72
88
|
subprocess.check_output(
|
|
@@ -100,6 +116,12 @@ def check_env(
|
|
|
100
116
|
else:
|
|
101
117
|
required_conda_deps = env_spec["dependencies"]
|
|
102
118
|
required_pip_deps = []
|
|
119
|
+
if relaxed:
|
|
120
|
+
log_func("Running in relaxed mode; combining pip and conda deps")
|
|
121
|
+
for dep in existing_pip_deps:
|
|
122
|
+
existing_conda_deps.append(dep.replace("==", "="))
|
|
123
|
+
for dep in required_pip_deps:
|
|
124
|
+
required_conda_deps.append(dep.replace("==", "="))
|
|
103
125
|
log_func("Checking conda dependencies")
|
|
104
126
|
for dep in required_conda_deps:
|
|
105
127
|
dep_split = dep.split("=")
|
|
@@ -121,7 +143,7 @@ def check_env(
|
|
|
121
143
|
log_func(f"Found missing dependency: {dep}")
|
|
122
144
|
env_needs_rebuild = True
|
|
123
145
|
break
|
|
124
|
-
if not env_needs_rebuild:
|
|
146
|
+
if not env_needs_rebuild and not relaxed:
|
|
125
147
|
log_func("Checking pip dependencies")
|
|
126
148
|
for dep in required_pip_deps:
|
|
127
149
|
dep_split = dep.split("==")
|
|
@@ -142,11 +164,13 @@ def check_env(
|
|
|
142
164
|
env_needs_rebuild = True
|
|
143
165
|
break
|
|
144
166
|
if env_needs_rebuild:
|
|
167
|
+
res.env_needs_rebuild = True
|
|
145
168
|
log_func(f"Rebuilding {env_name} since it does not match spec")
|
|
146
169
|
subprocess.check_call([conda, "env", "create", "-y", "-f", env_fpath])
|
|
147
170
|
env_needs_export = True
|
|
148
171
|
else:
|
|
149
172
|
log_func(f"Environment {env_name} matches spec")
|
|
173
|
+
res.env_needs_rebuild = False
|
|
150
174
|
# If the env was rebuilt, export the env check
|
|
151
175
|
if env_needs_export:
|
|
152
176
|
log_func(f"Exporting existing env to {env_check_fpath}")
|
|
@@ -171,8 +195,14 @@ def check_env(
|
|
|
171
195
|
if output_fpath is None:
|
|
172
196
|
fname, ext = os.path.splitext(env_fpath)
|
|
173
197
|
output_fpath = fname + "-lock" + ext
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
if (
|
|
199
|
+
not res.env_exists
|
|
200
|
+
or res.env_needs_rebuild
|
|
201
|
+
or not os.path.isfile(output_fpath)
|
|
202
|
+
):
|
|
203
|
+
log_func(f"Exporting lock file to {output_fpath}")
|
|
204
|
+
with open(output_fpath, "w") as f:
|
|
205
|
+
_ = env_check.pop("mtime")
|
|
206
|
+
_ = env_check.pop("prefix")
|
|
207
|
+
ryaml.dump(env_check, f)
|
|
208
|
+
return res
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Tests for the ``conda`` module."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from calkit.conda import check_env
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def delete_env(name: str):
|
|
12
|
+
subprocess.check_call(["mamba", "env", "remove", "-y", "-n", name])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def env_name():
|
|
17
|
+
# Setup code
|
|
18
|
+
name = "tmp_" + str(uuid.uuid4())[:12]
|
|
19
|
+
yield name
|
|
20
|
+
# Teardown code
|
|
21
|
+
delete_env(name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_check_env(tmp_dir, env_name):
|
|
25
|
+
subprocess.check_call(["git", "init"])
|
|
26
|
+
subprocess.check_call(["dvc", "init"])
|
|
27
|
+
subprocess.check_call(
|
|
28
|
+
[
|
|
29
|
+
"calkit",
|
|
30
|
+
"new",
|
|
31
|
+
"conda-env",
|
|
32
|
+
"-n",
|
|
33
|
+
env_name,
|
|
34
|
+
"python",
|
|
35
|
+
"pip",
|
|
36
|
+
"--pip",
|
|
37
|
+
"pxl",
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
res = check_env()
|
|
41
|
+
assert not res.env_exists
|
|
42
|
+
res = check_env()
|
|
43
|
+
assert res.env_exists
|
|
44
|
+
assert not res.env_needs_export
|
|
45
|
+
assert not res.env_needs_rebuild
|
|
46
|
+
# Now let's update the env spec so it needs a rebuild
|
|
47
|
+
subprocess.check_call(
|
|
48
|
+
[
|
|
49
|
+
"calkit",
|
|
50
|
+
"new",
|
|
51
|
+
"conda-env",
|
|
52
|
+
"--overwrite",
|
|
53
|
+
"-n",
|
|
54
|
+
env_name,
|
|
55
|
+
"python=3.11.0",
|
|
56
|
+
"pip",
|
|
57
|
+
"--pip",
|
|
58
|
+
"pxl",
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
res = check_env()
|
|
62
|
+
assert res.env_exists
|
|
63
|
+
assert not res.env_needs_export
|
|
64
|
+
assert res.env_needs_rebuild
|
|
65
|
+
res = check_env()
|
|
66
|
+
assert not res.env_needs_rebuild
|
|
67
|
+
# Check relaxed mode, where we allow dependencies to be in either the pip
|
|
68
|
+
# or conda section
|
|
69
|
+
subprocess.check_call(
|
|
70
|
+
[
|
|
71
|
+
"calkit",
|
|
72
|
+
"new",
|
|
73
|
+
"conda-env",
|
|
74
|
+
"--overwrite",
|
|
75
|
+
"-n",
|
|
76
|
+
env_name,
|
|
77
|
+
"python=3.11.0",
|
|
78
|
+
"pip",
|
|
79
|
+
"sqlalchemy",
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
subprocess.check_call(
|
|
83
|
+
[
|
|
84
|
+
"conda",
|
|
85
|
+
"run",
|
|
86
|
+
"-n",
|
|
87
|
+
env_name,
|
|
88
|
+
"pip",
|
|
89
|
+
"install",
|
|
90
|
+
"--upgrade",
|
|
91
|
+
"sqlalchemy",
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
res = check_env()
|
|
95
|
+
assert res.env_needs_rebuild
|
|
96
|
+
subprocess.check_call(
|
|
97
|
+
[
|
|
98
|
+
"calkit",
|
|
99
|
+
"new",
|
|
100
|
+
"conda-env",
|
|
101
|
+
"--overwrite",
|
|
102
|
+
"-n",
|
|
103
|
+
env_name,
|
|
104
|
+
"python=3.11.0",
|
|
105
|
+
"pip",
|
|
106
|
+
"sqlalchemy",
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
res = check_env(relaxed=True)
|
|
110
|
+
assert not res.env_needs_rebuild
|
|
111
|
+
subprocess.check_call(
|
|
112
|
+
[
|
|
113
|
+
"calkit",
|
|
114
|
+
"new",
|
|
115
|
+
"conda-env",
|
|
116
|
+
"--overwrite",
|
|
117
|
+
"-n",
|
|
118
|
+
env_name,
|
|
119
|
+
"python=3.11.0",
|
|
120
|
+
"pip",
|
|
121
|
+
"--pip",
|
|
122
|
+
"sqlalchemy",
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
res = check_env(relaxed=True)
|
|
126
|
+
assert not res.env_needs_rebuild
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|