calkit-python 0.23.0__tar.gz → 0.24.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.
Files changed (161) hide show
  1. {calkit_python-0.23.0 → calkit_python-0.24.0}/PKG-INFO +1 -1
  2. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/__init__.py +1 -1
  3. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/check.py +136 -21
  4. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/main.py +1 -0
  5. calkit_python-0.24.0/calkit/cli/notebooks.py +116 -0
  6. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/overleaf.py +1 -1
  7. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/dvc.py +10 -7
  8. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/magics.py +40 -11
  9. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/models/pipeline.py +115 -6
  10. calkit_python-0.24.0/calkit/notebooks.py +31 -0
  11. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/pipeline.py +25 -32
  12. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/models/test_pipeline.py +11 -1
  13. calkit_python-0.24.0/calkit/tests/test_dvc.py +37 -0
  14. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_magics.py +38 -1
  15. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_pipeline.py +46 -1
  16. calkit_python-0.24.0/docs/dependencies.md +51 -0
  17. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/environments.md +10 -3
  18. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/pipeline/index.md +2 -2
  19. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/notebook-pipeline.md +31 -8
  20. {calkit_python-0.23.0 → calkit_python-0.24.0}/mkdocs.yml +3 -2
  21. {calkit_python-0.23.0 → calkit_python-0.24.0}/test/pipeline.ipynb +4 -3
  22. calkit_python-0.23.0/calkit/cli/notebooks.py +0 -76
  23. calkit_python-0.23.0/calkit/tests/test_dvc.py +0 -23
  24. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/FUNDING.yml +0 -0
  25. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/workflows/docs.yml +0 -0
  26. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/workflows/format.yml +0 -0
  27. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/workflows/publish-test.yml +0 -0
  28. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/workflows/publish.yml +0 -0
  29. {calkit_python-0.23.0 → calkit_python-0.24.0}/.github/workflows/test.yml +0 -0
  30. {calkit_python-0.23.0 → calkit_python-0.24.0}/.gitignore +0 -0
  31. {calkit_python-0.23.0 → calkit_python-0.24.0}/.pre-commit-config.yaml +0 -0
  32. {calkit_python-0.23.0 → calkit_python-0.24.0}/.python-version +0 -0
  33. {calkit_python-0.23.0 → calkit_python-0.24.0}/CONTRIBUTING.md +0 -0
  34. {calkit_python-0.23.0 → calkit_python-0.24.0}/LICENSE +0 -0
  35. {calkit_python-0.23.0 → calkit_python-0.24.0}/Makefile +0 -0
  36. {calkit_python-0.23.0 → calkit_python-0.24.0}/README.md +0 -0
  37. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/__main__.py +0 -0
  38. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/calc.py +0 -0
  39. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/check.py +0 -0
  40. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/__init__.py +0 -0
  41. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/cloud.py +0 -0
  42. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/config.py +0 -0
  43. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/core.py +0 -0
  44. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/import_.py +0 -0
  45. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/list.py +0 -0
  46. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/new.py +0 -0
  47. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/office.py +0 -0
  48. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cli/update.py +0 -0
  49. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/cloud.py +0 -0
  50. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/conda.py +0 -0
  51. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/config.py +0 -0
  52. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/core.py +0 -0
  53. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/datasets.py +0 -0
  54. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/docker.py +0 -0
  55. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/environments.py +0 -0
  56. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/git.py +0 -0
  57. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/github.py +0 -0
  58. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/gui.py +0 -0
  59. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/jupyter.py +0 -0
  60. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/matlab.py +0 -0
  61. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/models/__init__.py +0 -0
  62. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/models/core.py +0 -0
  63. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/models/iteration.py +0 -0
  64. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/office.py +0 -0
  65. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/ops.py +0 -0
  66. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/releases.py +0 -0
  67. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/server.py +0 -0
  68. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/__init__.py +0 -0
  69. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/core.py +0 -0
  70. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/__init__.py +0 -0
  71. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/article/paper.tex +0 -0
  72. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/core.py +0 -0
  73. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
  74. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
  75. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  76. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/jfm/paper.tex +0 -0
  77. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
  78. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/__init__.py +0 -0
  79. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/cli/__init__.py +0 -0
  80. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/cli/test_config.py +0 -0
  81. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/cli/test_list.py +0 -0
  82. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/cli/test_main.py +0 -0
  83. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/cli/test_new.py +0 -0
  84. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/models/__init__.py +0 -0
  85. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_calc.py +0 -0
  86. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_check.py +0 -0
  87. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_conda.py +0 -0
  88. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_core.py +0 -0
  89. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_jupyter.py +0 -0
  90. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/tests/test_templates.py +0 -0
  91. {calkit_python-0.23.0 → calkit_python-0.24.0}/calkit/zenodo.py +0 -0
  92. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/CNAME +0 -0
  93. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/apps.md +0 -0
  94. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/calculations.md +0 -0
  95. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/calkit-yaml.md +0 -0
  96. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/cli-reference.md +0 -0
  97. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/cloud-integration.md +0 -0
  98. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/datasets.md +0 -0
  99. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/examples.md +0 -0
  100. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/help.md +0 -0
  101. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/img/c-to-the-k-white.svg +0 -0
  102. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/img/calkit-no-bg.png +0 -0
  103. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/img/connect-zenodo.png +0 -0
  104. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/index.md +0 -0
  105. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/installation.md +0 -0
  106. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/local-server.md +0 -0
  107. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/overleaf.md +0 -0
  108. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/pipeline/manual-steps.md +0 -0
  109. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/references.md +0 -0
  110. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/releases.md +0 -0
  111. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  112. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/conda-envs.md +0 -0
  113. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/existing-project.md +0 -0
  114. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/first-project.md +0 -0
  115. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  116. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  117. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  118. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  119. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  120. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  121. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  122. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  123. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  124. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  125. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  126. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  127. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  128. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  129. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/create-project.png +0 -0
  130. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  131. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/excel-chart.png +0 -0
  132. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/excel-data.png +0 -0
  133. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  134. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/needs-clone.png +0 -0
  135. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/new-stage.png +0 -0
  136. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  137. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  138. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
  139. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  140. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/untracked-data.png +0 -0
  141. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/updated-publication.png +0 -0
  142. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  143. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/office/workflow-page.png +0 -0
  144. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/clone.png +0 -0
  145. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
  146. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  147. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  148. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  149. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
  150. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
  151. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  152. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/img/run-proc.png +0 -0
  153. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/index.md +0 -0
  154. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/latex-codespaces.md +0 -0
  155. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/matlab.md +0 -0
  156. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/office.md +0 -0
  157. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/openfoam.md +0 -0
  158. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/tutorials/procedures.md +0 -0
  159. {calkit_python-0.23.0 → calkit_python-0.24.0}/docs/version-control.md +0 -0
  160. {calkit_python-0.23.0 → calkit_python-0.24.0}/pyproject.toml +0 -0
  161. {calkit_python-0.23.0 → calkit_python-0.24.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.23.0
3
+ Version: 0.24.0
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -1,4 +1,4 @@
1
- __version__ = "0.23.0"
1
+ __version__ = "0.24.0"
2
2
 
3
3
  from .core import * # noqa: F403, I001
4
4
  from . import git # noqa: F401
@@ -11,6 +11,8 @@ import subprocess
11
11
  import warnings
12
12
  from typing import Annotated
13
13
 
14
+ from calkit.environments import get_env_lock_fpath
15
+
14
16
  # See https://github.com/calkit/calkit/issues/346
15
17
  with warnings.catch_warnings():
16
18
  warnings.filterwarnings("ignore", category=UserWarning)
@@ -40,28 +42,101 @@ def check_repro(
40
42
  typer.echo(res.to_pretty().encode("utf-8", errors="replace"))
41
43
 
42
44
 
43
- @check_app.command(name="call")
44
- def check_call(
45
- cmd: Annotated[str, typer.Argument(help="Command to check.")],
46
- if_error: Annotated[
45
+ @check_app.command(
46
+ name="env",
47
+ help="Check that an environment is up-to-date (alias for 'environment').",
48
+ )
49
+ @check_app.command(name="environment")
50
+ def check_environment(
51
+ env_name: Annotated[
47
52
  str,
48
- typer.Option(
49
- "--if-error", help="Command to run if there is an error."
50
- ),
53
+ typer.Option("--name", "-n", help="Name of the environment to check."),
51
54
  ],
55
+ verbose: Annotated[
56
+ bool, typer.Option("--verbose", help="Print verbose output.")
57
+ ] = False,
52
58
  ):
53
- """Check that a command succeeds and run an alternate if not."""
54
- try:
55
- subprocess.check_call(cmd, shell=True)
56
- typer.echo("Command succeeded")
57
- except subprocess.CalledProcessError:
58
- typer.echo("Command failed")
59
+ """Check that an environment is up-to-date."""
60
+ dotenv.load_dotenv(dotenv_path=".env", verbose=verbose)
61
+ ck_info = calkit.load_calkit_info(process_includes="environments")
62
+ envs = ck_info.get("environments", {})
63
+ if not envs:
64
+ raise_error("No environments defined in calkit.yaml")
65
+ if isinstance(envs, list):
66
+ raise_error("Error: Environments should be a dict, not a list")
67
+ assert isinstance(envs, dict)
68
+ if env_name not in envs:
69
+ raise_error(f"Environment '{env_name}' does not exist")
70
+ env = envs[env_name]
71
+ if env["kind"] == "docker":
72
+ if "image" not in env:
73
+ raise_error("Image must be defined for Docker environments")
74
+ if "path" in env:
75
+ check_docker_env(
76
+ tag=env["image"],
77
+ fpath=env["path"],
78
+ lock_fpath=get_env_lock_fpath(
79
+ env=env, env_name=env_name, as_posix=False
80
+ ),
81
+ platform=env.get("platform"),
82
+ deps=env.get("deps", []),
83
+ quiet=not verbose,
84
+ )
85
+ elif env["kind"] == "conda":
86
+ check_conda_env(
87
+ env_fpath=env["path"],
88
+ output_fpath=get_env_lock_fpath(
89
+ env=env, env_name=env_name, as_posix=False
90
+ ),
91
+ relaxed=True, # TODO: Add option?
92
+ quiet=not verbose,
93
+ )
94
+ elif env["kind"] in ["pixi", "uv"]:
95
+ cmd = [env["kind"], "lock"]
96
+ if verbose:
97
+ typer.echo(f"Running command: {cmd}")
59
98
  try:
60
- typer.echo("Attempting fallback call")
61
- subprocess.check_call(if_error, shell=True)
62
- typer.echo("Fallback call succeeded")
99
+ subprocess.check_call(cmd)
63
100
  except subprocess.CalledProcessError:
64
- raise_error("Fallback call failed")
101
+ raise_error(f"Failed to check {env['kind']} environment")
102
+ elif (kind := env["kind"]) in ["uv-venv", "venv"]:
103
+ if "prefix" not in env:
104
+ raise_error("venv environments require a prefix")
105
+ if "path" not in env:
106
+ raise_error("venv environments require a path")
107
+ prefix = env["prefix"]
108
+ path = env["path"]
109
+ # Check environment
110
+ check_venv(
111
+ path=path,
112
+ prefix=prefix,
113
+ use_uv=kind == "uv-venv",
114
+ python=env.get("python"),
115
+ lock_fpath=get_env_lock_fpath(
116
+ env=env, env_name=env_name, as_posix=False
117
+ ),
118
+ verbose=verbose,
119
+ )
120
+ elif env["kind"] == "ssh":
121
+ # TODO: How to check SSH environments?
122
+ # Maybe just check that we can connect
123
+ raise_error(
124
+ "Environment checking not implemented for SSH environments"
125
+ )
126
+ elif env["kind"] == "renv":
127
+ try:
128
+ subprocess.check_call(["Rscript", "-e", "'renv::restore()'"])
129
+ except subprocess.CalledProcessError:
130
+ raise_error("Failed to check renv")
131
+ elif env["kind"] == "matlab":
132
+ check_matlab_env(
133
+ env_name=env_name,
134
+ output_fpath=get_env_lock_fpath(
135
+ env=env, env_name=env_name, as_posix=False
136
+ ), # type: ignore
137
+ )
138
+ else:
139
+ raise_error(f"Environment kind '{env['kind']}' not supported")
65
140
 
66
141
 
67
142
  @check_app.command(name="docker-env")
@@ -273,6 +348,9 @@ def check_venv(
273
348
  "--python", help="Python version to specify if using uv."
274
349
  ),
275
350
  ] = None,
351
+ quiet: Annotated[
352
+ bool, typer.Option("--quiet", help="Do not print any output")
353
+ ] = False,
276
354
  verbose: Annotated[
277
355
  bool, typer.Option("--verbose", help="Print verbose output.")
278
356
  ] = False,
@@ -283,7 +361,7 @@ def check_venv(
283
361
  ["uv", "venv"] if kind == "uv-venv" else ["python", "-m", "venv"]
284
362
  )
285
363
  pip_cmd = "pip" if kind == "venv" else "uv pip"
286
- pip_install_args = "-q"
364
+ pip_install_args = "-q" if quiet else ""
287
365
  if python is not None and not use_uv:
288
366
  raise_error("Python version cannot be specified if not using uv")
289
367
  if python is not None and use_uv:
@@ -319,7 +397,7 @@ def check_venv(
319
397
  try:
320
398
  if verbose:
321
399
  typer.echo(f"Running command: {check_cmd}")
322
- subprocess.check_output(
400
+ subprocess.check_call(
323
401
  check_cmd,
324
402
  shell=True,
325
403
  cwd=wdir,
@@ -374,7 +452,11 @@ def check_matlab_env(
374
452
 
375
453
 
376
454
  @check_app.command(name="env-vars")
377
- def check_env_vars():
455
+ def check_env_vars(
456
+ verbose: Annotated[
457
+ bool, typer.Option("--verbose", "-v", help="Print verbose output")
458
+ ] = False,
459
+ ):
378
460
  """Check that the project's required environmental variables exist."""
379
461
  typer.echo("Checking project environmental variables")
380
462
  dotenv.load_dotenv(dotenv_path=".env")
@@ -383,11 +465,20 @@ def check_env_vars():
383
465
  env_var_deps = {}
384
466
  for d in deps:
385
467
  if isinstance(d, dict):
386
- name = list(d.keys())[0]
468
+ keys = list(d.keys())
469
+ if len(keys) > 1:
470
+ raise_error(
471
+ f"Malformed dependency: {d}\n"
472
+ "Dependencies with attributes should have a single key "
473
+ "(their name)"
474
+ )
475
+ name = keys[0]
387
476
  attrs = list(d.values())[0]
388
477
  if attrs.get("kind") == "env-var":
389
478
  env_var_deps[name] = attrs
390
479
  for name, attrs in env_var_deps.items():
480
+ if verbose:
481
+ typer.echo(f"Checking for environmental variable '{name}'")
391
482
  if name not in os.environ:
392
483
  typer.echo(f"Missing env var '{name}'")
393
484
  if "default" in attrs:
@@ -446,3 +537,27 @@ def check_pipeline(
446
537
  raise_error(
447
538
  f"Failed to compile pipeline: {e.__class__.__name__}: {e}"
448
539
  )
540
+
541
+
542
+ @check_app.command(name="call")
543
+ def check_call(
544
+ cmd: Annotated[str, typer.Argument(help="Command to check.")],
545
+ if_error: Annotated[
546
+ str,
547
+ typer.Option(
548
+ "--if-error", help="Command to run if there is an error."
549
+ ),
550
+ ],
551
+ ):
552
+ """Check that a command succeeds and run an alternate if not."""
553
+ try:
554
+ subprocess.check_call(cmd, shell=True)
555
+ typer.echo("Command succeeded")
556
+ except subprocess.CalledProcessError:
557
+ typer.echo("Command failed")
558
+ try:
559
+ typer.echo("Attempting fallback call")
560
+ subprocess.check_call(if_error, shell=True)
561
+ typer.echo("Fallback call succeeded")
562
+ except subprocess.CalledProcessError:
563
+ raise_error("Fallback call failed")
@@ -989,6 +989,7 @@ def run_in_env(
989
989
  env=env, env_name=env_name, as_posix=False
990
990
  ),
991
991
  wdir=wdir,
992
+ quiet=True,
992
993
  verbose=verbose,
993
994
  )
994
995
  # Now run the command
@@ -0,0 +1,116 @@
1
+ """Notebooks CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+
8
+ import typer
9
+ from typing_extensions import Annotated
10
+
11
+ import calkit.notebooks
12
+ from calkit.cli.core import raise_error
13
+
14
+ notebooks_app = typer.Typer(no_args_is_help=True)
15
+
16
+
17
+ @notebooks_app.command("clean")
18
+ def clean_notebook_outputs(path: str):
19
+ """Clean notebook and place a copy in the cleaned notebooks directory.
20
+
21
+ This can be useful to use as a preprocessing DVC stage to use a clean
22
+ notebook as a dependency for a stage that caches and executed notebook.
23
+ """
24
+ if os.path.isabs(path):
25
+ raise ValueError("Path must be relative")
26
+ fpath_out = calkit.notebooks.get_cleaned_notebook_path(path)
27
+ folder = os.path.dirname(fpath_out)
28
+ os.makedirs(folder, exist_ok=True)
29
+ fpath_out = os.path.abspath(fpath_out)
30
+ subprocess.call(
31
+ [
32
+ "jupyter",
33
+ "nbconvert",
34
+ path,
35
+ "--clear-output",
36
+ "--to",
37
+ "notebook",
38
+ "--output",
39
+ fpath_out,
40
+ ]
41
+ )
42
+
43
+
44
+ @notebooks_app.command("execute")
45
+ def execute_notebook(
46
+ path: str,
47
+ env_name: Annotated[
48
+ str,
49
+ typer.Option(
50
+ "--environment",
51
+ "-e",
52
+ help="Environment name in which to run the notebook.",
53
+ ),
54
+ ],
55
+ to: Annotated[
56
+ list[str],
57
+ typer.Option("--to", help="Output format ('html' or 'notebook')."),
58
+ ] = ["notebook"],
59
+ no_check: Annotated[
60
+ bool,
61
+ typer.Option(
62
+ "--no-check", help="Do not check environment before executing."
63
+ ),
64
+ ] = False,
65
+ ):
66
+ """Execute notebook and place a copy in the relevant directory.
67
+
68
+ This can be useful to use as a preprocessing DVC stage to use a clean
69
+ notebook as a dependency for a stage that caches and executed notebook.
70
+ """
71
+ from calkit.cli.main import run_in_env
72
+
73
+ if os.path.isabs(path):
74
+ raise ValueError("Path must be relative")
75
+ # First, always execute the notebook and save as ipynb
76
+ fpath_out_exec = calkit.notebooks.get_executed_notebook_path(
77
+ notebook_path=path, to="notebook", as_posix=True
78
+ )
79
+ folder = os.path.dirname(fpath_out_exec)
80
+ os.makedirs(folder, exist_ok=True)
81
+ fpath_out_exec = os.path.abspath(fpath_out_exec)
82
+ cmd = [
83
+ "jupyter",
84
+ "nbconvert",
85
+ path,
86
+ "--execute",
87
+ "--to",
88
+ "notebook",
89
+ "--output",
90
+ fpath_out_exec,
91
+ ]
92
+ run_in_env(cmd=cmd, env_name=env_name, no_check=no_check)
93
+ for to_fmt in to:
94
+ if to_fmt != "notebook":
95
+ try:
96
+ fpath_out = calkit.notebooks.get_executed_notebook_path(
97
+ notebook_path=path,
98
+ to=to_fmt, # type: ignore
99
+ )
100
+ except ValueError:
101
+ raise_error(f"Invalid output format: '{to}'")
102
+ folder = os.path.dirname(fpath_out)
103
+ os.makedirs(folder, exist_ok=True)
104
+ fpath_out = os.path.abspath(fpath_out)
105
+ # Now convert without executing or checking the environment
106
+ cmd = [
107
+ "jupyter",
108
+ "nbconvert",
109
+ fpath_out_exec,
110
+ "--to",
111
+ to_fmt,
112
+ "--output",
113
+ fpath_out,
114
+ ]
115
+ typer.echo(f"Exporting {to_fmt}")
116
+ run_in_env(cmd=cmd, env_name=env_name, no_check=True)
@@ -141,7 +141,7 @@ def import_publication(
141
141
  pdf_path = sync_paths[0].removesuffix(".tex") + ".pdf"
142
142
  typer.echo(f"Using PDF path: {pdf_path}")
143
143
  tex_path = pdf_path.removesuffix(".pdf") + ".tex"
144
- pub_path = os.path.join(dest_dir, pdf_path)
144
+ pub_path = PurePosixPath(dest_dir, pdf_path).as_posix()
145
145
  pub_paths = [pub.get("path") for pub in pubs]
146
146
  if not overwrite and pub_path in pub_paths:
147
147
  raise_error(
@@ -155,7 +155,7 @@ def read_pipeline(wdir: str = ".") -> dict:
155
155
  return calkit.ryaml.load(f)
156
156
 
157
157
 
158
- def get_remotes(wdir: str = None) -> dict[str, str]:
158
+ def get_remotes(wdir: str | None = None) -> dict[str, str]:
159
159
  """Get a dictionary of DVC remotes, keyed by name, with URL as the
160
160
  value.
161
161
  """
@@ -169,13 +169,16 @@ def get_remotes(wdir: str = None) -> dict[str, str]:
169
169
  if not out:
170
170
  return {}
171
171
  resp = {}
172
- for line in out.split("\n"):
173
- split_line = line.split()
174
- if len(split_line) >= 2:
175
- # This is a remote
176
- name = split_line[0]
177
- url = split_line[1]
172
+ out = out.replace("(default)", "").strip()
173
+ is_name = True
174
+ for token in out.split():
175
+ token = token.strip()
176
+ if is_name:
177
+ name = token
178
+ else:
179
+ url = token
178
180
  resp[name] = url
181
+ is_name = not is_name
179
182
  return resp
180
183
 
181
184
 
@@ -38,12 +38,11 @@ class Calkit(Magics):
38
38
  )
39
39
  @magic_arguments.argument(
40
40
  "--env",
41
- nargs="?",
42
- const=True,
43
- help=(
44
- "Whether or not this cell should be run in an environment. "
45
- "If no environment name is provided, the default will be used."
46
- ),
41
+ "--environment",
42
+ "-e",
43
+ required=True,
44
+ help=("Environment with which to run this cell."),
45
+ type=_parse_string_arg,
47
46
  )
48
47
  @magic_arguments.argument(
49
48
  "--dep",
@@ -112,6 +111,19 @@ class Calkit(Magics):
112
111
  help="Description for Calkit output object.",
113
112
  type=_parse_string_arg,
114
113
  )
114
+ @magic_arguments.argument(
115
+ "--out-storage",
116
+ choices=["dvc", "git", "none"],
117
+ help="Which version control system to use to store the output.",
118
+ default="dvc",
119
+ type=_parse_string_arg,
120
+ )
121
+ @magic_arguments.argument(
122
+ "--verbose",
123
+ help="Print verbose output.",
124
+ default=False,
125
+ action="store_true",
126
+ )
115
127
  @cell_magic
116
128
  def stage(self, line, cell):
117
129
  """Turn a notebook cell into a DVC pipeline stage.
@@ -222,21 +234,22 @@ class Calkit(Magics):
222
234
  with open(script_fpath, "w") as f:
223
235
  f.write(script_txt)
224
236
  # Create a DVC stage that runs the script, defining the appropriate
225
- # dependencies and outputs, and run it
237
+ # dependencies and outputs
238
+ # TODO: Insert this into dvc.yaml directly, since DVC reformats
226
239
  cmd = [
227
240
  sys.executable,
228
241
  "-m",
229
242
  "dvc",
230
243
  "stage",
231
244
  "add",
232
- "-q",
233
245
  "-n",
234
246
  args.name,
235
- "--run",
236
247
  "--force",
237
248
  "-d",
238
249
  _posix_path(script_fpath),
239
250
  ]
251
+ if not args.verbose:
252
+ cmd.append("-q")
240
253
  if args.dep:
241
254
  for dep in args.dep:
242
255
  dep_split = dep.split(":")
@@ -259,8 +272,13 @@ class Calkit(Magics):
259
272
  kws = dict(stage_name=args.name, out_name=out_name)
260
273
  if len(out_split) > 1:
261
274
  kws["fmt"] = out_split[1]
275
+ # Figure out DVC caching
276
+ if args.out_storage == "dvc":
277
+ out_flag = "-o"
278
+ else:
279
+ out_flag = "--outs-no-cache"
262
280
  cmd += [
263
- "-o",
281
+ out_flag,
264
282
  _posix_path(calkit.get_notebook_stage_out_path(**kws)),
265
283
  ]
266
284
  if args.out_path:
@@ -274,7 +292,18 @@ class Calkit(Magics):
274
292
  stage_cmd = xenv + " -- " + stage_cmd
275
293
  cmd.append(stage_cmd)
276
294
  try:
277
- subprocess.run(cmd, check=True, capture_output=True, text=True)
295
+ subprocess.run(
296
+ cmd, check=True, capture_output=not args.verbose, text=True
297
+ )
298
+ except subprocess.CalledProcessError as e:
299
+ print(f"Error: {e.stderr}")
300
+ raise e
301
+ # Now run the stage
302
+ run_cmd = [sys.executable, "-m", "dvc", "repro", args.name]
303
+ try:
304
+ subprocess.run(
305
+ run_cmd, check=True, capture_output=False, text=True
306
+ )
278
307
  except subprocess.CalledProcessError as e:
279
308
  print(f"Error: {e.stderr}")
280
309
  raise e
@@ -13,6 +13,10 @@ from calkit.models.iteration import (
13
13
  ParametersType,
14
14
  RangeIteration,
15
15
  )
16
+ from calkit.notebooks import (
17
+ get_cleaned_notebook_path,
18
+ get_executed_notebook_path,
19
+ )
16
20
 
17
21
 
18
22
  class Input(BaseModel):
@@ -156,7 +160,7 @@ class Stage(BaseModel):
156
160
 
157
161
 
158
162
  class PythonScriptStage(Stage):
159
- kind: Literal["python-script"]
163
+ kind: Literal["python-script"] = "python-script"
160
164
  script_path: str
161
165
  args: list[str] = []
162
166
 
@@ -292,6 +296,9 @@ class JupyterNotebookStage(Stage):
292
296
  2. Notebook running, depending on the cleaned notebook, and optionally
293
297
  producing HTML output.
294
298
 
299
+ Alternatively, we could force the use of ``nbstripout`` so the cleaned
300
+ notebook is saved at the notebook path.
301
+
295
302
  TODO: Can/should we do something like Papermill and let users modify
296
303
  parameters in the notebook?
297
304
 
@@ -299,15 +306,115 @@ class JupyterNotebookStage(Stage):
299
306
  needing to be run from top to bottom every time they change.
300
307
  """
301
308
 
302
- kind: Literal["jupyter-notebook"]
309
+ kind: Literal["jupyter-notebook"] = "jupyter-notebook"
303
310
  notebook_path: str
304
- store_cleaned_with: Literal["git", "dvc"] | None = "git"
305
- store_executed_ipynb_with: Literal["git", "dvc"] | None = "dvc"
306
- store_executed_html_with: Literal["git", "dvc"] | None = "dvc"
311
+ cleaned_ipynb_storage: Literal["git", "dvc"] | None = "git"
312
+ executed_ipynb_storage: Literal["git", "dvc"] | None = "dvc"
313
+ html_storage: Literal["git", "dvc"] | None = "dvc"
314
+
315
+ @property
316
+ def cleaned_notebook_path(self) -> str:
317
+ return get_cleaned_notebook_path(self.notebook_path, as_posix=True)
318
+
319
+ @property
320
+ def executed_notebook_path(self) -> str:
321
+ return get_executed_notebook_path(
322
+ self.notebook_path, to="notebook", as_posix=True
323
+ )
324
+
325
+ @property
326
+ def html_path(self) -> str:
327
+ return get_executed_notebook_path(
328
+ self.notebook_path, to="html", as_posix=True
329
+ )
307
330
 
308
331
  @property
309
332
  def dvc_deps(self) -> list[str]:
310
- return [self.notebook_path] + super().dvc_deps
333
+ return [self.cleaned_notebook_path] + super().dvc_deps
334
+
335
+ @property
336
+ def dvc_cmd(self) -> str:
337
+ cmd = f"calkit nb execute --environment {self.environment} --no-check"
338
+ if self.html_storage:
339
+ cmd += " --to html"
340
+ cmd += f' "{self.notebook_path}"'
341
+ return cmd
342
+
343
+ @property
344
+ def dvc_outs(self) -> list[str | dict]:
345
+ outs = super().dvc_outs
346
+ # TODO: This should also export HTML?
347
+ exec_nb_path = self.executed_notebook_path
348
+ exec_nb_out = {
349
+ exec_nb_path: {"cache": self.executed_ipynb_storage == "dvc"}
350
+ }
351
+ outs = outs + [exec_nb_out]
352
+ return outs
353
+
354
+ @property
355
+ def dvc_clean_stage(self) -> dict:
356
+ """Create a DVC stage for notebook cleaning so the cleaned notebook
357
+ can be used as a DVC dependency.
358
+
359
+ TODO: Should we use Jupytext for this so diffs are nice?
360
+ """
361
+ clean_nb_path = self.cleaned_notebook_path
362
+ stage = {
363
+ "cmd": f'calkit nb clean "{self.notebook_path}"',
364
+ "deps": [self.notebook_path],
365
+ "outs": [
366
+ {clean_nb_path: {"cache": self.cleaned_ipynb_storage == "dvc"}}
367
+ ],
368
+ }
369
+ return stage
370
+
371
+ @property
372
+ def notebook_outputs(self) -> list[PathOutput]:
373
+ """Return a list of special notebook outputs so their storage can be
374
+ respected.
375
+ """
376
+ return [
377
+ PathOutput(
378
+ path=self.cleaned_notebook_path,
379
+ storage=self.cleaned_ipynb_storage,
380
+ ),
381
+ PathOutput(
382
+ path=self.executed_notebook_path,
383
+ storage=self.executed_ipynb_storage,
384
+ ),
385
+ PathOutput(path=self.html_path, storage=self.html_storage),
386
+ ]
387
+
388
+
389
+ class WordToPdfStage(Stage):
390
+ kind: Literal["word-to-pdf"] = "word-to-pdf"
391
+ word_doc_path: str
392
+ environment: str = "_system"
393
+
394
+ @property
395
+ def dvc_deps(self) -> list[str]:
396
+ return [self.word_doc_path] + super().dvc_deps
397
+
398
+ @property
399
+ def out_path(self) -> str:
400
+ return PurePosixPath(
401
+ self.word_doc_path.removesuffix(".docx") + ".pdf"
402
+ ).as_posix()
403
+
404
+ @property
405
+ def dvc_outs(self) -> list[str | dict]:
406
+ outs = super().dvc_outs
407
+ out_path = self.out_path
408
+ if out_path not in outs:
409
+ outs.append(out_path)
410
+ return outs
411
+
412
+ @property
413
+ def dvc_cmd(self) -> str:
414
+ return (
415
+ f'calkit office word-to-pdf "{self.word_doc_path}" '
416
+ f'-o "{self.out_path}"'
417
+ )
311
418
 
312
419
 
313
420
  class Pipeline(BaseModel):
@@ -322,6 +429,8 @@ class Pipeline(BaseModel):
322
429
  | ShellScriptStage
323
430
  | DockerCommandStage
324
431
  | RScriptStage
432
+ | WordToPdfStage
433
+ | JupyterNotebookStage
325
434
  ),
326
435
  Discriminator("kind"),
327
436
  ],
@@ -0,0 +1,31 @@
1
+ """Functionality for working with notebooks."""
2
+
3
+ import os
4
+ from pathlib import PurePosixPath
5
+ from typing import Literal
6
+
7
+
8
+ def get_executed_notebook_path(
9
+ notebook_path: str, to: Literal["html", "notebook"], as_posix: bool = True
10
+ ) -> str:
11
+ """Return the path of an executed notebook."""
12
+ nb_dir = os.path.dirname(notebook_path)
13
+ nb_fname = os.path.basename(notebook_path)
14
+ if to == "html":
15
+ fname_out = nb_fname.removesuffix(".ipynb") + ".html"
16
+ else:
17
+ fname_out = nb_fname
18
+ # Different output types go to different subdirectories
19
+ subdirs = {"html": "html", "notebook": "executed"}
20
+ p = os.path.join(".calkit", "notebooks", subdirs[to], nb_dir, fname_out)
21
+ if as_posix:
22
+ p = PurePosixPath(p).as_posix()
23
+ return p
24
+
25
+
26
+ def get_cleaned_notebook_path(path: str, as_posix: bool = True) -> str:
27
+ """Return the path of a cleaned notebook."""
28
+ p = os.path.join(".calkit", "notebooks", "cleaned", path)
29
+ if as_posix:
30
+ p = PurePosixPath(p).as_posix()
31
+ return p