calkit-python 0.41.7__tar.gz → 0.41.9__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 (338) hide show
  1. {calkit_python-0.41.7 → calkit_python-0.41.9}/.gitignore +1 -0
  2. {calkit_python-0.41.7 → calkit_python-0.41.9}/PKG-INFO +1 -1
  3. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/main/core.py +10 -1
  4. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/new.py +80 -55
  5. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/core.py +8 -0
  6. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/invenio.py +20 -0
  7. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/pipeline.py +113 -2
  8. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/releases.py +103 -0
  9. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/main/test_subprojects.py +53 -0
  10. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_new.py +331 -0
  11. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_core.py +19 -0
  12. calkit_python-0.41.9/calkit/tests/test_invenio.py +35 -0
  13. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_pipeline.py +111 -0
  14. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_releases.py +85 -0
  15. calkit_python-0.41.7/calkit/tests/test_invenio.py +0 -15
  16. {calkit_python-0.41.7 → calkit_python-0.41.9}/.claude-plugin/marketplace.json +0 -0
  17. {calkit_python-0.41.7 → calkit_python-0.41.9}/.gitattributes +0 -0
  18. {calkit_python-0.41.7 → calkit_python-0.41.9}/.pre-commit-config.yaml +0 -0
  19. {calkit_python-0.41.7 → calkit_python-0.41.9}/.prettierignore +0 -0
  20. {calkit_python-0.41.7 → calkit_python-0.41.9}/.python-version +0 -0
  21. {calkit_python-0.41.7 → calkit_python-0.41.9}/.vscode/launch.json +0 -0
  22. {calkit_python-0.41.7 → calkit_python-0.41.9}/.vscode/tasks.json +0 -0
  23. {calkit_python-0.41.7 → calkit_python-0.41.9}/.yarnrc.yml +0 -0
  24. {calkit_python-0.41.7 → calkit_python-0.41.9}/AGENTS.md +0 -0
  25. {calkit_python-0.41.7 → calkit_python-0.41.9}/CITATION.cff +0 -0
  26. {calkit_python-0.41.7 → calkit_python-0.41.9}/CODE_OF_CONDUCT.md +0 -0
  27. {calkit_python-0.41.7 → calkit_python-0.41.9}/CONTRIBUTING.md +0 -0
  28. {calkit_python-0.41.7 → calkit_python-0.41.9}/LICENSE +0 -0
  29. {calkit_python-0.41.7 → calkit_python-0.41.9}/Makefile +0 -0
  30. {calkit_python-0.41.7 → calkit_python-0.41.9}/README.md +0 -0
  31. {calkit_python-0.41.7 → calkit_python-0.41.9}/agent-plugin/.claude-plugin/plugin.json +0 -0
  32. {calkit_python-0.41.7 → calkit_python-0.41.9}/agent-plugin/skills/add-pipeline-stage/SKILL.md +0 -0
  33. {calkit_python-0.41.7 → calkit_python-0.41.9}/agent-plugin/skills/conventions/SKILL.md +0 -0
  34. {calkit_python-0.41.7 → calkit_python-0.41.9}/agent-plugin/skills/create-pipeline/SKILL.md +0 -0
  35. {calkit_python-0.41.7 → calkit_python-0.41.9}/babel.config.js +0 -0
  36. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/__init__.py +0 -0
  37. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/__main__.py +0 -0
  38. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/calc.py +0 -0
  39. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/check.py +0 -0
  40. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/__init__.py +0 -0
  41. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/check.py +0 -0
  42. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/cloud.py +0 -0
  43. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/config.py +0 -0
  44. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/core.py +0 -0
  45. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/describe.py +0 -0
  46. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/dev.py +0 -0
  47. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/import_.py +0 -0
  48. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/latex.py +0 -0
  49. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/list.py +0 -0
  50. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/main/__init__.py +0 -0
  51. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/main/xr.py +0 -0
  52. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/notebooks.py +0 -0
  53. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/office.py +0 -0
  54. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/overleaf.py +0 -0
  55. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/scheduler.py +0 -0
  56. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cli/update.py +0 -0
  57. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/cloud.py +0 -0
  58. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/conda.py +0 -0
  59. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/config.py +0 -0
  60. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/datasets.py +0 -0
  61. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/dependencies.py +0 -0
  62. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/detect.py +0 -0
  63. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/docker.py +0 -0
  64. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/dvc/__init__.py +0 -0
  65. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/dvc/core.py +0 -0
  66. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/dvc/zip.py +0 -0
  67. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/environments.py +0 -0
  68. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/fs.py +0 -0
  69. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/git.py +0 -0
  70. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/github.py +0 -0
  71. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/gui.py +0 -0
  72. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/install.py +0 -0
  73. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/julia.py +0 -0
  74. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/jupyter.py +0 -0
  75. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/jupyterlab/__init__.py +0 -0
  76. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/jupyterlab/routes.py +0 -0
  77. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/package.json +0 -0
  78. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  79. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  80. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  81. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  82. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  83. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  84. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  85. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  86. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  87. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  88. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/style.js +0 -0
  89. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/labextension/static/third-party-licenses.json +0 -0
  90. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/licenses.py +0 -0
  91. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/magics.py +0 -0
  92. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/matlab.py +0 -0
  93. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/models/__init__.py +0 -0
  94. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/models/core.py +0 -0
  95. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/models/io.py +0 -0
  96. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/models/iteration.py +0 -0
  97. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/models/pipeline.py +0 -0
  98. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/notebooks.py +0 -0
  99. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/office.py +0 -0
  100. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/ops.py +0 -0
  101. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/overleaf.py +0 -0
  102. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/server.py +0 -0
  103. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/__init__.py +0 -0
  104. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/core.py +0 -0
  105. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/__init__.py +0 -0
  106. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/article/paper.tex +0 -0
  107. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/core.py +0 -0
  108. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/jfm/jfm.bst +0 -0
  109. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/jfm/jfm.cls +0 -0
  110. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  111. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/jfm/paper.tex +0 -0
  112. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/templates/latex/jfm/upmath.sty +0 -0
  113. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/__init__.py +0 -0
  114. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/__init__.py +0 -0
  115. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/main/__init__.py +0 -0
  116. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/main/test_core.py +0 -0
  117. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/main/test_xr.py +0 -0
  118. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_check.py +0 -0
  119. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_cloud.py +0 -0
  120. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_config.py +0 -0
  121. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_import.py +0 -0
  122. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_latex.py +0 -0
  123. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_list.py +0 -0
  124. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_notebooks.py +0 -0
  125. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_overleaf.py +0 -0
  126. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_scheduler.py +0 -0
  127. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/cli/test_update.py +0 -0
  128. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/dvc/__init__.py +0 -0
  129. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/dvc/test_core.py +0 -0
  130. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/dvc/test_zip.py +0 -0
  131. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/jupyterlab/__init__.py +0 -0
  132. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/jupyterlab/test_routes.py +0 -0
  133. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/models/__init__.py +0 -0
  134. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/models/test_iteration.py +0 -0
  135. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/models/test_pipeline.py +0 -0
  136. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_calc.py +0 -0
  137. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_check.py +0 -0
  138. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_cloud.py +0 -0
  139. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_conda.py +0 -0
  140. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_dependencies.py +0 -0
  141. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_detect.py +0 -0
  142. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_docker.py +0 -0
  143. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_environments.py +0 -0
  144. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_fs.py +0 -0
  145. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_git.py +0 -0
  146. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_install.py +0 -0
  147. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_julia.py +0 -0
  148. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_jupyter.py +0 -0
  149. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_licenses.py +0 -0
  150. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_magics.py +0 -0
  151. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_matlab.py +0 -0
  152. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_notebooks.py +0 -0
  153. {calkit_python-0.41.7 → calkit_python-0.41.9}/calkit/tests/test_templates.py +0 -0
  154. {calkit_python-0.41.7 → calkit_python-0.41.9}/conftest.py +0 -0
  155. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/CNAME +0 -0
  156. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/ai-tools.md +0 -0
  157. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/apps.md +0 -0
  158. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/calculations.md +0 -0
  159. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/calkit-yaml.md +0 -0
  160. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/cli-reference.md +0 -0
  161. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/cloud-integration.md +0 -0
  162. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/datasets.md +0 -0
  163. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/dependencies.md +0 -0
  164. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/environments.md +0 -0
  165. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/examples.md +0 -0
  166. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/governance.md +0 -0
  167. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/help.md +0 -0
  168. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/hpc.md +0 -0
  169. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/c-to-the-k-white.svg +0 -0
  170. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/calkit-fragmentation-compendium.png +0 -0
  171. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/calkit-no-bg.png +0 -0
  172. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/connect-zenodo.png +0 -0
  173. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab/all-green.png +0 -0
  174. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  175. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab/new-env.png +0 -0
  176. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab/new-notebook.png +0 -0
  177. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  178. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/jupyterlab-params.png +0 -0
  179. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/plos-osi-code-2024-03.png +0 -0
  180. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/img/vscode-nb-params.png +0 -0
  181. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/index.md +0 -0
  182. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/installation.md +0 -0
  183. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/jupyterlab.md +0 -0
  184. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/local-server.md +0 -0
  185. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/notebooks.md +0 -0
  186. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/overleaf.md +0 -0
  187. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/pipeline/index.md +0 -0
  188. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/pipeline/manual-steps.md +0 -0
  189. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/pipeline/running-and-logging.md +0 -0
  190. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/quickstart.md +0 -0
  191. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/references.md +0 -0
  192. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/releases.md +0 -0
  193. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/reproducibility.md +0 -0
  194. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/subprojects.md +0 -0
  195. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  196. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/conda-envs.md +0 -0
  197. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/existing-project.md +0 -0
  198. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/first-project.md +0 -0
  199. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/github-actions.md +0 -0
  200. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  201. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  202. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  203. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  204. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  205. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  206. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  207. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  208. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  209. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  210. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  211. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  212. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  213. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  214. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  215. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/create-project.png +0 -0
  216. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  217. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/excel-chart.png +0 -0
  218. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/excel-data.png +0 -0
  219. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  220. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/needs-clone.png +0 -0
  221. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/new-stage.png +0 -0
  222. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  223. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  224. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/status-more-rows.png +0 -0
  225. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  226. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/untracked-data.png +0 -0
  227. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/updated-publication.png +0 -0
  228. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  229. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/office/workflow-page.png +0 -0
  230. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/clone.png +0 -0
  231. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/create-project.png +0 -0
  232. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  233. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  234. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  235. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/new-token.png +0 -0
  236. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/reclone.png +0 -0
  237. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  238. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/quick-actions.png +0 -0
  239. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/run-proc.png +0 -0
  240. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
  241. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
  242. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
  243. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
  244. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
  245. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
  246. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
  247. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/index.md +0 -0
  248. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/jupyterlab.md +0 -0
  249. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/latex-codespaces.md +0 -0
  250. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/matlab.md +0 -0
  251. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/notebook-pipeline.md +0 -0
  252. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/office.md +0 -0
  253. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/openfoam.md +0 -0
  254. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/procedures.md +0 -0
  255. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/tutorials/vscode-slurm-notebook.md +0 -0
  256. {calkit_python-0.41.7 → calkit_python-0.41.9}/docs/version-control.md +0 -0
  257. {calkit_python-0.41.7 → calkit_python-0.41.9}/flake.lock +0 -0
  258. {calkit_python-0.41.7 → calkit_python-0.41.9}/flake.nix +0 -0
  259. {calkit_python-0.41.7 → calkit_python-0.41.9}/install.json +0 -0
  260. {calkit_python-0.41.7 → calkit_python-0.41.9}/jest.config.js +0 -0
  261. {calkit_python-0.41.7 → calkit_python-0.41.9}/jupyter-config/server-config/calkit.json +0 -0
  262. {calkit_python-0.41.7 → calkit_python-0.41.9}/mkdocs.yml +0 -0
  263. {calkit_python-0.41.7 → calkit_python-0.41.9}/package.json +0 -0
  264. {calkit_python-0.41.7 → calkit_python-0.41.9}/pyproject.toml +0 -0
  265. {calkit_python-0.41.7 → calkit_python-0.41.9}/schema/plugin.json +0 -0
  266. {calkit_python-0.41.7 → calkit_python-0.41.9}/scripts/generate-docs-references.py +0 -0
  267. {calkit_python-0.41.7 → calkit_python-0.41.9}/scripts/install.ps1 +0 -0
  268. {calkit_python-0.41.7 → calkit_python-0.41.9}/scripts/install.sh +0 -0
  269. {calkit_python-0.41.7 → calkit_python-0.41.9}/scripts/make-calk9.sh +0 -0
  270. {calkit_python-0.41.7 → calkit_python-0.41.9}/scripts/sync-docs.py +0 -0
  271. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/__tests__/useQueries.spec.ts +0 -0
  272. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/calkit-config.ts +0 -0
  273. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/cell-output-marker.ts +0 -0
  274. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/commit-dialog.tsx +0 -0
  275. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/environment-editor.tsx +0 -0
  276. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/notebook-registration.tsx +0 -0
  277. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/notebook-toolbar.tsx +0 -0
  278. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/pipeline-status-bar.tsx +0 -0
  279. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/project-info-editor.tsx +0 -0
  280. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/sidebar-settings.tsx +0 -0
  281. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/sidebar.tsx +0 -0
  282. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/components/stage-editor.tsx +0 -0
  283. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/feature-flags.ts +0 -0
  284. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/file-browser-menu.ts +0 -0
  285. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/hooks/__tests__/useQueries.test.tsx +0 -0
  286. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/hooks/useQueries.ts +0 -0
  287. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/icons.ts +0 -0
  288. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/index.ts +0 -0
  289. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/io-tracker.ts +0 -0
  290. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/pipeline-state.ts +0 -0
  291. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/queryClient.ts +0 -0
  292. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/request.ts +0 -0
  293. {calkit_python-0.41.7 → calkit_python-0.41.9}/src/shims-mainmenu.d.ts +0 -0
  294. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/base.css +0 -0
  295. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/cell-output-marker.css +0 -0
  296. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/environment-editor.css +0 -0
  297. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/environment-selector.css +0 -0
  298. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/img/calkit-no-bg.png +0 -0
  299. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/index.css +0 -0
  300. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/index.js +0 -0
  301. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/notebook-toolbar.css +0 -0
  302. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/pipeline-status-bar.css +0 -0
  303. {calkit_python-0.41.7 → calkit_python-0.41.9}/style/sidebar.css +0 -0
  304. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  305. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/nb-julia.ipynb +0 -0
  306. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/nb-params.ipynb +0 -0
  307. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/nb-subdir.ipynb +0 -0
  308. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/pipeline.ipynb +0 -0
  309. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/script.py +0 -0
  310. {calkit_python-0.41.7 → calkit_python-0.41.9}/test/test-log.log +0 -0
  311. {calkit_python-0.41.7 → calkit_python-0.41.9}/tsconfig.json +0 -0
  312. {calkit_python-0.41.7 → calkit_python-0.41.9}/tsconfig.test.json +0 -0
  313. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/.gitignore +0 -0
  314. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/README.md +0 -0
  315. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/jupyter_server_test_config.py +0 -0
  316. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/package.json +0 -0
  317. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/playwright.config.js +0 -0
  318. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/tests/calkit.spec.ts +0 -0
  319. {calkit_python-0.41.7 → calkit_python-0.41.9}/ui-tests/yarn.lock +0 -0
  320. {calkit_python-0.41.7 → calkit_python-0.41.9}/uv.lock +0 -0
  321. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/.gitignore +0 -0
  322. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/.vscodeignore +0 -0
  323. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/CHANGELOG.md +0 -0
  324. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/LICENSE +0 -0
  325. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/README.md +0 -0
  326. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/images/calkit-no-bg.png +0 -0
  327. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/package-lock.json +0 -0
  328. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/package.json +0 -0
  329. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/scripts/set-proposed-api.js +0 -0
  330. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/environments.ts +0 -0
  331. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/extension.ts +0 -0
  332. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/notebooks.ts +0 -0
  333. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/test/environments.test.ts +0 -0
  334. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/test/kernel-selection.test.ts +0 -0
  335. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/test/notebooks.test.ts +0 -0
  336. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/src/types.ts +0 -0
  337. {calkit_python-0.41.7 → calkit_python-0.41.9}/vscode-ext/tsconfig.json +0 -0
  338. {calkit_python-0.41.7 → calkit_python-0.41.9}/yarn.lock +0 -0
@@ -21,3 +21,4 @@ dist
21
21
  vscode-ext/out
22
22
  .claude
23
23
  TODO.md
24
+ **/__pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.41.7
3
+ Version: 0.41.9
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -2418,7 +2418,16 @@ def run_in_env(
2418
2418
  )
2419
2419
  # TODO: Prefix should only be in the env file or calkit.yaml, not both?
2420
2420
  prefix = env.get("prefix")
2421
- conda_cmd = ["conda", "run"]
2421
+ # Conda is often not on the PATH (especially on Windows), so search
2422
+ # typical install locations rather than relying on the bare name,
2423
+ # which would fail with a confusing FileNotFoundError traceback.
2424
+ conda_exe = calkit.conda.find_conda_exe()
2425
+ if conda_exe is None:
2426
+ raise_error(
2427
+ "Cannot find Conda executable; "
2428
+ "ensure Conda is installed and on the PATH"
2429
+ )
2430
+ conda_cmd = [conda_exe, "run"]
2422
2431
  if prefix is not None:
2423
2432
  conda_cmd += ["--prefix", os.path.abspath(prefix)]
2424
2433
  else:
@@ -7,6 +7,7 @@ import os
7
7
  import pathlib
8
8
  import shutil
9
9
  import subprocess
10
+ import time
10
11
  from enum import Enum
11
12
 
12
13
  import typer
@@ -3142,7 +3143,7 @@ def new_release(
3142
3143
  )
3143
3144
  with open("LICENSE", "w") as f:
3144
3145
  f.write(license_txt)
3145
- repo.git.add("LICENSE")
3146
+ repo.git.add("LICENSE")
3146
3147
  license_ids = ["mit", "cc-by-4.0"]
3147
3148
  else:
3148
3149
  typer.echo(f"Detecting license(s) from {license_file}")
@@ -3325,22 +3326,23 @@ def new_release(
3325
3326
  f"/records/{record_id}/draft/files/{filename}/commit",
3326
3327
  service=to, # type: ignore
3327
3328
  )
3328
- # Conditionally publish or reserve DOI based on --draft flag
3329
- if draft_only:
3330
- # Reserve a DOI for the draft record
3331
- typer.echo(f"Reserving DOI for {to} draft record ID {record_id}")
3332
- doi_resp = calkit.invenio.post(
3333
- f"/records/{record_id}/draft/pids/doi",
3334
- service=to, # type: ignore
3329
+ # Reserve a DOI on the draft before publishing. The publish action's
3330
+ # response only echoes back the DOI under "pids" if one was reserved
3331
+ # on the draft first (otherwise "pids" comes back empty), so always
3332
+ # reserve here to get a stable identifier.
3333
+ typer.echo(f"Reserving DOI for {to} draft record ID {record_id}")
3334
+ doi_resp = calkit.invenio.post(
3335
+ f"/records/{record_id}/draft/pids/doi",
3336
+ service=to, # type: ignore
3337
+ )
3338
+ if verbose:
3339
+ typer.echo(f"DOI reservation response:\n{doi_resp}")
3340
+ doi = calkit.invenio.extract_doi(doi_resp)
3341
+ if doi is None:
3342
+ raise_error(
3343
+ f"Failed to reserve DOI for {to} draft record {record_id}"
3335
3344
  )
3336
- if verbose:
3337
- typer.echo(f"DOI response for draft:\n{doi_resp}")
3338
- try:
3339
- doi = doi_resp["pids"]["doi"]["identifier"]
3340
- except KeyError:
3341
- doi = doi_resp["doi"]
3342
- except Exception as e:
3343
- raise_error(f"Failed to reserve DOI for draft: {e}")
3345
+ if draft_only:
3344
3346
  url = f"https://doi.org/{doi}"
3345
3347
  typer.echo(f"Created {to} draft with reserved DOI: {doi}")
3346
3348
  else:
@@ -3350,8 +3352,37 @@ def new_release(
3350
3352
  f"/records/{record_id}/draft/actions/publish",
3351
3353
  service=to, # type: ignore
3352
3354
  )
3355
+ if verbose:
3356
+ typer.echo(f"Publish response:\n{invenio_dep}")
3353
3357
  record_id = invenio_dep["id"]
3354
- doi = invenio_dep["pids"]["doi"]["identifier"]
3358
+ # Prefer the DOI from the publish response, but fall back to the
3359
+ # reserved DOI (and a fetch of the published record) since the
3360
+ # publish action returns 202 and may not echo "pids" immediately.
3361
+ # Polling failures are tolerated: we already have the reserved DOI,
3362
+ # so a transient error while the record settles should not abort
3363
+ # the release.
3364
+ published_doi = calkit.invenio.extract_doi(invenio_dep)
3365
+ for _ in range(10):
3366
+ if published_doi is not None:
3367
+ break
3368
+ time.sleep(1)
3369
+ try:
3370
+ record = calkit.invenio.get(
3371
+ f"/records/{record_id}",
3372
+ service=to, # type: ignore
3373
+ )
3374
+ except Exception as e:
3375
+ if verbose:
3376
+ typer.echo(f"Polling for published DOI failed: {e}")
3377
+ continue
3378
+ published_doi = calkit.invenio.extract_doi(record)
3379
+ if published_doi is not None:
3380
+ doi = published_doi
3381
+ else:
3382
+ typer.echo(
3383
+ "Could not confirm minted DOI from published record; "
3384
+ f"falling back to reserved DOI {doi}"
3385
+ )
3355
3386
  url = f"https://doi.org/{doi}"
3356
3387
  typer.echo(f"Published to {to} with DOI: {doi}")
3357
3388
  else:
@@ -3363,38 +3394,23 @@ def new_release(
3363
3394
  doi_base_url = calkit.releases.SERVICES[to]["url"]
3364
3395
  doi_md = (
3365
3396
  f"[![DOI]({doi_base_url}/badge/DOI/{doi}.svg)]"
3366
- f"(https://handle.stage.datacite.org/{doi})"
3397
+ f"(https://doi.org/{doi})"
3367
3398
  )
3368
3399
  if os.path.isfile("README.md"):
3369
3400
  with open("README.md") as f:
3370
3401
  readme_txt = f.read()
3371
3402
  else:
3372
- readme_txt = f"# {title}\n"
3373
- existing_lines = readme_txt.split("\n")
3374
- new_lines = []
3375
- first_content_line_index = None
3376
- for n, line in enumerate(existing_lines):
3377
- if line.startswith(doi_md[:6]):
3378
- pass # Skip DOI lines
3379
- else:
3380
- if (
3381
- n != 0
3382
- and line.strip()
3383
- and first_content_line_index is None
3384
- ):
3385
- first_content_line_index = len(new_lines)
3386
- new_lines.append(line)
3387
- # Ensure first 3 lines are title, blank, DOI lines
3388
- new_lines = (
3389
- [new_lines[0]]
3390
- + ["", doi_md, ""]
3391
- + new_lines[first_content_line_index:]
3403
+ readme_txt = ""
3404
+ readme_txt = calkit.releases.add_doi_badge_to_readme(
3405
+ readme_txt, badge=doi_md, title=title
3392
3406
  )
3393
- readme_txt = "\n".join(new_lines)
3394
3407
  if not dry_run:
3395
3408
  with open("README.md", "w") as f:
3396
3409
  f.write(readme_txt)
3397
- repo.git.add("README.md")
3410
+ # Stage only after the file is closed; otherwise the buffered
3411
+ # contents may not be flushed to disk yet and Git would stage an
3412
+ # empty file
3413
+ repo.git.add("README.md")
3398
3414
  else:
3399
3415
  typer.echo(f"Would have updated README.md to:\n{readme_txt}")
3400
3416
  # Create Git tag
@@ -3449,11 +3465,13 @@ def new_release(
3449
3465
  references = reference_collections[0]
3450
3466
  ref_path = references.get("path", "references.bib")
3451
3467
  try:
3468
+ # Read the existing references as raw text so we can append the new
3469
+ # entry without reformatting the user's existing entries
3452
3470
  if os.path.isfile(ref_path):
3453
3471
  with open(ref_path) as f:
3454
- reflib = bibtexparser.load(f)
3472
+ existing_text = f.read()
3455
3473
  else:
3456
- reflib = bibtexparser.bibdatabase.BibDatabase()
3474
+ existing_text = ""
3457
3475
  bibtex_doi = doi
3458
3476
  if bibtex_doi is None and dry_run:
3459
3477
  mock_suffix = "".join(
@@ -3473,31 +3491,38 @@ def new_release(
3473
3491
  if not new_entries:
3474
3492
  raise ValueError("Failed to parse generated BibTeX entry")
3475
3493
  new_entry = new_entries[0]
3476
- # Search through entries for one with the same DOI, and replace if
3477
- # there is a match
3494
+ # Search through existing entries for any with the same DOI, and
3495
+ # replace them (by citation key) if there is a match. Tolerate parse
3496
+ # errors here (e.g., non-standard or temporarily-invalid BibTeX) by
3497
+ # skipping replacement detection and still appending the new entry.
3478
3498
  new_doi = new_entry.get("doi")
3479
- matching_indices = []
3499
+ replace_ids = []
3480
3500
  if new_doi:
3481
- matching_indices = [
3482
- n
3483
- for n, entry in enumerate(reflib.entries)
3501
+ try:
3502
+ existing_entries = bibtexparser.loads(existing_text).entries
3503
+ except Exception as e:
3504
+ warn(f"Could not parse existing references to dedupe: {e}")
3505
+ existing_entries = []
3506
+ replace_ids = [
3507
+ entry["ID"]
3508
+ for entry in existing_entries
3484
3509
  if entry.get("doi") == new_doi
3485
3510
  ]
3486
- if matching_indices:
3511
+ if replace_ids:
3487
3512
  typer.echo(
3488
3513
  "Found matching DOI in existing references "
3489
- f"({len(matching_indices)} entr"
3490
- f"{'y' if len(matching_indices) == 1 else 'ies'}); "
3514
+ f"({len(replace_ids)} entr"
3515
+ f"{'y' if len(replace_ids) == 1 else 'ies'}); "
3491
3516
  "replacing"
3492
3517
  )
3493
- for n in reversed(matching_indices):
3494
- _ = reflib.entries.pop(n)
3495
- reflib.entries.append(new_entry)
3518
+ new_text = calkit.releases.add_bibtex_entry(
3519
+ existing_text, invenio_bibtex, replace_ids=replace_ids
3520
+ )
3496
3521
  if dry_run:
3497
3522
  typer.echo(f"Would write updated references to {ref_path}")
3498
3523
  else:
3499
3524
  with open(ref_path, "w") as f:
3500
- bibtexparser.dump(reflib, f)
3525
+ f.write(new_text)
3501
3526
  repo.git.add(ref_path)
3502
3527
  except Exception as e:
3503
3528
  warn(f"Failed to add to references: {e}")
@@ -341,6 +341,14 @@ def check_dep_exists(
341
341
  return True
342
342
  if system_info is not None and system_info.get(f"{name}_version"):
343
343
  return True
344
+ # Conda and mamba are frequently installed but not on the PATH (most
345
+ # commonly on Windows), so search their typical install locations
346
+ # rather than relying on the bare name being directly executable.
347
+ if name in ("conda", "mamba"):
348
+ from calkit.conda import find_conda_exe, find_mamba_exe
349
+
350
+ exe = find_conda_exe() if name == "conda" else find_mamba_exe()
351
+ return exe is not None
344
352
  cmd = [name]
345
353
  # Executables with non-conventional CLIs
346
354
  if name == "matlab":
@@ -107,6 +107,26 @@ put = partial(_request, "put")
107
107
  delete = partial(_request, "delete")
108
108
 
109
109
 
110
+ def extract_doi(record: dict) -> str | None:
111
+ """Extract the DOI identifier from an InvenioRDM record or response.
112
+
113
+ Depending on the endpoint and whether the DOI has been minted yet, the
114
+ identifier can live in different places, so check all known locations and
115
+ return ``None`` if it isn't present yet.
116
+ """
117
+ pids = record.get("pids")
118
+ if isinstance(pids, dict):
119
+ doi = pids.get("doi")
120
+ if isinstance(doi, dict) and doi.get("identifier"):
121
+ return doi["identifier"]
122
+ if record.get("doi"):
123
+ return record["doi"]
124
+ metadata = record.get("metadata")
125
+ if isinstance(metadata, dict) and metadata.get("doi"):
126
+ return metadata["doi"]
127
+ return None
128
+
129
+
110
130
  def get_download_urls(
111
131
  record_id: int | str,
112
132
  service: ServiceName = DEFAULT_SERVICE,
@@ -96,10 +96,14 @@ class PipelineStatus(BaseModel):
96
96
  def always_run_stage_names(self) -> list[str]:
97
97
  # Only list stages whose sole change indicator is always_run; stages
98
98
  # that also have real changes are reported under stale_stage_names.
99
+ # Subproject stages are excluded: their always-changed status is a
100
+ # delegation detail, not a user-meaningful always-run stage of the
101
+ # parent project.
99
102
  return [
100
103
  name
101
104
  for name, stage in self.stale_stages.items()
102
105
  if stage.always_run
106
+ and not stage.is_subproject
103
107
  and not stage.modified_command
104
108
  and not stage.modified_inputs
105
109
  and not stage.modified_outputs
@@ -145,6 +149,12 @@ class StaleStage(BaseModel):
145
149
  modified_outputs: list[str] = Field(default_factory=list)
146
150
  modified_command: bool = False
147
151
  always_run: bool = False
152
+ # True for stages that originate from a subproject (either an individual
153
+ # "{sp}:{stage}" stage or a kept "{sp} (subproject)" wrapper). Subproject
154
+ # wrapper stages are marked always-changed purely as a delegation
155
+ # mechanism, so they should not be advertised as always-run stages of the
156
+ # parent project.
157
+ is_subproject: bool = False
148
158
 
149
159
  @staticmethod
150
160
  def _as_path_list(paths: object) -> list[str]:
@@ -227,6 +237,7 @@ class StaleStage(BaseModel):
227
237
  status_data: list | dict | str,
228
238
  configured_outputs: list[str] | None = None,
229
239
  path_prefix: str | None = None,
240
+ is_subproject: bool = False,
230
241
  ) -> "StaleStage":
231
242
  modified_inputs = []
232
243
  output_paths = []
@@ -312,6 +323,7 @@ class StaleStage(BaseModel):
312
323
  modified_outputs=modified_outputs,
313
324
  modified_command=modified_command,
314
325
  always_run=always_run,
326
+ is_subproject=is_subproject,
315
327
  )
316
328
 
317
329
 
@@ -546,6 +558,74 @@ def collapse_dvc_stages(
546
558
  return stage
547
559
 
548
560
 
561
+ def _status_target_matches(
562
+ target: str,
563
+ display_name: str,
564
+ bare_name: str,
565
+ subproject: str | None,
566
+ stale_stage: StaleStage,
567
+ ) -> bool:
568
+ """Return True if a status ``target`` selects this stage.
569
+
570
+ Because the full DVC status is now computed and filtered locally (rather
571
+ than asking DVC to filter by target), this is where each ``target`` passed
572
+ to :func:`get_status` is resolved against a single stage. A ``target`` may
573
+ be a stage name in any of calkit's forms (``stage``, ``dvc.yaml:stage``,
574
+ ``subproject:stage``, or a whole ``subproject``) or a repo path, which is
575
+ matched against the stage's stale/modified inputs and outputs.
576
+
577
+ Iterated/matrix stages use a ``name@param`` key in DVC status, so the base
578
+ name (the part before ``@``) is matched too — targeting the base name
579
+ selects every iteration, consistent with how ``calkit run`` expands matrix
580
+ targets.
581
+ """
582
+ base_name = bare_name.split("@")[0]
583
+ # Stage-name matching is done against the raw target so it covers all of
584
+ # calkit's stage-target forms as well as DVC's native ``dvc.yaml:stage``
585
+ # and ``<subproject>/dvc.yaml:stage`` forms (the latter is what
586
+ # translate_run_targets emits for inline subprojects).
587
+ stage_aliases = {display_name, bare_name, base_name}
588
+ if subproject is not None:
589
+ subproject_name = Path(subproject).name
590
+ if target in {subproject, subproject_name}:
591
+ return display_name.startswith(
592
+ f"{subproject}:"
593
+ ) or display_name == (f"{subproject} (subproject)")
594
+ stage_aliases |= {
595
+ f"{subproject}:{bare_name}",
596
+ f"{subproject_name}:{bare_name}",
597
+ f"{subproject}:{base_name}",
598
+ f"{subproject_name}:{base_name}",
599
+ f"{subproject}/dvc.yaml:{bare_name}",
600
+ f"{subproject}/dvc.yaml:{base_name}",
601
+ }
602
+ else:
603
+ stage_aliases |= {
604
+ f"dvc.yaml:{bare_name}",
605
+ f"dvc.yaml:{base_name}",
606
+ }
607
+ if target in stage_aliases:
608
+ return True
609
+ # Otherwise treat the target as a repo path and match it against the
610
+ # stage's stale/modified inputs and outputs. A target is path-like if it
611
+ # exists on disk or contains a separator; this keeps root-level paths
612
+ # without separators (e.g. ``data`` or ``out.csv``) selectable, which a
613
+ # separator-only heuristic would miss. A target that looks like a stage
614
+ # selector (contains ``:``) but matched no stage above is not a path.
615
+ if ":" in target and not os.path.exists(target):
616
+ return False
617
+ is_pathlike = os.path.exists(target) or "/" in target or "\\" in target
618
+ if not is_pathlike:
619
+ return False
620
+ norm_target = Path(target).as_posix().rstrip("/")
621
+ candidate_paths = (
622
+ stale_stage.stale_outputs
623
+ + stale_stage.modified_inputs
624
+ + stale_stage.modified_outputs
625
+ )
626
+ return any(_paths_overlap(norm_target, p) for p in candidate_paths)
627
+
628
+
549
629
  def get_status(
550
630
  ck_info: dict | None = None,
551
631
  targets: list[str] | None = None,
@@ -628,7 +708,11 @@ def get_status(
628
708
  dvc_repo = calkit.dvc.get_dvc_repo()
629
709
  # calkit.dvc.core installs a filter on dvc.repo.status that drops
630
710
  # the noisy frozen-stage warning, so the call here stays quiet.
631
- raw_status = dvc_repo.status(targets=targets)
711
+ # Ask DVC for the complete stale set, then filter locally.
712
+ # DVC's own target filtering can miss stale propagation from
713
+ # isolated subprojects, which makes targeted status disagree with
714
+ # the full-project status view.
715
+ raw_status = dvc_repo.status()
632
716
  raw_status = calkit.dvc.status_as_posix(raw_status)
633
717
  except Exception as e:
634
718
  result["errors"].append(
@@ -794,20 +878,47 @@ def get_status(
794
878
  for output in stage_cfg.get("outputs", [])
795
879
  ]
796
880
  if subproject:
881
+ # Use posix separators so these match DVC's reported paths
882
+ # (which are always forward-slash, repo-root-relative). Using
883
+ # the OS-native separator here would, on Windows, produce a
884
+ # backslash variant that escapes path dedup and shows up as a
885
+ # duplicate stale output alongside DVC's posix path.
797
886
  configured_outputs = [
798
- str(Path(subproject) / p) for p in raw_outputs
887
+ (Path(subproject) / p).as_posix() for p in raw_outputs
799
888
  ]
800
889
  else:
801
890
  configured_outputs = raw_outputs
802
891
  # For isolated subprojects, DVC reports paths relative to the
803
892
  # subproject dir; prefix them so all paths are parent-relative.
893
+ # A stage is subproject-originating if it carries a subproject
894
+ # path ("{sp}:{stage}") or is a kept wrapper for a subproject
895
+ # ("{sp} (subproject)", whose bare name is a known subproject).
896
+ is_subproject = (
897
+ subproject is not None or bare_name in sp_by_stage_name
898
+ )
804
899
  ordered_stale_stages[display_name] = StaleStage.from_status_data(
805
900
  status_data=status_data,
806
901
  configured_outputs=configured_outputs,
807
902
  path_prefix=subproject
808
903
  if subproject in isolated_sp_paths
809
904
  else None,
905
+ is_subproject=is_subproject,
810
906
  )
907
+ if targets:
908
+ ordered_stale_stages = {
909
+ display_name: stale_stage
910
+ for display_name, stale_stage in ordered_stale_stages.items()
911
+ if any(
912
+ _status_target_matches(
913
+ target,
914
+ display_name,
915
+ raw_stale_stages[display_name][0],
916
+ raw_stale_stages[display_name][1],
917
+ stale_stage,
918
+ )
919
+ for target in targets
920
+ )
921
+ }
811
922
  result["stale_stages"] = ordered_stale_stages
812
923
  return PipelineStatus(
813
924
  has_pipeline=result["has_pipeline"],
@@ -86,6 +86,109 @@ def create_bibtex(
86
86
  )
87
87
 
88
88
 
89
+ def _find_bibtex_entry_span(
90
+ text: str, entry_id: str
91
+ ) -> tuple[int, int] | None:
92
+ """Locate the raw character span of a BibTeX entry by its citation key.
93
+
94
+ Returns ``(start, end)`` offsets into ``text`` where ``end`` is exclusive
95
+ (just past the entry's closing brace), or ``None`` if no entry with that
96
+ key is found.
97
+ """
98
+ # Match e.g. ``@misc{the-key,`` allowing arbitrary surrounding whitespace
99
+ pattern = re.compile(
100
+ r"@\w+\s*\{\s*" + re.escape(entry_id) + r"\s*,", re.IGNORECASE
101
+ )
102
+ match = pattern.search(text)
103
+ if match is None:
104
+ return None
105
+ # Walk from the entry's opening brace to its matching close, counting depth
106
+ depth = 0
107
+ i = text.index("{", match.start())
108
+ while i < len(text):
109
+ if text[i] == "{":
110
+ depth += 1
111
+ elif text[i] == "}":
112
+ depth -= 1
113
+ if depth == 0:
114
+ return match.start(), i + 1
115
+ i += 1
116
+ return None
117
+
118
+
119
+ def add_bibtex_entry(
120
+ existing_text: str, new_entry: str, replace_ids: list[str] | None = None
121
+ ) -> str:
122
+ """Append a BibTeX entry to existing references without reformatting them.
123
+
124
+ Any entries whose citation keys are in ``replace_ids`` are removed first
125
+ (so a re-released version replaces its prior entry); all other existing
126
+ content is preserved byte-for-byte. ``new_entry`` is appended at the end.
127
+ """
128
+ text = existing_text
129
+ # Remove replaced entries from the raw text, last-to-first so earlier spans
130
+ # stay valid as we splice. De-duplicate spans in case a key matched more
131
+ # than once.
132
+ spans = set()
133
+ for entry_id in replace_ids or []:
134
+ span = _find_bibtex_entry_span(text, entry_id)
135
+ if span is not None:
136
+ spans.add(span)
137
+ for start, end in sorted(spans, reverse=True):
138
+ # Consume any trailing newlines so we don't leave a gap behind
139
+ while end < len(text) and text[end] in "\r\n":
140
+ end += 1
141
+ text = text[:start] + text[end:]
142
+ # Append the new entry, leaving the existing content untouched and adding
143
+ # only the separator newlines needed for exactly one blank line between the
144
+ # last existing entry and the new one
145
+ if not text.strip():
146
+ return new_entry.strip() + "\n"
147
+ if text.endswith("\n\n"):
148
+ separator = ""
149
+ elif text.endswith("\n"):
150
+ separator = "\n"
151
+ else:
152
+ separator = "\n\n"
153
+ return text + separator + new_entry.strip() + "\n"
154
+
155
+
156
+ def add_doi_badge_to_readme(
157
+ readme_text: str, badge: str, title: str | None
158
+ ) -> str:
159
+ """Insert (or refresh) a DOI badge near the top of a README.
160
+
161
+ The badge is placed directly beneath the title with exactly one blank line
162
+ on either side. Any pre-existing DOI badge line is replaced rather than
163
+ duplicated, the title is never duplicated, and all other existing content
164
+ is preserved.
165
+ """
166
+ # Drop any existing DOI badge line(s); the current badge is re-added below
167
+ lines = [
168
+ line
169
+ for line in (readme_text.split("\n") if readme_text else [])
170
+ if not line.startswith("[![DOI")
171
+ ]
172
+ # The first non-blank line is treated as the title; synthesize one from the
173
+ # project title if the README has no content yet
174
+ while lines and not lines[0].strip():
175
+ lines.pop(0)
176
+ if lines:
177
+ title_line = lines[0]
178
+ rest = lines[1:]
179
+ else:
180
+ title_line = f"# {title}"
181
+ rest = []
182
+ # Drop leading blank lines from the remaining content so the spacing around
183
+ # the badge is controlled entirely here
184
+ while rest and not rest[0].strip():
185
+ rest.pop(0)
186
+ new_lines = [title_line, "", badge]
187
+ if rest:
188
+ new_lines += [""] + rest
189
+ return "\n".join(new_lines)
190
+
191
+
89
192
  CITATION_CFF_TEMPLATE = """
90
193
  cff-version: 1.2.0
91
194
  message: If you use this software, please cite it using these metadata.
@@ -296,3 +296,56 @@ def test_isolated_subproject_external_dep(tmp_dir):
296
296
  check_environments=False, compile_to_dvc=False
297
297
  )
298
298
  assert not status.is_stale
299
+
300
+
301
+ def test_targeted_status_uses_full_subproject_status_for_stage_targets(
302
+ tmp_dir, monkeypatch
303
+ ):
304
+ os.makedirs("solver/.dvc", exist_ok=True)
305
+ root_ck = {
306
+ "subprojects": [{"path": "solver"}],
307
+ "pipeline": {
308
+ "stages": {
309
+ "post-process": {
310
+ "outputs": [{"path": "final.txt", "storage": "git"}]
311
+ }
312
+ }
313
+ },
314
+ }
315
+ solver_ck = {
316
+ "pipeline": {
317
+ "stages": {
318
+ "package-paper": {
319
+ "outputs": [{"path": "paper.txt", "storage": "git"}]
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ class FakeRepo:
326
+ def status(self, targets=None):
327
+ if targets is None:
328
+ return {
329
+ "solver/dvc.yaml:package-paper": ["always changed"],
330
+ "post-process": [
331
+ {"changed deps": {"solver/docs/figs": "modified"}}
332
+ ],
333
+ }
334
+ return {}
335
+
336
+ monkeypatch.setattr(
337
+ calkit,
338
+ "load_calkit_info",
339
+ lambda wdir=None: solver_ck if wdir == "solver" else root_ck,
340
+ )
341
+ monkeypatch.setattr(calkit.dvc, "get_dvc_repo", lambda *args: FakeRepo())
342
+ monkeypatch.setattr(calkit.dvc, "status_as_posix", lambda status: status)
343
+ status = calkit.pipeline.get_status(
344
+ ck_info=root_ck,
345
+ targets=["post-process"],
346
+ check_environments=False,
347
+ clean_notebooks=False,
348
+ compile_to_dvc=False,
349
+ )
350
+ assert status.is_stale
351
+ assert status.stale_stage_names == ["post-process"]