calkit-python 0.35.4__tar.gz → 0.35.6__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 (286) hide show
  1. {calkit_python-0.35.4 → calkit_python-0.35.6}/Makefile +7 -2
  2. {calkit_python-0.35.4 → calkit_python-0.35.6}/PKG-INFO +1 -1
  3. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/conda.py +78 -28
  4. calkit_python-0.35.6/calkit/git.py +297 -0
  5. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/overleaf.py +32 -1
  6. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/pipeline.py +27 -8
  7. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_overleaf.py +38 -0
  8. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_conda.py +120 -2
  9. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_git.py +59 -10
  10. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_pipeline.py +100 -0
  11. calkit_python-0.35.6/docs/cli-reference.md +2586 -0
  12. calkit_python-0.35.6/scripts/generate-cli-reference.py +257 -0
  13. calkit_python-0.35.4/calkit/git.py +0 -197
  14. calkit_python-0.35.4/docs/cli-reference.md +0 -3
  15. {calkit_python-0.35.4 → calkit_python-0.35.6}/.gitignore +0 -0
  16. {calkit_python-0.35.4 → calkit_python-0.35.6}/.pre-commit-config.yaml +0 -0
  17. {calkit_python-0.35.4 → calkit_python-0.35.6}/.prettierignore +0 -0
  18. {calkit_python-0.35.4 → calkit_python-0.35.6}/.python-version +0 -0
  19. {calkit_python-0.35.4 → calkit_python-0.35.6}/.yarnrc.yml +0 -0
  20. {calkit_python-0.35.4 → calkit_python-0.35.6}/CITATION.cff +0 -0
  21. {calkit_python-0.35.4 → calkit_python-0.35.6}/CODE_OF_CONDUCT.md +0 -0
  22. {calkit_python-0.35.4 → calkit_python-0.35.6}/CONTRIBUTING.md +0 -0
  23. {calkit_python-0.35.4 → calkit_python-0.35.6}/LICENSE +0 -0
  24. {calkit_python-0.35.4 → calkit_python-0.35.6}/README.md +0 -0
  25. {calkit_python-0.35.4 → calkit_python-0.35.6}/babel.config.js +0 -0
  26. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/__init__.py +0 -0
  27. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/__main__.py +0 -0
  28. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/calc.py +0 -0
  29. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/check.py +0 -0
  30. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/__init__.py +0 -0
  31. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/check.py +0 -0
  32. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/cloud.py +0 -0
  33. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/config.py +0 -0
  34. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/core.py +0 -0
  35. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/describe.py +0 -0
  36. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/import_.py +0 -0
  37. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/latex.py +0 -0
  38. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/list.py +0 -0
  39. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/main/__init__.py +0 -0
  40. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/main/core.py +0 -0
  41. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/main/xr.py +0 -0
  42. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/new.py +0 -0
  43. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/notebooks.py +0 -0
  44. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/office.py +0 -0
  45. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/overleaf.py +0 -0
  46. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/slurm.py +0 -0
  47. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cli/update.py +0 -0
  48. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/cloud.py +0 -0
  49. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/config.py +0 -0
  50. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/core.py +0 -0
  51. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/datasets.py +0 -0
  52. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/detect.py +0 -0
  53. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/docker.py +0 -0
  54. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/dvc.py +0 -0
  55. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/environments.py +0 -0
  56. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/fs.py +0 -0
  57. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/github.py +0 -0
  58. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/gui.py +0 -0
  59. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/invenio.py +0 -0
  60. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/julia.py +0 -0
  61. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/jupyter.py +0 -0
  62. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/jupyterlab/__init__.py +0 -0
  63. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/jupyterlab/routes.py +0 -0
  64. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/package.json +0 -0
  65. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  66. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  67. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  68. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  69. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  70. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/909.651be47ca47390b78a92.js +0 -0
  71. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  72. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  73. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  74. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/remoteEntry.c091821b3d7f2d287a67.js +0 -0
  75. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/style.js +0 -0
  76. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/labextension/static/third-party-licenses.json +0 -0
  77. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/licenses.py +0 -0
  78. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/magics.py +0 -0
  79. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/matlab.py +0 -0
  80. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/models/__init__.py +0 -0
  81. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/models/core.py +0 -0
  82. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/models/io.py +0 -0
  83. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/models/iteration.py +0 -0
  84. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/models/pipeline.py +0 -0
  85. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/notebooks.py +0 -0
  86. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/office.py +0 -0
  87. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/ops.py +0 -0
  88. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/releases.py +0 -0
  89. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/server.py +0 -0
  90. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/__init__.py +0 -0
  91. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/core.py +0 -0
  92. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/__init__.py +0 -0
  93. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/article/paper.tex +0 -0
  94. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/core.py +0 -0
  95. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/jfm/jfm.bst +0 -0
  96. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/jfm/jfm.cls +0 -0
  97. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  98. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/jfm/paper.tex +0 -0
  99. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/templates/latex/jfm/upmath.sty +0 -0
  100. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/__init__.py +0 -0
  101. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/__init__.py +0 -0
  102. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/main/__init__.py +0 -0
  103. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/main/test_core.py +0 -0
  104. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/main/test_xr.py +0 -0
  105. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_check.py +0 -0
  106. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_config.py +0 -0
  107. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_import.py +0 -0
  108. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_latex.py +0 -0
  109. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_list.py +0 -0
  110. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_new.py +0 -0
  111. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/cli/test_notebooks.py +0 -0
  112. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/jupyterlab/__init__.py +0 -0
  113. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/jupyterlab/test_routes.py +0 -0
  114. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/models/__init__.py +0 -0
  115. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/models/test_iteration.py +0 -0
  116. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/models/test_pipeline.py +0 -0
  117. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_calc.py +0 -0
  118. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_check.py +0 -0
  119. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_core.py +0 -0
  120. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_detect.py +0 -0
  121. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_docker.py +0 -0
  122. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_dvc.py +0 -0
  123. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_environments.py +0 -0
  124. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_fs.py +0 -0
  125. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_invenio.py +0 -0
  126. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_julia.py +0 -0
  127. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_jupyter.py +0 -0
  128. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_magics.py +0 -0
  129. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_matlab.py +0 -0
  130. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_notebooks.py +0 -0
  131. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_releases.py +0 -0
  132. {calkit_python-0.35.4 → calkit_python-0.35.6}/calkit/tests/test_templates.py +0 -0
  133. {calkit_python-0.35.4 → calkit_python-0.35.6}/conftest.py +0 -0
  134. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/CNAME +0 -0
  135. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/apps.md +0 -0
  136. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/calculations.md +0 -0
  137. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/calkit-yaml.md +0 -0
  138. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/cloud-integration.md +0 -0
  139. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/datasets.md +0 -0
  140. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/dependencies.md +0 -0
  141. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/environments.md +0 -0
  142. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/examples.md +0 -0
  143. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/governance.md +0 -0
  144. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/help.md +0 -0
  145. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/c-to-the-k-white.svg +0 -0
  146. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/calkit-fragmentation-compendium.png +0 -0
  147. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/calkit-no-bg.png +0 -0
  148. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/connect-zenodo.png +0 -0
  149. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab/all-green.png +0 -0
  150. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  151. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab/new-env.png +0 -0
  152. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab/new-notebook.png +0 -0
  153. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  154. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/jupyterlab-params.png +0 -0
  155. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/plos-osi-code-2024-03.png +0 -0
  156. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/img/vscode-nb-params.png +0 -0
  157. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/index.md +0 -0
  158. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/installation.md +0 -0
  159. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/jupyterlab.md +0 -0
  160. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/local-server.md +0 -0
  161. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/notebooks.md +0 -0
  162. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/overleaf.md +0 -0
  163. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/pipeline/index.md +0 -0
  164. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/pipeline/manual-steps.md +0 -0
  165. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/pipeline/running-and-logging.md +0 -0
  166. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/pipeline/slurm.md +0 -0
  167. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/quickstart.md +0 -0
  168. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/references.md +0 -0
  169. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/releases.md +0 -0
  170. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/reproducibility.md +0 -0
  171. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  172. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/conda-envs.md +0 -0
  173. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/existing-project.md +0 -0
  174. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/first-project.md +0 -0
  175. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/github-actions.md +0 -0
  176. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  177. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  178. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  179. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  180. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  181. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  182. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  183. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  184. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  185. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  186. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  187. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  188. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  189. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  190. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  191. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/create-project.png +0 -0
  192. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  193. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/excel-chart.png +0 -0
  194. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/excel-data.png +0 -0
  195. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  196. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/needs-clone.png +0 -0
  197. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/new-stage.png +0 -0
  198. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  199. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  200. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/status-more-rows.png +0 -0
  201. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  202. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/untracked-data.png +0 -0
  203. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/updated-publication.png +0 -0
  204. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  205. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/office/workflow-page.png +0 -0
  206. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/clone.png +0 -0
  207. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/create-project.png +0 -0
  208. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  209. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  210. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  211. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/new-token.png +0 -0
  212. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/reclone.png +0 -0
  213. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  214. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/quick-actions.png +0 -0
  215. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/img/run-proc.png +0 -0
  216. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/index.md +0 -0
  217. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/jupyterlab.md +0 -0
  218. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/latex-codespaces.md +0 -0
  219. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/matlab.md +0 -0
  220. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/notebook-pipeline.md +0 -0
  221. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/office.md +0 -0
  222. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/openfoam.md +0 -0
  223. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/tutorials/procedures.md +0 -0
  224. {calkit_python-0.35.4 → calkit_python-0.35.6}/docs/version-control.md +0 -0
  225. {calkit_python-0.35.4 → calkit_python-0.35.6}/install.json +0 -0
  226. {calkit_python-0.35.4 → calkit_python-0.35.6}/jest.config.js +0 -0
  227. {calkit_python-0.35.4 → calkit_python-0.35.6}/jupyter-config/server-config/calkit.json +0 -0
  228. {calkit_python-0.35.4 → calkit_python-0.35.6}/mkdocs.yml +0 -0
  229. {calkit_python-0.35.4 → calkit_python-0.35.6}/package.json +0 -0
  230. {calkit_python-0.35.4 → calkit_python-0.35.6}/pyproject.toml +0 -0
  231. {calkit_python-0.35.4 → calkit_python-0.35.6}/schema/plugin.json +0 -0
  232. {calkit_python-0.35.4 → calkit_python-0.35.6}/scripts/install.ps1 +0 -0
  233. {calkit_python-0.35.4 → calkit_python-0.35.6}/scripts/install.sh +0 -0
  234. {calkit_python-0.35.4 → calkit_python-0.35.6}/scripts/make-calk9.sh +0 -0
  235. {calkit_python-0.35.4 → calkit_python-0.35.6}/scripts/sync-docs.py +0 -0
  236. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/__tests__/useQueries.spec.ts +0 -0
  237. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/calkit-config.ts +0 -0
  238. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/cell-output-marker.ts +0 -0
  239. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/commit-dialog.tsx +0 -0
  240. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/environment-editor.tsx +0 -0
  241. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/notebook-registration.tsx +0 -0
  242. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/notebook-toolbar.tsx +0 -0
  243. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/pipeline-status-bar.tsx +0 -0
  244. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/project-info-editor.tsx +0 -0
  245. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/sidebar-settings.tsx +0 -0
  246. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/sidebar.tsx +0 -0
  247. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/components/stage-editor.tsx +0 -0
  248. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/feature-flags.ts +0 -0
  249. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/file-browser-menu.ts +0 -0
  250. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/hooks/__tests__/useQueries.test.tsx +0 -0
  251. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/hooks/useQueries.ts +0 -0
  252. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/icons.ts +0 -0
  253. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/index.ts +0 -0
  254. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/io-tracker.ts +0 -0
  255. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/pipeline-state.ts +0 -0
  256. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/queryClient.ts +0 -0
  257. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/request.ts +0 -0
  258. {calkit_python-0.35.4 → calkit_python-0.35.6}/src/shims-mainmenu.d.ts +0 -0
  259. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/base.css +0 -0
  260. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/cell-output-marker.css +0 -0
  261. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/environment-editor.css +0 -0
  262. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/environment-selector.css +0 -0
  263. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/img/calkit-no-bg.png +0 -0
  264. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/index.css +0 -0
  265. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/index.js +0 -0
  266. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/notebook-toolbar.css +0 -0
  267. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/pipeline-status-bar.css +0 -0
  268. {calkit_python-0.35.4 → calkit_python-0.35.6}/style/sidebar.css +0 -0
  269. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  270. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/nb-julia.ipynb +0 -0
  271. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/nb-params.ipynb +0 -0
  272. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/nb-subdir.ipynb +0 -0
  273. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/pipeline.ipynb +0 -0
  274. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/script.py +0 -0
  275. {calkit_python-0.35.4 → calkit_python-0.35.6}/test/test-log.log +0 -0
  276. {calkit_python-0.35.4 → calkit_python-0.35.6}/tsconfig.json +0 -0
  277. {calkit_python-0.35.4 → calkit_python-0.35.6}/tsconfig.test.json +0 -0
  278. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/.gitignore +0 -0
  279. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/README.md +0 -0
  280. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/jupyter_server_test_config.py +0 -0
  281. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/package.json +0 -0
  282. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/playwright.config.js +0 -0
  283. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/tests/calkit.spec.ts +0 -0
  284. {calkit_python-0.35.4 → calkit_python-0.35.6}/ui-tests/yarn.lock +0 -0
  285. {calkit_python-0.35.4 → calkit_python-0.35.6}/uv.lock +0 -0
  286. {calkit_python-0.35.4 → calkit_python-0.35.6}/yarn.lock +0 -0
@@ -37,12 +37,17 @@ test-docs: sync-docs ## Test if documentation can be built without warnings or e
37
37
  @uv run mkdocs build -s
38
38
 
39
39
  .PHONY: sync-docs
40
- sync-docs: ## Sync documentation content from docs/*.md into README.md.
40
+ sync-docs: cli-reference ## Sync documentation content from docs/*.md into README.md.
41
41
  @echo "🚀 Syncing documentation"
42
42
  @uv run python scripts/sync-docs.py
43
43
 
44
+ .PHONY: cli-reference
45
+ cli-reference: ## Generate docs/cli-reference.md from CLI help output.
46
+ @echo "🚀 Generating CLI reference"
47
+ @uv run python scripts/generate-cli-reference.py
48
+
44
49
  .PHONY: docs
45
- docs: ## Build and serve the documentation.
50
+ docs: sync-docs ## Build and serve the documentation.
46
51
  @uv run mkdocs serve --livereload
47
52
 
48
53
  .PHONY: import-profile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.35.4
3
+ Version: 0.35.6
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -9,6 +9,7 @@ import shutil
9
9
  import subprocess
10
10
  import warnings
11
11
  from pathlib import Path
12
+ from typing import cast
12
13
 
13
14
  import toml
14
15
  from packaging.specifiers import SpecifierSet
@@ -123,7 +124,13 @@ def _editable_package_name_from_dir(dir_path: str) -> str:
123
124
  elif os.path.isfile(os.path.join(dir_path, "pyproject.toml")):
124
125
  # Read pyproject.toml to get the package name
125
126
  with open(os.path.join(dir_path, "pyproject.toml")) as f:
126
- pyproject = toml.load(f)
127
+ try:
128
+ pyproject = toml.load(f)
129
+ except Exception as e:
130
+ raise type(e)(
131
+ f"Failed to load pyproject.toml from {dir_path}; "
132
+ "check that it is valid TOML"
133
+ ) from e
127
134
  if "project" in pyproject:
128
135
  if "name" in pyproject["project"]:
129
136
  return pyproject["project"]["name"]
@@ -139,6 +146,7 @@ def _check_single(
139
146
  """
140
147
  # If this is an editable install it needs to be handled specially
141
148
  # It also needs to be relative to the env spec dir
149
+ editable = False
142
150
  if req.startswith("-e ") or req.startswith("--editable "):
143
151
  req = req.split(" ", 1)[1]
144
152
  if "#" in req:
@@ -147,6 +155,7 @@ def _check_single(
147
155
  # Create path relative to env spec dir
148
156
  req = os.path.join(env_spec_dir, req)
149
157
  req = _editable_package_name_from_dir(req)
158
+ editable = True
150
159
  # If this is a Git version, we can't check it
151
160
  # TODO: Clone Git repos to check?
152
161
  if "@git" in req:
@@ -182,7 +191,7 @@ def _check_single(
182
191
  # TODO: Check exact version only
183
192
  return True
184
193
  spec = SpecifierSet(req_spec)
185
- return spec.contains(version)
194
+ return spec.contains(version, prereleases=editable)
186
195
 
187
196
 
188
197
  def _check_list(
@@ -193,6 +202,10 @@ def _check_list(
193
202
  if "::" in req:
194
203
  req = req.split("::", 1)[1]
195
204
  for installed in actual:
205
+ if not isinstance(installed, str):
206
+ raise ValueError(
207
+ f"Expected installed package to be a string, got {installed}"
208
+ )
196
209
  if _check_single(
197
210
  req, installed, env_spec_dir=env_spec_dir, conda=conda
198
211
  ):
@@ -200,6 +213,46 @@ def _check_list(
200
213
  return False
201
214
 
202
215
 
216
+ def _split_env_dependencies(
217
+ dependencies: list[str | dict[str, str | list[str]]],
218
+ ) -> tuple[list[str], list[str]]:
219
+ """Split an environment dependency list into conda and pip deps.
220
+
221
+ Conda environment files commonly include both the plain ``"pip"`` package
222
+ marker and a nested ``{"pip": [...]}`` section. This helper normalizes the
223
+ latter so callers do not need to assume it is the final list entry or that
224
+ the pip section is already represented as a list.
225
+ """
226
+ conda_deps = []
227
+ pip_deps = []
228
+ for dep in dependencies:
229
+ if isinstance(dep, dict):
230
+ dep_pip = dep.get("pip", [])
231
+ if isinstance(dep_pip, str):
232
+ dep_pip = [dep_pip]
233
+ elif dep_pip is None:
234
+ dep_pip = []
235
+ pip_deps.extend(dep_pip)
236
+ else:
237
+ conda_deps.append(dep)
238
+ return conda_deps, pip_deps
239
+
240
+
241
+ def _get_pip_dependency_list(
242
+ dependencies: list[str | dict[str, str | list[str]]],
243
+ ) -> list[str]:
244
+ """Return a mutable pip dependency list from an env dependency list."""
245
+ for dep in dependencies:
246
+ if isinstance(dep, dict) and "pip" in dep:
247
+ dep_pip = dep["pip"]
248
+ if isinstance(dep_pip, str):
249
+ dep["pip"] = [dep_pip]
250
+ elif dep_pip is None:
251
+ dep["pip"] = []
252
+ return cast(list[str], dep["pip"])
253
+ return []
254
+
255
+
203
256
  class EnvCheckResult(BaseModel):
204
257
  env_exists: bool | None = None
205
258
  env_needs_export: bool | None = None
@@ -393,18 +446,12 @@ def check_env(
393
446
  ryaml.dump(env_check, f)
394
447
  # Determine if the env matches
395
448
  env_needs_rebuild = False
396
- if isinstance(env_check["dependencies"][-1], dict):
397
- existing_conda_deps = env_check["dependencies"][:-1]
398
- existing_pip_deps = env_check["dependencies"][-1]["pip"]
399
- else:
400
- existing_conda_deps = env_check["dependencies"]
401
- existing_pip_deps = []
402
- if isinstance(env_spec["dependencies"][-1], dict):
403
- required_conda_deps = env_spec["dependencies"][:-1]
404
- required_pip_deps = env_spec["dependencies"][-1]["pip"]
405
- else:
406
- required_conda_deps = env_spec["dependencies"]
407
- required_pip_deps = []
449
+ existing_conda_deps, existing_pip_deps = _split_env_dependencies(
450
+ env_check["dependencies"]
451
+ )
452
+ required_conda_deps, required_pip_deps = _split_env_dependencies(
453
+ env_spec["dependencies"]
454
+ )
408
455
  if relaxed:
409
456
  log_func("Running in relaxed mode; combining pip and conda deps")
410
457
  for dep in existing_pip_deps:
@@ -516,20 +563,23 @@ def check_env(
516
563
  # Note that this needs to be relative to the env lock directory,
517
564
  # since that's how pip will interpret it
518
565
  editable_pip_deps = {}
519
- if isinstance(env_spec["dependencies"][-1], dict):
520
- # Map editable install dir to package name we'd see in lock
521
- required_pip_deps = env_spec["dependencies"][-1]["pip"]
522
- for dep in required_pip_deps:
523
- if dep.startswith("-e ") or dep.startswith("--editable "):
524
- dir_path = dep.split(" ", 1)[1]
525
- if "#" in dir_path:
526
- dir_path = dir_path.split("#", 1)[0]
527
- dir_path = dir_path.strip()
528
- dir_path = os.path.join(env_spec_dir, dir_path)
529
- pkg_name = _editable_package_name_from_dir(dir_path)
530
- editable_pip_deps[pkg_name] = dir_path
531
- if isinstance(env_export["dependencies"][-1], dict):
532
- export_pip_deps = env_export["dependencies"][-1]["pip"]
566
+ required_pip_deps = _get_pip_dependency_list(env_spec["dependencies"])
567
+ for dep in required_pip_deps:
568
+ if dep.startswith("-e ") or dep.startswith("--editable "):
569
+ dir_path = dep.split(" ", 1)[1]
570
+ if "#" in dir_path:
571
+ dir_path = dir_path.split("#", 1)[0]
572
+ dir_path = dir_path.strip()
573
+ dir_path = os.path.join(env_spec_dir, dir_path)
574
+ pkg_name = _editable_package_name_from_dir(dir_path)
575
+ if verbose:
576
+ log_func(
577
+ f"Found editable pip dependency '{pkg_name}' "
578
+ f"at '{dir_path}'"
579
+ )
580
+ editable_pip_deps[pkg_name] = dir_path
581
+ export_pip_deps = _get_pip_dependency_list(env_export["dependencies"])
582
+ if export_pip_deps:
533
583
  for i, dep in enumerate(export_pip_deps):
534
584
  dep_name = re.split("[=<>]+", dep, maxsplit=1)[0]
535
585
  if dep_name in editable_pip_deps:
@@ -0,0 +1,297 @@
1
+ """Git-related functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from os import PathLike
7
+ from pathlib import Path
8
+
9
+ import git
10
+
11
+
12
+ def get_staged_files(
13
+ path: str | None = None, repo: git.Repo | None = None
14
+ ) -> list[str]:
15
+ """Get a list of staged files for the repo at ``path`` or the provided
16
+ repo.
17
+ """
18
+ if repo is None:
19
+ repo = git.Repo(path)
20
+ cmd = ["--staged", "--name-only"]
21
+ if path is not None:
22
+ cmd.append(path)
23
+ diff = repo.git.diff(cmd)
24
+ paths = diff.split("\n")
25
+ return [p for p in paths if p]
26
+
27
+
28
+ def get_changed_files(
29
+ path: str | None = None, repo: git.Repo | None = None
30
+ ) -> list[str]:
31
+ """Get a list of files that have been changed but not staged."""
32
+ if repo is None:
33
+ repo = git.Repo(path)
34
+ return [
35
+ item.a_path
36
+ for item in repo.index.diff(None)
37
+ if item.a_path is not None
38
+ ]
39
+
40
+
41
+ def get_untracked_files(
42
+ path: str | None = None, repo: git.Repo | None = None
43
+ ) -> list[str]:
44
+ """Get a list of untracked files."""
45
+ if repo is None:
46
+ repo = git.Repo(path)
47
+ return repo.untracked_files
48
+
49
+
50
+ def get_staged_files_with_status(
51
+ path: str | None = None, repo: git.Repo | None = None
52
+ ) -> list[dict]:
53
+ if repo is None:
54
+ repo = git.Repo(path)
55
+ cmd = ["--staged", "--name-status"]
56
+ if path is not None:
57
+ cmd.append(path)
58
+ diff = repo.git.diff(cmd)
59
+ paths = diff.split("\n")
60
+ res = []
61
+ for pathi in paths:
62
+ # Make sure line is not empty, e.g., a trailing newline
63
+ if pathi:
64
+ status, p = pathi.split("\t")
65
+ res.append({"status": status, "path": p})
66
+ return res
67
+
68
+
69
+ def ls_files(repo: git.Repo, *args, **kwargs) -> list[str]:
70
+ """Get a list of all files tracked by git."""
71
+ output = repo.git.ls_files(*args, **kwargs)
72
+ return [f for f in output.split("\n") if f]
73
+
74
+
75
+ def _resolve_repo_and_ignore_path(
76
+ repo: git.Repo, path: str | PathLike
77
+ ) -> tuple[git.Repo, str]:
78
+ """Resolve which repo should own ignore rules for ``path``."""
79
+ # Normalize target path to absolute from the current repo root.
80
+ repo_root = Path(repo.working_dir).resolve()
81
+ path_obj = Path(path)
82
+ if path_obj.is_absolute():
83
+ abs_path = path_obj.resolve()
84
+ else:
85
+ abs_path = (repo_root / path_obj).resolve()
86
+ # If the path is inside a submodule, use that repo and relative path.
87
+ for submodule in repo.submodules:
88
+ submodule_root = (repo_root / submodule.path).resolve()
89
+ if abs_path == submodule_root:
90
+ continue
91
+ if abs_path.is_relative_to(submodule_root):
92
+ sub_repo = submodule.module()
93
+ rel_path = abs_path.relative_to(submodule_root).as_posix()
94
+ return sub_repo, rel_path
95
+ # Fall back to a repo-relative path when possible.
96
+ try:
97
+ rel_path = abs_path.relative_to(repo_root).as_posix()
98
+ except ValueError:
99
+ rel_path = path_obj.as_posix()
100
+ return repo, rel_path
101
+
102
+
103
+ def _get_matching_gitignore_details(
104
+ repo: git.Repo, path: str
105
+ ) -> tuple[Path | None, str | None]:
106
+ """Return the repo-local gitignore file and pattern matching ``path``."""
107
+ try:
108
+ check_ignore = repo.git.check_ignore("-v", "--", path)
109
+ except git.GitCommandError:
110
+ return None, None
111
+ line = check_ignore.splitlines()[0]
112
+ try:
113
+ source_info, _ = line.split("\t", 1)
114
+ source_path, _, pattern = source_info.rsplit(":", 2)
115
+ except ValueError:
116
+ return None, None
117
+ if not source_path.endswith(".gitignore"):
118
+ return None, pattern
119
+ gitignore_path = (Path(repo.working_dir) / source_path).resolve()
120
+ try:
121
+ gitignore_path.relative_to(Path(repo.working_dir).resolve())
122
+ except ValueError:
123
+ return None, pattern
124
+ return gitignore_path, pattern
125
+
126
+
127
+ def ensure_path_is_ignored(
128
+ repo: git.Repo, path: str | PathLike
129
+ ) -> None | bool:
130
+ """Ensure that the given path is ignored by Git.
131
+
132
+ Returns True if ``.gitignore`` was modified.
133
+ """
134
+ # Resolve whether the ignore rule belongs to this repo or a submodule.
135
+ target_repo, target_path = _resolve_repo_and_ignore_path(repo, path)
136
+ # No-op if Git already ignores this path.
137
+ if target_repo.ignored(target_path):
138
+ return
139
+ # Read gitignore first to check if the path is already ignored
140
+ # If not, we don't want to add a line for it since it was added
141
+ # TODO: Add an option to remove cached (`git rm --cached`)
142
+ gitignore_path = os.path.join(target_repo.working_dir, ".gitignore")
143
+ if os.path.isfile(gitignore_path):
144
+ with open(gitignore_path) as f:
145
+ gitignore_txt = f.read()
146
+ lines = gitignore_txt.splitlines()
147
+ if target_path in lines:
148
+ return
149
+ with open(gitignore_path, "a") as f:
150
+ if (
151
+ os.path.isfile(gitignore_path)
152
+ and os.path.getsize(gitignore_path) > 0
153
+ ):
154
+ f.write("\n")
155
+ f.write(f"{target_path}\n")
156
+ return True
157
+
158
+
159
+ def ensure_path_is_not_ignored(
160
+ repo: git.Repo, path: str | PathLike
161
+ ) -> None | bool:
162
+ """Ensure a path is not ignored by Git."""
163
+ # Resolve whether the unignore rule belongs to this repo or a submodule.
164
+ target_repo, target_path = _resolve_repo_and_ignore_path(repo, path)
165
+ # No-op if Git does not ignore this path.
166
+ if not target_repo.ignored(target_path):
167
+ return
168
+ matching_gitignore_path, matched_pattern = _get_matching_gitignore_details(
169
+ target_repo, target_path
170
+ )
171
+ if matching_gitignore_path is not None:
172
+ gitignore_path = matching_gitignore_path.as_posix()
173
+ path_for_gitignore = (
174
+ (Path(target_repo.working_dir) / target_path)
175
+ .resolve()
176
+ .relative_to(matching_gitignore_path.parent.resolve())
177
+ .as_posix()
178
+ )
179
+ else:
180
+ gitignore_path = os.path.join(target_repo.working_dir, ".gitignore")
181
+ path_for_gitignore = target_path
182
+ if not os.path.isfile(gitignore_path):
183
+ with open(gitignore_path, "w") as f:
184
+ f.write(f"!{path_for_gitignore}\n")
185
+ return True
186
+ with open(gitignore_path) as f:
187
+ gitignore_txt = f.read()
188
+ lines = gitignore_txt.splitlines()
189
+ direct_rule_variants = [path_for_gitignore, f"/{path_for_gitignore}"]
190
+ if matched_pattern is not None and matched_pattern.startswith("/"):
191
+ no_ignore_line = f"!/{path_for_gitignore}"
192
+ else:
193
+ no_ignore_line = f"!{path_for_gitignore}"
194
+ path_parts = Path(path_for_gitignore).parts
195
+
196
+ def ancestor_requires_recursive_unignore() -> bool:
197
+ """Return True if any ancestor-level ignore rule would block this path.
198
+
199
+ This includes explicit directory ignores (e.g. 'dir/' or '/dir/')
200
+ as well as ancestor-based glob patterns like 'dir/*' or '/dir/*',
201
+ i.e., any rule that would prevent reaching the nested path without
202
+ adding recursive unignore patterns.
203
+ """
204
+ for i in range(1, len(path_parts)):
205
+ ancestor = "/".join(path_parts[:i])
206
+ if (
207
+ ancestor in lines
208
+ or f"/{ancestor}" in lines
209
+ or f"{ancestor}/" in lines
210
+ or f"/{ancestor}/" in lines
211
+ or f"{ancestor}/*" in lines
212
+ or f"/{ancestor}/*" in lines
213
+ ):
214
+ return True
215
+ return False
216
+
217
+ if len(path_parts) == 1:
218
+ # Simple (non-nested) path: remove the direct ignore rule, or add a
219
+ # negation if the ignore comes from a glob or other pattern
220
+ direct_rule = next(
221
+ (rule for rule in direct_rule_variants if rule in lines), None
222
+ )
223
+ if direct_rule is not None:
224
+ lines.remove(direct_rule)
225
+ else:
226
+ # Remove any stale negation and re-append at the end so it takes
227
+ # precedence over any later re-ignore rule
228
+ if no_ignore_line in lines:
229
+ lines.remove(no_ignore_line)
230
+ lines.append(no_ignore_line)
231
+ else:
232
+ # Nested path: only apply recursive un-ignore rules when an ancestor
233
+ # directory is explicitly ignored
234
+ # Otherwise, remove a direct ignore
235
+ # rule for this path or add a simple negation if needed
236
+ removed_direct_rule = False
237
+ direct_rule = next(
238
+ (rule for rule in direct_rule_variants if rule in lines), None
239
+ )
240
+ if direct_rule is not None:
241
+ lines.remove(direct_rule)
242
+ removed_direct_rule = True
243
+ if ancestor_requires_recursive_unignore():
244
+ # Git will not traverse into a directory excluded by a "dir/"
245
+ # pattern, so a bare "!dir/sub/file" negation has no effect.
246
+ # We need to:
247
+ # 1. Convert any "ancestor/" (or "ancestor") exclude to
248
+ # "ancestor/*" so git traverses the directory while still
249
+ # ignoring direct children by default.
250
+ # 2. Add "!ancestor/" rules for intermediate directories.
251
+ # 3. Add "ancestor/*" re-ignore rules for each intermediate dir.
252
+ # 4. Add "!target_path" for the specific file.
253
+ for i in range(1, len(path_parts)):
254
+ ancestor = "/".join(path_parts[:i])
255
+ reignore_glob = f"{ancestor}/*"
256
+ if f"{ancestor}/" in lines:
257
+ idx = lines.index(f"{ancestor}/")
258
+ lines[idx] = reignore_glob
259
+ elif f"/{ancestor}/" in lines:
260
+ idx = lines.index(f"/{ancestor}/")
261
+ lines[idx] = f"/{ancestor}/*"
262
+ elif ancestor in lines:
263
+ idx = lines.index(ancestor)
264
+ lines[idx] = reignore_glob
265
+ elif f"/{ancestor}" in lines:
266
+ idx = lines.index(f"/{ancestor}")
267
+ lines[idx] = f"/{ancestor}/*"
268
+ no_ignore_dir = f"!{ancestor}/"
269
+ anchored_no_ignore_dir = f"!/{ancestor}/"
270
+ # The first ancestor does not need an explicit un-ignore once
271
+ # converted to "ancestor/*". Deeper ancestors do.
272
+ if i > 1:
273
+ # Remove stale entry and re-append so it takes precedence
274
+ if no_ignore_dir in lines:
275
+ lines.remove(no_ignore_dir)
276
+ elif anchored_no_ignore_dir in lines:
277
+ lines.remove(anchored_no_ignore_dir)
278
+ lines.append(no_ignore_dir)
279
+ if (
280
+ reignore_glob not in lines
281
+ and f"/{ancestor}/*" not in lines
282
+ ):
283
+ lines.append(reignore_glob)
284
+ # Remove stale negation and re-append at the end so it takes
285
+ # precedence over any later re-ignore rule
286
+ if no_ignore_line in lines:
287
+ lines.remove(no_ignore_line)
288
+ lines.append(no_ignore_line)
289
+ elif not removed_direct_rule:
290
+ # The path may be ignored by a non-directory pattern (e.g., glob);
291
+ # remove stale negation and append at end so it takes precedence
292
+ if no_ignore_line in lines:
293
+ lines.remove(no_ignore_line)
294
+ lines.append(no_ignore_line)
295
+ with open(gitignore_path, "w") as f:
296
+ f.write(os.linesep.join(lines))
297
+ return True
@@ -317,6 +317,32 @@ class OverleafSyncPaths:
317
317
  - self.files_in_overleaf_last_sync
318
318
  )
319
319
 
320
+ @cached_property
321
+ def dvc_files(self) -> set[str]:
322
+ """Files tracked by DVC within the Overleaf project folder.
323
+
324
+ These paths are relative to the project directory (i.e., relative to
325
+ the Overleaf repo root). Files tracked by DVC may not exist on disk if
326
+ they haven't been pulled, but should still be kept on Overleaf rather
327
+ than deleted.
328
+ """
329
+ try:
330
+ import calkit.dvc
331
+
332
+ dvc_paths = calkit.dvc.list_paths(
333
+ wdir=str(self.main_repo.working_dir), recursive=True
334
+ )
335
+ except Exception as e:
336
+ warnings.warn(f"Could not list DVC files: {e}")
337
+ return set()
338
+ prefix = Path(self.path_in_project).as_posix().rstrip("/") + "/"
339
+ result = set()
340
+ for p in dvc_paths:
341
+ p_posix = Path(p).as_posix()
342
+ if p_posix.startswith(prefix):
343
+ result.add(p_posix[len(prefix) :])
344
+ return result
345
+
320
346
  @cached_property
321
347
  def files_to_keep_on_overleaf(self) -> set[str]:
322
348
  """Files that should be preserved on Overleaf.
@@ -324,9 +350,14 @@ class OverleafSyncPaths:
324
350
  This includes:
325
351
  1. All files being copied from local
326
352
  2. Any files newly added on Overleaf since last sync
353
+ 3. Any files tracked by DVC within the project path (these may not
354
+ exist on disk if not pulled, but should not be deleted from
355
+ Overleaf)
327
356
  """
328
357
  return (
329
- set(self.files_to_copy_to_overleaf) | self.newly_added_on_overleaf
358
+ set(self.files_to_copy_to_overleaf)
359
+ | self.newly_added_on_overleaf
360
+ | self.dvc_files
330
361
  )
331
362
 
332
363
  @cached_property
@@ -117,6 +117,20 @@ def to_dvc(
117
117
  except Exception as e:
118
118
  raise ValueError(f"Pipeline is not defined properly: {e}")
119
119
  dvc_stages = {}
120
+ # Read existing dvc.yaml now so we can clean up stale .gitignore entries
121
+ # when stage outputs are renamed or removed
122
+ if write:
123
+ dvc_yaml_path = os.path.join(wdir, "dvc.yaml") if wdir else "dvc.yaml"
124
+ if os.path.isfile(dvc_yaml_path):
125
+ with open(dvc_yaml_path) as f:
126
+ existing_dvc_yaml = calkit.ryaml.load(f)
127
+ else:
128
+ existing_dvc_yaml = {}
129
+ if existing_dvc_yaml is None:
130
+ existing_dvc_yaml = {}
131
+ existing_dvc_stages = existing_dvc_yaml.get("stages", {})
132
+ else:
133
+ existing_dvc_stages = {}
120
134
  # First, gather up any env lock paths we might need for DVC deps
121
135
  used_envs = set(
122
136
  [stage.inner_environment for stage in pipeline.stages.values()]
@@ -227,6 +241,17 @@ def to_dvc(
227
241
  outputs += stage.notebook_outputs
228
242
  elif stage.kind == "sbatch":
229
243
  outputs.append(stage.log_output)
244
+ # Build the set of current DVC output paths so we can detect stale
245
+ # .gitignore entries from the previous version of the stage,
246
+ # including synthesized outputs like LaTeX PDFs
247
+ current_out_paths = set(calkit.dvc.out_paths_from_stage(dvc_stage))
248
+ # If this stage already existed, un-ignore any outputs that have
249
+ # been renamed or removed so .gitignore does not accumulate stale
250
+ # entries (e.g., after a capitalization change in the path)
251
+ old_stage = existing_dvc_stages.get(stage_name, {})
252
+ for old_path in calkit.dvc.out_paths_from_stage(old_stage):
253
+ if old_path not in current_out_paths:
254
+ calkit.git.ensure_path_is_not_ignored(repo, path=old_path)
230
255
  # Deal with any gitignore changes necessary
231
256
  for out in outputs:
232
257
  if isinstance(out, PathOutput) and out.storage is None:
@@ -268,14 +293,8 @@ def to_dvc(
268
293
  else:
269
294
  dvc_stages[stage_name]["deps"].append(out)
270
295
  if write:
271
- if os.path.isfile("dvc.yaml"):
272
- with open("dvc.yaml") as f:
273
- dvc_yaml = calkit.ryaml.load(f)
274
- else:
275
- dvc_yaml = {}
276
- if dvc_yaml is None:
277
- dvc_yaml = {}
278
- existing_stages = dvc_yaml.get("stages", {})
296
+ dvc_yaml = existing_dvc_yaml
297
+ existing_stages = existing_dvc_stages
279
298
  for stage_name, stage in existing_stages.items():
280
299
  # Skip private stages (ones whose names start with an underscore)
281
300
  # and stages that are automatically generated
@@ -181,6 +181,44 @@ def test_overleaf(tmp_dir):
181
181
  subprocess.run(["calkit", "overleaf", "sync", "--verbose"], check=True)
182
182
  print("Overleaf Git show after adding fig2 back:", ol_repo.git.show())
183
183
  assert "ol-project/figs/fig2.txt" in ls_files(repo)
184
+ # Test that if a file is deleted from Git but added to DVC, it is not
185
+ # deleted from Overleaf (the file still logically exists in the DVC repo)
186
+ with open(
187
+ os.path.join(repo.working_dir, "ol-project", "figs", "fig3.txt"), "w"
188
+ ) as f:
189
+ f.write("Fig3 created in main repo")
190
+ repo.git.add("ol-project/figs/fig3.txt")
191
+ repo.git.commit(["-m", "Add figure 3"])
192
+ assert "ol-project/figs/fig3.txt" in ls_files(repo)
193
+ subprocess.run(["calkit", "overleaf", "sync", "--verbose"], check=True)
194
+ ol_repo_git_show = ol_repo.git.show()
195
+ assert "diff --git a/figs/fig3.txt b/figs/fig3.txt" in ol_repo_git_show
196
+ # Now move from Git to DVC: first remove from Git index (keeping file on
197
+ # disk), then add to DVC so it gets moved to DVC cache
198
+ repo.git.rm(["--cached", "ol-project/figs/fig3.txt"])
199
+ subprocess.run(
200
+ ["dvc", "add", "ol-project/figs/fig3.txt"],
201
+ check=True,
202
+ cwd=repo.working_dir,
203
+ )
204
+ # Commit the DVC pointer file (fig3.txt is now tracked by DVC, not Git)
205
+ repo.git.add("ol-project/figs/fig3.txt.dvc", "ol-project/figs/.gitignore")
206
+ repo.git.commit(["-m", "Move figure 3 from git to DVC"])
207
+ assert "ol-project/figs/fig3.txt" not in ls_files(repo)
208
+ # Also remove the local file to simulate the file not being pulled from
209
+ # DVC (i.e., only the DVC pointer exists locally, not the actual file)
210
+ fig3_path = os.path.join(
211
+ repo.working_dir, "ol-project", "figs", "fig3.txt"
212
+ )
213
+ if os.path.exists(fig3_path):
214
+ os.remove(fig3_path)
215
+ assert not os.path.exists(fig3_path)
216
+ subprocess.run(["calkit", "overleaf", "sync", "--verbose"], check=True)
217
+ ol_repo_git_show = ol_repo.git.show()
218
+ print("Git show in OL repo after moving fig3 to DVC:\n", ol_repo_git_show)
219
+ # The file should not have been deleted from Overleaf
220
+ assert "deleted file mode" not in ol_repo_git_show
221
+ assert "--- a/figs/fig3.txt" not in ol_repo_git_show
184
222
 
185
223
 
186
224
  def test_extract_title_from_tex(tmp_dir):