calkit-python 0.41.11__tar.gz → 0.41.13__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 (341) hide show
  1. {calkit_python-0.41.11 → calkit_python-0.41.13}/PKG-INFO +1 -1
  2. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/list.py +94 -28
  3. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/main/core.py +5 -3
  4. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/scheduler.py +231 -31
  5. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cloud.py +29 -12
  6. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/detect.py +433 -75
  7. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/dvc/core.py +67 -2
  8. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/fs.py +78 -40
  9. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/main/test_core.py +3 -2
  10. calkit_python-0.41.13/calkit/tests/cli/test_scheduler.py +404 -0
  11. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/dvc/test_core.py +69 -0
  12. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_cloud.py +96 -0
  13. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_detect.py +116 -0
  14. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_fs.py +121 -0
  15. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/cli-reference.md +16 -2
  16. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/package.json +34 -1
  17. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/extension.ts +198 -152
  18. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/sidebar.ts +5 -0
  19. calkit_python-0.41.11/calkit/tests/cli/test_scheduler.py +0 -201
  20. {calkit_python-0.41.11 → calkit_python-0.41.13}/.claude-plugin/marketplace.json +0 -0
  21. {calkit_python-0.41.11 → calkit_python-0.41.13}/.gitattributes +0 -0
  22. {calkit_python-0.41.11 → calkit_python-0.41.13}/.gitignore +0 -0
  23. {calkit_python-0.41.11 → calkit_python-0.41.13}/.pre-commit-config.yaml +0 -0
  24. {calkit_python-0.41.11 → calkit_python-0.41.13}/.prettierignore +0 -0
  25. {calkit_python-0.41.11 → calkit_python-0.41.13}/.python-version +0 -0
  26. {calkit_python-0.41.11 → calkit_python-0.41.13}/.vscode/launch.json +0 -0
  27. {calkit_python-0.41.11 → calkit_python-0.41.13}/.vscode/tasks.json +0 -0
  28. {calkit_python-0.41.11 → calkit_python-0.41.13}/.yarnrc.yml +0 -0
  29. {calkit_python-0.41.11 → calkit_python-0.41.13}/AGENTS.md +0 -0
  30. {calkit_python-0.41.11 → calkit_python-0.41.13}/CITATION.cff +0 -0
  31. {calkit_python-0.41.11 → calkit_python-0.41.13}/CODE_OF_CONDUCT.md +0 -0
  32. {calkit_python-0.41.11 → calkit_python-0.41.13}/CONTRIBUTING.md +0 -0
  33. {calkit_python-0.41.11 → calkit_python-0.41.13}/LICENSE +0 -0
  34. {calkit_python-0.41.11 → calkit_python-0.41.13}/Makefile +0 -0
  35. {calkit_python-0.41.11 → calkit_python-0.41.13}/README.md +0 -0
  36. {calkit_python-0.41.11 → calkit_python-0.41.13}/agent-plugin/.claude-plugin/plugin.json +0 -0
  37. {calkit_python-0.41.11 → calkit_python-0.41.13}/agent-plugin/skills/add-pipeline-stage/SKILL.md +0 -0
  38. {calkit_python-0.41.11 → calkit_python-0.41.13}/agent-plugin/skills/conventions/SKILL.md +0 -0
  39. {calkit_python-0.41.11 → calkit_python-0.41.13}/agent-plugin/skills/create-pipeline/SKILL.md +0 -0
  40. {calkit_python-0.41.11 → calkit_python-0.41.13}/babel.config.js +0 -0
  41. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/__init__.py +0 -0
  42. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/__main__.py +0 -0
  43. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/calc.py +0 -0
  44. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/check.py +0 -0
  45. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/__init__.py +0 -0
  46. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/check.py +0 -0
  47. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/cloud.py +0 -0
  48. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/config.py +0 -0
  49. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/core.py +0 -0
  50. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/describe.py +0 -0
  51. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/dev.py +0 -0
  52. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/import_.py +0 -0
  53. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/latex.py +0 -0
  54. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/main/__init__.py +0 -0
  55. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/main/xr.py +0 -0
  56. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/new.py +0 -0
  57. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/notebooks.py +0 -0
  58. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/office.py +0 -0
  59. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/overleaf.py +0 -0
  60. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/cli/update.py +0 -0
  61. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/conda.py +0 -0
  62. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/config.py +0 -0
  63. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/core.py +0 -0
  64. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/datasets.py +0 -0
  65. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/dependencies.py +0 -0
  66. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/docker.py +0 -0
  67. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/dvc/__init__.py +0 -0
  68. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/dvc/zip.py +0 -0
  69. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/environments.py +0 -0
  70. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/git.py +0 -0
  71. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/github.py +0 -0
  72. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/gui.py +0 -0
  73. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/install.py +0 -0
  74. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/invenio.py +0 -0
  75. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/julia.py +0 -0
  76. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/jupyter.py +0 -0
  77. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/jupyterlab/__init__.py +0 -0
  78. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/jupyterlab/routes.py +0 -0
  79. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/package.json +0 -0
  80. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  81. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  82. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  83. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  84. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  85. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  86. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  87. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  88. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  89. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  90. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/style.js +0 -0
  91. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/labextension/static/third-party-licenses.json +0 -0
  92. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/licenses.py +0 -0
  93. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/magics.py +0 -0
  94. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/matlab.py +0 -0
  95. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/models/__init__.py +0 -0
  96. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/models/core.py +0 -0
  97. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/models/io.py +0 -0
  98. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/models/iteration.py +0 -0
  99. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/models/pipeline.py +0 -0
  100. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/notebooks.py +0 -0
  101. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/office.py +0 -0
  102. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/ops.py +0 -0
  103. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/overleaf.py +0 -0
  104. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/pipeline.py +0 -0
  105. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/releases.py +0 -0
  106. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/server.py +0 -0
  107. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/__init__.py +0 -0
  108. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/core.py +0 -0
  109. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/__init__.py +0 -0
  110. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/article/paper.tex +0 -0
  111. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/core.py +0 -0
  112. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/jfm/jfm.bst +0 -0
  113. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/jfm/jfm.cls +0 -0
  114. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  115. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/jfm/paper.tex +0 -0
  116. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/templates/latex/jfm/upmath.sty +0 -0
  117. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/__init__.py +0 -0
  118. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/__init__.py +0 -0
  119. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/main/__init__.py +0 -0
  120. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/main/test_subprojects.py +0 -0
  121. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/main/test_xr.py +0 -0
  122. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_check.py +0 -0
  123. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_cloud.py +0 -0
  124. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_config.py +0 -0
  125. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_import.py +0 -0
  126. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_latex.py +0 -0
  127. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_list.py +0 -0
  128. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_new.py +0 -0
  129. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_notebooks.py +0 -0
  130. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_overleaf.py +0 -0
  131. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/cli/test_update.py +0 -0
  132. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/dvc/__init__.py +0 -0
  133. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/dvc/test_zip.py +0 -0
  134. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/jupyterlab/__init__.py +0 -0
  135. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/jupyterlab/test_routes.py +0 -0
  136. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/models/__init__.py +0 -0
  137. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/models/test_iteration.py +0 -0
  138. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/models/test_pipeline.py +0 -0
  139. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_calc.py +0 -0
  140. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_check.py +0 -0
  141. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_conda.py +0 -0
  142. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_core.py +0 -0
  143. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_dependencies.py +0 -0
  144. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_docker.py +0 -0
  145. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_environments.py +0 -0
  146. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_git.py +0 -0
  147. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_install.py +0 -0
  148. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_invenio.py +0 -0
  149. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_julia.py +0 -0
  150. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_jupyter.py +0 -0
  151. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_licenses.py +0 -0
  152. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_magics.py +0 -0
  153. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_matlab.py +0 -0
  154. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_notebooks.py +0 -0
  155. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_pipeline.py +0 -0
  156. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_releases.py +0 -0
  157. {calkit_python-0.41.11 → calkit_python-0.41.13}/calkit/tests/test_templates.py +0 -0
  158. {calkit_python-0.41.11 → calkit_python-0.41.13}/conftest.py +0 -0
  159. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/CNAME +0 -0
  160. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/ai-tools.md +0 -0
  161. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/apps.md +0 -0
  162. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/calculations.md +0 -0
  163. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/calkit-yaml.md +0 -0
  164. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/cloud-integration.md +0 -0
  165. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/datasets.md +0 -0
  166. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/dependencies.md +0 -0
  167. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/environments.md +0 -0
  168. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/examples.md +0 -0
  169. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/governance.md +0 -0
  170. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/help.md +0 -0
  171. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/hpc.md +0 -0
  172. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/c-to-the-k-white.svg +0 -0
  173. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/calkit-fragmentation-compendium.png +0 -0
  174. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/calkit-no-bg.png +0 -0
  175. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/connect-zenodo.png +0 -0
  176. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab/all-green.png +0 -0
  177. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  178. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab/new-env.png +0 -0
  179. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab/new-notebook.png +0 -0
  180. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  181. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/jupyterlab-params.png +0 -0
  182. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/plos-osi-code-2024-03.png +0 -0
  183. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/img/vscode-nb-params.png +0 -0
  184. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/index.md +0 -0
  185. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/installation.md +0 -0
  186. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/jupyterlab.md +0 -0
  187. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/local-server.md +0 -0
  188. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/notebooks.md +0 -0
  189. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/overleaf.md +0 -0
  190. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/pipeline/index.md +0 -0
  191. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/pipeline/manual-steps.md +0 -0
  192. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/pipeline/running-and-logging.md +0 -0
  193. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/quickstart.md +0 -0
  194. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/references.md +0 -0
  195. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/releases.md +0 -0
  196. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/reproducibility.md +0 -0
  197. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/subprojects.md +0 -0
  198. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  199. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/ai-agents.md +0 -0
  200. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/conda-envs.md +0 -0
  201. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/existing-project.md +0 -0
  202. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/first-project.md +0 -0
  203. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/github-actions.md +0 -0
  204. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  205. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  206. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  207. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  208. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  209. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  210. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  211. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  212. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  213. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  214. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  215. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  216. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  217. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  218. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  219. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/create-project.png +0 -0
  220. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  221. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/excel-chart.png +0 -0
  222. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/excel-data.png +0 -0
  223. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  224. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/needs-clone.png +0 -0
  225. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/new-stage.png +0 -0
  226. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  227. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  228. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/status-more-rows.png +0 -0
  229. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  230. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/untracked-data.png +0 -0
  231. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/updated-publication.png +0 -0
  232. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  233. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/office/workflow-page.png +0 -0
  234. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/clone.png +0 -0
  235. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/create-project.png +0 -0
  236. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  237. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  238. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  239. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/new-token.png +0 -0
  240. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/reclone.png +0 -0
  241. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  242. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/quick-actions.png +0 -0
  243. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/run-proc.png +0 -0
  244. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
  245. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
  246. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
  247. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
  248. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
  249. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
  250. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
  251. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/index.md +0 -0
  252. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/jupyterlab.md +0 -0
  253. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/latex-codespaces.md +0 -0
  254. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/matlab.md +0 -0
  255. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/notebook-pipeline.md +0 -0
  256. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/office.md +0 -0
  257. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/openfoam.md +0 -0
  258. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/procedures.md +0 -0
  259. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/tutorials/vscode-slurm-notebook.md +0 -0
  260. {calkit_python-0.41.11 → calkit_python-0.41.13}/docs/version-control.md +0 -0
  261. {calkit_python-0.41.11 → calkit_python-0.41.13}/flake.lock +0 -0
  262. {calkit_python-0.41.11 → calkit_python-0.41.13}/flake.nix +0 -0
  263. {calkit_python-0.41.11 → calkit_python-0.41.13}/install.json +0 -0
  264. {calkit_python-0.41.11 → calkit_python-0.41.13}/jest.config.js +0 -0
  265. {calkit_python-0.41.11 → calkit_python-0.41.13}/jupyter-config/server-config/calkit.json +0 -0
  266. {calkit_python-0.41.11 → calkit_python-0.41.13}/mkdocs.yml +0 -0
  267. {calkit_python-0.41.11 → calkit_python-0.41.13}/package.json +0 -0
  268. {calkit_python-0.41.11 → calkit_python-0.41.13}/pyproject.toml +0 -0
  269. {calkit_python-0.41.11 → calkit_python-0.41.13}/schema/plugin.json +0 -0
  270. {calkit_python-0.41.11 → calkit_python-0.41.13}/scripts/generate-docs-references.py +0 -0
  271. {calkit_python-0.41.11 → calkit_python-0.41.13}/scripts/install.ps1 +0 -0
  272. {calkit_python-0.41.11 → calkit_python-0.41.13}/scripts/install.sh +0 -0
  273. {calkit_python-0.41.11 → calkit_python-0.41.13}/scripts/make-calk9.sh +0 -0
  274. {calkit_python-0.41.11 → calkit_python-0.41.13}/scripts/sync-docs.py +0 -0
  275. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/__tests__/useQueries.spec.ts +0 -0
  276. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/calkit-config.ts +0 -0
  277. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/cell-output-marker.ts +0 -0
  278. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/commit-dialog.tsx +0 -0
  279. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/environment-editor.tsx +0 -0
  280. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/notebook-registration.tsx +0 -0
  281. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/notebook-toolbar.tsx +0 -0
  282. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/pipeline-status-bar.tsx +0 -0
  283. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/project-info-editor.tsx +0 -0
  284. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/sidebar-settings.tsx +0 -0
  285. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/sidebar.tsx +0 -0
  286. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/components/stage-editor.tsx +0 -0
  287. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/feature-flags.ts +0 -0
  288. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/file-browser-menu.ts +0 -0
  289. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/hooks/__tests__/useQueries.test.tsx +0 -0
  290. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/hooks/useQueries.ts +0 -0
  291. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/icons.ts +0 -0
  292. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/index.ts +0 -0
  293. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/io-tracker.ts +0 -0
  294. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/pipeline-state.ts +0 -0
  295. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/queryClient.ts +0 -0
  296. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/request.ts +0 -0
  297. {calkit_python-0.41.11 → calkit_python-0.41.13}/src/shims-mainmenu.d.ts +0 -0
  298. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/base.css +0 -0
  299. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/cell-output-marker.css +0 -0
  300. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/environment-editor.css +0 -0
  301. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/environment-selector.css +0 -0
  302. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/img/calkit-no-bg.png +0 -0
  303. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/index.css +0 -0
  304. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/index.js +0 -0
  305. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/notebook-toolbar.css +0 -0
  306. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/pipeline-status-bar.css +0 -0
  307. {calkit_python-0.41.11 → calkit_python-0.41.13}/style/sidebar.css +0 -0
  308. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  309. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/nb-julia.ipynb +0 -0
  310. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/nb-params.ipynb +0 -0
  311. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/nb-subdir.ipynb +0 -0
  312. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/pipeline.ipynb +0 -0
  313. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/script.py +0 -0
  314. {calkit_python-0.41.11 → calkit_python-0.41.13}/test/test-log.log +0 -0
  315. {calkit_python-0.41.11 → calkit_python-0.41.13}/tsconfig.json +0 -0
  316. {calkit_python-0.41.11 → calkit_python-0.41.13}/tsconfig.test.json +0 -0
  317. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/.gitignore +0 -0
  318. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/README.md +0 -0
  319. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/jupyter_server_test_config.py +0 -0
  320. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/package.json +0 -0
  321. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/playwright.config.js +0 -0
  322. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/tests/calkit.spec.ts +0 -0
  323. {calkit_python-0.41.11 → calkit_python-0.41.13}/ui-tests/yarn.lock +0 -0
  324. {calkit_python-0.41.11 → calkit_python-0.41.13}/uv.lock +0 -0
  325. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/.gitignore +0 -0
  326. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/.vscodeignore +0 -0
  327. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/CHANGELOG.md +0 -0
  328. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/LICENSE +0 -0
  329. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/README.md +0 -0
  330. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/images/calkit-icon.svg +0 -0
  331. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/images/calkit-no-bg.png +0 -0
  332. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/package-lock.json +0 -0
  333. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/scripts/set-proposed-api.js +0 -0
  334. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/environments.ts +0 -0
  335. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/notebooks.ts +0 -0
  336. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/test/environments.test.ts +0 -0
  337. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/test/kernel-selection.test.ts +0 -0
  338. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/test/notebooks.test.ts +0 -0
  339. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/src/types.ts +0 -0
  340. {calkit_python-0.41.11 → calkit_python-0.41.13}/vscode-ext/tsconfig.json +0 -0
  341. {calkit_python-0.41.11 → calkit_python-0.41.13}/yarn.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.41.11
3
+ Version: 0.41.13
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from typing import Annotated, Literal
6
7
 
7
8
  import typer
@@ -12,6 +13,32 @@ from calkit.cli import AliasGroup, raise_error, warn
12
13
  list_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
13
14
 
14
15
 
16
+ def _echo_object(obj: dict) -> None:
17
+ """Print a single object in the human-readable YAML-ish listing format."""
18
+ # Copy so popping 'path' doesn't mutate the caller's dict.
19
+ obj = dict(obj)
20
+ path = obj.pop("path", None)
21
+ typer.echo(f"- path: {path}")
22
+ for k, v in obj.items():
23
+ if isinstance(v, dict):
24
+ typer.echo(f" {k}:")
25
+ for k1, v1 in v.items():
26
+ typer.echo(f" {k1}: {v1}")
27
+ elif isinstance(v, list):
28
+ typer.echo(f" {k}:")
29
+ for item in v:
30
+ if isinstance(item, dict):
31
+ for n, (k1, v1) in enumerate(item.items()):
32
+ if n == 0:
33
+ typer.echo(f" - {k1}: {v1}")
34
+ else:
35
+ typer.echo(f" {k1}: {v1}")
36
+ else:
37
+ typer.echo(f" - {item}")
38
+ else:
39
+ typer.echo(f" {k}: {v}")
40
+
41
+
15
42
  def _list_objects(
16
43
  kind: Literal[
17
44
  "notebooks",
@@ -21,33 +48,44 @@ def _list_objects(
21
48
  "publications",
22
49
  ],
23
50
  ):
24
- """List objects.
51
+ """List objects."""
52
+ ck_info = calkit.load_calkit_info()
53
+ for obj in ck_info.get(kind, []) or []:
54
+ _echo_object(obj)
55
+
25
56
 
26
- TODO: This should probably just use some library to dump YAML to string.
57
+ def _list_artifacts(
58
+ kind: Literal["figures", "datasets"],
59
+ json_output: bool,
60
+ declared_only: bool,
61
+ ):
62
+ """List figures or datasets, optionally including auto-detected ones.
63
+
64
+ By default, artifacts declared in ``calkit.yaml`` are merged with any
65
+ auto-detected from the project's files; ``--declared-only`` returns just the
66
+ declared ones. Each entry carries a ``detected`` flag so callers can tell
67
+ declared and auto-detected artifacts apart.
27
68
  """
28
69
  ck_info = calkit.load_calkit_info()
29
- objects = ck_info.get(kind, [])
30
- for obj in objects:
31
- path = obj.pop("path")
32
- typer.echo(f"- path: {path}")
33
- for k, v in obj.items():
34
- if isinstance(v, dict):
35
- typer.echo(f" {k}:")
36
- for k1, v1 in v.items():
37
- typer.echo(f" {k1}: {v1}")
38
- elif isinstance(v, list):
39
- typer.echo(f" {k}:")
40
- for item in v:
41
- if isinstance(item, dict):
42
- for n, (k1, v1) in enumerate(item.items()):
43
- if n == 0:
44
- typer.echo(f" - {k1}: {v1}")
45
- else:
46
- typer.echo(f" {k1}: {v1}")
47
- else:
48
- typer.echo(f" - {item}")
49
- else:
50
- typer.echo(f" {k}: {v}")
70
+ declared = ck_info.get(kind, []) or []
71
+ declared_paths = {o.get("path") for o in declared if isinstance(o, dict)}
72
+ detected: list[dict] = []
73
+ if not declared_only:
74
+ found = calkit.detect.detect_project_artifacts(ck_info=ck_info)
75
+ for path in found.get(kind, []):
76
+ if path not in declared_paths:
77
+ detected.append({"path": path})
78
+ if json_output:
79
+ result = [
80
+ {**o, "detected": False} for o in declared if isinstance(o, dict)
81
+ ]
82
+ result += [{**o, "detected": True} for o in detected]
83
+ typer.echo(json.dumps(result))
84
+ return
85
+ for obj in declared:
86
+ _echo_object(obj)
87
+ for obj in detected:
88
+ _echo_object({**obj, "detected": True})
51
89
 
52
90
 
53
91
  @list_app.command(name="notebooks|nb")
@@ -57,15 +95,43 @@ def list_notebooks():
57
95
 
58
96
 
59
97
  @list_app.command(name="figures|figs")
60
- def list_figures():
98
+ def list_figures(
99
+ json_output: Annotated[
100
+ bool, typer.Option("--json", help="Output result as JSON.")
101
+ ] = False,
102
+ declared_only: Annotated[
103
+ bool,
104
+ typer.Option(
105
+ "--declared-only",
106
+ help=(
107
+ "Only list figures declared in calkit.yaml; "
108
+ "skip auto-detection."
109
+ ),
110
+ ),
111
+ ] = False,
112
+ ):
61
113
  """List figures in the project."""
62
- _list_objects("figures")
114
+ _list_artifacts("figures", json_output, declared_only)
63
115
 
64
116
 
65
117
  @list_app.command(name="datasets")
66
- def list_datasets():
118
+ def list_datasets(
119
+ json_output: Annotated[
120
+ bool, typer.Option("--json", help="Output result as JSON.")
121
+ ] = False,
122
+ declared_only: Annotated[
123
+ bool,
124
+ typer.Option(
125
+ "--declared-only",
126
+ help=(
127
+ "Only list datasets declared in calkit.yaml; "
128
+ "skip auto-detection."
129
+ ),
130
+ ),
131
+ ] = False,
132
+ ):
67
133
  """List datasets in the project."""
68
- _list_objects("datasets")
134
+ _list_artifacts("datasets", json_output, declared_only)
69
135
 
70
136
 
71
137
  @list_app.command(name="publications|pubs")
@@ -1172,7 +1172,7 @@ def pull(
1172
1172
  and "-R" not in dvc_args
1173
1173
  ):
1174
1174
  dvc_args.append("--recursive")
1175
- result = calkit.dvc.run_dvc_command(["pull"] + dvc_args)
1175
+ result = calkit.dvc.run_dvc_command(["pull"] + dvc_args, lock_retries=5)
1176
1176
  if result != 0:
1177
1177
  raise_error("DVC pull failed")
1178
1178
  calkit.dvc.zip.sync_all(direction="to-workspace")
@@ -1187,7 +1187,7 @@ def pull(
1187
1187
  continue
1188
1188
  typer.echo(f"DVC pulling subproject: {sp_path}")
1189
1189
  sp_result = calkit.dvc.run_dvc_command(
1190
- ["pull"] + dvc_args, cwd=sp_path
1190
+ ["pull"] + dvc_args, cwd=sp_path, lock_retries=5
1191
1191
  )
1192
1192
  if sp_result != 0:
1193
1193
  raise_error(f"DVC pull failed for subproject: {sp_path}")
@@ -1224,7 +1224,9 @@ def push(
1224
1224
  calkit.dvc.set_remote_auth(remote_name=name)
1225
1225
  if remotes:
1226
1226
  typer.echo("Pushing to DVC remote")
1227
- result = calkit.dvc.run_dvc_command(["push"] + dvc_args)
1227
+ result = calkit.dvc.run_dvc_command(
1228
+ ["push"] + dvc_args, lock_retries=5
1229
+ )
1228
1230
  if result != 0:
1229
1231
  raise_error("DVC push failed")
1230
1232
  else:
@@ -15,15 +15,17 @@ import shutil
15
15
  import signal
16
16
  import socket
17
17
  import subprocess
18
+ import sys
18
19
  import time
19
20
  import uuid
21
+ from typing import Any
20
22
 
21
23
  import typer
22
24
  from sqlitedict import SqliteDict
23
25
  from typing_extensions import Annotated
24
26
 
25
27
  import calkit
26
- from calkit.cli import AliasGroup, raise_error
28
+ from calkit.cli import AliasGroup, raise_error, warn
27
29
 
28
30
  scheduler_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
29
31
 
@@ -177,15 +179,20 @@ def _mock_submit(job_id: str, job_command: str, log_path: str) -> int:
177
179
  env = dict(os.environ)
178
180
  env.setdefault("PBS_O_WORKDIR", os.getcwd())
179
181
  with open(log_path, "w") as log_file:
180
- popen_kwargs = dict(
182
+ # Annotated so the mixed value types don't defeat Popen's overloads.
183
+ popen_kwargs: dict[str, Any] = dict(
181
184
  stdout=log_file,
182
185
  stderr=subprocess.STDOUT,
183
186
  env=env,
184
187
  )
185
- if os.name == "posix":
186
- popen_kwargs["start_new_session"] = True
187
- elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
188
+ # Detach the job into its own session/process group so it outlives
189
+ # this command and can be signaled as a group when canceled. Guard on
190
+ # `sys.platform` (not `os.name`) so the unused branch reads as
191
+ # platform-specific dead code rather than an unreachable bug.
192
+ if sys.platform == "win32":
188
193
  popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
194
+ else:
195
+ popen_kwargs["start_new_session"] = True
189
196
  proc = subprocess.Popen(
190
197
  ["bash", "-c", wrapped],
191
198
  **popen_kwargs,
@@ -213,31 +220,145 @@ def _mock_cancel(job_id: str) -> tuple[bool, str]:
213
220
  return True, ""
214
221
 
215
222
 
216
- def _is_active(kind: str, job_id: str) -> bool:
223
+ def _mock_exit_code(job_id: str) -> int | None:
224
+ # The job writes its exit code to the status sentinel before exiting; a
225
+ # canceled job writes "canceled" instead, which isn't an integer.
226
+ try:
227
+ with open(_mock_status_path(job_id)) as f:
228
+ content = f.read().strip()
229
+ except OSError:
230
+ return None
231
+ try:
232
+ return int(content)
233
+ except ValueError:
234
+ return None
235
+
236
+
237
+ def _parse_slurm_exit_code(value: str) -> int | None:
238
+ # SLURM reports exit status as "<code>:<signal>"; a job killed by a signal
239
+ # (e.g. out-of-memory, walltime) has a non-zero signal even if code is 0.
240
+ code_part, _, signal_part = value.strip().partition(":")
241
+ try:
242
+ code = int(code_part)
243
+ # An empty signal part means "no signal"; a non-empty one that won't
244
+ # parse is malformed, so report the whole value as unknown rather than
245
+ # silently treating it as a clean exit.
246
+ sig = int(signal_part) if signal_part else 0
247
+ except ValueError:
248
+ return None
249
+ return code if sig == 0 else 128 + sig
250
+
251
+
252
+ def _slurm_exit_code(job_id: str) -> int | None:
253
+ # Prefer scontrol, which reports a finished job while it lingers in the
254
+ # controller's memory and is available even without accounting; fall back
255
+ # to sacct for jobs that have already aged out of scontrol.
256
+ p = subprocess.run(
257
+ ["scontrol", "show", "job", job_id],
258
+ capture_output=True,
259
+ text=True,
260
+ check=False,
261
+ )
262
+ if p.returncode == 0 and p.stdout.strip():
263
+ state = None
264
+ exit_code = None
265
+ for token in p.stdout.split():
266
+ if token.startswith("JobState="):
267
+ state = token.split("=", 1)[1]
268
+ elif token.startswith("ExitCode="):
269
+ exit_code = _parse_slurm_exit_code(token.split("=", 1)[1])
270
+ if state is not None:
271
+ if state == "COMPLETED":
272
+ return exit_code or 0
273
+ return exit_code or 1
274
+ p = subprocess.run(
275
+ ["sacct", "-j", job_id, "-n", "-P", "-o", "State,ExitCode"],
276
+ capture_output=True,
277
+ text=True,
278
+ check=False,
279
+ )
280
+ if p.returncode == 0 and p.stdout.strip():
281
+ # The first line is the job itself (later lines are its steps).
282
+ state, _, code = p.stdout.strip().splitlines()[0].partition("|")
283
+ exit_code = _parse_slurm_exit_code(code)
284
+ if state.strip() == "COMPLETED":
285
+ return exit_code or 0
286
+ return exit_code or 1
287
+ return None
288
+
289
+
290
+ def _poll_job(kind: str, job_id: str) -> tuple[bool, int | None]:
291
+ """Return ``(active, exit_code)`` for a scheduler job.
292
+
293
+ ``active`` is whether the job is still queued or running. ``exit_code`` is
294
+ the job's exit status once it has finished---0 for success, non-zero for
295
+ failure---or ``None`` while the job is still active or when the scheduler
296
+ no longer has a record from which to read it. A ``None`` exit code on a
297
+ finished job is an unknown outcome the caller must decide how to treat.
298
+ """
217
299
  if _mock_enabled():
218
- return _mock_active(job_id)
300
+ if _mock_active(job_id):
301
+ return True, None
302
+ return False, _mock_exit_code(job_id)
219
303
  if kind == "slurm":
220
304
  p = subprocess.run(
221
- ["squeue", "--job", job_id], capture_output=True, text=True
305
+ ["squeue", "--job", job_id],
306
+ capture_output=True,
307
+ text=True,
308
+ check=False,
222
309
  )
223
- if p.returncode != 0:
224
- return False
225
- return len(p.stdout.strip().split("\n")) > 1
310
+ if p.returncode == 0:
311
+ # squeue prints a header row plus one row per matching job, so more
312
+ # than the header means the job is still queued or running.
313
+ if len(p.stdout.strip().split("\n")) > 1:
314
+ return True, None
315
+ return False, _slurm_exit_code(job_id)
316
+ # A non-zero exit is ambiguous like PBS: an invalid/unknown job id
317
+ # means the job is gone, but a transient controller failure should not
318
+ # end the wait early (nor let `_slurm_exit_code` call a still-running
319
+ # job failed), so only conclude the job is done when squeue positively
320
+ # reports the id is invalid; otherwise keep waiting.
321
+ stderr = (p.stderr or "").lower()
322
+ if "invalid job id" in stderr:
323
+ return False, _slurm_exit_code(job_id)
324
+ return True, None
226
325
  # Use `qstat -f` and parse job_state: on Torque/OpenPBS, plain `qstat
227
326
  # <id>` returns exit 0 even for completed (C) jobs, so checking the
228
327
  # return code alone would cause `calkit sched batch` to hang forever
229
- # after a PBS job finishes. States C and F mean the job is done.
328
+ # after a PBS job finishes. States C and F mean the job is done, and a
329
+ # finished job's record carries its `Exit_status`.
230
330
  p = subprocess.run(
231
331
  ["qstat", "-f", job_id], capture_output=True, text=True, check=False
232
332
  )
233
333
  if p.returncode != 0:
234
- return False
334
+ # A non-zero exit is ambiguous: either the job is gone (completed and
335
+ # purged from history) or `qstat` itself failed transiently---a busy
336
+ # PBS server periodically refuses connections or times out. Treating a
337
+ # transient failure as completion would stop the wait while the job is
338
+ # still running and writing its log, so only conclude the job is done
339
+ # when qstat positively reports it is unknown; otherwise keep waiting.
340
+ # A purged job (unknown to qstat) is done but with an unknowable exit
341
+ # status; any other error is transient, so report the job as active.
342
+ stderr = (p.stderr or "").lower()
343
+ return ("unknown job" not in stderr), None
344
+ state = None
345
+ exit_code = None
235
346
  for line in p.stdout.splitlines():
236
347
  stripped = line.strip()
237
348
  if stripped.startswith("job_state"):
238
349
  state = stripped.split("=", 1)[-1].strip()
239
- return state not in ("C", "F")
240
- return False
350
+ elif stripped.lower().startswith("exit_status"):
351
+ try:
352
+ exit_code = int(stripped.split("=", 1)[-1].strip())
353
+ except ValueError:
354
+ exit_code = None
355
+ if state in ("C", "F"):
356
+ return False, exit_code
357
+ return True, None
358
+
359
+
360
+ def _is_active(kind: str, job_id: str) -> bool:
361
+ return _poll_job(kind, job_id)[0]
241
362
 
242
363
 
243
364
  def _cancel(kind: str, job_id: str) -> tuple[bool, str]:
@@ -250,14 +371,19 @@ def _cancel(kind: str, job_id: str) -> tuple[bool, str]:
250
371
  return p.returncode == 0, p.stderr
251
372
 
252
373
 
253
- def _wait_until_done(kind: str, job_id: str, name: str) -> None:
254
- """Poll until the job finishes, canceling it on Ctrl+C.
374
+ def _wait_until_done(kind: str, job_id: str, name: str) -> int | None:
375
+ """Poll until the job finishes, returning its exit code.
255
376
 
377
+ The exit code is the scheduler-reported status of the finished job (0 for
378
+ success, non-zero for failure), or ``None`` when it can't be determined.
256
379
  The job is submitted and tracked, so interrupting the local wait cancels
257
380
  the scheduler job before exiting rather than leaving it orphaned.
258
381
  """
259
382
  try:
260
- while _is_active(kind, job_id):
383
+ while True:
384
+ active, exit_code = _poll_job(kind, job_id)
385
+ if not active:
386
+ return exit_code
261
387
  time.sleep(1)
262
388
  except KeyboardInterrupt:
263
389
  typer.echo(f"Interrupted; canceling job '{name}' ({job_id})")
@@ -267,6 +393,68 @@ def _wait_until_done(kind: str, job_id: str, name: str) -> None:
267
393
  raise typer.Exit(130)
268
394
 
269
395
 
396
+ def _wait_for_output_file(log_path: str, timeout: float = 120.0) -> None:
397
+ """Wait for a finished job's output log to be fully written.
398
+
399
+ A scheduler reports a job as done as soon as it leaves the queue, but the
400
+ job's ``-o`` log---which is the batch stage's declared DVC output---may not
401
+ be in place yet: PBS stages it back from the exec host's spool after the
402
+ job exits, and a job can still be flushing its final lines. Returning
403
+ before the file lands (or while it is still growing) makes the subsequent
404
+ ``dvc repro`` see a missing or mid-write output and wrongly report the job
405
+ as failed. Poll until the file exists and its size holds steady, or until
406
+ ``timeout`` elapses (then return and let DVC surface the real state).
407
+ """
408
+ deadline = time.monotonic() + timeout
409
+ last_size = -1
410
+ # Require the size to repeat across polls so we don't snapshot mid-write;
411
+ # a missing file reports -1, which never counts as stable.
412
+ stable_polls = 0
413
+ while time.monotonic() < deadline:
414
+ try:
415
+ size = os.path.getsize(log_path)
416
+ except OSError:
417
+ size = -1
418
+ if size >= 0 and size == last_size:
419
+ stable_polls += 1
420
+ if stable_polls >= 2:
421
+ return
422
+ else:
423
+ stable_polls = 0
424
+ last_size = size
425
+ time.sleep(1)
426
+
427
+
428
+ def _finalize_job(
429
+ name: str, job_id: str, exit_code: int | None, log_path: str
430
+ ) -> None:
431
+ """Fail the command if the finished job did not succeed.
432
+
433
+ Waiting for the job to leave the queue only tells us it stopped, not
434
+ whether it worked, so a non-zero scheduler exit code is surfaced as an
435
+ error---raising a non-zero exit that propagates up through ``dvc repro``
436
+ so the stage is marked failed rather than silently recorded as done. An
437
+ unknown exit code (the scheduler purged the record before we could read
438
+ it) can't be judged, so we warn and let the stage's declared outputs be
439
+ the arbiter.
440
+ """
441
+ # Wait for the job's `-o` log---this stage's declared DVC output---to be
442
+ # staged back before we read from or point at it.
443
+ if not _mock_enabled():
444
+ _wait_for_output_file(log_path)
445
+ if exit_code is None:
446
+ warn(
447
+ f"Could not determine exit status for job '{name}' (ID {job_id}); "
448
+ f"assuming success. Check the log at {log_path}"
449
+ )
450
+ return
451
+ if exit_code != 0:
452
+ raise_error(
453
+ f"Job '{name}' (ID {job_id}) failed with exit code {exit_code}. "
454
+ f"See the log at {log_path}"
455
+ )
456
+
457
+
270
458
  @scheduler_app.command(name="batch")
271
459
  def run_batch(
272
460
  name: Annotated[
@@ -496,7 +684,11 @@ def run_batch(
496
684
  # The recorded job may have been submitted under a different
497
685
  # scheduler kind; use its own kind for activity/cancel checks.
498
686
  prev_kind = job_info.get("kind", kind)
499
- running_or_queued = _is_active(prev_kind, job_id)
687
+ # Capture the exit code alongside liveness: if the job already left the
688
+ # queue (e.g. while we were disconnected) the scheduler may still have
689
+ # its status in history, and reading it now avoids losing it to a later
690
+ # purge.
691
+ running_or_queued, prev_exit_code = _poll_job(prev_kind, job_id)
500
692
  should_wait = True
501
693
 
502
694
  def _cancel_with_reason(reason: str) -> None:
@@ -543,14 +735,19 @@ def run_batch(
543
735
  break
544
736
  if should_wait:
545
737
  typer.echo("Waiting for job to finish")
546
- _wait_until_done(prev_kind, job_id, name)
738
+ exit_code = _wait_until_done(prev_kind, job_id, name)
739
+ _finalize_job(name, job_id, exit_code, log_path)
547
740
  raise typer.Exit(0)
548
741
  elif not os.environ.get("CALKIT_FORCE"):
549
- # The job has left the queue. If nothing it depends on changed and
550
- # its outputs are present, it already finished successfully (e.g.
551
- # while the master process was disconnected), so treat it as done
552
- # rather than resubmitting and discarding that work. Under --force
553
- # (CALKIT_FORCE) we skip this check and resubmit.
742
+ # The job has left the queue (e.g. it finished while the master
743
+ # process was disconnected). If nothing it depends on changed and
744
+ # the prior submission succeeded---or its status was purged, in
745
+ # which case we assume success since there is no record left to
746
+ # judge by and the stage's declared outputs are the final
747
+ # arbiter---harvest it rather than resubmitting and discarding the
748
+ # work. A known failure instead falls through to resubmit, so
749
+ # `calkit run` retries it (e.g. after the user fixed the cause).
750
+ # Under --force (CALKIT_FORCE) we skip this and always resubmit.
554
751
  job_dep_md5s = job_info.get("dep_md5s", {})
555
752
  deps_unchanged = set(job_deps) == set(deps) and all(
556
753
  current_dep_md5s.get(dep) == job_dep_md5s.get(dep)
@@ -561,13 +758,15 @@ def run_batch(
561
758
  and job_args == args
562
759
  and job_setup == setup_cmds
563
760
  )
564
- outputs_present = bool(outs) and all(
565
- os.path.exists(out) for out in outs
566
- )
567
- if deps_unchanged and command_unchanged and outputs_present:
761
+ if (
762
+ deps_unchanged
763
+ and command_unchanged
764
+ and prev_exit_code in (0, None)
765
+ ):
568
766
  typer.echo(
569
- f"Job '{name}' already completed; skipping resubmission"
767
+ f"Job '{name}' already left the queue; using its result"
570
768
  )
769
+ _finalize_job(name, job_id, prev_exit_code, log_path)
571
770
  raise typer.Exit(0)
572
771
  # Job is not running or queued, so we can submit. First, delete any
573
772
  # non-persistent outputs.
@@ -632,7 +831,8 @@ def run_batch(
632
831
  )
633
832
  raise typer.Exit(130)
634
833
  typer.echo("Waiting for job to finish")
635
- _wait_until_done(kind, job_id, name)
834
+ exit_code = _wait_until_done(kind, job_id, name)
835
+ _finalize_job(name, job_id, exit_code, log_path)
636
836
 
637
837
 
638
838
  def _detect_interpreter(target: str) -> list[str]:
@@ -15,7 +15,8 @@ from functools import partial
15
15
  from typing import Literal
16
16
 
17
17
  import requests
18
- from requests.exceptions import HTTPError
18
+ from requests.exceptions import ConnectionError as RequestsConnectionError
19
+ from requests.exceptions import HTTPError, Timeout
19
20
 
20
21
  from . import config
21
22
 
@@ -104,8 +105,7 @@ def run_device_flow() -> str:
104
105
  ) from e
105
106
  if "Device code not found" in txt:
106
107
  raise DeviceLoginError(
107
- "Device code not found; "
108
- "Run 'calkit cloud login' again"
108
+ "Device code not found; Run 'calkit cloud login' again"
109
109
  ) from e
110
110
  raise DeviceLoginError(
111
111
  f"Error while polling for device authorization: {e}"
@@ -304,6 +304,9 @@ def _request(
304
304
  max_retries = 10
305
305
  base_delay_seconds = 0.25
306
306
  max_delay_seconds = 30
307
+ # Bound how long a single attempt can hang so stalled connections become
308
+ # retryable timeouts rather than blocking forever. Callers can override.
309
+ kwargs.setdefault("timeout", (10, 120))
307
310
  func = getattr(requests, kind)
308
311
  if base_url is None:
309
312
  base_url = get_base_url()
@@ -333,15 +336,29 @@ def _request(
333
336
  run_device_flow()
334
337
  continue
335
338
  raise
336
- resp = func(
337
- base_url + path,
338
- params=params,
339
- json=json,
340
- data=data,
341
- headers=req_headers,
342
- **kwargs,
343
- )
344
- if resp.status_code == 502 and retry_num < max_retries:
339
+ try:
340
+ resp = func(
341
+ base_url + path,
342
+ params=params,
343
+ json=json,
344
+ data=data,
345
+ headers=req_headers,
346
+ **kwargs,
347
+ )
348
+ except (Timeout, RequestsConnectionError):
349
+ # Transient network failure (read/connect timeout, connection
350
+ # reset/refused). Retry with the same backoff as transient 5xx
351
+ # responses; re-raise once retries are exhausted.
352
+ if retry_num >= max_retries:
353
+ raise
354
+ wait = min(base_delay_seconds * (2**retry_num), max_delay_seconds)
355
+ time.sleep(wait)
356
+ continue
357
+ # Retry transient server-side errors (bad gateway, overloaded,
358
+ # unavailable, gateway timeout) with exponential backoff. These are
359
+ # typically infrastructure hiccups that resolve on a retry.
360
+ is_transient_5xx = resp.status_code in (500, 502, 503, 504)
361
+ if is_transient_5xx and retry_num < max_retries:
345
362
  wait = min(base_delay_seconds * (2**retry_num), max_delay_seconds)
346
363
  time.sleep(wait)
347
364
  continue