calkit-python 0.26.11__tar.gz → 0.27.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 (174) hide show
  1. {calkit_python-0.26.11 → calkit_python-0.27.0}/PKG-INFO +3 -1
  2. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/__init__.py +1 -1
  3. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/check.py +20 -0
  4. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/main.py +31 -3
  5. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/notebooks.py +101 -17
  6. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/core.py +11 -4
  7. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/models/pipeline.py +12 -3
  8. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/notebooks.py +9 -3
  9. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_main.py +1 -0
  10. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/models/test_pipeline.py +15 -0
  11. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_core.py +19 -0
  12. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_pipeline.py +56 -0
  13. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/calkit-yaml.md +1 -1
  14. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/dependencies.md +8 -8
  15. calkit_python-0.27.0/docs/img/jupyterlab-params.png +0 -0
  16. calkit_python-0.27.0/docs/img/vscode-nb-params.png +0 -0
  17. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/notebooks.md +87 -0
  18. {calkit_python-0.26.11 → calkit_python-0.27.0}/pyproject.toml +3 -1
  19. calkit_python-0.27.0/uv.lock +4776 -0
  20. calkit_python-0.26.11/uv.lock +0 -4406
  21. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/FUNDING.yml +0 -0
  22. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/workflows/docs.yml +0 -0
  23. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/workflows/format.yml +0 -0
  24. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/workflows/publish-test.yml +0 -0
  25. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/workflows/publish.yml +0 -0
  26. {calkit_python-0.26.11 → calkit_python-0.27.0}/.github/workflows/test.yml +0 -0
  27. {calkit_python-0.26.11 → calkit_python-0.27.0}/.gitignore +0 -0
  28. {calkit_python-0.26.11 → calkit_python-0.27.0}/.pre-commit-config.yaml +0 -0
  29. {calkit_python-0.26.11 → calkit_python-0.27.0}/.python-version +0 -0
  30. {calkit_python-0.26.11 → calkit_python-0.27.0}/CITATION.cff +0 -0
  31. {calkit_python-0.26.11 → calkit_python-0.27.0}/CONTRIBUTING.md +0 -0
  32. {calkit_python-0.26.11 → calkit_python-0.27.0}/LICENSE +0 -0
  33. {calkit_python-0.26.11 → calkit_python-0.27.0}/Makefile +0 -0
  34. {calkit_python-0.26.11 → calkit_python-0.27.0}/README.md +0 -0
  35. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/__main__.py +0 -0
  36. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/calc.py +0 -0
  37. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/check.py +0 -0
  38. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/__init__.py +0 -0
  39. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/cloud.py +0 -0
  40. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/config.py +0 -0
  41. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/core.py +0 -0
  42. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/describe.py +0 -0
  43. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/import_.py +0 -0
  44. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/list.py +0 -0
  45. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/new.py +0 -0
  46. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/office.py +0 -0
  47. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/overleaf.py +0 -0
  48. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cli/update.py +0 -0
  49. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/cloud.py +0 -0
  50. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/conda.py +0 -0
  51. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/config.py +0 -0
  52. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/datasets.py +0 -0
  53. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/docker.py +0 -0
  54. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/dvc.py +0 -0
  55. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/environments.py +0 -0
  56. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/git.py +0 -0
  57. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/github.py +0 -0
  58. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/gui.py +0 -0
  59. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/jupyter.py +0 -0
  60. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/magics.py +0 -0
  61. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/matlab.py +0 -0
  62. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/models/__init__.py +0 -0
  63. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/models/core.py +0 -0
  64. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/models/io.py +0 -0
  65. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/models/iteration.py +0 -0
  66. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/office.py +0 -0
  67. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/ops.py +0 -0
  68. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/pipeline.py +0 -0
  69. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/releases.py +0 -0
  70. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/server.py +0 -0
  71. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/__init__.py +0 -0
  72. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/core.py +0 -0
  73. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/__init__.py +0 -0
  74. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/article/paper.tex +0 -0
  75. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/core.py +0 -0
  76. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
  77. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
  78. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  79. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/jfm/paper.tex +0 -0
  80. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
  81. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/__init__.py +0 -0
  82. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/__init__.py +0 -0
  83. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_check.py +0 -0
  84. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_config.py +0 -0
  85. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_list.py +0 -0
  86. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_new.py +0 -0
  87. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/cli/test_notebooks.py +0 -0
  88. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/models/__init__.py +0 -0
  89. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_calc.py +0 -0
  90. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_check.py +0 -0
  91. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_conda.py +0 -0
  92. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_dvc.py +0 -0
  93. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_jupyter.py +0 -0
  94. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_magics.py +0 -0
  95. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_notebooks.py +0 -0
  96. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/tests/test_templates.py +0 -0
  97. {calkit_python-0.26.11 → calkit_python-0.27.0}/calkit/zenodo.py +0 -0
  98. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/CNAME +0 -0
  99. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/apps.md +0 -0
  100. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/calculations.md +0 -0
  101. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/cli-reference.md +0 -0
  102. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/cloud-integration.md +0 -0
  103. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/datasets.md +0 -0
  104. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/environments.md +0 -0
  105. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/examples.md +0 -0
  106. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/help.md +0 -0
  107. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/img/c-to-the-k-white.svg +0 -0
  108. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/img/calkit-no-bg.png +0 -0
  109. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/img/connect-zenodo.png +0 -0
  110. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/index.md +0 -0
  111. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/installation.md +0 -0
  112. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/local-server.md +0 -0
  113. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/overleaf.md +0 -0
  114. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/pipeline/index.md +0 -0
  115. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/pipeline/manual-steps.md +0 -0
  116. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/pipeline/running-and-logging.md +0 -0
  117. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/quickstart.md +0 -0
  118. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/references.md +0 -0
  119. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/releases.md +0 -0
  120. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  121. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/conda-envs.md +0 -0
  122. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/existing-project.md +0 -0
  123. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/first-project.md +0 -0
  124. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  125. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  126. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  127. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  128. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  129. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  130. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  131. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  132. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  133. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  134. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  135. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  136. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  137. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  138. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/create-project.png +0 -0
  139. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  140. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/excel-chart.png +0 -0
  141. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/excel-data.png +0 -0
  142. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  143. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/needs-clone.png +0 -0
  144. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/new-stage.png +0 -0
  145. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  146. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  147. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
  148. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  149. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/untracked-data.png +0 -0
  150. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/updated-publication.png +0 -0
  151. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  152. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/office/workflow-page.png +0 -0
  153. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/clone.png +0 -0
  154. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
  155. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  156. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  157. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  158. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
  159. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
  160. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  161. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/img/run-proc.png +0 -0
  162. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/index.md +0 -0
  163. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/latex-codespaces.md +0 -0
  164. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/matlab.md +0 -0
  165. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/notebook-pipeline.md +0 -0
  166. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/office.md +0 -0
  167. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/openfoam.md +0 -0
  168. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/tutorials/procedures.md +0 -0
  169. {calkit_python-0.26.11 → calkit_python-0.27.0}/docs/version-control.md +0 -0
  170. {calkit_python-0.26.11 → calkit_python-0.27.0}/mkdocs.yml +0 -0
  171. {calkit_python-0.26.11 → calkit_python-0.27.0}/scripts/install.ps1 +0 -0
  172. {calkit_python-0.26.11 → calkit_python-0.27.0}/scripts/install.sh +0 -0
  173. {calkit_python-0.26.11 → calkit_python-0.27.0}/test/pipeline.ipynb +0 -0
  174. {calkit_python-0.26.11 → calkit_python-0.27.0}/test/test-log.log +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.26.11
3
+ Version: 0.27.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
@@ -18,8 +18,10 @@ Requires-Dist: docx2pdf
18
18
  Requires-Dist: dvc==3.61.0
19
19
  Requires-Dist: fastapi
20
20
  Requires-Dist: gitpython
21
+ Requires-Dist: jupyterlab>=4.4.5
21
22
  Requires-Dist: keyring
22
23
  Requires-Dist: nbconvert
24
+ Requires-Dist: papermill>=2.6.0
23
25
  Requires-Dist: pillow
24
26
  Requires-Dist: psutil>=7.0.0
25
27
  Requires-Dist: pydantic-settings
@@ -1,4 +1,4 @@
1
- __version__ = "0.26.11"
1
+ __version__ = "0.27.0"
2
2
 
3
3
  from .core import * # noqa: F403, I001
4
4
  from . import git # noqa: F401
@@ -544,6 +544,26 @@ def check_matlab_env(
544
544
  )
545
545
 
546
546
 
547
+ @check_app.command(name="dependencies")
548
+ @check_app.command(name="deps")
549
+ def check_dependencies(
550
+ verbose: Annotated[
551
+ bool, typer.Option("--verbose", "-v", help="Print verbose output")
552
+ ] = False,
553
+ ):
554
+ """Check that a project's system-level dependencies are set up
555
+ correctly.
556
+ """
557
+ typer.echo("Checking project dependencies")
558
+ dotenv.load_dotenv(dotenv_path=".env", verbose=verbose)
559
+ try:
560
+ calkit.check_system_deps()
561
+ except Exception as e:
562
+ raise_error(str(e))
563
+ message = "✅ All set!"
564
+ typer.echo(message.encode("utf-8", errors="replace"))
565
+
566
+
547
567
  @check_app.command(name="env-vars")
548
568
  def check_env_vars(
549
569
  verbose: Annotated[
@@ -380,7 +380,12 @@ def add(
380
380
  else:
381
381
  commit_message = f"Add {paths[0]}"
382
382
  if to is not None:
383
- subprocess.call([to, "add"] + paths)
383
+ if to == "git":
384
+ subprocess.call(["git", "add"] + paths)
385
+ elif to == "dvc":
386
+ subprocess.call([sys.executable, "-m", "dvc", "add"] + paths)
387
+ else:
388
+ raise_error(f"Invalid option for 'to': {to}")
384
389
  else:
385
390
  if "." in paths:
386
391
  paths.remove(".")
@@ -388,8 +393,14 @@ def add(
388
393
  for dvc_uncommitted in dvc_status["uncommitted"].get(
389
394
  "modified", []
390
395
  ):
391
- typer.echo(f"Adding {dvc_uncommitted} to DVC")
392
- dvc_repo.commit(dvc_uncommitted, force=True)
396
+ if os.path.exists(dvc_uncommitted):
397
+ typer.echo(f"Adding {dvc_uncommitted} to DVC")
398
+ dvc_repo.commit(dvc_uncommitted, force=True)
399
+ else:
400
+ warn(
401
+ f"DVC uncommitted '{dvc_uncommitted}' does not exist; "
402
+ "skipping"
403
+ )
393
404
  if not disable_auto_ignore:
394
405
  for untracked_file in untracked_git_files:
395
406
  if (
@@ -1779,3 +1790,20 @@ def call_dvc(
1779
1790
  """
1780
1791
  process = subprocess.run([sys.executable, "-m", "dvc"] + sys.argv[2:])
1781
1792
  sys.exit(process.returncode)
1793
+
1794
+
1795
+ @app.command(
1796
+ name="jupyter",
1797
+ add_help_option=False,
1798
+ context_settings={
1799
+ "ignore_unknown_options": True,
1800
+ "allow_extra_args": True,
1801
+ },
1802
+ )
1803
+ def run_jupyter(
1804
+ ctx: typer.Context,
1805
+ help: Annotated[bool, typer.Option("-h", "--help")] = False,
1806
+ ):
1807
+ """Run a command with the Jupyter CLI."""
1808
+ process = subprocess.run([sys.executable, "-m", "jupyter"] + sys.argv[2:])
1809
+ sys.exit(process.returncode)
@@ -6,7 +6,9 @@ import os
6
6
  import subprocess
7
7
  import sys
8
8
  from pathlib import PurePosixPath
9
+ from typing import Any
9
10
 
11
+ import papermill
10
12
  import typer
11
13
  from typing_extensions import Annotated
12
14
 
@@ -45,6 +47,70 @@ def clean_notebook_outputs(path: str):
45
47
  )
46
48
 
47
49
 
50
+ def _parse_params(params: list[str]) -> dict[str, Any]:
51
+ """Parse parameters from command line arguments."""
52
+ parameters = {}
53
+ for param in params:
54
+ if "=" not in param:
55
+ raise ValueError(f"Parameter must be in key=value format: {param}")
56
+ key, value = param.split("=", 1)
57
+ # Try to convert to appropriate types
58
+ try:
59
+ if "." in value:
60
+ parameters[key] = float(value)
61
+ elif value.isdigit() or (
62
+ value.startswith("-") and value[1:].isdigit()
63
+ ):
64
+ parameters[key] = int(value)
65
+ elif value.lower() in ("true", "false"):
66
+ parameters[key] = value.lower() == "true"
67
+ else:
68
+ parameters[key] = value
69
+ except ValueError:
70
+ parameters[key] = value
71
+ return parameters
72
+
73
+
74
+ @notebooks_app.command("check-env-kernel")
75
+ def check_env_kernel(
76
+ env_name: Annotated[
77
+ str,
78
+ typer.Option(
79
+ "--environment",
80
+ "-e",
81
+ help="Environment name in which to run the notebook.",
82
+ ),
83
+ ],
84
+ no_check: Annotated[
85
+ bool,
86
+ typer.Option(
87
+ "--no-check", help="Do not check environment before executing."
88
+ ),
89
+ ] = False,
90
+ verbose: Annotated[
91
+ bool, typer.Option("--verbose", "-v", help="Print verbose output.")
92
+ ] = False,
93
+ ):
94
+ """Check that an environment has a registered kernel."""
95
+ from calkit.cli.main import run_in_env
96
+
97
+ project_name = calkit.detect_project_name(prepend_owner=False)
98
+ kernel_name = calkit.to_kebab_case(f"{project_name}-{env_name}")
99
+ cmd = [
100
+ "python",
101
+ "-m",
102
+ "ipykernel",
103
+ "install",
104
+ "--user",
105
+ "--name",
106
+ kernel_name,
107
+ "--display-name",
108
+ f"{project_name}: {env_name}",
109
+ ]
110
+ run_in_env(cmd=cmd, env_name=env_name, no_check=no_check, verbose=verbose)
111
+ return kernel_name
112
+
113
+
48
114
  @notebooks_app.command("execute")
49
115
  def execute_notebook(
50
116
  path: str,
@@ -66,6 +132,14 @@ def execute_notebook(
66
132
  "--no-check", help="Do not check environment before executing."
67
133
  ),
68
134
  ] = False,
135
+ params: Annotated[
136
+ list[str],
137
+ typer.Option(
138
+ "--param",
139
+ "-p",
140
+ help="Parameter to pass to the notebook in key=value format.",
141
+ ),
142
+ ] = [],
69
143
  verbose: Annotated[
70
144
  bool, typer.Option("--verbose", "-v", help="Print verbose output.")
71
145
  ] = False,
@@ -79,33 +153,41 @@ def execute_notebook(
79
153
 
80
154
  if os.path.isabs(path):
81
155
  raise ValueError("Path must be relative")
82
- # First, always execute the notebook and save as ipynb
156
+ # First, ensure the specified environment has a kernel we can use
157
+ kernel_name = check_env_kernel(
158
+ env_name=env_name, no_check=no_check, verbose=verbose
159
+ )
160
+ # Parse parameters
161
+ if params:
162
+ try:
163
+ parsed_params = _parse_params(params)
164
+ except ValueError as e:
165
+ raise_error(str(e))
166
+ else:
167
+ parsed_params = {}
168
+ # Next, always execute the notebook and save as ipynb
83
169
  fpath_out_exec = calkit.notebooks.get_executed_notebook_path(
84
- notebook_path=path, to="notebook", as_posix=True
170
+ notebook_path=path,
171
+ to="notebook",
172
+ as_posix=True,
173
+ parameters=parsed_params,
85
174
  )
86
175
  folder = os.path.dirname(fpath_out_exec)
87
176
  os.makedirs(folder, exist_ok=True)
88
- fname = os.path.basename(fpath_out_exec)
89
- fpath_out_exec = PurePosixPath(fpath_out_exec).as_posix()
90
- cmd = [
91
- "jupyter",
92
- "nbconvert",
93
- path,
94
- "--execute",
95
- "--to",
96
- "notebook",
97
- "--output-dir",
98
- PurePosixPath(folder).as_posix(),
99
- "--output",
100
- fname,
101
- ]
102
- run_in_env(cmd=cmd, env_name=env_name, no_check=no_check, verbose=verbose)
177
+ papermill.execute_notebook(
178
+ input_path=path,
179
+ output_path=fpath_out_exec,
180
+ kernel_name=kernel_name,
181
+ log_output=True,
182
+ parameters=parsed_params,
183
+ )
103
184
  for to_fmt in to:
104
185
  if to_fmt != "notebook":
105
186
  try:
106
187
  fpath_out = calkit.notebooks.get_executed_notebook_path(
107
188
  notebook_path=path,
108
189
  to=to_fmt, # type: ignore
190
+ parameters=parsed_params,
109
191
  )
110
192
  except ValueError:
111
193
  raise_error(f"Invalid output format: '{to}'")
@@ -114,6 +196,8 @@ def execute_notebook(
114
196
  fname_out = os.path.basename(fpath_out)
115
197
  # Now convert without executing or checking the environment
116
198
  cmd = [
199
+ sys.executable,
200
+ "-m",
117
201
  "jupyter",
118
202
  "nbconvert",
119
203
  fpath_out_exec,
@@ -318,11 +318,12 @@ def check_system_deps(
318
318
  raise ValueError(f"Malformed dependency: {dep}")
319
319
  if len(keys) == 1:
320
320
  dep_name = keys[0]
321
+ dep_kind = dep[dep_name].get("kind", "app")
321
322
  elif "name" in keys:
322
323
  dep_name = dep["name"]
324
+ dep_kind = dep.get("kind", "app")
323
325
  else:
324
326
  raise ValueError(f"Malformed dependency: {dep}")
325
- dep_kind = dep[dep_name].get("kind", "app")
326
327
  else:
327
328
  dep_name = re.split("[=<>]", dep)[0]
328
329
  dep_kind = "app"
@@ -478,12 +479,16 @@ def get_latest_project_status(wdir: str | None = None) -> ProjectStatus | None:
478
479
  return statuses[-1] # type: ignore
479
480
 
480
481
 
481
- def detect_project_name(wdir: str | None = None) -> str:
482
+ def detect_project_name(
483
+ wdir: str | None = None, prepend_owner: bool = True
484
+ ) -> str:
482
485
  """Detect a Calkit project owner and name."""
483
486
  ck_info = load_calkit_info(wdir=wdir)
484
487
  name = ck_info.get("name")
488
+ if name is not None and not prepend_owner:
489
+ return name
485
490
  owner = ck_info.get("owner")
486
- if name is None or owner is None:
491
+ if name is None and owner is None:
487
492
  try:
488
493
  url = Repo(path=wdir).remote().url
489
494
  except ValueError:
@@ -494,7 +499,9 @@ def detect_project_name(wdir: str | None = None) -> str:
494
499
  name = project_name
495
500
  if owner is None:
496
501
  owner = owner_name
497
- return f"{owner}/{name}"
502
+ if prepend_owner:
503
+ return f"{owner}/{name}"
504
+ return name
498
505
 
499
506
 
500
507
  def get_dep_version(dep_name: str) -> str | None:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import PurePosixPath
6
- from typing import Literal
6
+ from typing import Any, Literal
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict, Discriminator
9
9
  from typing_extensions import Annotated
@@ -273,6 +273,7 @@ class JupyterNotebookStage(Stage):
273
273
  cleaned_ipynb_storage: Literal["git", "dvc"] | None = "git"
274
274
  executed_ipynb_storage: Literal["git", "dvc"] | None = "dvc"
275
275
  html_storage: Literal["git", "dvc"] | None = "dvc"
276
+ parameters: dict[str, Any] = {}
276
277
 
277
278
  @property
278
279
  def cleaned_notebook_path(self) -> str:
@@ -281,13 +282,19 @@ class JupyterNotebookStage(Stage):
281
282
  @property
282
283
  def executed_notebook_path(self) -> str:
283
284
  return get_executed_notebook_path(
284
- self.notebook_path, to="notebook", as_posix=True
285
+ self.notebook_path,
286
+ to="notebook",
287
+ as_posix=True,
288
+ parameters=self.parameters,
285
289
  )
286
290
 
287
291
  @property
288
292
  def html_path(self) -> str:
289
293
  return get_executed_notebook_path(
290
- self.notebook_path, to="html", as_posix=True
294
+ self.notebook_path,
295
+ to="html",
296
+ as_posix=True,
297
+ parameters=self.parameters,
291
298
  )
292
299
 
293
300
  @property
@@ -299,6 +306,8 @@ class JupyterNotebookStage(Stage):
299
306
  cmd = f"calkit nb execute --environment {self.environment} --no-check"
300
307
  if self.html_storage:
301
308
  cmd += " --to html"
309
+ for k, v in self.parameters.items():
310
+ cmd += f" -p {k}={v}"
302
311
  cmd += f' "{self.notebook_path}"'
303
312
  return cmd
304
313
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from pathlib import PurePosixPath
5
- from typing import Literal
5
+ from typing import Any, Literal
6
6
 
7
7
  import git
8
8
 
@@ -11,16 +11,22 @@ from calkit.models.io import InputsFromStageOutputs, PathOutput
11
11
 
12
12
 
13
13
  def get_executed_notebook_path(
14
- notebook_path: str, to: Literal["html", "notebook"], as_posix: bool = True
14
+ notebook_path: str,
15
+ to: Literal["html", "notebook"],
16
+ as_posix: bool = True,
17
+ parameters: dict[str, Any] | None = None,
15
18
  ) -> str:
16
19
  """Return the path of an executed notebook."""
17
20
  nb_dir = os.path.dirname(notebook_path)
18
21
  nb_fname = os.path.basename(notebook_path)
22
+ # If we have any parameters, add these to the notebook name
23
+ if parameters:
24
+ params_txt = "-".join(f"{k}-{v}" for k, v in parameters.items())
25
+ nb_fname = f"{nb_fname.removesuffix('.ipynb')}-{params_txt}.ipynb"
19
26
  if to == "html":
20
27
  fname_out = nb_fname.removesuffix(".ipynb") + ".html"
21
28
  else:
22
29
  fname_out = nb_fname
23
- # Different output types go to different subdirectories
24
30
  subdirs = {"html": "html", "notebook": "executed"}
25
31
  p = os.path.join(".calkit", "notebooks", subdirs[to], nb_dir, fname_out)
26
32
  if as_posix:
@@ -322,6 +322,7 @@ def test_add(tmp_dir):
322
322
  f.write(os.urandom(2_000_000))
323
323
  subprocess.check_call(["calkit", "add", "data2", "-M"])
324
324
  assert repo.head.commit.message.strip() == "Add data2"
325
+ subprocess.check_call(["calkit", "add", "--to", "dvc", "large.bin"])
325
326
 
326
327
 
327
328
  def test_status(tmp_dir):
@@ -87,3 +87,18 @@ def test_jupyternotebookstage():
87
87
  assert s.html_path not in outs
88
88
  assert s.executed_notebook_path in outs
89
89
  assert "html" not in dvc_stage["cmd"]
90
+ # Test with parameters
91
+ s = JupyterNotebookStage(
92
+ environment="main",
93
+ notebook_path="something.ipynb",
94
+ inputs=["file.txt"],
95
+ html_storage=None,
96
+ parameters={"param1": "value1", "param2": "value2"},
97
+ )
98
+ dvc_stage = s.to_dvc()
99
+ outs = dvc_outs_to_str_list(dvc_stage)
100
+ assert s.html_path not in outs
101
+ assert s.executed_notebook_path in outs
102
+ assert "html" not in dvc_stage["cmd"]
103
+ assert " -p param1=value1 " in dvc_stage["cmd"]
104
+ assert " -p param2=value2 " in dvc_stage["cmd"]
@@ -4,6 +4,7 @@ import os
4
4
  import subprocess
5
5
 
6
6
  import git
7
+ import pytest
7
8
 
8
9
  import calkit
9
10
 
@@ -78,3 +79,21 @@ def test_get_env_var_dep_names():
78
79
  "MY_ENV_VAR",
79
80
  "MY_OTHER_ENV_VAR",
80
81
  ]
82
+
83
+
84
+ def test_check_system_deps(tmp_dir):
85
+ ck_info = {
86
+ "dependencies": [
87
+ "uv",
88
+ {"kind": "env-var", "name": "MY_ENV_VAR"},
89
+ {"MY_ENV_VAR2": {"kind": "env-var"}},
90
+ ]
91
+ }
92
+ with open("calkit.yaml", "w") as f:
93
+ calkit.ryaml.dump(ck_info, f)
94
+ subprocess.check_call(
95
+ ["calkit", "check", "dependencies"],
96
+ env=os.environ.copy() | {"MY_ENV_VAR": "5", "MY_ENV_VAR2": "55"},
97
+ )
98
+ with pytest.raises(ValueError):
99
+ calkit.check_system_deps()
@@ -219,6 +219,62 @@ def test_to_dvc_notebook_stage():
219
219
  found_html = True
220
220
  assert out[p]["cache"]
221
221
  assert found_html
222
+ # Test we can use parameters and iterations in notebook stages
223
+ ck_info = {
224
+ "parameters": {
225
+ "param1": [{"range": {"start": 5, "stop": 23, "step": 2}}],
226
+ "param2": ["s", "m", "l"],
227
+ "param3": [
228
+ {"range": {"start": 0.1, "stop": 1.1, "step": 0.11}},
229
+ 55,
230
+ "something",
231
+ ],
232
+ },
233
+ "pipeline": {
234
+ "stages": {
235
+ "notebook-1": {
236
+ "kind": "jupyter-notebook",
237
+ "environment": "something",
238
+ "notebook_path": nb_path,
239
+ "outputs": [
240
+ "figures/fig1.png",
241
+ ],
242
+ "html_storage": "dvc",
243
+ "cleaned_ipynb_storage": "git",
244
+ "executed_ipynb_storage": None,
245
+ "parameters": {
246
+ "param1": "{param1}",
247
+ "param2": "{param2}",
248
+ "param3": "{param3}",
249
+ },
250
+ "iterate_over": [
251
+ {
252
+ "arg_name": "param1",
253
+ "values": [{"parameter": "param1"}],
254
+ },
255
+ {
256
+ "arg_name": "param2",
257
+ "values": [{"parameter": "param2"}],
258
+ },
259
+ {
260
+ "arg_name": "param3",
261
+ "values": [{"parameter": "param3"}],
262
+ },
263
+ ],
264
+ },
265
+ }
266
+ },
267
+ }
268
+ dvc_stages = calkit.pipeline.to_dvc(ck_info=ck_info, write=False)
269
+ print(dvc_stages)
270
+ stage = dvc_stages["notebook-1"]
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"]
275
+ assert stage["matrix"]["param1"][0] == 5.0
276
+ assert "m" in stage["matrix"]["param2"]
277
+ assert "something" in stage["matrix"]["param3"]
222
278
 
223
279
 
224
280
  def test_remove_stage(tmp_dir):
@@ -3,7 +3,7 @@
3
3
  The `calkit.yaml` file serves as a small "database"
4
4
  for the project's important metadata, which includes its:
5
5
 
6
- - Global or system-level dependencies
6
+ - Global or system-level [dependencies](dependencies.md)
7
7
  (applications, libraries, environmental variables)
8
8
  - Questions the project seeks to answer
9
9
  - [Environments](environments.md)
@@ -40,14 +40,14 @@ changing anything.
40
40
  ```yaml
41
41
  dependencies:
42
42
  - docker
43
- - STRAVA_CLIENT_ID:
44
- kind: env-var
45
- notes: >
46
- The STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET environmental
47
- variables can be set in the .env file after creating a Strava
48
- application at https://www.strava.com/settings/api
49
- - STRAVA_CLIENT_SECRET:
50
- kind: env-var
43
+ - name: STRAVA_CLIENT_ID
44
+ kind: env-var
45
+ notes: >
46
+ The STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET environmental
47
+ variables can be set in the .env file after creating a Strava
48
+ application at https://www.strava.com/settings/api
49
+ - name: STRAVA_CLIENT_SECRET
50
+ kind: env-var
51
51
  ```
52
52
 
53
53
  As we can see in the notes for `STRAVA_CLIENT_ID`,
@@ -236,3 +236,90 @@ about that cell's code or environment has changed.
236
236
 
237
237
  For a more in-depth look at using the `%%stage` cell magic,
238
238
  see [this tutorial](tutorials/notebook-pipeline.md).
239
+
240
+ ## Parameterizing notebooks
241
+
242
+ Thanks to [Papermill](https://github.com/nteract/papermill),
243
+ Calkit can run notebooks that have been parameterized,
244
+ and executed versions will be saved with parameters in their names.
245
+ To parameterize a notebook,
246
+ first add a cell with the "parameters" tag and add your parameters there
247
+ as variable declarations, e.g.,
248
+
249
+ ```python
250
+ param1 = 5
251
+ param2 = "something"
252
+ ```
253
+
254
+ In JupyterLab, you can use the property inspector to edit cell tags:
255
+
256
+ ![JupyterLab property inspector](img/jupyterlab-params.png)
257
+
258
+ In VS Code, the cell context menu can be used to mark the cell as parameters:
259
+
260
+ ![VS Code context menu for marking parameters cell](img/vscode-nb-params.png)
261
+
262
+ Then, in the Calkit pipeline, add `parameters` to the notebook stage:
263
+
264
+ ```yaml
265
+ # In calkit.yaml
266
+ environments:
267
+ main:
268
+ kind: uv-venv
269
+ path: requirements.txt
270
+ prefix: .venv
271
+ python: "3.13"
272
+ pipeline:
273
+ stages:
274
+ notebook-56-a:
275
+ kind: jupyter-notebook
276
+ notebook_path: notebook.ipynb
277
+ environment: main
278
+ parameters:
279
+ param1: 56
280
+ param2: a
281
+ notebook-58-b:
282
+ kind: jupyter-notebook
283
+ notebook_path: notebook.ipynb
284
+ environment: main
285
+ parameters:
286
+ param1: 58
287
+ param2: b
288
+ ```
289
+
290
+ ### Iterating over parameterized notebooks
291
+
292
+ The example below shows project-level parameters used to iterate over a
293
+ notebook stage:
294
+
295
+ ```yaml
296
+ parameters:
297
+ param1:
298
+ - range:
299
+ start: 0
300
+ stop: 10
301
+ step: 2
302
+ param2:
303
+ - random-forest
304
+ - lightgbm
305
+ pipeline:
306
+ stages:
307
+ my-notebook-with-params:
308
+ kind: jupyter-notebook
309
+ environment: my-env
310
+ notebook_path: notebook.ipynb
311
+ iterate_over:
312
+ - arg_name: param1
313
+ values:
314
+ - parameter: param1 # Args and params can be named differently
315
+ - arg_name: param2
316
+ values:
317
+ - parameter: param2
318
+ parameters:
319
+ param1: "{param1}" # Notebook params can also be named differently
320
+ param2: "{param2}"
321
+ outputs:
322
+ - results/param1={param1}/param2={param2}/results.csv
323
+ ```
324
+
325
+ For more information, see [pipeline iteration](/pipeline#iteration).
@@ -32,6 +32,8 @@ dependencies = [
32
32
  "uvicorn",
33
33
  "tqdm>=4.67.1",
34
34
  "psutil>=7.0.0",
35
+ "papermill>=2.6.0",
36
+ "jupyterlab>=4.4.5",
35
37
  ]
36
38
  description = "Reproducibility simplified."
37
39
  dynamic = ["version"]
@@ -94,7 +96,7 @@ show_error_codes = true
94
96
  target-version = "py310"
95
97
  line-length = 79
96
98
  fix = true
97
- extend-select = ["I"]
99
+ lint.extend-select = ["I"]
98
100
 
99
101
  [tool.pytest.ini_options]
100
102
  env = ["CALKIT_ENV = test"]