calkit-python 0.41.3__tar.gz → 0.41.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. calkit_python-0.41.5/.gitattributes +1 -0
  2. {calkit_python-0.41.3 → calkit_python-0.41.5}/AGENTS.md +2 -0
  3. {calkit_python-0.41.3 → calkit_python-0.41.5}/PKG-INFO +1 -1
  4. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/check.py +1 -1
  5. calkit_python-0.41.5/calkit/cli/cloud.py +64 -0
  6. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/core.py +6 -4
  7. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/new.py +3 -3
  8. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/scheduler.py +16 -7
  9. calkit_python-0.41.5/calkit/cloud.py +420 -0
  10. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/detect.py +18 -11
  11. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/core.py +70 -0
  12. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/zip.py +7 -2
  13. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/environments.py +17 -9
  14. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/git.py +4 -4
  15. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/overleaf.py +4 -1
  16. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/pipeline.py +16 -39
  17. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/releases.py +2 -2
  18. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_core.py +25 -0
  19. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_xr.py +5 -0
  20. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_check.py +9 -0
  21. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_cloud.py +18 -8
  22. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_latex.py +15 -0
  23. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_new.py +5 -0
  24. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_notebooks.py +9 -0
  25. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_overleaf.py +5 -1
  26. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_scheduler.py +7 -0
  27. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_update.py +5 -0
  28. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/test_core.py +88 -0
  29. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_cloud.py +76 -0
  30. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_conda.py +17 -2
  31. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_git.py +14 -0
  32. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_install.py +13 -0
  33. calkit_python-0.41.3/calkit/cli/cloud.py +0 -126
  34. calkit_python-0.41.3/calkit/cloud.py +0 -236
  35. {calkit_python-0.41.3 → calkit_python-0.41.5}/.claude-plugin/marketplace.json +0 -0
  36. {calkit_python-0.41.3 → calkit_python-0.41.5}/.gitignore +0 -0
  37. {calkit_python-0.41.3 → calkit_python-0.41.5}/.pre-commit-config.yaml +0 -0
  38. {calkit_python-0.41.3 → calkit_python-0.41.5}/.prettierignore +0 -0
  39. {calkit_python-0.41.3 → calkit_python-0.41.5}/.python-version +0 -0
  40. {calkit_python-0.41.3 → calkit_python-0.41.5}/.vscode/launch.json +0 -0
  41. {calkit_python-0.41.3 → calkit_python-0.41.5}/.vscode/tasks.json +0 -0
  42. {calkit_python-0.41.3 → calkit_python-0.41.5}/.yarnrc.yml +0 -0
  43. {calkit_python-0.41.3 → calkit_python-0.41.5}/CITATION.cff +0 -0
  44. {calkit_python-0.41.3 → calkit_python-0.41.5}/CODE_OF_CONDUCT.md +0 -0
  45. {calkit_python-0.41.3 → calkit_python-0.41.5}/CONTRIBUTING.md +0 -0
  46. {calkit_python-0.41.3 → calkit_python-0.41.5}/LICENSE +0 -0
  47. {calkit_python-0.41.3 → calkit_python-0.41.5}/Makefile +0 -0
  48. {calkit_python-0.41.3 → calkit_python-0.41.5}/README.md +0 -0
  49. {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/.claude-plugin/plugin.json +0 -0
  50. {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/add-pipeline-stage/SKILL.md +0 -0
  51. {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/conventions/SKILL.md +0 -0
  52. {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/create-pipeline/SKILL.md +0 -0
  53. {calkit_python-0.41.3 → calkit_python-0.41.5}/babel.config.js +0 -0
  54. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/__init__.py +0 -0
  55. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/__main__.py +0 -0
  56. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/calc.py +0 -0
  57. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/check.py +0 -0
  58. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/__init__.py +0 -0
  59. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/config.py +0 -0
  60. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/core.py +0 -0
  61. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/describe.py +0 -0
  62. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/dev.py +0 -0
  63. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/import_.py +0 -0
  64. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/latex.py +0 -0
  65. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/list.py +0 -0
  66. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/__init__.py +0 -0
  67. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/xr.py +0 -0
  68. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/notebooks.py +0 -0
  69. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/office.py +0 -0
  70. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/overleaf.py +0 -0
  71. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/update.py +0 -0
  72. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/conda.py +0 -0
  73. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/config.py +0 -0
  74. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/core.py +0 -0
  75. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/datasets.py +0 -0
  76. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dependencies.py +0 -0
  77. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/docker.py +0 -0
  78. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/__init__.py +0 -0
  79. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/fs.py +0 -0
  80. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/github.py +0 -0
  81. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/gui.py +0 -0
  82. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/install.py +0 -0
  83. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/invenio.py +0 -0
  84. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/julia.py +0 -0
  85. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyter.py +0 -0
  86. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyterlab/__init__.py +0 -0
  87. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyterlab/routes.py +0 -0
  88. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/package.json +0 -0
  89. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  90. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  91. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  92. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  93. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  94. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  95. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  96. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  97. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  98. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  99. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/style.js +0 -0
  100. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/third-party-licenses.json +0 -0
  101. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/licenses.py +0 -0
  102. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/magics.py +0 -0
  103. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/matlab.py +0 -0
  104. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/__init__.py +0 -0
  105. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/core.py +0 -0
  106. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/io.py +0 -0
  107. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/iteration.py +0 -0
  108. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/pipeline.py +0 -0
  109. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/notebooks.py +0 -0
  110. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/office.py +0 -0
  111. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/ops.py +0 -0
  112. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/server.py +0 -0
  113. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/__init__.py +0 -0
  114. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/core.py +0 -0
  115. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/__init__.py +0 -0
  116. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/article/paper.tex +0 -0
  117. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/core.py +0 -0
  118. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/jfm.bst +0 -0
  119. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/jfm.cls +0 -0
  120. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  121. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/paper.tex +0 -0
  122. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/upmath.sty +0 -0
  123. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/__init__.py +0 -0
  124. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/__init__.py +0 -0
  125. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/__init__.py +0 -0
  126. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_subprojects.py +0 -0
  127. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_config.py +0 -0
  128. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_import.py +0 -0
  129. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_list.py +0 -0
  130. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/__init__.py +0 -0
  131. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/test_zip.py +0 -0
  132. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/jupyterlab/__init__.py +0 -0
  133. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/jupyterlab/test_routes.py +0 -0
  134. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/__init__.py +0 -0
  135. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/test_iteration.py +0 -0
  136. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/test_pipeline.py +0 -0
  137. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_calc.py +0 -0
  138. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_check.py +0 -0
  139. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_core.py +0 -0
  140. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_dependencies.py +0 -0
  141. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_detect.py +0 -0
  142. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_docker.py +0 -0
  143. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_environments.py +0 -0
  144. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_fs.py +0 -0
  145. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_invenio.py +0 -0
  146. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_julia.py +0 -0
  147. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_jupyter.py +0 -0
  148. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_magics.py +0 -0
  149. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_matlab.py +0 -0
  150. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_notebooks.py +0 -0
  151. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_pipeline.py +0 -0
  152. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_releases.py +0 -0
  153. {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_templates.py +0 -0
  154. {calkit_python-0.41.3 → calkit_python-0.41.5}/conftest.py +0 -0
  155. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/CNAME +0 -0
  156. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/ai-tools.md +0 -0
  157. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/apps.md +0 -0
  158. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/calculations.md +0 -0
  159. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/calkit-yaml.md +0 -0
  160. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/cli-reference.md +0 -0
  161. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/cloud-integration.md +0 -0
  162. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/datasets.md +0 -0
  163. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/dependencies.md +0 -0
  164. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/environments.md +0 -0
  165. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/examples.md +0 -0
  166. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/governance.md +0 -0
  167. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/help.md +0 -0
  168. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/hpc.md +0 -0
  169. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/c-to-the-k-white.svg +0 -0
  170. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/calkit-fragmentation-compendium.png +0 -0
  171. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/calkit-no-bg.png +0 -0
  172. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/connect-zenodo.png +0 -0
  173. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/all-green.png +0 -0
  174. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  175. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/new-env.png +0 -0
  176. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/new-notebook.png +0 -0
  177. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  178. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab-params.png +0 -0
  179. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/plos-osi-code-2024-03.png +0 -0
  180. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/vscode-nb-params.png +0 -0
  181. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/index.md +0 -0
  182. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/installation.md +0 -0
  183. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/jupyterlab.md +0 -0
  184. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/local-server.md +0 -0
  185. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/notebooks.md +0 -0
  186. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/overleaf.md +0 -0
  187. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/index.md +0 -0
  188. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/manual-steps.md +0 -0
  189. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/running-and-logging.md +0 -0
  190. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/quickstart.md +0 -0
  191. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/references.md +0 -0
  192. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/releases.md +0 -0
  193. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/reproducibility.md +0 -0
  194. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/subprojects.md +0 -0
  195. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  196. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/conda-envs.md +0 -0
  197. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/existing-project.md +0 -0
  198. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/first-project.md +0 -0
  199. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/github-actions.md +0 -0
  200. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  201. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  202. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  203. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  204. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  205. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  206. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  207. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  208. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  209. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  210. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  211. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  212. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  213. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  214. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  215. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/create-project.png +0 -0
  216. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  217. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/excel-chart.png +0 -0
  218. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/excel-data.png +0 -0
  219. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  220. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/needs-clone.png +0 -0
  221. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/new-stage.png +0 -0
  222. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  223. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  224. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/status-more-rows.png +0 -0
  225. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  226. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/untracked-data.png +0 -0
  227. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/updated-publication.png +0 -0
  228. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  229. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/workflow-page.png +0 -0
  230. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/clone.png +0 -0
  231. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/create-project.png +0 -0
  232. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  233. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  234. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  235. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/new-token.png +0 -0
  236. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/reclone.png +0 -0
  237. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  238. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/quick-actions.png +0 -0
  239. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/run-proc.png +0 -0
  240. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
  241. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
  242. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
  243. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
  244. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
  245. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
  246. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
  247. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/index.md +0 -0
  248. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/jupyterlab.md +0 -0
  249. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/latex-codespaces.md +0 -0
  250. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/matlab.md +0 -0
  251. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/notebook-pipeline.md +0 -0
  252. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/office.md +0 -0
  253. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/openfoam.md +0 -0
  254. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/procedures.md +0 -0
  255. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/vscode-slurm-notebook.md +0 -0
  256. {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/version-control.md +0 -0
  257. {calkit_python-0.41.3 → calkit_python-0.41.5}/flake.lock +0 -0
  258. {calkit_python-0.41.3 → calkit_python-0.41.5}/flake.nix +0 -0
  259. {calkit_python-0.41.3 → calkit_python-0.41.5}/install.json +0 -0
  260. {calkit_python-0.41.3 → calkit_python-0.41.5}/jest.config.js +0 -0
  261. {calkit_python-0.41.3 → calkit_python-0.41.5}/jupyter-config/server-config/calkit.json +0 -0
  262. {calkit_python-0.41.3 → calkit_python-0.41.5}/mkdocs.yml +0 -0
  263. {calkit_python-0.41.3 → calkit_python-0.41.5}/package.json +0 -0
  264. {calkit_python-0.41.3 → calkit_python-0.41.5}/pyproject.toml +0 -0
  265. {calkit_python-0.41.3 → calkit_python-0.41.5}/schema/plugin.json +0 -0
  266. {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/generate-docs-references.py +0 -0
  267. {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/install.ps1 +0 -0
  268. {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/install.sh +0 -0
  269. {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/make-calk9.sh +0 -0
  270. {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/sync-docs.py +0 -0
  271. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/__tests__/useQueries.spec.ts +0 -0
  272. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/calkit-config.ts +0 -0
  273. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/cell-output-marker.ts +0 -0
  274. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/commit-dialog.tsx +0 -0
  275. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/environment-editor.tsx +0 -0
  276. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/notebook-registration.tsx +0 -0
  277. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/notebook-toolbar.tsx +0 -0
  278. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/pipeline-status-bar.tsx +0 -0
  279. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/project-info-editor.tsx +0 -0
  280. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/sidebar-settings.tsx +0 -0
  281. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/sidebar.tsx +0 -0
  282. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/stage-editor.tsx +0 -0
  283. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/feature-flags.ts +0 -0
  284. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/file-browser-menu.ts +0 -0
  285. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/hooks/__tests__/useQueries.test.tsx +0 -0
  286. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/hooks/useQueries.ts +0 -0
  287. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/icons.ts +0 -0
  288. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/index.ts +0 -0
  289. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/io-tracker.ts +0 -0
  290. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/pipeline-state.ts +0 -0
  291. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/queryClient.ts +0 -0
  292. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/request.ts +0 -0
  293. {calkit_python-0.41.3 → calkit_python-0.41.5}/src/shims-mainmenu.d.ts +0 -0
  294. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/base.css +0 -0
  295. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/cell-output-marker.css +0 -0
  296. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/environment-editor.css +0 -0
  297. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/environment-selector.css +0 -0
  298. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/img/calkit-no-bg.png +0 -0
  299. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/index.css +0 -0
  300. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/index.js +0 -0
  301. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/notebook-toolbar.css +0 -0
  302. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/pipeline-status-bar.css +0 -0
  303. {calkit_python-0.41.3 → calkit_python-0.41.5}/style/sidebar.css +0 -0
  304. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  305. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-julia.ipynb +0 -0
  306. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-params.ipynb +0 -0
  307. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-subdir.ipynb +0 -0
  308. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/pipeline.ipynb +0 -0
  309. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/script.py +0 -0
  310. {calkit_python-0.41.3 → calkit_python-0.41.5}/test/test-log.log +0 -0
  311. {calkit_python-0.41.3 → calkit_python-0.41.5}/tsconfig.json +0 -0
  312. {calkit_python-0.41.3 → calkit_python-0.41.5}/tsconfig.test.json +0 -0
  313. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/.gitignore +0 -0
  314. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/README.md +0 -0
  315. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/jupyter_server_test_config.py +0 -0
  316. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/package.json +0 -0
  317. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/playwright.config.js +0 -0
  318. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/tests/calkit.spec.ts +0 -0
  319. {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/yarn.lock +0 -0
  320. {calkit_python-0.41.3 → calkit_python-0.41.5}/uv.lock +0 -0
  321. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/.gitignore +0 -0
  322. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/.vscodeignore +0 -0
  323. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/CHANGELOG.md +0 -0
  324. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/LICENSE +0 -0
  325. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/README.md +0 -0
  326. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/images/calkit-no-bg.png +0 -0
  327. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/package-lock.json +0 -0
  328. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/package.json +0 -0
  329. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/scripts/set-proposed-api.js +0 -0
  330. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/environments.ts +0 -0
  331. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/extension.ts +0 -0
  332. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/notebooks.ts +0 -0
  333. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/environments.test.ts +0 -0
  334. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/kernel-selection.test.ts +0 -0
  335. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/notebooks.test.ts +0 -0
  336. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/types.ts +0 -0
  337. {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/tsconfig.json +0 -0
  338. {calkit_python-0.41.3 → calkit_python-0.41.5}/yarn.lock +0 -0
@@ -0,0 +1 @@
1
+ test/dvc-md5-dir/** text eol=lf
@@ -34,3 +34,5 @@ For prose, only use one space after punctuation.
34
34
  Don't overzealously split up functions just because they're long.
35
35
  Functions should usually be used ~3 times before abstracting.
36
36
  Otherwise, split up long ones into logical sections with comments.
37
+ The only exception here is if splitting up a function makes it easier to
38
+ write meaningful unit tests.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.41.3
3
+ Version: 0.41.5
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -381,7 +381,7 @@ def check_environment(
381
381
  quiet=not verbose,
382
382
  )
383
383
  elif env["kind"] == "pixi":
384
- cmd = ["pixi", "lock"]
384
+ cmd = ["pixi", "install"]
385
385
  env_dir = os.path.dirname(env["path"])
386
386
  if env_dir:
387
387
  cmd += ["--manifest-path", env["path"]]
@@ -0,0 +1,64 @@
1
+ """CLI for interacting with Calkit Cloud instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ import calkit
10
+ from calkit.cli import raise_error
11
+
12
+ cloud_app = typer.Typer(no_args_is_help=True)
13
+
14
+
15
+ @cloud_app.command(name="get")
16
+ def get(endpoint: Annotated[str, typer.Argument(help="API endpoint")]):
17
+ """Get a resource from the Cloud API."""
18
+ if not endpoint.startswith("/"):
19
+ endpoint = "/" + endpoint
20
+ try:
21
+ resp = calkit.cloud.get(endpoint)
22
+ typer.echo(resp)
23
+ except Exception as e:
24
+ raise_error(str(e))
25
+
26
+
27
+ @cloud_app.command(name="login")
28
+ def login(
29
+ force: Annotated[
30
+ bool,
31
+ typer.Option(
32
+ "--force",
33
+ "-f",
34
+ help=(
35
+ "Force logging in again even if already authenticated. "
36
+ "Will store a new token in your local config."
37
+ ),
38
+ ),
39
+ ] = False,
40
+ ):
41
+ """Login to the Calkit Cloud.
42
+
43
+ First try a GET request to the /user endpoint to check if the user is
44
+ already logged in. If not, perform OAuth device flow.
45
+ """
46
+ from requests.exceptions import HTTPError
47
+
48
+ if not force:
49
+ try:
50
+ calkit.cloud.get("/user")
51
+ calkit.echo("Authenticated successfully ✅")
52
+ return
53
+ except (ValueError, HTTPError) as e:
54
+ # Any auth failure (no token, 401, 403) falls through to the
55
+ # device flow so the user can re-authenticate. Other HTTP errors
56
+ # (e.g. 5xx) are surfaced.
57
+ if isinstance(e, HTTPError) and not any(
58
+ code in str(e) for code in ("401", "403")
59
+ ):
60
+ raise_error(str(e))
61
+ try:
62
+ calkit.cloud.run_device_flow()
63
+ except calkit.cloud.DeviceLoginError as e:
64
+ raise_error(str(e))
@@ -502,10 +502,11 @@ def get_status(
502
502
  if "dvc" in categories:
503
503
  try:
504
504
  dvc_repo = calkit.dvc.get_dvc_repo()
505
- data_status = dvc_repo.data_status()
506
- if isinstance(data_status, dict):
507
- data_status.pop("git", None)
508
- status_dict["dvc"] = data_status
505
+ data_status = dict(dvc_repo.data_status())
506
+ data_status.pop("git", None)
507
+ status_dict["dvc"] = calkit.dvc.data_status_as_posix(
508
+ data_status
509
+ )
509
510
  except Exception as e:
510
511
  status_dict["dvc"] = {"error": f"{e.__class__.__name__}: {e}"}
511
512
  if "pipeline" in categories or "dvc" in categories:
@@ -557,6 +558,7 @@ def get_status(
557
558
  dvc_repo = calkit.dvc.get_dvc_repo()
558
559
  raw = dict(dvc_repo.data_status())
559
560
  raw.pop("git", None)
561
+ raw = calkit.dvc.data_status_as_posix(raw)
560
562
  typer.echo(_format_dvc_data_status(raw, zip_path_map))
561
563
  if "pipeline" in categories or "dvc" in categories:
562
564
  print_sep("Pipeline")
@@ -1152,10 +1152,10 @@ def new_publication(
1152
1152
  deps += stage.get("outs", [])
1153
1153
  # Create publication object
1154
1154
  if template_type == "latex":
1155
- pub_fpath = os.path.join(
1155
+ pub_fpath = pathlib.Path(
1156
1156
  path,
1157
1157
  template_obj.target.removesuffix(".tex") + ".pdf", # type: ignore
1158
- )
1158
+ ).as_posix()
1159
1159
  else:
1160
1160
  pub_fpath = path
1161
1161
  if not overwrite and pub_fpath in pub_paths:
@@ -1190,7 +1190,7 @@ def new_publication(
1190
1190
  stage = LatexStage(
1191
1191
  kind="latex",
1192
1192
  environment=env_name,
1193
- target_path=os.path.join(path, template_obj.target), # type: ignore
1193
+ target_path=pathlib.Path(path, template_obj.target).as_posix(), # type: ignore
1194
1194
  outputs=[pub_fpath],
1195
1195
  ).model_dump()
1196
1196
  if "pipeline" not in ck_info:
@@ -92,9 +92,9 @@ def _mock_enabled() -> bool:
92
92
 
93
93
 
94
94
  def _require_posix_mock() -> None:
95
- # The mock backend uses bash, new sessions, and process-group signals.
96
- if os.name != "posix":
97
- raise_error("CALKIT_MOCK_SCHEDULER is only supported on POSIX systems")
95
+ # The mock backend uses bash; if that is missing, the mock cannot run.
96
+ if shutil.which("bash") is None:
97
+ raise_error("CALKIT_MOCK_SCHEDULER requires bash on PATH")
98
98
 
99
99
 
100
100
  def _ensure_mock_dir() -> None:
@@ -177,13 +177,19 @@ def _mock_submit(job_id: str, job_command: str, log_path: str) -> int:
177
177
  env = dict(os.environ)
178
178
  env.setdefault("PBS_O_WORKDIR", os.getcwd())
179
179
  with open(log_path, "w") as log_file:
180
- proc = subprocess.Popen(
181
- ["bash", "-c", wrapped],
180
+ popen_kwargs = dict(
182
181
  stdout=log_file,
183
182
  stderr=subprocess.STDOUT,
184
- start_new_session=True,
185
183
  env=env,
186
184
  )
185
+ if os.name == "posix":
186
+ popen_kwargs["start_new_session"] = True
187
+ elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
188
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
189
+ proc = subprocess.Popen(
190
+ ["bash", "-c", wrapped],
191
+ **popen_kwargs,
192
+ )
187
193
  return proc.pid
188
194
 
189
195
 
@@ -192,7 +198,10 @@ def _mock_cancel(job_id: str) -> tuple[bool, str]:
192
198
  pid = _mock_pid(job_id)
193
199
  if pid is not None:
194
200
  try:
195
- os.killpg(os.getpgid(pid), signal.SIGTERM)
201
+ if os.name == "posix":
202
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
203
+ else:
204
+ os.kill(pid, signal.SIGTERM)
196
205
  except (ProcessLookupError, PermissionError):
197
206
  pass
198
207
  # Drop a sentinel so later liveness checks report the job as finished.
@@ -0,0 +1,420 @@
1
+ """The REST API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import logging
8
+ import os
9
+ import socket
10
+ import sys
11
+ import threading
12
+ import time
13
+ import webbrowser
14
+ from functools import partial
15
+ from typing import Literal
16
+
17
+ import requests
18
+ from requests.exceptions import HTTPError
19
+
20
+ from . import config
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # A dictionary of tokens keyed by base URL
25
+ _tokens = {}
26
+
27
+ # Single lock guarding all token-refresh operations to prevent thundering herds
28
+ # (e.g., many concurrent fsspec threads all attempting to refresh at once).
29
+ _refresh_lock = threading.Lock()
30
+
31
+ # Seconds before JWT expiry at which we proactively refresh.
32
+ _REFRESH_BUFFER_SECONDS = 60
33
+
34
+ # Serializes interactive device-login attempts so that, when several
35
+ # concurrent requests all hit an auth failure, only one of them actually
36
+ # prompts the user; the rest wait on the lock and then reuse the freshly
37
+ # minted token. The device-flow's own polling calls use ``auth=False`` so
38
+ # they don't recurse into the auth-retry paths in ``_request``.
39
+ _device_login_lock = threading.Lock()
40
+
41
+
42
+ class DeviceLoginError(RuntimeError):
43
+ """Raised when the OAuth device login flow cannot complete."""
44
+
45
+
46
+ def run_device_flow() -> str:
47
+ """Run the OAuth device login flow and return the new access token.
48
+
49
+ On success the token is written to the config file and the in-memory
50
+ token cache. Raises :class:`DeviceLoginError` on any failure (server
51
+ error, expired/missing device code, timeout, etc.).
52
+ """
53
+ # Serialize concurrent attempts (e.g. multiple fsspec threads all hitting
54
+ # 403 at the same time): the first thread runs the flow, the rest wait
55
+ # and then reuse the freshly minted token.
56
+ base_url = get_base_url()
57
+ token_before = _tokens.get(base_url)
58
+ with _device_login_lock:
59
+ token_after = _tokens.get(base_url)
60
+ # If the cached token changed while we were waiting on the lock,
61
+ # another thread just completed a device flow — reuse its result.
62
+ if token_after is not None and token_after != token_before:
63
+ return token_after
64
+ try:
65
+ hostname = socket.gethostname()
66
+ except Exception:
67
+ hostname = None
68
+ print("Initiating device login flow", flush=True)
69
+ try:
70
+ resp = post(
71
+ "/login/device",
72
+ json={"hostname": hostname},
73
+ auth=False,
74
+ )
75
+ device_code = resp["device_code"]
76
+ verification_uri = resp["verification_uri"]
77
+ expires_in = int(resp["expires_in"])
78
+ interval = int(resp["interval"])
79
+ except Exception as e:
80
+ raise DeviceLoginError(
81
+ f"Failed to initiate device login flow: {e}"
82
+ ) from e
83
+ print("Authorize this device by opening this URL:", flush=True)
84
+ print(verification_uri, flush=True)
85
+ print("Waiting for authorization", flush=True)
86
+ try:
87
+ webbrowser.open(verification_uri)
88
+ except Exception:
89
+ pass
90
+ deadline = time.monotonic() + expires_in
91
+ while time.monotonic() < deadline:
92
+ try:
93
+ token_resp = post(
94
+ "/login/device/token",
95
+ json={"device_code": device_code},
96
+ auth=False,
97
+ )
98
+ except Exception as e:
99
+ txt = str(e)
100
+ if "Device code has expired" in txt:
101
+ raise DeviceLoginError(
102
+ "Device code has expired; "
103
+ "Run 'calkit cloud login' again"
104
+ ) from e
105
+ if "Device code not found" in txt:
106
+ raise DeviceLoginError(
107
+ "Device code not found; "
108
+ "Run 'calkit cloud login' again"
109
+ ) from e
110
+ raise DeviceLoginError(
111
+ f"Error while polling for device authorization: {e}"
112
+ ) from e
113
+ access_token = token_resp.get("access_token")
114
+ if access_token:
115
+ refresh_token = token_resp.get("refresh_token")
116
+ try:
117
+ cfg = config.read()
118
+ cfg.access_token = access_token
119
+ if refresh_token:
120
+ cfg.refresh_token = refresh_token
121
+ # A stored PAT (``token``) takes priority over
122
+ # ``access_token`` in get_token(), so a stale PAT would
123
+ # keep being used by future processes and re-trigger
124
+ # the device flow every time. The user just
125
+ # re-authenticated, so device-flow credentials win.
126
+ if getattr(cfg, "token", None) is not None:
127
+ cfg.token = None
128
+ # A stored DVC token may have been revoked alongside
129
+ # the access token that just failed; clear it so the
130
+ # next remote-auth check mints a fresh one.
131
+ if getattr(cfg, "dvc_token", None) is not None:
132
+ cfg.dvc_token = None
133
+ cfg.write()
134
+ _tokens[base_url] = access_token
135
+ except Exception as e:
136
+ raise DeviceLoginError(
137
+ f"Failed to save token in config: {e}"
138
+ ) from e
139
+ print("Logged in successfully ✅", flush=True)
140
+ return access_token
141
+ sleep_seconds = min(
142
+ interval, max(0.0, deadline - time.monotonic())
143
+ )
144
+ if sleep_seconds > 0:
145
+ time.sleep(sleep_seconds)
146
+ raise DeviceLoginError(
147
+ "Timed out waiting for device authorization; "
148
+ "Run 'calkit cloud login' again"
149
+ )
150
+
151
+
152
+ def get_base_url() -> str:
153
+ """Get the API base URL."""
154
+ override = os.environ.get("CALKIT_CLOUD_BASE_URL")
155
+ if override:
156
+ return override
157
+ urls = {
158
+ "local": "http://api.localhost",
159
+ "staging": "https://api.staging.calkit.io",
160
+ "production": "https://api.calkit.io",
161
+ "test": "http://api.localhost",
162
+ }
163
+ return urls[config.get_env()]
164
+
165
+
166
+ def _jwt_exp(token: str) -> float | None:
167
+ """Return the ``exp`` claim of a JWT as a UTC timestamp, or ``None``.
168
+
169
+ Does not verify the signature — only used for proactive expiry checks.
170
+ """
171
+ try:
172
+ # JWTs are <header>.<payload>.<sig>, all base64url-encoded
173
+ payload_b64 = token.split(".")[1]
174
+ # Pad to a multiple of 4 for standard base64 decoding
175
+ padding = 4 - len(payload_b64) % 4
176
+ if padding != 4:
177
+ payload_b64 += "=" * padding
178
+ payload = json.loads(base64.urlsafe_b64decode(payload_b64))
179
+ return float(payload["exp"])
180
+ except Exception:
181
+ return None
182
+
183
+
184
+ def _token_is_expiring(token: str) -> bool:
185
+ """Return True if ``token`` is a JWT that expires within the refresh
186
+ buffer.
187
+ """
188
+ exp = _jwt_exp(token)
189
+ if exp is None:
190
+ return False # Not a JWT (e.g. a PAT) — never proactively refresh
191
+ return time.time() >= exp - _REFRESH_BUFFER_SECONDS
192
+
193
+
194
+ def get_token() -> str:
195
+ """Return a valid bearer token.
196
+
197
+ Priority: in-memory cache (refreshed proactively if expiring), then PAT
198
+ (``token`` / ``CALKIT_TOKEN`` env var), then short-lived ``access_token``
199
+ from device login.
200
+
201
+ For access tokens, expiry is read from the JWT ``exp`` claim and a refresh
202
+ is attempted before the token expires. Only one thread performs the
203
+ refresh at a time; all others wait behind ``_refresh_lock`` and then use
204
+ the new token that was stored in the cache.
205
+ """
206
+ base_url = get_base_url()
207
+ cached = _tokens.get(base_url)
208
+ if cached is not None and not _token_is_expiring(cached):
209
+ return cached
210
+ # Token missing or expiring — acquire the lock so only one thread refreshes
211
+ with _refresh_lock:
212
+ # Re-check after acquiring lock; another thread may have refreshed
213
+ cached = _tokens.get(base_url)
214
+ if cached is not None and not _token_is_expiring(cached):
215
+ return cached
216
+ # If we have a refresh token, try to get a new access token now
217
+ if cached is not None and _token_is_expiring(cached):
218
+ new_token = _do_refresh()
219
+ if new_token is not None:
220
+ return new_token
221
+ # No usable cached token — load from config
222
+ cfg = config.read()
223
+ # Prefer long-lived PAT if present (env var CALKIT_TOKEN or config)
224
+ if cfg.token is not None:
225
+ _tokens[base_url] = cfg.token
226
+ return cfg.token
227
+ # Fall back to short-lived access token from device login
228
+ if cfg.access_token is not None:
229
+ if not _token_is_expiring(cfg.access_token):
230
+ _tokens[base_url] = cfg.access_token
231
+ return cfg.access_token
232
+ # Config token is also expiring — attempt refresh
233
+ new_token = _do_refresh()
234
+ if new_token is not None:
235
+ return new_token
236
+ raise ValueError(
237
+ "No token found; Run 'calkit cloud login' to authenticate"
238
+ )
239
+
240
+
241
+ def _do_refresh() -> str | None:
242
+ """Perform a token refresh request.
243
+
244
+ Must be called with ``_refresh_lock`` already held (or from within
245
+ ``_try_refresh`` which acquires it). Returns the new access token on
246
+ success or ``None`` on failure.
247
+ """
248
+ cfg = config.read()
249
+ if cfg.refresh_token is None:
250
+ return None
251
+ base_url = get_base_url()
252
+ try:
253
+ resp = requests.post(
254
+ base_url + "/login/refresh",
255
+ json={"refresh_token": cfg.refresh_token},
256
+ )
257
+ resp.raise_for_status()
258
+ data = resp.json()
259
+ new_access = data["access_token"]
260
+ new_refresh = data.get("refresh_token", cfg.refresh_token)
261
+ cfg.access_token = new_access
262
+ cfg.refresh_token = new_refresh
263
+ cfg.write()
264
+ _tokens[base_url] = new_access
265
+ return new_access
266
+ except Exception as exc:
267
+ logger.debug("Token refresh failed: %s", exc)
268
+ return None
269
+
270
+
271
+ def _try_refresh() -> str | None:
272
+ """Attempt a token refresh from outside the normal request cycle.
273
+
274
+ Acquires ``_refresh_lock`` to prevent concurrent refresh storms.
275
+ Returns the new access token on success, or ``None``.
276
+ """
277
+ with _refresh_lock:
278
+ return _do_refresh()
279
+
280
+
281
+ def get_headers(headers: dict | None = None, auth: bool = True) -> dict:
282
+ if auth:
283
+ base_headers = {"Authorization": f"Bearer {get_token()}"}
284
+ else:
285
+ base_headers = {}
286
+ if headers is not None:
287
+ return base_headers | headers
288
+ else:
289
+ return base_headers
290
+
291
+
292
+ def _request(
293
+ kind: Literal["get", "post", "put", "patch", "delete"],
294
+ path: str,
295
+ params: dict | None = None,
296
+ json: dict | None = None,
297
+ data: dict | None = None,
298
+ headers: dict | None = None,
299
+ as_json=True,
300
+ auth: bool = True,
301
+ base_url: str | None = None,
302
+ **kwargs,
303
+ ):
304
+ max_retries = 10
305
+ base_delay_seconds = 0.25
306
+ max_delay_seconds = 30
307
+ func = getattr(requests, kind)
308
+ if base_url is None:
309
+ base_url = get_base_url()
310
+ refresh_attempted = False
311
+ device_login_attempted = False
312
+ # We may prompt the user via the device login flow only when stdin and
313
+ # stdout are TTYs (i.e. not in CI, daemons, or piped subprocesses) and
314
+ # the user hasn't opted out via the env var.
315
+ can_prompt = auth and not os.environ.get("CALKIT_NO_INTERACTIVE_LOGIN")
316
+ if can_prompt:
317
+ try:
318
+ can_prompt = sys.stdin.isatty() and sys.stdout.isatty()
319
+ except Exception:
320
+ can_prompt = False
321
+ for retry_num in range(max_retries + 1):
322
+ try:
323
+ req_headers = get_headers(headers, auth=auth)
324
+ except ValueError:
325
+ # No token in config at all. If we can prompt the user, run
326
+ # the device flow and try again; otherwise re-raise so callers
327
+ # see the original "no token" error. ``run_device_flow``
328
+ # serializes concurrent callers via ``_device_login_lock`` and
329
+ # reuses the freshly minted token, so threads that arrive
330
+ # mid-flow simply wait their turn rather than re-prompting.
331
+ if can_prompt and not device_login_attempted:
332
+ device_login_attempted = True
333
+ run_device_flow()
334
+ continue
335
+ 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:
345
+ wait = min(base_delay_seconds * (2**retry_num), max_delay_seconds)
346
+ time.sleep(wait)
347
+ continue
348
+ # Decide whether this response is a credential rejection (vs. a
349
+ # permission/authorization failure that happens to also be 403).
350
+ # 401 is always a credential rejection. For 403, only treat it as
351
+ # one if the API's ``detail`` identifies it as such — otherwise
352
+ # we'd hijack legitimate "you can't do that" errors and pop a
353
+ # browser-based login flow at the user.
354
+ looks_like_auth_failure = auth and resp.status_code == 401
355
+ if auth and resp.status_code == 403:
356
+ try:
357
+ detail = resp.json().get("detail")
358
+ except Exception:
359
+ detail = None
360
+ looks_like_auth_failure = isinstance(detail, str) and any(
361
+ s in detail
362
+ for s in (
363
+ "Could not validate credentials",
364
+ "Not authenticated",
365
+ "Invalid token",
366
+ "Token has expired",
367
+ )
368
+ )
369
+ # On credential rejection, attempt a token refresh once.
370
+ # ``_try_refresh`` returns ``None`` when no refresh token is stored
371
+ # (e.g. PAT-only sessions). ``get_headers()`` re-calls
372
+ # ``get_token()`` on the next iteration, so it will automatically
373
+ # pick up the new token stored in ``_tokens`` by ``_try_refresh``.
374
+ if looks_like_auth_failure and not refresh_attempted:
375
+ refresh_attempted = True
376
+ new_token = _try_refresh()
377
+ if new_token is not None:
378
+ continue
379
+ # If refresh didn't help, fall back to the interactive device login
380
+ # flow once. This covers both HTTP remotes (where /user/tokens
381
+ # 401/403s) and ck:// remotes (which go through this same
382
+ # ``_request`` path). ``run_device_flow`` serializes via
383
+ # ``_device_login_lock`` and reuses the new token, so concurrent
384
+ # callers wait rather than each starting their own flow.
385
+ if (
386
+ looks_like_auth_failure
387
+ and can_prompt
388
+ and not device_login_attempted
389
+ ):
390
+ device_login_attempted = True
391
+ try:
392
+ run_device_flow()
393
+ except DeviceLoginError as e:
394
+ logger.warning("Device login failed: %s", e)
395
+ else:
396
+ continue
397
+ try:
398
+ resp.raise_for_status()
399
+ except HTTPError as e:
400
+ try:
401
+ detail = resp.json()["detail"]
402
+ except Exception:
403
+ raise e
404
+ raise HTTPError(f"{resp.status_code}: {detail}")
405
+ break
406
+ if as_json:
407
+ return resp.json()
408
+ else:
409
+ return resp
410
+
411
+
412
+ get = partial(_request, "get")
413
+ post = partial(_request, "post")
414
+ patch = partial(_request, "patch")
415
+ put = partial(_request, "put")
416
+ delete = partial(_request, "delete")
417
+
418
+
419
+ def get_current_user() -> dict:
420
+ return get("/user")