calkit-python 0.8.5__tar.gz → 0.9.1__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.5 → calkit_python-0.9.1}/PKG-INFO +5 -4
- {calkit_python-0.8.5 → calkit_python-0.9.1}/README.md +4 -3
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/__init__.py +1 -1
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/config.py +8 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/main.py +158 -22
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/new.py +7 -5
- calkit_python-0.9.1/calkit/cli/update.py +39 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/config.py +31 -10
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/docker.py +5 -4
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/core.py +1 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_main.py +2 -2
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_new.py +1 -1
- {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/conda-envs.md +7 -6
- {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/FUNDING.yml +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/.gitignore +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/LICENSE +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/core.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/import_.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/list.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/office.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cloud.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/conda.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/core.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/data.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/dvc.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/git.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/gui.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/jupyter.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/magics.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/models.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/office.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/server.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/core.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_conda.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_dvc.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/pyproject.toml +0 -0
- {calkit_python-0.8.5 → calkit_python-0.9.1}/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.1
|
|
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
|
|
@@ -105,8 +106,8 @@ If you want to use [Docker](https://docker.com) containers,
|
|
|
105
106
|
which is typically a good idea,
|
|
106
107
|
that should also be installed.
|
|
107
108
|
For Python, we recommend
|
|
108
|
-
[
|
|
109
|
-
If you're a Windows user and decide to install
|
|
109
|
+
[Miniforge](https://conda-forge.org/miniforge/).
|
|
110
|
+
If you're a Windows user and decide to install Miniforge or any other
|
|
110
111
|
Conda-based distribution,
|
|
111
112
|
e.g., Anaconda, you'll probably want to ensure that environment is
|
|
112
113
|
[activated by default in Git Bash](https://discuss.codecademy.com/t/setting-up-conda-in-git-bash/534473).
|
|
@@ -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
|
|
@@ -74,8 +75,8 @@ If you want to use [Docker](https://docker.com) containers,
|
|
|
74
75
|
which is typically a good idea,
|
|
75
76
|
that should also be installed.
|
|
76
77
|
For Python, we recommend
|
|
77
|
-
[
|
|
78
|
-
If you're a Windows user and decide to install
|
|
78
|
+
[Miniforge](https://conda-forge.org/miniforge/).
|
|
79
|
+
If you're a Windows user and decide to install Miniforge or any other
|
|
79
80
|
Conda-based distribution,
|
|
80
81
|
e.g., Anaconda, you'll probably want to ensure that environment is
|
|
81
82
|
[activated by default in Git Bash](https://discuss.codecademy.com/t/setting-up-conda-in-git-bash/534473).
|
|
@@ -57,3 +57,11 @@ def setup_remote_auth():
|
|
|
57
57
|
if name == "calkit" or name.startswith("calkit:"):
|
|
58
58
|
typer.echo(f"Setting up authentication for DVC remote: {name}")
|
|
59
59
|
set_remote_auth(remote_name=name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@config_app.command(name="list")
|
|
63
|
+
def list_config_keys():
|
|
64
|
+
"""List keys in the config."""
|
|
65
|
+
cfg = config.read()
|
|
66
|
+
for key in cfg.model_dump():
|
|
67
|
+
typer.echo(key)
|
|
@@ -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
|
|
@@ -22,6 +23,7 @@ from calkit.cli.list import list_app
|
|
|
22
23
|
from calkit.cli.new import new_app
|
|
23
24
|
from calkit.cli.notebooks import notebooks_app
|
|
24
25
|
from calkit.cli.office import office_app
|
|
26
|
+
from calkit.cli.update import update_app
|
|
25
27
|
from calkit.models import Procedure
|
|
26
28
|
|
|
27
29
|
app = typer.Typer(
|
|
@@ -41,6 +43,7 @@ app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
|
|
|
41
43
|
app.add_typer(list_app, name="list", help="List Calkit objects.")
|
|
42
44
|
app.add_typer(import_app, name="import", help="Import objects.")
|
|
43
45
|
app.add_typer(office_app, name="office", help="Work with Microsoft Office.")
|
|
46
|
+
app.add_typer(update_app, name="update", help="Update objects.")
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
@app.callback()
|
|
@@ -290,26 +293,57 @@ def save(
|
|
|
290
293
|
push()
|
|
291
294
|
|
|
292
295
|
|
|
293
|
-
@app.command(name="pull"
|
|
294
|
-
def pull(
|
|
296
|
+
@app.command(name="pull")
|
|
297
|
+
def pull(
|
|
298
|
+
no_check_auth: Annotated[bool, typer.Option("--no-check-auth")] = False
|
|
299
|
+
):
|
|
300
|
+
"""Pull with both Git and DVC."""
|
|
295
301
|
typer.echo("Git pulling")
|
|
296
|
-
|
|
302
|
+
try:
|
|
303
|
+
subprocess.check_call(["git", "pull"])
|
|
304
|
+
except subprocess.CalledProcessError:
|
|
305
|
+
raise_error("Git pull failed")
|
|
297
306
|
typer.echo("DVC pulling")
|
|
298
|
-
|
|
307
|
+
if not no_check_auth:
|
|
308
|
+
# Check that our dvc remotes all have our DVC token set for them
|
|
309
|
+
remotes = calkit.dvc.get_remotes()
|
|
310
|
+
for name, url in remotes.items():
|
|
311
|
+
if name == "calkit" or name.startswith("calkit:"):
|
|
312
|
+
typer.echo(f"Checking authentication for DVC remote: {name}")
|
|
313
|
+
calkit.dvc.set_remote_auth(remote_name=name)
|
|
314
|
+
try:
|
|
315
|
+
subprocess.check_call(["dvc", "pull"])
|
|
316
|
+
except subprocess.CalledProcessError:
|
|
317
|
+
raise_error("DVC pull failed")
|
|
299
318
|
|
|
300
319
|
|
|
301
|
-
@app.command(name="push"
|
|
302
|
-
def push(
|
|
320
|
+
@app.command(name="push")
|
|
321
|
+
def push(
|
|
322
|
+
no_check_auth: Annotated[bool, typer.Option("--no-check-auth")] = False
|
|
323
|
+
):
|
|
324
|
+
"""Push with both Git and DVC."""
|
|
303
325
|
typer.echo("Pushing to Git remote")
|
|
304
|
-
|
|
326
|
+
try:
|
|
327
|
+
subprocess.check_call(["git", "push"])
|
|
328
|
+
except subprocess.CalledProcessError:
|
|
329
|
+
raise_error("Git push failed")
|
|
305
330
|
typer.echo("Pushing to DVC remote")
|
|
306
|
-
|
|
331
|
+
if not no_check_auth:
|
|
332
|
+
# Check that our dvc remotes all have our DVC token set for them
|
|
333
|
+
remotes = calkit.dvc.get_remotes()
|
|
334
|
+
for name, url in remotes.items():
|
|
335
|
+
if name == "calkit" or name.startswith("calkit:"):
|
|
336
|
+
typer.echo(f"Checking authentication for DVC remote: {name}")
|
|
337
|
+
calkit.dvc.set_remote_auth(remote_name=name)
|
|
338
|
+
try:
|
|
339
|
+
subprocess.check_call(["dvc", "push"])
|
|
340
|
+
except subprocess.CalledProcessError:
|
|
341
|
+
raise_error("DVC push failed")
|
|
307
342
|
|
|
308
343
|
|
|
309
|
-
@app.command(
|
|
310
|
-
name="local-server", help="Run the local server to interact over HTTP."
|
|
311
|
-
)
|
|
344
|
+
@app.command(name="local-server")
|
|
312
345
|
def run_local_server():
|
|
346
|
+
"""Run the local server to interact over HTTP."""
|
|
313
347
|
import uvicorn
|
|
314
348
|
|
|
315
349
|
uvicorn.run(
|
|
@@ -387,7 +421,10 @@ def run_dvc_repro(
|
|
|
387
421
|
args += ["--pipeline", pipeline]
|
|
388
422
|
if downstream is not None:
|
|
389
423
|
args += downstream
|
|
390
|
-
|
|
424
|
+
try:
|
|
425
|
+
subprocess.check_call(["dvc", "repro"] + args)
|
|
426
|
+
except subprocess.CalledProcessError:
|
|
427
|
+
raise_error("DVC pipeline failed")
|
|
391
428
|
# Now parse stage metadata for calkit objects
|
|
392
429
|
if not os.path.isfile("dvc.yaml"):
|
|
393
430
|
raise_error("No dvc.yaml file found")
|
|
@@ -504,6 +541,20 @@ def run_in_env(
|
|
|
504
541
|
),
|
|
505
542
|
),
|
|
506
543
|
] = None,
|
|
544
|
+
no_check: Annotated[
|
|
545
|
+
bool,
|
|
546
|
+
typer.Option(
|
|
547
|
+
"--no-check",
|
|
548
|
+
help="Don't check the environment is valid before running in it.",
|
|
549
|
+
),
|
|
550
|
+
] = False,
|
|
551
|
+
relaxed_check: Annotated[
|
|
552
|
+
bool,
|
|
553
|
+
typer.Option(
|
|
554
|
+
"--relaxed",
|
|
555
|
+
help="Check the environment in a relaxed way, if applicable.",
|
|
556
|
+
),
|
|
557
|
+
] = False,
|
|
507
558
|
verbose: Annotated[
|
|
508
559
|
bool, typer.Option("--verbose", "-v", help="Print verbose output.")
|
|
509
560
|
] = False,
|
|
@@ -529,6 +580,8 @@ def run_in_env(
|
|
|
529
580
|
env_name = default_env_name
|
|
530
581
|
if env_name is None:
|
|
531
582
|
raise_error("Environment must be specified if there are multiple")
|
|
583
|
+
if env_name not in envs:
|
|
584
|
+
raise_error(f"Environment '{env_name}' does not exist")
|
|
532
585
|
env = envs[env_name]
|
|
533
586
|
if wdir is not None:
|
|
534
587
|
cwd = os.path.abspath(wdir)
|
|
@@ -539,6 +592,15 @@ def run_in_env(
|
|
|
539
592
|
shell = env.get("shell", "sh")
|
|
540
593
|
platform = env.get("platform")
|
|
541
594
|
if env["kind"] == "docker":
|
|
595
|
+
if "image" not in env:
|
|
596
|
+
raise_error("Image must be defined for Docker environments")
|
|
597
|
+
if "path" in env and not no_check:
|
|
598
|
+
check_docker_env(
|
|
599
|
+
tag=env["image"],
|
|
600
|
+
fpath=env["path"],
|
|
601
|
+
platform=env.get("platform"),
|
|
602
|
+
quiet=True,
|
|
603
|
+
)
|
|
542
604
|
shell_cmd = " ".join(cmd)
|
|
543
605
|
docker_cmd = [
|
|
544
606
|
"docker",
|
|
@@ -560,14 +622,75 @@ def run_in_env(
|
|
|
560
622
|
]
|
|
561
623
|
if verbose:
|
|
562
624
|
typer.echo(f"Running command: {docker_cmd}")
|
|
563
|
-
|
|
625
|
+
try:
|
|
626
|
+
subprocess.check_call(docker_cmd, cwd=wdir)
|
|
627
|
+
except subprocess.CalledProcessError:
|
|
628
|
+
raise_error("Failed to run in Docker environment")
|
|
564
629
|
elif env["kind"] == "conda":
|
|
565
630
|
with open(env["path"]) as f:
|
|
566
631
|
conda_env = calkit.ryaml.load(f)
|
|
632
|
+
if not no_check:
|
|
633
|
+
check_conda_env(
|
|
634
|
+
env_fpath=env["path"], relaxed=relaxed_check, quiet=True
|
|
635
|
+
)
|
|
567
636
|
cmd = ["conda", "run", "-n", conda_env["name"]] + cmd
|
|
568
637
|
if verbose:
|
|
569
638
|
typer.echo(f"Running command: {cmd}")
|
|
570
|
-
|
|
639
|
+
try:
|
|
640
|
+
subprocess.check_call(cmd, cwd=wdir)
|
|
641
|
+
except subprocess.CalledProcessError:
|
|
642
|
+
raise_error("Failed to run in Conda environment")
|
|
643
|
+
elif env["kind"] in ["pixi", "uv"]:
|
|
644
|
+
env_cmd = []
|
|
645
|
+
if "name" in env:
|
|
646
|
+
env_cmd = ["--environment", env["name"]]
|
|
647
|
+
cmd = [env["kind"], "run"] + env_cmd + cmd
|
|
648
|
+
if verbose:
|
|
649
|
+
typer.echo(f"Running command: {cmd}")
|
|
650
|
+
try:
|
|
651
|
+
subprocess.check_call(cmd, cwd=wdir)
|
|
652
|
+
except subprocess.CalledProcessError:
|
|
653
|
+
raise_error(f"Failed to run in {env['kind']} environment")
|
|
654
|
+
elif env["kind"] == "uv-venv":
|
|
655
|
+
# TODO: This doesn't work on Windows
|
|
656
|
+
if "prefix" not in env:
|
|
657
|
+
raise_error("uv-venv environments require a prefix")
|
|
658
|
+
if "path" not in env:
|
|
659
|
+
raise_error("uv-venv environments require a path")
|
|
660
|
+
prefix = env["prefix"]
|
|
661
|
+
path = env["path"]
|
|
662
|
+
shell_cmd = " ".join(cmd)
|
|
663
|
+
# Check environment
|
|
664
|
+
if not no_check:
|
|
665
|
+
if not os.path.isdir(prefix):
|
|
666
|
+
if verbose:
|
|
667
|
+
typer.echo(f"Creating uv-venv at {prefix}")
|
|
668
|
+
try:
|
|
669
|
+
subprocess.check_call(["uv", "venv", prefix], cwd=wdir)
|
|
670
|
+
except subprocess.CalledProcessError:
|
|
671
|
+
raise_error(f"Failed to create uv-venv at {prefix}")
|
|
672
|
+
fname, ext = os.path.splitext(path)
|
|
673
|
+
lock_fpath = fname + "-lock" + ext
|
|
674
|
+
check_cmd = (
|
|
675
|
+
f". {prefix}/bin/activate "
|
|
676
|
+
f"&& uv pip install -q -r {path} "
|
|
677
|
+
f"&& uv pip freeze > {lock_fpath} "
|
|
678
|
+
"&& deactivate"
|
|
679
|
+
)
|
|
680
|
+
try:
|
|
681
|
+
if verbose:
|
|
682
|
+
typer.echo(f"Running command: {check_cmd}")
|
|
683
|
+
subprocess.check_output(check_cmd, shell=True, cwd=wdir)
|
|
684
|
+
except subprocess.CalledProcessError:
|
|
685
|
+
raise_error("Failed to check uv-venv")
|
|
686
|
+
# Now run the command
|
|
687
|
+
cmd = f". {prefix}/bin/activate && {shell_cmd} && deactivate"
|
|
688
|
+
if verbose:
|
|
689
|
+
typer.echo(f"Running command: {cmd}")
|
|
690
|
+
try:
|
|
691
|
+
subprocess.check_call(cmd, shell=True, cwd=wdir)
|
|
692
|
+
except subprocess.CalledProcessError:
|
|
693
|
+
raise_error("Failed to run in uv-venv")
|
|
571
694
|
else:
|
|
572
695
|
raise_error("Environment kind not supported")
|
|
573
696
|
|
|
@@ -605,7 +728,7 @@ def check_call(
|
|
|
605
728
|
name="build-docker",
|
|
606
729
|
help="Build Docker image if missing or different from lock file.",
|
|
607
730
|
)
|
|
608
|
-
def
|
|
731
|
+
def check_docker_env(
|
|
609
732
|
tag: Annotated[str, typer.Argument(help="Image tag.")],
|
|
610
733
|
fpath: Annotated[
|
|
611
734
|
str, typer.Option("-i", "--input", help="Path to input Dockerfile.")
|
|
@@ -613,6 +736,9 @@ def build_docker(
|
|
|
613
736
|
platform: Annotated[
|
|
614
737
|
str, typer.Option("--platform", help="Which platform(s) to build for.")
|
|
615
738
|
] = None,
|
|
739
|
+
quiet: Annotated[
|
|
740
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
741
|
+
] = False,
|
|
616
742
|
):
|
|
617
743
|
def get_docker_inspect():
|
|
618
744
|
out = json.loads(
|
|
@@ -626,28 +752,31 @@ def build_docker(
|
|
|
626
752
|
_ = out[0].pop("DockerVersion")
|
|
627
753
|
return out
|
|
628
754
|
|
|
629
|
-
|
|
755
|
+
outfile = open(os.devnull, "w") if quiet else None
|
|
756
|
+
typer.echo(f"Checking for existing image with tag {tag}", file=outfile)
|
|
630
757
|
# First call Docker inspect
|
|
631
758
|
try:
|
|
632
759
|
inspect = get_docker_inspect()
|
|
633
760
|
except subprocess.CalledProcessError:
|
|
634
|
-
typer.echo(f"No image with tag {tag} found locally")
|
|
761
|
+
typer.echo(f"No image with tag {tag} found locally", file=outfile)
|
|
635
762
|
inspect = []
|
|
636
|
-
typer.echo(f"Reading Dockerfile from {fpath}")
|
|
763
|
+
typer.echo(f"Reading Dockerfile from {fpath}", file=outfile)
|
|
637
764
|
with open(fpath) as f:
|
|
638
765
|
dockerfile = f.read()
|
|
639
766
|
dockerfile_md5 = hashlib.md5(dockerfile.encode()).hexdigest()
|
|
640
767
|
lock_fpath = fpath + "-lock.json"
|
|
641
768
|
rebuild = True
|
|
642
769
|
if os.path.isfile(lock_fpath):
|
|
643
|
-
typer.echo(f"Reading lock file: {lock_fpath}")
|
|
770
|
+
typer.echo(f"Reading lock file: {lock_fpath}", file=outfile)
|
|
644
771
|
with open(lock_fpath) as f:
|
|
645
772
|
lock = json.load(f)
|
|
646
773
|
else:
|
|
647
|
-
typer.echo(f"Lock file ({lock_fpath}) does not exist")
|
|
774
|
+
typer.echo(f"Lock file ({lock_fpath}) does not exist", file=outfile)
|
|
648
775
|
lock = None
|
|
649
776
|
if inspect and lock:
|
|
650
|
-
typer.echo(
|
|
777
|
+
typer.echo(
|
|
778
|
+
"Checking image and Dockerfile against lock file", file=outfile
|
|
779
|
+
)
|
|
651
780
|
rebuild = inspect[0]["RootFS"]["Layers"] != lock[0]["RootFS"][
|
|
652
781
|
"Layers"
|
|
653
782
|
] or dockerfile_md5 != lock[0].get("DockerfileMD5")
|
|
@@ -833,10 +962,17 @@ def check_conda_env(
|
|
|
833
962
|
"--relaxed", help="Treat conda and pip dependencies as equivalent."
|
|
834
963
|
),
|
|
835
964
|
] = False,
|
|
965
|
+
quiet: Annotated[
|
|
966
|
+
bool, typer.Option("--quiet", "-q", help="Be quiet.")
|
|
967
|
+
] = False,
|
|
836
968
|
):
|
|
969
|
+
if quiet:
|
|
970
|
+
log_func = functools.partial(typer.echo, file=open(os.devnull, "w"))
|
|
971
|
+
else:
|
|
972
|
+
log_func = typer.echo
|
|
837
973
|
calkit.conda.check_env(
|
|
838
974
|
env_fpath=env_fpath,
|
|
839
975
|
output_fpath=output_fpath,
|
|
840
|
-
log_func=
|
|
976
|
+
log_func=log_func,
|
|
841
977
|
relaxed=relaxed,
|
|
842
978
|
)
|
|
@@ -216,7 +216,7 @@ def new_docker_env(
|
|
|
216
216
|
layers: Annotated[
|
|
217
217
|
list[str],
|
|
218
218
|
typer.Option(
|
|
219
|
-
"--add-layer", help="Add a layer (options:
|
|
219
|
+
"--add-layer", help="Add a layer (options: miniforge, foampy)."
|
|
220
220
|
),
|
|
221
221
|
] = [],
|
|
222
222
|
wdir: Annotated[
|
|
@@ -623,9 +623,8 @@ def new_publication(
|
|
|
623
623
|
envs[env_name] = env
|
|
624
624
|
env_remote = dict(
|
|
625
625
|
kind="docker",
|
|
626
|
-
image="
|
|
627
|
-
description="TeXlive full
|
|
628
|
-
platform="linux/amd64",
|
|
626
|
+
image="texlive/texlive:latest-full",
|
|
627
|
+
description="TeXlive full.",
|
|
629
628
|
)
|
|
630
629
|
with open(env_path, "w") as f:
|
|
631
630
|
calkit.ryaml.dump(env_remote, f)
|
|
@@ -644,7 +643,10 @@ def new_publication(
|
|
|
644
643
|
repo.git.add(path)
|
|
645
644
|
# Create stage if applicable
|
|
646
645
|
if stage_name is not None and template_type == "latex":
|
|
647
|
-
cmd =
|
|
646
|
+
cmd = (
|
|
647
|
+
f"cd {path} && latexmk -interaction=nonstopmode "
|
|
648
|
+
f"-pdf {template_obj.target}"
|
|
649
|
+
)
|
|
648
650
|
if env_name is not None:
|
|
649
651
|
cmd = f'calkit runenv -n {env_name} "{cmd}"'
|
|
650
652
|
target_dep = os.path.join(path, template_obj.target)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""CLI for updating objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import typer
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
update_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@update_app.command(name="devcontainer")
|
|
15
|
+
def update_devcontainer(
|
|
16
|
+
wdir: Annotated[
|
|
17
|
+
str,
|
|
18
|
+
typer.Option(
|
|
19
|
+
"--wdir",
|
|
20
|
+
help=(
|
|
21
|
+
"Working directory. "
|
|
22
|
+
"By default will run current working directory."
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
] = None,
|
|
26
|
+
):
|
|
27
|
+
"""Update a project's devcontainer to match the latest Calkit spec."""
|
|
28
|
+
url = (
|
|
29
|
+
"https://raw.githubusercontent.com/calkit/devcontainer/"
|
|
30
|
+
"refs/heads/main/devcontainer.json"
|
|
31
|
+
)
|
|
32
|
+
typer.echo(f"Downloading {url}")
|
|
33
|
+
resp = requests.get(url)
|
|
34
|
+
out_dir = os.path.join(wdir or ".", ".devcontainer")
|
|
35
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
36
|
+
out_fpath = os.path.join(out_dir, "devcontainer.json")
|
|
37
|
+
typer.echo(f"Writing to {out_fpath}")
|
|
38
|
+
with open(out_fpath, "w") as f:
|
|
39
|
+
f.write(resp.text)
|
|
@@ -7,8 +7,14 @@ from typing import Literal
|
|
|
7
7
|
|
|
8
8
|
import keyring
|
|
9
9
|
import yaml
|
|
10
|
+
from keyring.errors import NoKeyringError
|
|
10
11
|
from pydantic import computed_field
|
|
11
|
-
from pydantic_settings import
|
|
12
|
+
from pydantic_settings import (
|
|
13
|
+
BaseSettings,
|
|
14
|
+
PydanticBaseSettingsSource,
|
|
15
|
+
SettingsConfigDict,
|
|
16
|
+
YamlConfigSettingsSource,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
def get_env() -> Literal["local", "staging", "production"]:
|
|
@@ -21,9 +27,9 @@ def set_env(name: Literal["local", "staging", "production"]) -> None:
|
|
|
21
27
|
os.environ[f"{__package__.upper()}_ENV"] = name
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
def get_env_suffix() -> str:
|
|
30
|
+
def get_env_suffix(sep: str = "-") -> str:
|
|
25
31
|
if get_env() != "production":
|
|
26
|
-
return
|
|
32
|
+
return sep + get_env()
|
|
27
33
|
return ""
|
|
28
34
|
|
|
29
35
|
|
|
@@ -39,16 +45,35 @@ class Settings(BaseSettings):
|
|
|
39
45
|
f"config{get_env_suffix()}.yaml",
|
|
40
46
|
),
|
|
41
47
|
extra="ignore",
|
|
48
|
+
env_prefix="CALKIT" + get_env_suffix(sep="_") + "_",
|
|
42
49
|
)
|
|
43
50
|
username: str | None = None
|
|
44
51
|
token: str | None = None
|
|
45
52
|
dvc_token: str | None = None
|
|
46
53
|
dataframe_engine: Literal["pandas", "polars"] = "pandas"
|
|
47
54
|
|
|
55
|
+
@classmethod
|
|
56
|
+
def settings_customise_sources(
|
|
57
|
+
cls,
|
|
58
|
+
settings_cls: type[BaseSettings],
|
|
59
|
+
init_settings: PydanticBaseSettingsSource,
|
|
60
|
+
env_settings: PydanticBaseSettingsSource,
|
|
61
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
62
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
63
|
+
) -> tuple[PydanticBaseSettingsSource]:
|
|
64
|
+
return (
|
|
65
|
+
init_settings,
|
|
66
|
+
env_settings,
|
|
67
|
+
YamlConfigSettingsSource(settings_cls),
|
|
68
|
+
)
|
|
69
|
+
|
|
48
70
|
@computed_field
|
|
49
71
|
@property
|
|
50
|
-
def password(self) -> str:
|
|
51
|
-
|
|
72
|
+
def password(self) -> str | None:
|
|
73
|
+
try:
|
|
74
|
+
return keyring.get_password(get_app_name(), self.username)
|
|
75
|
+
except NoKeyringError:
|
|
76
|
+
return None
|
|
52
77
|
|
|
53
78
|
@password.setter
|
|
54
79
|
def password(self, value: str) -> None:
|
|
@@ -66,8 +91,4 @@ class Settings(BaseSettings):
|
|
|
66
91
|
|
|
67
92
|
def read() -> Settings:
|
|
68
93
|
"""Read the config."""
|
|
69
|
-
|
|
70
|
-
if not os.path.isfile(fpath):
|
|
71
|
-
return Settings()
|
|
72
|
-
with open(fpath) as f:
|
|
73
|
-
return Settings.model_validate(yaml.safe_load(f))
|
|
94
|
+
return Settings()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Functionality for working with Docker."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MINIFORGE_LAYER_TXT = r"""
|
|
4
4
|
# Install Miniforge
|
|
5
|
-
ARG MINIFORGE_NAME=
|
|
6
|
-
ARG MINIFORGE_VERSION=24.
|
|
5
|
+
ARG MINIFORGE_NAME=Miniforge3
|
|
6
|
+
ARG MINIFORGE_VERSION=24.9.2-0
|
|
7
7
|
ARG TARGETPLATFORM
|
|
8
8
|
|
|
9
9
|
ENV CONDA_DIR=/opt/conda
|
|
@@ -47,6 +47,7 @@ RUN pip install --no-cache-dir numpy pandas matplotlib h5py \
|
|
|
47
47
|
""".strip()
|
|
48
48
|
|
|
49
49
|
LAYERS = {
|
|
50
|
-
"mambaforge":
|
|
50
|
+
"mambaforge": MINIFORGE_LAYER_TXT,
|
|
51
|
+
"miniforge": MINIFORGE_LAYER_TXT,
|
|
51
52
|
"foampy": FOAMPY_LAYER_TEXT,
|
|
52
53
|
}
|
|
@@ -17,7 +17,7 @@ def test_run_in_env(tmp_dir):
|
|
|
17
17
|
"--name my-image "
|
|
18
18
|
"--stage build-image "
|
|
19
19
|
"--from ubuntu "
|
|
20
|
-
"--add-layer
|
|
20
|
+
"--add-layer miniforge "
|
|
21
21
|
"--description 'This is a test image'",
|
|
22
22
|
shell=True,
|
|
23
23
|
)
|
|
@@ -37,7 +37,7 @@ def test_run_in_env(tmp_dir):
|
|
|
37
37
|
"--stage build-image-2 "
|
|
38
38
|
"--path Dockerfile.2 "
|
|
39
39
|
"--from ubuntu "
|
|
40
|
-
"--add-layer
|
|
40
|
+
"--add-layer miniforge "
|
|
41
41
|
"--add-layer foampy "
|
|
42
42
|
"--description 'This is a test image 2'",
|
|
43
43
|
shell=True,
|
|
@@ -167,5 +167,5 @@ def test_new_publication(tmp_dir):
|
|
|
167
167
|
stage = dvc_pipeline["stages"]["build-latex-article"]
|
|
168
168
|
assert stage["cmd"] == (
|
|
169
169
|
"calkit runenv -n my-latex-env "
|
|
170
|
-
'"cd my-paper && latexmk -pdf paper.tex"'
|
|
170
|
+
'"cd my-paper && latexmk -interaction=nonstopmode -pdf paper.tex"'
|
|
171
171
|
)
|
|
@@ -69,16 +69,17 @@ If you run something like:
|
|
|
69
69
|
|
|
70
70
|
```sh
|
|
71
71
|
calkit new conda-env \
|
|
72
|
-
-n my-project-
|
|
72
|
+
-n my-project-py311 \
|
|
73
73
|
python=3.11 \
|
|
74
74
|
pip \
|
|
75
75
|
matplotlib \
|
|
76
76
|
pandas \
|
|
77
77
|
jupyter \
|
|
78
|
-
--pip tensorflow
|
|
79
|
-
--stage check-conda-env
|
|
78
|
+
--pip tensorflow
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
Calkit will create an environment definition in `calkit.yaml
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
Calkit will create an environment definition in `calkit.yaml`,
|
|
82
|
+
which enables running a command in this environment with
|
|
83
|
+
`calkit runenv -n my-project-py311 my-command-here`.
|
|
84
|
+
That call will automatically create or update the Conda environment on the fly
|
|
85
|
+
as needed and export a lock file describing the actual environment.
|
|
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
|