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.
Files changed (60) hide show
  1. {calkit_python-0.8.5 → calkit_python-0.9.1}/PKG-INFO +5 -4
  2. {calkit_python-0.8.5 → calkit_python-0.9.1}/README.md +4 -3
  3. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/__init__.py +1 -1
  4. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/config.py +8 -0
  5. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/main.py +158 -22
  6. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/new.py +7 -5
  7. calkit_python-0.9.1/calkit/cli/update.py +39 -0
  8. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/config.py +31 -10
  9. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/docker.py +5 -4
  10. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/core.py +1 -0
  11. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_main.py +2 -2
  12. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_new.py +1 -1
  13. {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/conda-envs.md +7 -6
  14. {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/FUNDING.yml +0 -0
  15. {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/workflows/publish-test.yml +0 -0
  16. {calkit_python-0.8.5 → calkit_python-0.9.1}/.github/workflows/publish.yml +0 -0
  17. {calkit_python-0.8.5 → calkit_python-0.9.1}/.gitignore +0 -0
  18. {calkit_python-0.8.5 → calkit_python-0.9.1}/LICENSE +0 -0
  19. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/__init__.py +0 -0
  20. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/core.py +0 -0
  21. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/import_.py +0 -0
  22. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/list.py +0 -0
  23. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/notebooks.py +0 -0
  24. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cli/office.py +0 -0
  25. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/cloud.py +0 -0
  26. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/conda.py +0 -0
  27. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/core.py +0 -0
  28. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/data.py +0 -0
  29. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/dvc.py +0 -0
  30. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/git.py +0 -0
  31. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/gui.py +0 -0
  32. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/jupyter.py +0 -0
  33. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/magics.py +0 -0
  34. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/models.py +0 -0
  35. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/office.py +0 -0
  36. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/server.py +0 -0
  37. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/__init__.py +0 -0
  38. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/core.py +0 -0
  39. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/__init__.py +0 -0
  40. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/article/paper.tex +0 -0
  41. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/jfm.bst +0 -0
  42. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/jfm.cls +0 -0
  43. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  44. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/paper.tex +0 -0
  45. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/templates/latex/jfm/upmath.sty +0 -0
  46. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/__init__.py +0 -0
  47. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/__init__.py +0 -0
  48. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/cli/test_list.py +0 -0
  49. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_conda.py +0 -0
  50. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_core.py +0 -0
  51. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_dvc.py +0 -0
  52. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_jupyter.py +0 -0
  53. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_magics.py +0 -0
  54. {calkit_python-0.8.5 → calkit_python-0.9.1}/calkit/tests/test_templates.py +0 -0
  55. {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  56. {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/img/run-proc.png +0 -0
  57. {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/notebook-pipeline.md +0 -0
  58. {calkit_python-0.8.5 → calkit_python-0.9.1}/docs/tutorials/procedures.md +0 -0
  59. {calkit_python-0.8.5 → calkit_python-0.9.1}/pyproject.toml +0 -0
  60. {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.8.5
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. The [Calkit cloud](https://calkit.io)
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
- [Mambaforge](https://conda-forge.org/miniforge/).
109
- If you're a Windows user and decide to install Mambaforge or any other
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. The [Calkit cloud](https://calkit.io)
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
- [Mambaforge](https://conda-forge.org/miniforge/).
78
- If you're a Windows user and decide to install Mambaforge or any other
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).
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.5"
1
+ __version__ = "0.9.1"
2
2
 
3
3
  from .core import *
4
4
  from . import git
@@ -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", help="Pull with both Git and DVC.")
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
- subprocess.call(["git", "pull"])
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
- subprocess.call(["dvc", "pull"])
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", help="Push with both Git and DVC.")
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
- subprocess.call(["git", "push"])
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
- subprocess.call(["dvc", "push"])
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
- subprocess.check_call(["dvc", "repro"] + args)
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
- subprocess.check_call(docker_cmd, cwd=wdir)
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
- subprocess.check_call(cmd, cwd=wdir)
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 build_docker(
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
- typer.echo(f"Checking for existing image with tag {tag}")
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("Checking image and Dockerfile against lock file")
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=typer.echo,
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: mambaforge, foampy)."
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="kjarosh/latex:2024.4",
627
- description="TeXlive full from kjarosh.",
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 = f"cd {path} && latexmk -pdf {template_obj.target}"
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 BaseSettings, SettingsConfigDict
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 "-" + get_env()
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
- return keyring.get_password(get_app_name(), self.username)
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
- fpath = Settings.model_config["yaml_file"]
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
- MAMBAFORGE_LAYER_TXT = r"""
3
+ MINIFORGE_LAYER_TXT = r"""
4
4
  # Install Miniforge
5
- ARG MINIFORGE_NAME=Mambaforge
6
- ARG MINIFORGE_VERSION=24.3.0-0
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": MAMBAFORGE_LAYER_TXT,
50
+ "mambaforge": MINIFORGE_LAYER_TXT,
51
+ "miniforge": MINIFORGE_LAYER_TXT,
51
52
  "foampy": FOAMPY_LAYER_TEXT,
52
53
  }
@@ -7,5 +7,6 @@ GITIGNORE = """
7
7
  *.log
8
8
  *.pdf
9
9
  *.out
10
+ *.gz
10
11
  *.DS_Store
11
12
  """
@@ -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 mambaforge "
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 mambaforge "
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-py11 \
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` for use with
83
- `calkit runenv`, and since `--stage` was specified, Calkit will also add
84
- an environment check stage to the pipeline automatically.
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