calkit-python 0.27.1__tar.gz → 0.27.3__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 (175) hide show
  1. {calkit_python-0.27.1 → calkit_python-0.27.3}/PKG-INFO +1 -1
  2. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/__init__.py +1 -1
  3. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/main.py +13 -0
  4. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/models/iteration.py +10 -1
  5. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/models/pipeline.py +72 -11
  6. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/pipeline.py +30 -6
  7. calkit_python-0.27.3/calkit/tests/models/test_iteration.py +16 -0
  8. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/models/test_pipeline.py +17 -0
  9. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_pipeline.py +54 -0
  10. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/pipeline/index.md +22 -0
  11. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/FUNDING.yml +0 -0
  12. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/workflows/docs.yml +0 -0
  13. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/workflows/format.yml +0 -0
  14. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/workflows/publish-test.yml +0 -0
  15. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/workflows/publish.yml +0 -0
  16. {calkit_python-0.27.1 → calkit_python-0.27.3}/.github/workflows/test.yml +0 -0
  17. {calkit_python-0.27.1 → calkit_python-0.27.3}/.gitignore +0 -0
  18. {calkit_python-0.27.1 → calkit_python-0.27.3}/.pre-commit-config.yaml +0 -0
  19. {calkit_python-0.27.1 → calkit_python-0.27.3}/.python-version +0 -0
  20. {calkit_python-0.27.1 → calkit_python-0.27.3}/CITATION.cff +0 -0
  21. {calkit_python-0.27.1 → calkit_python-0.27.3}/CONTRIBUTING.md +0 -0
  22. {calkit_python-0.27.1 → calkit_python-0.27.3}/LICENSE +0 -0
  23. {calkit_python-0.27.1 → calkit_python-0.27.3}/Makefile +0 -0
  24. {calkit_python-0.27.1 → calkit_python-0.27.3}/README.md +0 -0
  25. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/__main__.py +0 -0
  26. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/calc.py +0 -0
  27. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/check.py +0 -0
  28. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/__init__.py +0 -0
  29. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/check.py +0 -0
  30. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/cloud.py +0 -0
  31. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/config.py +0 -0
  32. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/core.py +0 -0
  33. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/describe.py +0 -0
  34. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/import_.py +0 -0
  35. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/list.py +0 -0
  36. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/new.py +0 -0
  37. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/notebooks.py +0 -0
  38. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/office.py +0 -0
  39. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/overleaf.py +0 -0
  40. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cli/update.py +0 -0
  41. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/cloud.py +0 -0
  42. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/conda.py +0 -0
  43. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/config.py +0 -0
  44. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/core.py +0 -0
  45. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/datasets.py +0 -0
  46. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/docker.py +0 -0
  47. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/dvc.py +0 -0
  48. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/environments.py +0 -0
  49. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/git.py +0 -0
  50. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/github.py +0 -0
  51. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/gui.py +0 -0
  52. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/jupyter.py +0 -0
  53. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/magics.py +0 -0
  54. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/matlab.py +0 -0
  55. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/models/__init__.py +0 -0
  56. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/models/core.py +0 -0
  57. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/models/io.py +0 -0
  58. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/notebooks.py +0 -0
  59. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/office.py +0 -0
  60. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/ops.py +0 -0
  61. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/releases.py +0 -0
  62. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/server.py +0 -0
  63. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/__init__.py +0 -0
  64. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/core.py +0 -0
  65. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/__init__.py +0 -0
  66. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/article/paper.tex +0 -0
  67. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/core.py +0 -0
  68. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/jfm/jfm.bst +0 -0
  69. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/jfm/jfm.cls +0 -0
  70. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  71. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/jfm/paper.tex +0 -0
  72. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/templates/latex/jfm/upmath.sty +0 -0
  73. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/__init__.py +0 -0
  74. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/__init__.py +0 -0
  75. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_check.py +0 -0
  76. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_config.py +0 -0
  77. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_list.py +0 -0
  78. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_main.py +0 -0
  79. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_new.py +0 -0
  80. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/cli/test_notebooks.py +0 -0
  81. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/models/__init__.py +0 -0
  82. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_calc.py +0 -0
  83. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_check.py +0 -0
  84. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_conda.py +0 -0
  85. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_core.py +0 -0
  86. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_dvc.py +0 -0
  87. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_jupyter.py +0 -0
  88. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_magics.py +0 -0
  89. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_notebooks.py +0 -0
  90. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/tests/test_templates.py +0 -0
  91. {calkit_python-0.27.1 → calkit_python-0.27.3}/calkit/zenodo.py +0 -0
  92. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/CNAME +0 -0
  93. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/apps.md +0 -0
  94. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/calculations.md +0 -0
  95. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/calkit-yaml.md +0 -0
  96. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/cli-reference.md +0 -0
  97. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/cloud-integration.md +0 -0
  98. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/datasets.md +0 -0
  99. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/dependencies.md +0 -0
  100. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/environments.md +0 -0
  101. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/examples.md +0 -0
  102. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/help.md +0 -0
  103. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/img/c-to-the-k-white.svg +0 -0
  104. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/img/calkit-no-bg.png +0 -0
  105. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/img/connect-zenodo.png +0 -0
  106. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/img/jupyterlab-params.png +0 -0
  107. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/img/vscode-nb-params.png +0 -0
  108. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/index.md +0 -0
  109. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/installation.md +0 -0
  110. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/local-server.md +0 -0
  111. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/notebooks.md +0 -0
  112. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/overleaf.md +0 -0
  113. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/pipeline/manual-steps.md +0 -0
  114. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/pipeline/running-and-logging.md +0 -0
  115. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/quickstart.md +0 -0
  116. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/references.md +0 -0
  117. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/releases.md +0 -0
  118. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  119. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/conda-envs.md +0 -0
  120. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/existing-project.md +0 -0
  121. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/first-project.md +0 -0
  122. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  123. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  124. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  125. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  126. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  127. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  128. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  129. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  130. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  131. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  132. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  133. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  134. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  135. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  136. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/create-project.png +0 -0
  137. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  138. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/excel-chart.png +0 -0
  139. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/excel-data.png +0 -0
  140. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  141. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/needs-clone.png +0 -0
  142. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/new-stage.png +0 -0
  143. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  144. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  145. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/status-more-rows.png +0 -0
  146. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  147. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/untracked-data.png +0 -0
  148. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/updated-publication.png +0 -0
  149. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  150. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/office/workflow-page.png +0 -0
  151. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/clone.png +0 -0
  152. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/create-project.png +0 -0
  153. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  154. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  155. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  156. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/new-token.png +0 -0
  157. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/reclone.png +0 -0
  158. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  159. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/img/run-proc.png +0 -0
  160. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/index.md +0 -0
  161. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/latex-codespaces.md +0 -0
  162. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/matlab.md +0 -0
  163. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/notebook-pipeline.md +0 -0
  164. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/office.md +0 -0
  165. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/openfoam.md +0 -0
  166. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/tutorials/procedures.md +0 -0
  167. {calkit_python-0.27.1 → calkit_python-0.27.3}/docs/version-control.md +0 -0
  168. {calkit_python-0.27.1 → calkit_python-0.27.3}/mkdocs.yml +0 -0
  169. {calkit_python-0.27.1 → calkit_python-0.27.3}/pyproject.toml +0 -0
  170. {calkit_python-0.27.1 → calkit_python-0.27.3}/scripts/install.ps1 +0 -0
  171. {calkit_python-0.27.1 → calkit_python-0.27.3}/scripts/install.sh +0 -0
  172. {calkit_python-0.27.1 → calkit_python-0.27.3}/test/nb-subdir.ipynb +0 -0
  173. {calkit_python-0.27.1 → calkit_python-0.27.3}/test/pipeline.ipynb +0 -0
  174. {calkit_python-0.27.1 → calkit_python-0.27.3}/test/test-log.log +0 -0
  175. {calkit_python-0.27.1 → calkit_python-0.27.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.27.1
3
+ Version: 0.27.3
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.27.1"
1
+ __version__ = "0.27.3"
2
2
 
3
3
  from .core import * # noqa: F403, I001
4
4
  from . import git # noqa: F401
@@ -620,9 +620,22 @@ def pull(
620
620
  list[str],
621
621
  typer.Option("--dvc-arg", help="Additional DVC args."),
622
622
  ] = [],
623
+ force: Annotated[
624
+ bool,
625
+ typer.Option(
626
+ "--force",
627
+ "-f",
628
+ help="Force pull, potentially overwriting local changes.",
629
+ ),
630
+ ] = False,
623
631
  ):
624
632
  """Pull with both Git and DVC."""
625
633
  typer.echo("Git pulling")
634
+ if force:
635
+ if "-f" not in git_args and "--force" not in git_args:
636
+ git_args.append("-f")
637
+ if "-f" not in dvc_args and "--force" not in dvc_args:
638
+ dvc_args.append("-f")
626
639
  try:
627
640
  subprocess.check_call(["git", "pull"] + git_args)
628
641
  except subprocess.CalledProcessError:
@@ -16,10 +16,19 @@ class RangeIteration(BaseModel):
16
16
 
17
17
  @property
18
18
  def values(self) -> list[int | float]:
19
+ # Determine precision from inputs
20
+ def get_decimal_places(num):
21
+ return len(str(num).split(".")[-1]) if "." in str(num) else 0
22
+
23
+ max_precision = max(
24
+ get_decimal_places(self.range.start),
25
+ get_decimal_places(self.range.stop),
26
+ get_decimal_places(self.range.step),
27
+ )
19
28
  vals = []
20
29
  current = self.range.start
21
30
  while current < self.range.stop:
22
- vals.append(current)
31
+ vals.append(round(current, max_precision))
23
32
  current += self.range.step
24
33
  return vals
25
34
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import PurePosixPath
6
6
  from typing import Any, Literal
7
7
 
8
- from pydantic import BaseModel, ConfigDict, Discriminator
8
+ from pydantic import BaseModel, ConfigDict, Discriminator, field_validator
9
9
  from typing_extensions import Annotated
10
10
 
11
11
  from calkit.models.io import InputsFromStageOutputs, PathOutput
@@ -21,18 +21,79 @@ from calkit.notebooks import (
21
21
 
22
22
 
23
23
  class StageIteration(BaseModel):
24
- arg_name: str
25
- values: list[int | float | str | RangeIteration | ParameterIteration]
24
+ """A model for the ``iterate_over`` key in a stage definition.
26
25
 
27
- def expand_values(self, params: ParametersType) -> list[int | float | str]:
26
+ If ``arg_name`` is a list, ``values`` also must be a list of lists with
27
+ each sublist the length of ``arg_name``.
28
+ """
29
+
30
+ arg_name: str | list[str]
31
+ values: list[
32
+ int
33
+ | float
34
+ | str
35
+ | RangeIteration
36
+ | ParameterIteration
37
+ | list[int | float | str]
38
+ ]
39
+
40
+ @field_validator("values")
41
+ @classmethod
42
+ def validate_values_structure(cls, v, info):
43
+ """Validate that values are structured correctly based on arg_name."""
44
+ arg_name = info.data.get("arg_name")
45
+ # If arg_name is a list, check that values contains lists of the
46
+ # correct length
47
+ if isinstance(arg_name, list):
48
+ expected_length = len(arg_name)
49
+ for i, value in enumerate(v):
50
+ # TODO: Support RangeIteration and ParameterIteration
51
+ if isinstance(value, (RangeIteration, ParameterIteration)):
52
+ raise ValueError(
53
+ "RangeIteration and ParameterIteration are not "
54
+ "allowed when arg_name is a list"
55
+ )
56
+ # Check if the value is a list and has the correct length
57
+ if not isinstance(value, list):
58
+ raise ValueError(
59
+ f"When arg_name is a list, all values must be lists; "
60
+ f"Value at index {i} is {type(value).__name__}"
61
+ )
62
+ if len(value) != expected_length:
63
+ raise ValueError(
64
+ f"When arg_name has {expected_length} elements, "
65
+ f"each value list must have {expected_length} "
66
+ f"elements; Value at index {i} has {len(value)} "
67
+ "elements"
68
+ )
69
+ return v
70
+
71
+ def expand_values(
72
+ self, params: ParametersType
73
+ ) -> list[int | float | str | dict[str, int | float | str]]:
28
74
  vals = []
29
- for vals_i in self.values:
30
- if isinstance(vals_i, ParameterIteration):
31
- vals += vals_i.values_from_params(params)
32
- elif isinstance(vals_i, RangeIteration):
33
- vals += vals_i.values
34
- else:
35
- vals.append(vals_i)
75
+ if isinstance(self.arg_name, list):
76
+ # Expand into a list of dictionaries, in which case the DVC arg
77
+ # name must be auto-generated
78
+ for vals_list in self.values:
79
+ if not isinstance(vals_list, list):
80
+ raise ValueError(
81
+ "Expected a list for vals_list, got "
82
+ f"{type(vals_list).__name__}"
83
+ )
84
+ v = {}
85
+ for n, name in enumerate(self.arg_name):
86
+ v[name] = vals_list[n]
87
+ vals.append(v)
88
+ else:
89
+ # arg_name is a string
90
+ for vals_i in self.values:
91
+ if isinstance(vals_i, ParameterIteration):
92
+ vals += vals_i.values_from_params(params)
93
+ elif isinstance(vals_i, RangeIteration):
94
+ vals += vals_i.values
95
+ else:
96
+ vals.append(vals_i)
36
97
  return vals
37
98
 
38
99
 
@@ -28,7 +28,23 @@ def _expand_matrix(input_dict: dict[str, list]) -> list[dict]:
28
28
  list_of_dicts = []
29
29
  for combination in combinations:
30
30
  list_of_dicts.append(dict(zip(keys, combination)))
31
- return list_of_dicts
31
+ # After expanding the matrix, flatten any nested dictionaries in the result
32
+ # This handles cases where list-of-lists iterations produce dictionaries as
33
+ # values, by concatenating parent and child keys (e.g., "parent.child")
34
+ # so all permutations are represented as flat dictionaries
35
+ final_list = []
36
+ for item in list_of_dicts:
37
+ keys = list(item.keys())
38
+ vals = list(item.values())
39
+ vd = {}
40
+ for key, val in zip(keys, vals):
41
+ if isinstance(val, dict):
42
+ for k, v in val.items():
43
+ vd[f"{key}.{k}"] = v
44
+ else:
45
+ vd[key] = val
46
+ final_list.append(vd)
47
+ return final_list
32
48
 
33
49
 
34
50
  def to_dvc(
@@ -90,16 +106,24 @@ def to_dvc(
90
106
  # stage
91
107
  if stage.iterate_over is not None:
92
108
  # Process a list of iterations into a DVC matrix stage
109
+ # Initialize a DVC matrix
93
110
  dvc_matrix = {}
111
+ # Initialize a dict for doing string formatting on the DVC stage
94
112
  format_dict = {}
95
- for iteration in stage.iterate_over:
113
+ for n, iteration in enumerate(stage.iterate_over):
96
114
  arg_name = iteration.arg_name
97
- dvc_matrix[arg_name] = iteration.expand_values(
115
+ exp_vals = iteration.expand_values(
98
116
  params=ck_info.get("parameters", {})
99
117
  )
100
- # Now replace arg name in cmd, deps, and outs with
101
- # ${item.{arg_name}}
102
- format_dict[arg_name] = f"${{item.{arg_name}}}"
118
+ if isinstance(arg_name, list):
119
+ dvc_arg_name = f"_arg{n}"
120
+ for arg_name_i in arg_name:
121
+ item_string = f"${{item.{dvc_arg_name}.{arg_name_i}}}"
122
+ format_dict[arg_name_i] = item_string
123
+ else:
124
+ dvc_arg_name = arg_name
125
+ format_dict[arg_name] = f"${{item.{arg_name}}}"
126
+ dvc_matrix[dvc_arg_name] = exp_vals
103
127
  try:
104
128
  cmd = dvc_stage["cmd"]
105
129
  cmd = cmd.format(**format_dict)
@@ -0,0 +1,16 @@
1
+ """Tests for ``calkit.models.iteration``."""
2
+
3
+ from calkit.models.iteration import RangeIteration, RangeIterationParams
4
+
5
+
6
+ def test_rangeiteration():
7
+ r = RangeIteration(range=RangeIterationParams(start=0, stop=10, step=2))
8
+ assert r.values == [0, 2, 4, 6, 8]
9
+ r = RangeIteration(
10
+ range=RangeIterationParams(start=0.05, stop=0.25, step=0.1)
11
+ )
12
+ assert r.values == [0.05, 0.15]
13
+ r = RangeIteration(range=RangeIterationParams(start=0.5, stop=1, step=0.2))
14
+ assert r.values == [0.5, 0.7, 0.9]
15
+ r = RangeIteration(range=RangeIterationParams(start=0, stop=1, step=0.2))
16
+ assert r.values == [0.0, 0.2, 0.4, 0.6, 0.8]
@@ -1,9 +1,13 @@
1
1
  """Tests for ``calkit.models.pipeline``."""
2
2
 
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
3
6
  from calkit.models.pipeline import (
4
7
  JupyterNotebookStage,
5
8
  LatexStage,
6
9
  PythonScriptStage,
10
+ StageIteration,
7
11
  WordToPdfStage,
8
12
  )
9
13
 
@@ -102,3 +106,16 @@ def test_jupyternotebookstage():
102
106
  assert "html" not in dvc_stage["cmd"]
103
107
  assert " -p param1=value1 " in dvc_stage["cmd"]
104
108
  assert " -p param2=value2 " in dvc_stage["cmd"]
109
+
110
+
111
+ def test_stageiteration():
112
+ StageIteration(
113
+ arg_name="param1",
114
+ values=[1, 2, 3],
115
+ )
116
+ with pytest.raises(ValidationError):
117
+ StageIteration(arg_name=["param1", "param2"], values=[1, 2, 3])
118
+ i = StageIteration(arg_name=["param1", "param2"], values=[[1, 2], [3, 4]])
119
+ i.values
120
+ exp_vals = i.expand_values(params={})
121
+ assert exp_vals == [{"param1": 1, "param2": 2}, {"param1": 3, "param2": 4}]
@@ -277,6 +277,60 @@ def test_to_dvc_notebook_stage():
277
277
  assert "something" in stage["matrix"]["param3"]
278
278
 
279
279
 
280
+ def test_to_dvc_list_of_list_iteration():
281
+ # Test that we can define iteration over a list of lists
282
+ ck_info = {
283
+ "pipeline": {
284
+ "stages": {
285
+ "get-data": {
286
+ "kind": "python-script",
287
+ "environment": "something",
288
+ "script_path": "something/my-cool-script.py",
289
+ "args": ["--a1={var1}", "--a2={var2}"],
290
+ "iterate_over": [
291
+ {
292
+ "arg_name": ["var1", "var2"],
293
+ "values": [[1, "a"], [2, "b"], [3, "c"]],
294
+ },
295
+ ],
296
+ "outputs": [
297
+ "out-{var1}-{var2}.out",
298
+ "out2-{var1}-{var2}.out",
299
+ ],
300
+ },
301
+ "process-data": {
302
+ "kind": "python-script",
303
+ "script_path": "something.py",
304
+ "environment": "py",
305
+ "inputs": [
306
+ {"from_stage_outputs": "get-data"},
307
+ "something.else.txt",
308
+ ],
309
+ },
310
+ }
311
+ }
312
+ }
313
+ dvc_stages = calkit.pipeline.to_dvc(ck_info=ck_info, write=False)
314
+ stage = dvc_stages["get-data"]
315
+ assert "--a1=${item._arg0.var1} --a2=${item._arg0.var2}" in stage["cmd"]
316
+ matrix = stage["matrix"]
317
+ assert matrix == {
318
+ "_arg0": [
319
+ {"var1": 1, "var2": "a"},
320
+ {"var1": 2, "var2": "b"},
321
+ {"var1": 3, "var2": "c"},
322
+ ]
323
+ }
324
+ assert stage["outs"] == [
325
+ "out-${item._arg0.var1}-${item._arg0.var2}.out",
326
+ "out2-${item._arg0.var1}-${item._arg0.var2}.out",
327
+ ]
328
+ print(dvc_stages)
329
+ stage2 = dvc_stages["process-data"]
330
+ assert "out-1-a.out" in stage2["deps"]
331
+ assert "out2-2-b.out" in stage2["deps"]
332
+
333
+
280
334
  def test_remove_stage(tmp_dir):
281
335
  subprocess.check_call(["calkit", "init"])
282
336
  # Test that we can remove a stage from the pipeline
@@ -105,6 +105,28 @@ pipeline:
105
105
  - models/{var}.h5
106
106
  ```
107
107
 
108
+ ### Over a table (or list of lists)
109
+
110
+ ```yaml
111
+ pipeline:
112
+ stages:
113
+ my-iter-stage:
114
+ kind: python-script
115
+ script_path: scripts/my-script.py
116
+ args:
117
+ - "--model={var1}"
118
+ - "--n_estimators={var2}"
119
+ iterate_over:
120
+ - arg_name: [var1, var2]
121
+ values:
122
+ - [some-model, 5]
123
+ - [some-other-model, 7]
124
+ inputs:
125
+ - data/raw
126
+ outputs:
127
+ - models/{var1}-{var2}.h5
128
+ ```
129
+
108
130
  ### Over ranges of numbers
109
131
 
110
132
  ```yaml
File without changes
File without changes
File without changes
File without changes