calkit-python 0.28.2__tar.gz → 0.28.4__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 (181) hide show
  1. {calkit_python-0.28.2 → calkit_python-0.28.4}/PKG-INFO +1 -1
  2. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/__init__.py +1 -1
  3. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/new.py +31 -3
  4. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/notebooks.py +39 -0
  5. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/models/iteration.py +25 -1
  6. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/models/pipeline.py +48 -4
  7. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/notebooks.py +21 -1
  8. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/pipeline.py +27 -13
  9. calkit_python-0.28.4/calkit/tests/cli/test_notebooks.py +137 -0
  10. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/models/test_pipeline.py +4 -2
  11. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_pipeline.py +1 -3
  12. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/notebooks.md +1 -1
  13. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/quickstart.md +1 -1
  14. calkit_python-0.28.4/docs/tutorials/github-actions.md +83 -0
  15. calkit_python-0.28.4/docs/tutorials/img/actions-repo-secrets.png +0 -0
  16. calkit_python-0.28.4/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  17. calkit_python-0.28.4/docs/tutorials/img/quick-actions.png +0 -0
  18. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/index.md +1 -0
  19. {calkit_python-0.28.2 → calkit_python-0.28.4}/mkdocs.yml +1 -0
  20. calkit_python-0.28.4/test/nb-params.ipynb +56 -0
  21. calkit_python-0.28.2/calkit/tests/cli/test_notebooks.py +0 -47
  22. calkit_python-0.28.2/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  23. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/FUNDING.yml +0 -0
  24. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/workflows/docs.yml +0 -0
  25. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/workflows/format.yml +0 -0
  26. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/workflows/publish-test.yml +0 -0
  27. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/workflows/publish.yml +0 -0
  28. {calkit_python-0.28.2 → calkit_python-0.28.4}/.github/workflows/test.yml +0 -0
  29. {calkit_python-0.28.2 → calkit_python-0.28.4}/.gitignore +0 -0
  30. {calkit_python-0.28.2 → calkit_python-0.28.4}/.pre-commit-config.yaml +0 -0
  31. {calkit_python-0.28.2 → calkit_python-0.28.4}/.python-version +0 -0
  32. {calkit_python-0.28.2 → calkit_python-0.28.4}/CITATION.cff +0 -0
  33. {calkit_python-0.28.2 → calkit_python-0.28.4}/CONTRIBUTING.md +0 -0
  34. {calkit_python-0.28.2 → calkit_python-0.28.4}/LICENSE +0 -0
  35. {calkit_python-0.28.2 → calkit_python-0.28.4}/Makefile +0 -0
  36. {calkit_python-0.28.2 → calkit_python-0.28.4}/README.md +0 -0
  37. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/__main__.py +0 -0
  38. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/calc.py +0 -0
  39. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/check.py +0 -0
  40. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/__init__.py +0 -0
  41. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/check.py +0 -0
  42. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/cloud.py +0 -0
  43. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/config.py +0 -0
  44. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/core.py +0 -0
  45. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/describe.py +0 -0
  46. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/import_.py +0 -0
  47. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/list.py +0 -0
  48. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/main.py +0 -0
  49. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/office.py +0 -0
  50. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/overleaf.py +0 -0
  51. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cli/update.py +0 -0
  52. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/cloud.py +0 -0
  53. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/conda.py +0 -0
  54. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/config.py +0 -0
  55. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/core.py +0 -0
  56. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/datasets.py +0 -0
  57. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/docker.py +0 -0
  58. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/dvc.py +0 -0
  59. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/environments.py +0 -0
  60. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/git.py +0 -0
  61. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/github.py +0 -0
  62. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/gui.py +0 -0
  63. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/jupyter.py +0 -0
  64. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/magics.py +0 -0
  65. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/matlab.py +0 -0
  66. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/models/__init__.py +0 -0
  67. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/models/core.py +0 -0
  68. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/models/io.py +0 -0
  69. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/office.py +0 -0
  70. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/ops.py +0 -0
  71. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/releases.py +0 -0
  72. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/server.py +0 -0
  73. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/__init__.py +0 -0
  74. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/core.py +0 -0
  75. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/__init__.py +0 -0
  76. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/article/paper.tex +0 -0
  77. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/core.py +0 -0
  78. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/jfm/jfm.bst +0 -0
  79. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/jfm/jfm.cls +0 -0
  80. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  81. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/jfm/paper.tex +0 -0
  82. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/templates/latex/jfm/upmath.sty +0 -0
  83. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/__init__.py +0 -0
  84. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/__init__.py +0 -0
  85. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/test_check.py +0 -0
  86. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/test_config.py +0 -0
  87. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/test_list.py +0 -0
  88. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/test_main.py +0 -0
  89. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/cli/test_new.py +0 -0
  90. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/models/__init__.py +0 -0
  91. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/models/test_iteration.py +0 -0
  92. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_calc.py +0 -0
  93. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_check.py +0 -0
  94. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_conda.py +0 -0
  95. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_core.py +0 -0
  96. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_dvc.py +0 -0
  97. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_jupyter.py +0 -0
  98. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_magics.py +0 -0
  99. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_notebooks.py +0 -0
  100. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/tests/test_templates.py +0 -0
  101. {calkit_python-0.28.2 → calkit_python-0.28.4}/calkit/zenodo.py +0 -0
  102. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/CNAME +0 -0
  103. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/apps.md +0 -0
  104. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/calculations.md +0 -0
  105. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/calkit-yaml.md +0 -0
  106. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/cli-reference.md +0 -0
  107. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/cloud-integration.md +0 -0
  108. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/datasets.md +0 -0
  109. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/dependencies.md +0 -0
  110. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/environments.md +0 -0
  111. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/examples.md +0 -0
  112. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/help.md +0 -0
  113. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/img/c-to-the-k-white.svg +0 -0
  114. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/img/calkit-no-bg.png +0 -0
  115. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/img/connect-zenodo.png +0 -0
  116. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/img/jupyterlab-params.png +0 -0
  117. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/img/vscode-nb-params.png +0 -0
  118. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/index.md +0 -0
  119. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/installation.md +0 -0
  120. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/local-server.md +0 -0
  121. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/overleaf.md +0 -0
  122. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/pipeline/index.md +0 -0
  123. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/pipeline/manual-steps.md +0 -0
  124. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/pipeline/running-and-logging.md +0 -0
  125. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/references.md +0 -0
  126. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/releases.md +0 -0
  127. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  128. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/conda-envs.md +0 -0
  129. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/existing-project.md +0 -0
  130. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/first-project.md +0 -0
  131. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  132. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  133. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  134. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  135. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  136. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  137. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  138. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  139. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  140. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  141. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  142. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  143. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  144. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/create-project.png +0 -0
  145. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  146. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/excel-chart.png +0 -0
  147. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/excel-data.png +0 -0
  148. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  149. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/needs-clone.png +0 -0
  150. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/new-stage.png +0 -0
  151. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  152. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  153. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/status-more-rows.png +0 -0
  154. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  155. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/untracked-data.png +0 -0
  156. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/updated-publication.png +0 -0
  157. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  158. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/office/workflow-page.png +0 -0
  159. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/clone.png +0 -0
  160. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/create-project.png +0 -0
  161. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  162. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  163. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  164. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/new-token.png +0 -0
  165. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/reclone.png +0 -0
  166. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  167. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/img/run-proc.png +0 -0
  168. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/latex-codespaces.md +0 -0
  169. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/matlab.md +0 -0
  170. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/notebook-pipeline.md +0 -0
  171. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/office.md +0 -0
  172. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/openfoam.md +0 -0
  173. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/tutorials/procedures.md +0 -0
  174. {calkit_python-0.28.2 → calkit_python-0.28.4}/docs/version-control.md +0 -0
  175. {calkit_python-0.28.2 → calkit_python-0.28.4}/pyproject.toml +0 -0
  176. {calkit_python-0.28.2 → calkit_python-0.28.4}/scripts/install.ps1 +0 -0
  177. {calkit_python-0.28.2 → calkit_python-0.28.4}/scripts/install.sh +0 -0
  178. {calkit_python-0.28.2 → calkit_python-0.28.4}/test/nb-subdir.ipynb +0 -0
  179. {calkit_python-0.28.2 → calkit_python-0.28.4}/test/pipeline.ipynb +0 -0
  180. {calkit_python-0.28.2 → calkit_python-0.28.4}/test/test-log.log +0 -0
  181. {calkit_python-0.28.2 → calkit_python-0.28.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.28.2
3
+ Version: 0.28.4
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.2"
1
+ __version__ = "0.28.4"
2
2
 
3
3
  from .core import * # noqa: F403, I001
4
4
  from . import git # noqa: F401
@@ -157,10 +157,16 @@ def new_project(
157
157
  repo.git.commit(["-m", "Initialize DVC"])
158
158
  ck_info_fpath = os.path.join(abs_path, "calkit.yaml")
159
159
  if os.path.isfile(ck_info_fpath) and not overwrite:
160
- raise_error(
160
+ ck_info = calkit.load_calkit_info(wdir=abs_path)
161
+ name = ck_info.get("name", name)
162
+ title = ck_info.get("title", title)
163
+ description = ck_info.get("description", description)
164
+ typer.echo(
161
165
  "Destination is already a Calkit project; "
162
- "use --overwrite to continue"
166
+ "will use existing project info where possible"
163
167
  )
168
+ else:
169
+ ck_info = {}
164
170
  if os.path.isdir(abs_path) and os.listdir(abs_path) and repo is None:
165
171
  warn(f"{abs_path} is not empty")
166
172
  if name is None:
@@ -224,7 +230,29 @@ def new_project(
224
230
  )
225
231
  repo.git.add("calkit.yaml")
226
232
  if not no_commit:
227
- repo.git.commit(["-m", "Create calkit.yaml"])
233
+ repo.git.commit(
234
+ ["calkit.yaml", "-m", "Create calkit.yaml"]
235
+ )
236
+ else:
237
+ # Merge with existing project info in calkit.yaml
238
+ typer.echo("Updating existing calkit.yaml file")
239
+ ck_info = (
240
+ dict(
241
+ owner=resp["owner_account_name"],
242
+ name=resp["name"],
243
+ title=resp["title"],
244
+ description=resp["description"],
245
+ git_repo_url=resp["git_repo_url"],
246
+ )
247
+ | ck_info
248
+ )
249
+ with open(calkit_fpath, "w") as f:
250
+ ryaml.dump(ck_info, f)
251
+ repo.git.add("calkit.yaml")
252
+ if not no_commit and repo.git.diff("--staged"):
253
+ repo.git.commit(
254
+ ["calkit.yaml", "-m", "Update calkit.yaml"]
255
+ )
228
256
  try:
229
257
  calkit.dvc.configure_remote(wdir=abs_path)
230
258
  except Exception:
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+ import json
5
7
  import os
6
8
  import subprocess
7
9
  import sys
@@ -140,6 +142,27 @@ def execute_notebook(
140
142
  help="Parameter to pass to the notebook in key=value format.",
141
143
  ),
142
144
  ] = [],
145
+ params_json: Annotated[
146
+ str | None,
147
+ typer.Option(
148
+ "--params-json",
149
+ "-j",
150
+ help=(
151
+ "JSON string to parse as parameters to pass to the notebook."
152
+ ),
153
+ ),
154
+ ] = None,
155
+ params_base64: Annotated[
156
+ str | None,
157
+ typer.Option(
158
+ "--params-base64",
159
+ "-b",
160
+ help=(
161
+ "Base64-encoded JSON string to parse as parameters to pass to "
162
+ "the notebook."
163
+ ),
164
+ ),
165
+ ] = None,
143
166
  verbose: Annotated[
144
167
  bool, typer.Option("--verbose", "-v", help="Print verbose output.")
145
168
  ] = False,
@@ -165,6 +188,17 @@ def execute_notebook(
165
188
  raise_error(str(e))
166
189
  else:
167
190
  parsed_params = {}
191
+ # Parse JSON parameters
192
+ if params_json is not None:
193
+ parsed_params_json = json.loads(params_json)
194
+ parsed_params |= parsed_params_json
195
+ # Parse base64 parameters
196
+ if params_base64 is not None:
197
+ try:
198
+ decoded_json = base64.b64decode(params_base64).decode("utf-8")
199
+ parsed_params |= json.loads(decoded_json)
200
+ except Exception as e:
201
+ raise_error(f"Failed to parse base64 parameters: {e}")
168
202
  # Next, always execute the notebook and save as ipynb
169
203
  fpath_out_exec = calkit.notebooks.get_executed_notebook_path(
170
204
  notebook_path=path,
@@ -175,6 +209,11 @@ def execute_notebook(
175
209
  folder = os.path.dirname(fpath_out_exec)
176
210
  os.makedirs(folder, exist_ok=True)
177
211
  notebook_dir = os.path.dirname(path) or None
212
+ if verbose:
213
+ typer.echo(f"Executing notebook {path} with params: {parsed_params}")
214
+ typer.echo(f"Using kernel: {kernel_name}")
215
+ typer.echo(f"Running with cwd: {notebook_dir}")
216
+ typer.echo(f"Output will be saved to: {fpath_out_exec}")
178
217
  papermill.execute_notebook(
179
218
  input_path=path,
180
219
  output_path=fpath_out_exec,
@@ -60,11 +60,15 @@ ParametersType = dict[
60
60
  int | float | str | list[int | float | str | RangeIteration],
61
61
  ]
62
62
 
63
+ ExpandedParametersType = dict[str, int | float | str | list[int | float | str]]
64
+
63
65
 
64
66
  class ParameterIteration(BaseModel):
65
67
  parameter: str
66
68
 
67
- def values_from_params(self, params: ParametersType) -> list:
69
+ def values_from_params(
70
+ self, params: ParametersType | ExpandedParametersType
71
+ ) -> list:
68
72
  """Convert parameters from calkit.yaml into a list of values."""
69
73
  if self.parameter not in params:
70
74
  raise ValueError(f"'{self.parameter}' not found in parameters")
@@ -79,3 +83,23 @@ class ParameterIteration(BaseModel):
79
83
  else:
80
84
  vals.append(val)
81
85
  return vals
86
+
87
+
88
+ def expand_project_parameters(
89
+ params: ParametersType,
90
+ ) -> ExpandedParametersType:
91
+ """Expand any range iterations in project parameters."""
92
+ expanded = {}
93
+ for key, value in params.items():
94
+ if isinstance(value, list):
95
+ expanded_list = []
96
+ for item in value:
97
+ try:
98
+ range_iter = RangeIteration.model_validate(item)
99
+ expanded_list.extend(range_iter.values)
100
+ except Exception:
101
+ expanded_list.append(item)
102
+ expanded[key] = expanded_list
103
+ else:
104
+ expanded[key] = value
105
+ return expanded
@@ -2,14 +2,23 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+ import json
5
7
  from pathlib import PurePosixPath
6
8
  from typing import Any, Literal
7
9
 
8
- from pydantic import BaseModel, ConfigDict, Discriminator, field_validator
10
+ from pydantic import (
11
+ BaseModel,
12
+ ConfigDict,
13
+ Discriminator,
14
+ ValidationError,
15
+ field_validator,
16
+ )
9
17
  from typing_extensions import Annotated
10
18
 
11
19
  from calkit.models.io import InputsFromStageOutputs, PathOutput
12
20
  from calkit.models.iteration import (
21
+ ExpandedParametersType,
13
22
  ParameterIteration,
14
23
  ParametersType,
15
24
  RangeIteration,
@@ -69,7 +78,7 @@ class StageIteration(BaseModel):
69
78
  return v
70
79
 
71
80
  def expand_values(
72
- self, params: ParametersType
81
+ self, params: ParametersType | ExpandedParametersType
73
82
  ) -> list[int | float | str | dict[str, int | float | str]]:
74
83
  vals = []
75
84
  if isinstance(self.arg_name, list):
@@ -365,6 +374,34 @@ class JupyterNotebookStage(Stage):
365
374
  html_storage: Literal["git", "dvc"] | None = "dvc"
366
375
  parameters: dict[str, Any] = {}
367
376
 
377
+ def update_parameters(self, params: dict) -> None:
378
+ """If we have any templated parameters, update those, e.g., from
379
+ project-level parameters.
380
+
381
+ This needs to happen before writing a DVC stage, so we can properly
382
+ create JSON for the notebook.
383
+ """
384
+ updated_params = {}
385
+ for k, v in self.parameters.items():
386
+ # If we have something like {var_name} in v, replace it with the
387
+ # value from params
388
+ if isinstance(v, str) and v.startswith("{") and v.endswith("}"):
389
+ var_name = v[1:-1]
390
+ if var_name in params:
391
+ updated_params[k] = params[var_name]
392
+ else:
393
+ updated_params[k] = v
394
+ else:
395
+ updated_params[k] = v
396
+ # Try parsing as a RangeIteration and expanding
397
+ try:
398
+ updated_params[k] = RangeIteration.model_validate(
399
+ updated_params[k]
400
+ ).values
401
+ except ValidationError:
402
+ pass
403
+ self.parameters = updated_params
404
+
368
405
  @property
369
406
  def cleaned_notebook_path(self) -> str:
370
407
  return get_cleaned_notebook_path(self.notebook_path, as_posix=True)
@@ -396,8 +433,15 @@ class JupyterNotebookStage(Stage):
396
433
  cmd = f"calkit nb execute --environment {self.environment} --no-check"
397
434
  if self.html_storage:
398
435
  cmd += " --to html"
399
- for k, v in self.parameters.items():
400
- cmd += f" -p {k}={v}"
436
+ if self.parameters:
437
+ # If we have parameters, we need to pass them as JSON, escaping
438
+ # double quotes
439
+ params_json = json.dumps(self.parameters)
440
+ # Now base64 encode
441
+ params_base64 = base64.b64encode(
442
+ params_json.encode("utf-8")
443
+ ).decode("utf-8")
444
+ cmd += f' --params-base64 "{params_base64}"'
401
445
  cmd += f' "{self.notebook_path}"'
402
446
  return cmd
403
447
 
@@ -1,5 +1,7 @@
1
1
  """Functionality for working with notebooks."""
2
2
 
3
+ import hashlib
4
+ import json
3
5
  import os
4
6
  from pathlib import PurePosixPath
5
7
  from typing import Any, Literal
@@ -21,7 +23,25 @@ def get_executed_notebook_path(
21
23
  nb_fname = os.path.basename(notebook_path)
22
24
  # If we have any parameters, add these to the notebook name
23
25
  if parameters:
24
- params_txt = "-".join(f"{k}-{v}" for k, v in parameters.items())
26
+ parameters_cleaned = {}
27
+ for k, v in parameters.items():
28
+ if isinstance(v, list):
29
+ if len(v) == 0:
30
+ v = "empty"
31
+ elif len(v) < 5:
32
+ v = ",".join(map(str, v))
33
+ else:
34
+ v = hashlib.md5(
35
+ json.dumps(v, sort_keys=True).encode()
36
+ ).hexdigest()[:7]
37
+ elif isinstance(v, dict):
38
+ v = hashlib.md5(
39
+ json.dumps(v, sort_keys=True).encode()
40
+ ).hexdigest()[:7]
41
+ parameters_cleaned[k] = v
42
+ params_txt = "-".join(
43
+ f"{k}-{v}" for k, v in parameters_cleaned.items()
44
+ )
25
45
  nb_fname = f"{nb_fname.removesuffix('.ipynb')}-{params_txt}.ipynb"
26
46
  if to == "html":
27
47
  fname_out = nb_fname.removesuffix(".ipynb") + ".html"
@@ -8,6 +8,7 @@ import typer
8
8
 
9
9
  import calkit
10
10
  from calkit.environments import get_env_lock_fpath
11
+ from calkit.models.iteration import expand_project_parameters
11
12
  from calkit.models.pipeline import (
12
13
  InputsFromStageOutputs,
13
14
  PathOutput,
@@ -92,8 +93,13 @@ def to_dvc(
92
93
  )
93
94
  dvc_stages[f"_check-env-{env_name}"] = stage
94
95
  env_lock_fpaths[env_name] = lock_fpath
96
+ project_params = expand_project_parameters(ck_info.get("parameters", {}))
95
97
  # Now convert Calkit stages into DVC stages
96
98
  for stage_name, stage in pipeline.stages.items():
99
+ # If this stage is a Jupyter notebook stage, we need to update its
100
+ # parameters if any reference project-level parameters
101
+ if stage.kind == "jupyter-notebook":
102
+ stage.update_parameters(params=project_params)
97
103
  dvc_stage = stage.to_dvc()
98
104
  # Add environment lock file to deps
99
105
  env_lock_fpath = env_lock_fpaths.get(stage.environment)
@@ -112,9 +118,7 @@ def to_dvc(
112
118
  format_dict = {}
113
119
  for n, iteration in enumerate(stage.iterate_over):
114
120
  arg_name = iteration.arg_name
115
- exp_vals = iteration.expand_values(
116
- params=ck_info.get("parameters", {})
117
- )
121
+ exp_vals = iteration.expand_values(params=project_params)
118
122
  if isinstance(arg_name, list):
119
123
  dvc_arg_name = f"_arg{n}"
120
124
  for arg_name_i in arg_name:
@@ -143,21 +147,31 @@ def to_dvc(
143
147
  except Exception as e:
144
148
  raise ValueError(
145
149
  (
146
- f"Failed to format dep '{dep}': "
150
+ f"Failed to format dep '{dep}' with "
151
+ f"'{format_dict}': "
147
152
  f"{e.__class__.__name__}: {e}"
148
153
  )
149
154
  )
150
155
  for out in dvc_stage.get("outs", []):
151
- if isinstance(out, dict):
152
- formatted_outs.append(
153
- {
154
- str(list(out.keys())[0]).format(
155
- **format_dict
156
- ): dict(list(out.values())[0])
157
- }
156
+ try:
157
+ if isinstance(out, dict):
158
+ formatted_outs.append(
159
+ {
160
+ str(list(out.keys())[0]).format(
161
+ **format_dict
162
+ ): dict(list(out.values())[0])
163
+ }
164
+ )
165
+ else:
166
+ formatted_outs.append(out.format(**format_dict))
167
+ except Exception as e:
168
+ raise ValueError(
169
+ (
170
+ f"Failed to format out '{out}' with "
171
+ f"'{format_dict}': "
172
+ f"{e.__class__.__name__}: {e}"
173
+ )
158
174
  )
159
- else:
160
- formatted_outs.append(out.format(**format_dict))
161
175
  dvc_stage["deps"] = formatted_deps
162
176
  dvc_stage["outs"] = formatted_outs
163
177
  dvc_stage["matrix"] = dvc_matrix
@@ -0,0 +1,137 @@
1
+ """Tests for ``calkit.cli.notebooks``."""
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+
9
+ import calkit
10
+
11
+
12
+ def test_clean_notebook_outputs(tmp_dir):
13
+ # Copy in a test notebook and clean it
14
+ nb_fpath = os.path.join(
15
+ os.path.dirname(__file__), "..", "..", "..", "test", "pipeline.ipynb"
16
+ )
17
+ shutil.copy(nb_fpath, "notebook.ipynb")
18
+ subprocess.check_call(["calkit", "nb", "clean", "notebook.ipynb"])
19
+
20
+
21
+ def test_execute_notebook(tmp_dir):
22
+ subprocess.check_call(
23
+ [
24
+ "calkit",
25
+ "new",
26
+ "project",
27
+ ".",
28
+ "-n",
29
+ "cool-project",
30
+ "--title",
31
+ "Cool project",
32
+ ]
33
+ )
34
+ subprocess.check_call(
35
+ ["calkit", "new", "uv-venv", "-n", "main", "ipykernel"]
36
+ )
37
+ nb_fpath = os.path.join(
38
+ os.path.dirname(__file__), "..", "..", "..", "test", "nb-subdir.ipynb"
39
+ )
40
+ os.makedirs("notebooks/results")
41
+ shutil.copy(nb_fpath, "notebooks/main.ipynb")
42
+ subprocess.check_call(
43
+ ["calkit", "nb", "execute", "notebooks/main.ipynb", "-e", "main"]
44
+ )
45
+ assert os.path.isfile("notebooks/results/something.txt")
46
+ os.makedirs("results")
47
+ shutil.copy("notebooks/main.ipynb", "main.ipynb")
48
+ subprocess.check_call(
49
+ ["calkit", "nb", "execute", "main.ipynb", "-e", "main"]
50
+ )
51
+ assert os.path.isfile("results/something.txt")
52
+ # Test we can execute with parameters
53
+ nb_fpath = os.path.join(
54
+ os.path.dirname(__file__), "..", "..", "..", "test", "nb-params.ipynb"
55
+ )
56
+ shutil.copy(nb_fpath, "nb-params.ipynb")
57
+ params1 = {
58
+ "my_value": 5,
59
+ "my_list": [1, 2, 3],
60
+ }
61
+ params2 = {
62
+ "my_dict": {"something": 55.5, "else": "b"},
63
+ }
64
+ subprocess.check_call(
65
+ [
66
+ "calkit",
67
+ "nb",
68
+ "execute",
69
+ "nb-params.ipynb",
70
+ "-e",
71
+ "main",
72
+ "--params-json",
73
+ json.dumps(params1),
74
+ "--params-base64",
75
+ base64.b64encode(json.dumps(params2).encode("utf-8")).decode(
76
+ "utf-8"
77
+ ),
78
+ "--verbose",
79
+ ]
80
+ )
81
+ params = params1 | params2
82
+ with open("params-out.json") as f:
83
+ params_out = json.load(f)
84
+ assert params_out["my_value"] == params["my_value"]
85
+ assert params_out["my_list"] == params["my_list"]
86
+ assert params_out["my_dict"] == params["my_dict"]
87
+ # Test we can execute in the pipeline with params
88
+ pipeline = {
89
+ "stages": {
90
+ "nb-params": {
91
+ "kind": "jupyter-notebook",
92
+ "notebook_path": "nb-params.ipynb",
93
+ "environment": "main",
94
+ "parameters": params,
95
+ "html_storage": None,
96
+ }
97
+ }
98
+ }
99
+ ck_info = calkit.load_calkit_info()
100
+ ck_info["pipeline"] = pipeline
101
+ with open("calkit.yaml", "w") as f:
102
+ calkit.ryaml.dump(ck_info, f)
103
+ subprocess.check_call(["calkit", "run"])
104
+ with open("params-out.json") as f:
105
+ params_out = json.load(f)
106
+ assert params_out["my_value"] == params["my_value"]
107
+ assert params_out["my_list"] == params["my_list"]
108
+ assert params_out["my_dict"] == params["my_dict"]
109
+ # Test we can patch in a range iteration from project parameters
110
+ project_params = {
111
+ "my_range": {"range": {"start": 1, "stop": 6, "step": 1}},
112
+ "my_project_value": 77,
113
+ }
114
+ pipeline = {
115
+ "stages": {
116
+ "nb-params": {
117
+ "kind": "jupyter-notebook",
118
+ "notebook_path": "nb-params.ipynb",
119
+ "environment": "main",
120
+ "parameters": params
121
+ | {"my_list": "{my_range}", "my_value": "{my_project_value}"},
122
+ "html_storage": None,
123
+ }
124
+ }
125
+ }
126
+ ck_info = calkit.load_calkit_info()
127
+ ck_info["parameters"] = project_params
128
+ ck_info["pipeline"] = pipeline
129
+ with open("calkit.yaml", "w") as f:
130
+ calkit.ryaml.dump(ck_info, f)
131
+ os.remove("params-out.json")
132
+ subprocess.check_call(["calkit", "run"])
133
+ with open("params-out.json") as f:
134
+ params_out = json.load(f)
135
+ assert params_out["my_value"] == project_params["my_project_value"]
136
+ assert params_out["my_list"] == list(range(1, 6))
137
+ assert params_out["my_dict"] == params["my_dict"]
@@ -105,8 +105,10 @@ def test_jupyternotebookstage():
105
105
  assert s.html_path not in outs
106
106
  assert s.executed_notebook_path in outs
107
107
  assert "html" not in dvc_stage["cmd"]
108
- assert " -p param1=value1 " in dvc_stage["cmd"]
109
- assert " -p param2=value2 " in dvc_stage["cmd"]
108
+ assert (
109
+ " --params-base64 "
110
+ '"eyJwYXJhbTEiOiAidmFsdWUxIiwgInBhcmFtMiI6ICJ2YWx1ZTIifQ==" '
111
+ ) in dvc_stage["cmd"]
110
112
 
111
113
 
112
114
  def test_stageiteration():
@@ -269,9 +269,7 @@ def test_to_dvc_notebook_stage():
269
269
  print(dvc_stages)
270
270
  stage = dvc_stages["notebook-1"]
271
271
  assert stage["cmd"].startswith("calkit nb execute")
272
- assert " -p param1=${item.param1} " in stage["cmd"]
273
- assert " -p param2=${item.param2} " in stage["cmd"]
274
- assert " -p param3=${item.param3} " in stage["cmd"]
272
+ assert "--params-base64" in stage["cmd"]
275
273
  assert stage["matrix"]["param1"][0] == 5.0
276
274
  assert "m" in stage["matrix"]["param2"]
277
275
  assert "something" in stage["matrix"]["param3"]
@@ -322,4 +322,4 @@ pipeline:
322
322
  - results/param1={param1}/param2={param2}/results.csv
323
323
  ```
324
324
 
325
- For more information, see [pipeline iteration](/pipeline#iteration).
325
+ For more information, see [pipeline iteration](pipeline/index.md#iteration).
@@ -25,7 +25,7 @@ If you're using Conda for environment management,
25
25
  e.g., with an `environment.yml` file,
26
26
  you can use the `calkit new conda-env` command.
27
27
 
28
- Next, we can start building our [pipeline](pipeline.md).
28
+ Next, we can start building our [pipeline](pipeline/index.md).
29
29
  Let's say we have a Jupyter notebook called `collect-data.ipynb`
30
30
  that produces raw data at `data/raw.h5`.
31
31
  We can add a pipeline stage to run this notebook in the `main` environment
@@ -0,0 +1,83 @@
1
+ # Running Calkit in GitHub Actions
2
+
3
+ A project can be set up to automatically run the pipeline every time a
4
+ change is pushed, either to the main branch or on a pull request,
5
+ using GitHub Actions.
6
+ The latter allows for inspection of outputs before merging into main.
7
+
8
+ To get started, generate a DVC token and set it in your GitHub Actions
9
+ secrets as `CALKIT_DVC_TOKEN`,
10
+ either at your account or project level.
11
+ On calkit.io, there are shortcuts on the project page for managing both
12
+ Calkit tokens and GitHub Actions secrets:
13
+
14
+ ![Project quick actions links](img/quick-actions.png)
15
+
16
+ ![DVC token](img/latex-codespaces/new-token.png)
17
+
18
+ ![Actions repo secrets](img/actions-repo-secrets.png)
19
+
20
+ Next, add a workflow to the project in the `.github/workflows` folder.
21
+ For example, we can put the content below into `.github/workflows/run.yml`:
22
+
23
+ ```yaml
24
+ name: Run pipeline
25
+
26
+ on:
27
+ push:
28
+ branches:
29
+ - main
30
+ pull_request:
31
+
32
+ permissions:
33
+ contents: write
34
+
35
+ # Make sure we only ever run one per branch so we don't have issues pushing
36
+ # after running the pipeline
37
+ concurrency:
38
+ group: calkit-run-${{ github.ref }}
39
+ cancel-in-progress: false
40
+
41
+ jobs:
42
+ main:
43
+ name: Run
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ with:
48
+ # For PRs, checkout the head ref to avoid detached HEAD
49
+ ref: ${{ github.head_ref || github.ref_name }}
50
+ token: ${{ secrets.GITHUB_TOKEN }}
51
+ - name: Configure Git credentials
52
+ run: |
53
+ git config user.name github-actions[bot]
54
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
55
+ - name: Setup uv
56
+ uses: astral-sh/setup-uv@v5
57
+ - name: Install Calkit
58
+ run: uv tool install calkit-python
59
+ - name: Run Calkit
60
+ uses: calkit/run-action@v1
61
+ with:
62
+ dvc_token: ${{ secrets.CALKIT_DVC_TOKEN }}
63
+ ```
64
+
65
+ This particular example installs Calkit with uv,
66
+ meaning `uv` and `uv-venv` environment types will work without any additional
67
+ configuration.
68
+ Docker is also installed by default on the `ubuntu-latest` machine.
69
+ If other environment types are used in the project,
70
+ setup steps may be necessary for those, e.g.,
71
+ [`setup-miniconda`](https://github.com/marketplace/actions/setup-miniconda)
72
+ or [`install-juliaup`](https://github.com/marketplace/actions/install-juliaup).
73
+
74
+ By default, the Calkit GitHub Action will run the pipeline and save results.
75
+ This is why the workflow needs write permissions and configures Git
76
+ credentials to act as the GitHub Actions bot.
77
+ The workflow also limits concurrency so multiple jobs don't attempt to push
78
+ to the same branch at the same time.
79
+
80
+ It's possible to configure the action to not save results, e.g., if you
81
+ just want to check that the pipeline can run without errors.
82
+ See the [documentation](https://github.com/marketplace/actions/run-calkit)
83
+ for all available options.
@@ -10,3 +10,4 @@
10
10
  - [Adding a new LaTeX-based publication with its own Docker build environment](adding-latex-pub-docker.md)
11
11
  - [A reproducible workflow using Microsoft Office (Word and Excel)](office.md)
12
12
  - [Using Calkit with MATLAB](matlab.md)
13
+ - [Automation with GitHub Actions](github-actions.md)
@@ -67,6 +67,7 @@ nav:
67
67
  - tutorials/conda-envs.md
68
68
  - tutorials/office.md
69
69
  - tutorials/matlab.md
70
+ - tutorials/github-actions.md
70
71
  - CLI reference: cli-reference.md
71
72
  - Help and support: help.md
72
73
  markdown_extensions: