calkit-python 0.28.4__tar.gz → 0.29.2__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 (180) hide show
  1. {calkit_python-0.28.4 → calkit_python-0.29.2}/PKG-INFO +1 -1
  2. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/__init__.py +1 -1
  3. calkit_python-0.29.2/calkit/cli/config.py +240 -0
  4. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/main.py +65 -16
  5. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/notebooks.py +59 -14
  6. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/dvc.py +23 -13
  7. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/models/pipeline.py +39 -15
  8. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/models/test_pipeline.py +16 -0
  9. calkit_python-0.28.4/calkit/cli/config.py +0 -116
  10. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/FUNDING.yml +0 -0
  11. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/workflows/docs.yml +0 -0
  12. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/workflows/format.yml +0 -0
  13. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/workflows/publish-test.yml +0 -0
  14. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/workflows/publish.yml +0 -0
  15. {calkit_python-0.28.4 → calkit_python-0.29.2}/.github/workflows/test.yml +0 -0
  16. {calkit_python-0.28.4 → calkit_python-0.29.2}/.gitignore +0 -0
  17. {calkit_python-0.28.4 → calkit_python-0.29.2}/.pre-commit-config.yaml +0 -0
  18. {calkit_python-0.28.4 → calkit_python-0.29.2}/.python-version +0 -0
  19. {calkit_python-0.28.4 → calkit_python-0.29.2}/CITATION.cff +0 -0
  20. {calkit_python-0.28.4 → calkit_python-0.29.2}/CONTRIBUTING.md +0 -0
  21. {calkit_python-0.28.4 → calkit_python-0.29.2}/LICENSE +0 -0
  22. {calkit_python-0.28.4 → calkit_python-0.29.2}/Makefile +0 -0
  23. {calkit_python-0.28.4 → calkit_python-0.29.2}/README.md +0 -0
  24. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/__main__.py +0 -0
  25. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/calc.py +0 -0
  26. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/check.py +0 -0
  27. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/__init__.py +0 -0
  28. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/check.py +0 -0
  29. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/cloud.py +0 -0
  30. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/core.py +0 -0
  31. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/describe.py +0 -0
  32. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/import_.py +0 -0
  33. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/list.py +0 -0
  34. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/new.py +0 -0
  35. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/office.py +0 -0
  36. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/overleaf.py +0 -0
  37. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cli/update.py +0 -0
  38. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/cloud.py +0 -0
  39. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/conda.py +0 -0
  40. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/config.py +0 -0
  41. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/core.py +0 -0
  42. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/datasets.py +0 -0
  43. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/docker.py +0 -0
  44. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/environments.py +0 -0
  45. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/git.py +0 -0
  46. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/github.py +0 -0
  47. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/gui.py +0 -0
  48. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/jupyter.py +0 -0
  49. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/magics.py +0 -0
  50. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/matlab.py +0 -0
  51. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/models/__init__.py +0 -0
  52. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/models/core.py +0 -0
  53. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/models/io.py +0 -0
  54. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/models/iteration.py +0 -0
  55. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/notebooks.py +0 -0
  56. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/office.py +0 -0
  57. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/ops.py +0 -0
  58. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/pipeline.py +0 -0
  59. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/releases.py +0 -0
  60. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/server.py +0 -0
  61. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/__init__.py +0 -0
  62. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/core.py +0 -0
  63. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/__init__.py +0 -0
  64. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/article/paper.tex +0 -0
  65. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/core.py +0 -0
  66. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/jfm/jfm.bst +0 -0
  67. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/jfm/jfm.cls +0 -0
  68. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  69. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/jfm/paper.tex +0 -0
  70. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/templates/latex/jfm/upmath.sty +0 -0
  71. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/__init__.py +0 -0
  72. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/__init__.py +0 -0
  73. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_check.py +0 -0
  74. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_config.py +0 -0
  75. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_list.py +0 -0
  76. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_main.py +0 -0
  77. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_new.py +0 -0
  78. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/cli/test_notebooks.py +0 -0
  79. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/models/__init__.py +0 -0
  80. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/models/test_iteration.py +0 -0
  81. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_calc.py +0 -0
  82. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_check.py +0 -0
  83. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_conda.py +0 -0
  84. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_core.py +0 -0
  85. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_dvc.py +0 -0
  86. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_jupyter.py +0 -0
  87. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_magics.py +0 -0
  88. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_notebooks.py +0 -0
  89. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_pipeline.py +0 -0
  90. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/tests/test_templates.py +0 -0
  91. {calkit_python-0.28.4 → calkit_python-0.29.2}/calkit/zenodo.py +0 -0
  92. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/CNAME +0 -0
  93. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/apps.md +0 -0
  94. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/calculations.md +0 -0
  95. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/calkit-yaml.md +0 -0
  96. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/cli-reference.md +0 -0
  97. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/cloud-integration.md +0 -0
  98. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/datasets.md +0 -0
  99. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/dependencies.md +0 -0
  100. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/environments.md +0 -0
  101. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/examples.md +0 -0
  102. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/help.md +0 -0
  103. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/img/c-to-the-k-white.svg +0 -0
  104. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/img/calkit-no-bg.png +0 -0
  105. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/img/connect-zenodo.png +0 -0
  106. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/img/jupyterlab-params.png +0 -0
  107. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/img/vscode-nb-params.png +0 -0
  108. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/index.md +0 -0
  109. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/installation.md +0 -0
  110. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/local-server.md +0 -0
  111. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/notebooks.md +0 -0
  112. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/overleaf.md +0 -0
  113. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/pipeline/index.md +0 -0
  114. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/pipeline/manual-steps.md +0 -0
  115. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/pipeline/running-and-logging.md +0 -0
  116. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/quickstart.md +0 -0
  117. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/references.md +0 -0
  118. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/releases.md +0 -0
  119. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  120. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/conda-envs.md +0 -0
  121. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/existing-project.md +0 -0
  122. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/first-project.md +0 -0
  123. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/github-actions.md +0 -0
  124. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  125. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  126. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  127. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  128. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  129. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  130. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  131. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  132. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  133. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  134. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  135. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  136. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  137. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  138. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  139. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/create-project.png +0 -0
  140. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  141. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/excel-chart.png +0 -0
  142. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/excel-data.png +0 -0
  143. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  144. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/needs-clone.png +0 -0
  145. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/new-stage.png +0 -0
  146. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  147. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  148. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/status-more-rows.png +0 -0
  149. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  150. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/untracked-data.png +0 -0
  151. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/updated-publication.png +0 -0
  152. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  153. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/office/workflow-page.png +0 -0
  154. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/clone.png +0 -0
  155. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/create-project.png +0 -0
  156. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  157. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  158. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  159. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/new-token.png +0 -0
  160. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/reclone.png +0 -0
  161. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  162. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/quick-actions.png +0 -0
  163. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/img/run-proc.png +0 -0
  164. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/index.md +0 -0
  165. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/latex-codespaces.md +0 -0
  166. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/matlab.md +0 -0
  167. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/notebook-pipeline.md +0 -0
  168. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/office.md +0 -0
  169. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/openfoam.md +0 -0
  170. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/tutorials/procedures.md +0 -0
  171. {calkit_python-0.28.4 → calkit_python-0.29.2}/docs/version-control.md +0 -0
  172. {calkit_python-0.28.4 → calkit_python-0.29.2}/mkdocs.yml +0 -0
  173. {calkit_python-0.28.4 → calkit_python-0.29.2}/pyproject.toml +0 -0
  174. {calkit_python-0.28.4 → calkit_python-0.29.2}/scripts/install.ps1 +0 -0
  175. {calkit_python-0.28.4 → calkit_python-0.29.2}/scripts/install.sh +0 -0
  176. {calkit_python-0.28.4 → calkit_python-0.29.2}/test/nb-params.ipynb +0 -0
  177. {calkit_python-0.28.4 → calkit_python-0.29.2}/test/nb-subdir.ipynb +0 -0
  178. {calkit_python-0.28.4 → calkit_python-0.29.2}/test/pipeline.ipynb +0 -0
  179. {calkit_python-0.28.4 → calkit_python-0.29.2}/test/test-log.log +0 -0
  180. {calkit_python-0.28.4 → calkit_python-0.29.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.28.4
3
+ Version: 0.29.2
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.28.4"
1
+ __version__ = "0.29.2"
2
2
 
3
3
  from .core import * # noqa: F403, I001
4
4
  from . import git # noqa: F401
@@ -0,0 +1,240 @@
1
+ """Config CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import glob
6
+ import os
7
+ import subprocess
8
+
9
+ import git
10
+ import typer
11
+ from git.exc import InvalidGitRepositoryError
12
+ from typing_extensions import Annotated
13
+
14
+ import calkit
15
+ from calkit import config
16
+ from calkit.cli.core import raise_error
17
+ from calkit.dvc import configure_remote, get_remotes, set_remote_auth
18
+
19
+ config_app = typer.Typer(no_args_is_help=True)
20
+
21
+
22
+ @config_app.command(name="set")
23
+ def set_config_value(key: str, value: str):
24
+ """Set a value in the config."""
25
+ keys = config.Settings.model_fields.keys()
26
+ if key not in keys:
27
+ raise_error(
28
+ f"Invalid config key: '{key}'; Valid keys are: {list(keys)}"
29
+ )
30
+ try:
31
+ cfg = config.read()
32
+ cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
33
+ except Exception as e:
34
+ raise_error(f"Failed to set {key} in config: {e}")
35
+ cfg.write()
36
+
37
+
38
+ @config_app.command(name="get")
39
+ def get_config_value(key: str) -> None:
40
+ """Get and print a value from the config."""
41
+ cfg = config.read().model_dump()
42
+ if key not in cfg:
43
+ raise_error(
44
+ f"Invalid config key: '{key}'; Valid keys are: {list(cfg.keys())}"
45
+ )
46
+ val = cfg[key]
47
+ if val is not None:
48
+ print(val)
49
+ else:
50
+ print()
51
+
52
+
53
+ @config_app.command(name="unset")
54
+ def unset_config_value(key: str):
55
+ """Unset a value in the config, returning it to default."""
56
+ model_fields = config.Settings.model_fields
57
+ if key not in model_fields:
58
+ raise_error(
59
+ f"Invalid config key: '{key}'; "
60
+ f"Valid keys: {list(model_fields.keys())}"
61
+ )
62
+ try:
63
+ cfg = config.read()
64
+ setattr(cfg, key, model_fields[key].default)
65
+ except Exception as e:
66
+ raise_error(f"Failed to unset {key} in config: {e}")
67
+ cfg.write()
68
+
69
+
70
+ @config_app.command(name="setup-remote", help="Alias for 'remote'.")
71
+ @config_app.command(name="remote")
72
+ def setup_remote(
73
+ no_commit: Annotated[
74
+ bool,
75
+ typer.Option(
76
+ "--no-commit", help="Do not commit changes to DVC config."
77
+ ),
78
+ ] = False,
79
+ ):
80
+ """Setup the Calkit cloud as the default DVC remote and store a token in
81
+ the local config.
82
+ """
83
+ try:
84
+ configure_remote()
85
+ set_remote_auth()
86
+ except subprocess.CalledProcessError:
87
+ if not os.path.isfile(".dvc/config"):
88
+ raise_error(
89
+ "DVC remote config failed; have you run `calkit init`?"
90
+ )
91
+ raise_error(
92
+ "Failed to configure DVC remote; check DVC config for errors"
93
+ )
94
+ except InvalidGitRepositoryError:
95
+ raise_error("Current directory is not a Git repository")
96
+ except (ValueError, RuntimeError) as e:
97
+ raise_error(f"Failed to set up DVC remote: {e}")
98
+ if not no_commit:
99
+ repo = git.Repo()
100
+ repo.git.add(".dvc/config")
101
+ if ".dvc/config" in calkit.git.get_staged_files():
102
+ typer.echo("Committing changes to DVC config")
103
+ repo.git.commit([".dvc/config", "-m", "Set DVC remote"])
104
+
105
+
106
+ @config_app.command(name="setup-remote-auth", help="Alias for 'remote-auth'.")
107
+ @config_app.command(name="remote-auth")
108
+ def setup_remote_auth():
109
+ """Store a Calkit cloud token in the local DVC config for all Calkit
110
+ remotes.
111
+ """
112
+ try:
113
+ remotes = get_remotes()
114
+ except Exception:
115
+ raise_error("Cannot list DVC remotes; check DVC config for errors")
116
+ for name, url in remotes.items():
117
+ if name == "calkit" or name.startswith("calkit:"):
118
+ typer.echo(f"Setting up authentication for DVC remote: {name}")
119
+ set_remote_auth(remote_name=name)
120
+
121
+
122
+ @config_app.command(name="list")
123
+ def list_config_keys():
124
+ """List keys in the config."""
125
+ cfg = config.read()
126
+ for key in cfg.model_dump():
127
+ typer.echo(key)
128
+
129
+
130
+ @config_app.command(name="github-ssh")
131
+ def config_github_ssh():
132
+ """Walk through the process of adding an SSH key to GitHub."""
133
+ typer.echo("Checking if you can already connect to GitHub via SSH")
134
+ # First check if we can already connect to GitHub
135
+ ssh_test_cmd = ["ssh", "-T", "git@github.com"]
136
+ p = subprocess.run(ssh_test_cmd, capture_output=True, text=True)
137
+ if "successfully authenticated" in p.stderr:
138
+ typer.echo("You can already connect to GitHub via SSH")
139
+ go_on = typer.confirm("Do you want to add a new SSH key anyway?")
140
+ if not go_on:
141
+ raise typer.Exit()
142
+ # If we can, ask the user if they still want to add a new key
143
+ # First check if the user has any SSH keys
144
+ ssh_dir = os.path.expanduser("~/.ssh")
145
+ existing_pub_keys = glob.glob(os.path.join(ssh_dir, "*.pub"))
146
+ # If not run ssh-keygen
147
+ if existing_pub_keys:
148
+ # Ask the user if they want to use an existing key or create a new one
149
+ typer.echo("Existing SSH public keys found:")
150
+ for i, key in enumerate(existing_pub_keys):
151
+ typer.echo(f"{i + 1}: {key}")
152
+ use_existing = typer.confirm("Do you want to use one of these keys?")
153
+ if use_existing:
154
+ key_choice = typer.prompt(
155
+ "Enter the number of the key to use", type=int
156
+ )
157
+ if 1 <= key_choice <= len(existing_pub_keys):
158
+ key_path = existing_pub_keys[key_choice - 1][:-4]
159
+ else:
160
+ typer.echo("Invalid choice")
161
+ # Keep asking until they give a valid choice
162
+ while True:
163
+ key_choice = typer.prompt(
164
+ "Enter the number of the key to use", type=int
165
+ )
166
+ if 1 <= key_choice <= len(existing_pub_keys):
167
+ key_path = existing_pub_keys[key_choice - 1][:-4]
168
+ break
169
+ else:
170
+ typer.echo("Invalid choice, please try again.")
171
+ else:
172
+ key_path = typer.prompt(
173
+ "Enter the path to save the new SSH key",
174
+ default=os.path.join(ssh_dir, "id_ed25519"),
175
+ )
176
+ else:
177
+ typer.echo("No existing SSH keys found")
178
+ key_path = typer.prompt(
179
+ "Enter the path to save the new SSH key",
180
+ default=os.path.join(ssh_dir, "id_ed25519"),
181
+ )
182
+ # Get the user's email from their Git config, and ask them if they want to
183
+ # use that or a different one
184
+ try:
185
+ user_git_email = git.Git().config("--get", "user.email").strip()
186
+ except Exception:
187
+ user_git_email = typer.prompt(
188
+ "No email found in Git config; enter email for SSH key"
189
+ )
190
+ git.Git().config("--global", "user.email", user_git_email)
191
+ # Do the same for user name even though we don't need it
192
+ try:
193
+ user_git_name = git.Git().config("--get", "user.name").strip()
194
+ except Exception:
195
+ user_git_name = typer.prompt(
196
+ "No name found in Git config; enter name for SSH key"
197
+ )
198
+ git.Git().config("--global", "user.name", user_git_name)
199
+ keygen_cmd = [
200
+ "ssh-keygen",
201
+ "-t",
202
+ "ed25519",
203
+ "-C",
204
+ user_git_email,
205
+ "-f",
206
+ key_path,
207
+ ]
208
+ subprocess.run(keygen_cmd)
209
+ # Start the SSH agent in the background
210
+ typer.echo("Checking that the SSH agent is running")
211
+ ssh_agent_cmd = subprocess.run(
212
+ ["ssh-agent", "-s"], capture_output=True, text=True
213
+ ).stdout
214
+ p = subprocess.run(ssh_agent_cmd, shell=True)
215
+ if p.returncode != 0:
216
+ raise_error("Failed to start ssh-agent")
217
+ # Add the SSH key to the ssh-agent
218
+ typer.echo(f"Adding SSH key to ssh-agent: {key_path}")
219
+ cmd = ["ssh-add", key_path]
220
+ p = subprocess.run(cmd)
221
+ if p.returncode != 0:
222
+ raise_error("Failed to add SSH key to ssh-agent; please try again")
223
+ # Now add to GitHub
224
+ gh_ssh_url = "https://github.com/settings/ssh/new"
225
+ typer.echo(
226
+ "Add the new SSH key to your GitHub account by visiting:\n"
227
+ f"{gh_ssh_url}"
228
+ )
229
+ with open(key_path + ".pub", "r") as f:
230
+ pub_key = f.read()
231
+ typer.echo(f"Paste this into the public key field:\n\n{pub_key}\n")
232
+ typer.confirm("Press Enter when done", default=True)
233
+ typer.echo("Testing SSH connection to GitHub")
234
+ p = subprocess.run(ssh_test_cmd, capture_output=True, text=True)
235
+ if "successfully authenticated" in p.stderr:
236
+ typer.echo("Successfully connected to GitHub via SSH!")
237
+ else:
238
+ raise_error(
239
+ "Failed to connect to GitHub via SSH; please check your setup"
240
+ )
@@ -981,22 +981,23 @@ def run(
981
981
  except Exception as e:
982
982
  os.environ.pop("CALKIT_PIPELINE_RUNNING", None)
983
983
  raise_error(f"Pipeline compilation failed: {e}")
984
- # Get status of Git repo before running
985
- repo = git.Repo()
986
- git_rev = repo.head.commit.hexsha
987
- try:
988
- git_branch = repo.active_branch.name
989
- except TypeError:
990
- # If no branch is checked out, we are in a detached HEAD state
991
- git_branch = None
992
- git_changed_files_before = calkit.git.get_changed_files(repo=repo)
993
- git_staged_files_before = calkit.git.get_staged_files(repo=repo)
994
- git_untracked_files_before = calkit.git.get_untracked_files(repo=repo)
995
- # Get status of DVC repo before running
996
- dvc_repo = dvc.repo.Repo()
997
- dvc_status_before = dvc_repo.status()
998
- dvc_data_status_before = dvc_repo.data_status()
999
- dvc_data_status_before.pop("git", None) # Remove git status
984
+ if save_log:
985
+ # Get status of Git repo before running
986
+ repo = git.Repo()
987
+ git_rev = repo.head.commit.hexsha
988
+ try:
989
+ git_branch = repo.active_branch.name
990
+ except TypeError:
991
+ # If no branch is checked out, we are in a detached HEAD state
992
+ git_branch = None
993
+ git_changed_files_before = calkit.git.get_changed_files(repo=repo)
994
+ git_staged_files_before = calkit.git.get_staged_files(repo=repo)
995
+ git_untracked_files_before = calkit.git.get_untracked_files(repo=repo)
996
+ # Get status of DVC repo before running
997
+ dvc_repo = dvc.repo.Repo()
998
+ dvc_status_before = dvc_repo.status()
999
+ dvc_data_status_before = dvc_repo.data_status()
1000
+ dvc_data_status_before.pop("git", None) # Remove git status
1000
1001
  if targets is None:
1001
1002
  targets = []
1002
1003
  args = deepcopy(targets)
@@ -1855,3 +1856,51 @@ def run_jupyter(
1855
1856
  """Run a command with the Jupyter CLI."""
1856
1857
  process = subprocess.run([sys.executable, "-m", "jupyter"] + sys.argv[2:])
1857
1858
  sys.exit(process.returncode)
1859
+
1860
+
1861
+ @app.command(name="latexmk")
1862
+ def run_latexmk(
1863
+ tex_file: Annotated[str, typer.Argument(help="The .tex file to compile.")],
1864
+ environment: Annotated[
1865
+ str | None,
1866
+ typer.Option(
1867
+ "--env",
1868
+ "-e",
1869
+ help=("Environment in which to run latexmk, if applicable."),
1870
+ ),
1871
+ ] = None,
1872
+ ):
1873
+ """Compile a LaTeX document with latexmk.
1874
+
1875
+ If a Calkit environment is not specified, latexmk will be run in the
1876
+ system environment if available. If not available, a TeX Live Docker
1877
+ container will be used.
1878
+ """
1879
+ latexmk_cmd = [
1880
+ "latexmk",
1881
+ "-pdf",
1882
+ "-cd",
1883
+ "-silent",
1884
+ "-synctex=1",
1885
+ "-interaction=nonstopmode",
1886
+ tex_file,
1887
+ ]
1888
+ if environment is not None:
1889
+ cmd = ["calkit", "xenv", "--name", environment] + latexmk_cmd
1890
+ elif calkit.check_dep_exists("latexmk"):
1891
+ cmd = latexmk_cmd
1892
+ else:
1893
+ cmd = [
1894
+ "docker",
1895
+ "run",
1896
+ "--rm",
1897
+ "-v",
1898
+ f"{os.getcwd()}:/work",
1899
+ "-w",
1900
+ "/work",
1901
+ "texlive/texlive:latest-full",
1902
+ ] + latexmk_cmd
1903
+ try:
1904
+ subprocess.check_call(cmd)
1905
+ except subprocess.CalledProcessError:
1906
+ raise_error("latexmk failed")
@@ -163,6 +163,17 @@ def execute_notebook(
163
163
  ),
164
164
  ),
165
165
  ] = None,
166
+ language: Annotated[
167
+ str,
168
+ typer.Option(
169
+ "--language",
170
+ "-l",
171
+ help=(
172
+ "Notebook language; if 'matlab', MATLAB kernel must be "
173
+ "available in environment."
174
+ ),
175
+ ),
176
+ ] = "python",
166
177
  verbose: Annotated[
167
178
  bool, typer.Option("--verbose", "-v", help="Print verbose output.")
168
179
  ] = False,
@@ -176,10 +187,24 @@ def execute_notebook(
176
187
 
177
188
  if os.path.isabs(path):
178
189
  raise ValueError("Path must be relative")
190
+ if language.lower() not in ["python", "matlab", "julia"]:
191
+ raise ValueError(
192
+ "Language must be one of 'python', 'matlab', or 'julia'"
193
+ )
179
194
  # First, ensure the specified environment has a kernel we can use
180
- kernel_name = check_env_kernel(
181
- env_name=env_name, no_check=no_check, verbose=verbose
182
- )
195
+ # We need to check the environment type and create the kernel if needed
196
+ if language.lower() == "python":
197
+ kernel_name = check_env_kernel(
198
+ env_name=env_name, no_check=no_check, verbose=verbose
199
+ )
200
+ elif language.lower() == "matlab":
201
+ kernel_name = "jupyter_matlab_kernel"
202
+ else:
203
+ raise_error(f"Language '{language}' not yet supported")
204
+ # We can't handle parameters unless language is Python
205
+ if language.lower() != "python":
206
+ if params or params_json is not None or params_base64 is not None:
207
+ raise_error("Parameters can only be passed to Python notebooks")
183
208
  # Parse parameters
184
209
  if params:
185
210
  try:
@@ -214,14 +239,34 @@ def execute_notebook(
214
239
  typer.echo(f"Using kernel: {kernel_name}")
215
240
  typer.echo(f"Running with cwd: {notebook_dir}")
216
241
  typer.echo(f"Output will be saved to: {fpath_out_exec}")
217
- papermill.execute_notebook(
218
- input_path=path,
219
- output_path=fpath_out_exec,
220
- kernel_name=kernel_name,
221
- log_output=True,
222
- parameters=parsed_params,
223
- cwd=notebook_dir,
224
- )
242
+ # If this is a Python or Julia notebook, we can use Papermill
243
+ # If it's a MATLAB notebook, we need to use the MATLAB kernel inside the
244
+ # specified environment
245
+ if language.lower() in ["python", "julia"]:
246
+ papermill.execute_notebook(
247
+ input_path=path,
248
+ output_path=fpath_out_exec,
249
+ kernel_name=kernel_name,
250
+ log_output=True,
251
+ parameters=parsed_params,
252
+ cwd=notebook_dir,
253
+ )
254
+ elif language.lower() == "matlab":
255
+ # Use nbconvert to execute the notebook with the MATLAB kernel
256
+ cmd = [
257
+ "python",
258
+ "-m",
259
+ "jupyter",
260
+ "nbconvert",
261
+ "--to",
262
+ "notebook",
263
+ "--execute",
264
+ f"--ExecutePreprocessor.kernel_name={kernel_name}",
265
+ "--output",
266
+ fpath_out_exec,
267
+ path,
268
+ ]
269
+ run_in_env(cmd, env_name=env_name, no_check=no_check, verbose=verbose)
225
270
  for to_fmt in to:
226
271
  if to_fmt != "notebook":
227
272
  try:
@@ -250,6 +295,6 @@ def execute_notebook(
250
295
  fname_out,
251
296
  ]
252
297
  typer.echo(f"Exporting {to_fmt}")
253
- run_in_env(
254
- cmd=cmd, env_name=env_name, no_check=True, verbose=verbose
255
- )
298
+ p = subprocess.run(cmd)
299
+ if p.returncode != 0:
300
+ raise_error(f"nbconvert failed for format '{to_fmt}'")
@@ -18,7 +18,7 @@ logger = logging.getLogger(__package__)
18
18
  logger.setLevel(logging.INFO)
19
19
 
20
20
 
21
- def configure_remote(wdir: str = None):
21
+ def configure_remote(wdir: str | None = None):
22
22
  try:
23
23
  project_name = calkit.detect_project_name(wdir=wdir)
24
24
  except ValueError as e:
@@ -70,7 +70,9 @@ def configure_remote(wdir: str = None):
70
70
 
71
71
 
72
72
  def set_remote_auth(
73
- remote_name: str = None, always_auth: bool = False, wdir: str = None
73
+ remote_name: str | None = None,
74
+ always_auth: bool = False,
75
+ wdir: str | None = None,
74
76
  ):
75
77
  """Get a token and set it in the local DVC config so we can interact with
76
78
  the cloud as an HTTP remote.
@@ -85,7 +87,7 @@ def set_remote_auth(
85
87
  )["access_token"]
86
88
  settings.dvc_token = token
87
89
  settings.write()
88
- subprocess.check_call(
90
+ p1 = subprocess.run(
89
91
  [
90
92
  sys.executable,
91
93
  "-m",
@@ -99,7 +101,7 @@ def set_remote_auth(
99
101
  ],
100
102
  cwd=wdir,
101
103
  )
102
- subprocess.check_call(
104
+ p2 = subprocess.run(
103
105
  [
104
106
  sys.executable,
105
107
  "-m",
@@ -113,6 +115,10 @@ def set_remote_auth(
113
115
  ],
114
116
  cwd=wdir,
115
117
  )
118
+ if p1.returncode != 0 or p2.returncode != 0:
119
+ raise RuntimeError(
120
+ f"Failed to set DVC remote authentication for {remote_name}"
121
+ )
116
122
 
117
123
 
118
124
  def add_external_remote(owner_name: str, project_name: str) -> dict:
@@ -159,13 +165,15 @@ def get_remotes(wdir: str | None = None) -> dict[str, str]:
159
165
  """Get a dictionary of DVC remotes, keyed by name, with URL as the
160
166
  value.
161
167
  """
162
- out = (
163
- subprocess.check_output(
164
- [sys.executable, "-m", "dvc", "remote", "list"], cwd=wdir
165
- )
166
- .decode()
167
- .strip()
168
+ p = subprocess.run(
169
+ [sys.executable, "-m", "dvc", "remote", "list"],
170
+ cwd=wdir,
171
+ capture_output=True,
172
+ text=True,
168
173
  )
174
+ if p.returncode != 0:
175
+ raise RuntimeError(f"Error getting DVC remotes: {p.stderr.strip()}")
176
+ out = p.stdout.strip()
169
177
  if not out:
170
178
  return {}
171
179
  resp = {}
@@ -182,12 +190,14 @@ def get_remotes(wdir: str | None = None) -> dict[str, str]:
182
190
  return resp
183
191
 
184
192
 
185
- def list_paths(wdir: str = None, recursive=False) -> list[str]:
193
+ def list_paths(wdir: str | None = None, recursive=False) -> list[str]:
186
194
  """List paths tracked with DVC."""
187
- return [p.get("path") for p in list_files(wdir=wdir, recursive=recursive)]
195
+ return [
196
+ p.get("path", "") for p in list_files(wdir=wdir, recursive=recursive)
197
+ ]
188
198
 
189
199
 
190
- def list_files(wdir: str = None, recursive=True) -> list[dict]:
200
+ def list_files(wdir: str | None = None, recursive=True) -> list[dict]:
191
201
  """Return a list with all files in DVC, including their path and md5
192
202
  checksum.
193
203
  """
@@ -113,6 +113,7 @@ class Stage(BaseModel):
113
113
  "python-script",
114
114
  "latex",
115
115
  "matlab-script",
116
+ "matlab-command",
116
117
  "docker-command",
117
118
  "shell-command",
118
119
  "shell-script",
@@ -164,7 +165,9 @@ class Stage(BaseModel):
164
165
 
165
166
  @property
166
167
  def xenv_cmd(self) -> str:
167
- return f"calkit xenv -n {self.environment} --no-check"
168
+ if self.environment == "_system":
169
+ return ""
170
+ return f"calkit xenv -n {self.environment} --no-check --"
168
171
 
169
172
  def to_dvc(self) -> dict:
170
173
  """Convert to a DVC stage.
@@ -193,7 +196,7 @@ class PythonScriptStage(Stage):
193
196
 
194
197
  @property
195
198
  def dvc_cmd(self) -> str:
196
- cmd = f"{self.xenv_cmd} -- python {self.script_path}"
199
+ cmd = f"{self.xenv_cmd} python {self.script_path}"
197
200
  for arg in self.args:
198
201
  cmd += f" {arg}"
199
202
  return cmd
@@ -212,7 +215,7 @@ class LatexStage(Stage):
212
215
 
213
216
  @property
214
217
  def dvc_cmd(self) -> str:
215
- cmd = f"{self.xenv_cmd} -- latexmk -cd -interaction=nonstopmode"
218
+ cmd = f"{self.xenv_cmd} latexmk -cd -interaction=nonstopmode"
216
219
  if not self.verbose:
217
220
  cmd += " -silent"
218
221
  if self.force:
@@ -247,7 +250,26 @@ class MatlabScriptStage(Stage):
247
250
 
248
251
  @property
249
252
  def dvc_cmd(self) -> str:
250
- return f"{self.xenv_cmd} -- \"run('{self.script_path}');\""
253
+ cmd = self.xenv_cmd
254
+ if self.environment == "_system":
255
+ cmd += "matlab -batch"
256
+ cmd += f" \"run('{self.script_path}');\""
257
+ return cmd
258
+
259
+
260
+ class MatlabCommandStage(Stage):
261
+ kind: Literal["matlab-command"] = "matlab-command"
262
+ command: str
263
+
264
+ @property
265
+ def dvc_cmd(self) -> str:
266
+ # We need to escape quotes in the command
267
+ matlab_cmd = self.command.replace('"', '\\"')
268
+ cmd = self.xenv_cmd
269
+ if self.environment == "_system":
270
+ cmd += "matlab -batch"
271
+ cmd += f' "{matlab_cmd}"'
272
+ return cmd
251
273
 
252
274
 
253
275
  class ShellCommandStage(Stage):
@@ -257,14 +279,13 @@ class ShellCommandStage(Stage):
257
279
 
258
280
  @property
259
281
  def dvc_cmd(self) -> str:
260
- cmd = ""
261
- if self.environment != "_system":
262
- cmd = f"{self.xenv_cmd} -- "
282
+ shell_cmd = self.command.replace('"', '\\"')
283
+ cmd = self.xenv_cmd
263
284
  if self.shell == "zsh":
264
285
  norc_args = "-f"
265
286
  else:
266
287
  norc_args = "--noprofile --norc"
267
- cmd += f'{self.shell} {norc_args} -c "{self.command}"'
288
+ cmd += f' {self.shell} {norc_args} -c "{shell_cmd}"'
268
289
  return cmd
269
290
 
270
291
 
@@ -280,14 +301,12 @@ class ShellScriptStage(Stage):
280
301
 
281
302
  @property
282
303
  def dvc_cmd(self) -> str:
283
- cmd = ""
284
- if self.environment != "_system":
285
- cmd = f"{self.xenv_cmd} -- "
304
+ cmd = self.xenv_cmd
286
305
  if self.shell == "zsh":
287
306
  norc_args = "-f"
288
307
  else:
289
308
  norc_args = "--noprofile --norc"
290
- cmd += f"{self.shell} {norc_args} {self.script_path}"
309
+ cmd += f" {self.shell} {norc_args} {self.script_path}"
291
310
  for arg in self.args:
292
311
  cmd += f" {arg}"
293
312
  return cmd
@@ -327,7 +346,7 @@ class JuliaScriptStage(Stage):
327
346
 
328
347
  @property
329
348
  def dvc_cmd(self) -> str:
330
- cmd = f'{self.xenv_cmd} -- "include(\\"{self.script_path}\\")"'
349
+ cmd = f'{self.xenv_cmd} "include(\\"{self.script_path}\\")"'
331
350
  return cmd
332
351
 
333
352
  @property
@@ -343,7 +362,7 @@ class JuliaCommandStage(Stage):
343
362
  def dvc_cmd(self) -> str:
344
363
  # We need to escape quotes in the command
345
364
  julia_cmd = self.command.replace('"', '\\"')
346
- cmd = f'{self.xenv_cmd} -- "{julia_cmd}"'
365
+ cmd = f'{self.xenv_cmd} "{julia_cmd}"'
347
366
  return cmd
348
367
 
349
368
 
@@ -373,6 +392,7 @@ class JupyterNotebookStage(Stage):
373
392
  executed_ipynb_storage: Literal["git", "dvc"] | None = "dvc"
374
393
  html_storage: Literal["git", "dvc"] | None = "dvc"
375
394
  parameters: dict[str, Any] = {}
395
+ language: Literal["python", "matlab", "julia"] = "python"
376
396
 
377
397
  def update_parameters(self, params: dict) -> None:
378
398
  """If we have any templated parameters, update those, e.g., from
@@ -430,7 +450,10 @@ class JupyterNotebookStage(Stage):
430
450
 
431
451
  @property
432
452
  def dvc_cmd(self) -> str:
433
- cmd = f"calkit nb execute --environment {self.environment} --no-check"
453
+ cmd = (
454
+ f"calkit nb execute --environment {self.environment} "
455
+ f"--no-check --language {self.language}"
456
+ )
434
457
  if self.html_storage:
435
458
  cmd += " --to html"
436
459
  if self.parameters:
@@ -533,6 +556,7 @@ class Pipeline(BaseModel):
533
556
  PythonScriptStage
534
557
  | LatexStage
535
558
  | MatlabScriptStage
559
+ | MatlabCommandStage
536
560
  | ShellCommandStage
537
561
  | ShellScriptStage
538
562
  | DockerCommandStage