calkit-python 0.37.6__tar.gz → 0.38.0__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 (321) hide show
  1. calkit_python-0.38.0/AGENTS.md +13 -0
  2. {calkit_python-0.37.6 → calkit_python-0.38.0}/PKG-INFO +6 -2
  3. {calkit_python-0.37.6 → calkit_python-0.38.0}/README.md +5 -1
  4. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/check.py +4 -4
  5. calkit_python-0.38.0/calkit/cli/cloud.py +125 -0
  6. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/config.py +4 -1
  7. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/main/core.py +1 -6
  8. calkit_python-0.38.0/calkit/cloud.py +232 -0
  9. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/config.py +2 -2
  10. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/core.py +11 -0
  11. calkit_python-0.38.0/calkit/tests/cli/test_cloud.py +170 -0
  12. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_config.py +7 -7
  13. calkit_python-0.38.0/calkit/tests/test_cloud.py +266 -0
  14. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/cli-reference.md +24 -3
  15. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/cloud-integration.md +5 -5
  16. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/installation.md +5 -1
  17. calkit_python-0.37.6/calkit/cli/cloud.py +0 -24
  18. calkit_python-0.37.6/calkit/cloud.py +0 -126
  19. {calkit_python-0.37.6 → calkit_python-0.38.0}/.gitignore +0 -0
  20. {calkit_python-0.37.6 → calkit_python-0.38.0}/.pre-commit-config.yaml +0 -0
  21. {calkit_python-0.37.6 → calkit_python-0.38.0}/.prettierignore +0 -0
  22. {calkit_python-0.37.6 → calkit_python-0.38.0}/.python-version +0 -0
  23. {calkit_python-0.37.6 → calkit_python-0.38.0}/.vscode/launch.json +0 -0
  24. {calkit_python-0.37.6 → calkit_python-0.38.0}/.vscode/tasks.json +0 -0
  25. {calkit_python-0.37.6 → calkit_python-0.38.0}/.yarnrc.yml +0 -0
  26. {calkit_python-0.37.6 → calkit_python-0.38.0}/CITATION.cff +0 -0
  27. {calkit_python-0.37.6 → calkit_python-0.38.0}/CODE_OF_CONDUCT.md +0 -0
  28. {calkit_python-0.37.6 → calkit_python-0.38.0}/CONTRIBUTING.md +0 -0
  29. {calkit_python-0.37.6 → calkit_python-0.38.0}/LICENSE +0 -0
  30. {calkit_python-0.37.6 → calkit_python-0.38.0}/Makefile +0 -0
  31. {calkit_python-0.37.6 → calkit_python-0.38.0}/babel.config.js +0 -0
  32. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/__init__.py +0 -0
  33. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/__main__.py +0 -0
  34. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/calc.py +0 -0
  35. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/check.py +0 -0
  36. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/__init__.py +0 -0
  37. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/core.py +0 -0
  38. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/describe.py +0 -0
  39. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/dev.py +0 -0
  40. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/import_.py +0 -0
  41. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/latex.py +0 -0
  42. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/list.py +0 -0
  43. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/main/__init__.py +0 -0
  44. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/main/xr.py +0 -0
  45. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/new.py +0 -0
  46. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/notebooks.py +0 -0
  47. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/office.py +0 -0
  48. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/overleaf.py +0 -0
  49. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/slurm.py +0 -0
  50. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/cli/update.py +0 -0
  51. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/conda.py +0 -0
  52. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/datasets.py +0 -0
  53. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/detect.py +0 -0
  54. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/docker.py +0 -0
  55. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/dvc/__init__.py +0 -0
  56. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/dvc/core.py +0 -0
  57. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/dvc/zip.py +0 -0
  58. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/environments.py +0 -0
  59. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/fs.py +0 -0
  60. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/git.py +0 -0
  61. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/github.py +0 -0
  62. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/gui.py +0 -0
  63. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/invenio.py +0 -0
  64. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/julia.py +0 -0
  65. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/jupyter.py +0 -0
  66. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/jupyterlab/__init__.py +0 -0
  67. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/jupyterlab/routes.py +0 -0
  68. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/package.json +0 -0
  69. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
  70. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/schemas/calkit/plugin.json +0 -0
  71. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
  72. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
  73. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
  74. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  75. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
  76. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  77. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
  78. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  79. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/style.js +0 -0
  80. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/labextension/static/third-party-licenses.json +0 -0
  81. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/licenses.py +0 -0
  82. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/magics.py +0 -0
  83. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/matlab.py +0 -0
  84. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/models/__init__.py +0 -0
  85. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/models/core.py +0 -0
  86. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/models/io.py +0 -0
  87. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/models/iteration.py +0 -0
  88. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/models/pipeline.py +0 -0
  89. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/notebooks.py +0 -0
  90. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/office.py +0 -0
  91. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/ops.py +0 -0
  92. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/overleaf.py +0 -0
  93. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/pipeline.py +0 -0
  94. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/releases.py +0 -0
  95. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/server.py +0 -0
  96. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/__init__.py +0 -0
  97. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/core.py +0 -0
  98. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/__init__.py +0 -0
  99. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/article/paper.tex +0 -0
  100. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/core.py +0 -0
  101. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/jfm/jfm.bst +0 -0
  102. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/jfm/jfm.cls +0 -0
  103. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
  104. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/jfm/paper.tex +0 -0
  105. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/templates/latex/jfm/upmath.sty +0 -0
  106. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/__init__.py +0 -0
  107. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/__init__.py +0 -0
  108. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/main/__init__.py +0 -0
  109. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/main/test_core.py +0 -0
  110. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/main/test_xr.py +0 -0
  111. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_check.py +0 -0
  112. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_import.py +0 -0
  113. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_latex.py +0 -0
  114. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_list.py +0 -0
  115. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_new.py +0 -0
  116. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_notebooks.py +0 -0
  117. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_overleaf.py +0 -0
  118. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/cli/test_update.py +0 -0
  119. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/dvc/__init__.py +0 -0
  120. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/dvc/test_core.py +0 -0
  121. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/dvc/test_zip.py +0 -0
  122. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/jupyterlab/__init__.py +0 -0
  123. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/jupyterlab/test_routes.py +0 -0
  124. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/models/__init__.py +0 -0
  125. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/models/test_iteration.py +0 -0
  126. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/models/test_pipeline.py +0 -0
  127. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_calc.py +0 -0
  128. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_check.py +0 -0
  129. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_conda.py +0 -0
  130. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_core.py +0 -0
  131. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_detect.py +0 -0
  132. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_docker.py +0 -0
  133. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_environments.py +0 -0
  134. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_fs.py +0 -0
  135. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_git.py +0 -0
  136. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_invenio.py +0 -0
  137. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_julia.py +0 -0
  138. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_jupyter.py +0 -0
  139. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_magics.py +0 -0
  140. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_matlab.py +0 -0
  141. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_notebooks.py +0 -0
  142. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_pipeline.py +0 -0
  143. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_releases.py +0 -0
  144. {calkit_python-0.37.6 → calkit_python-0.38.0}/calkit/tests/test_templates.py +0 -0
  145. {calkit_python-0.37.6 → calkit_python-0.38.0}/conftest.py +0 -0
  146. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/CNAME +0 -0
  147. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/apps.md +0 -0
  148. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/calculations.md +0 -0
  149. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/calkit-yaml.md +0 -0
  150. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/datasets.md +0 -0
  151. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/dependencies.md +0 -0
  152. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/environments.md +0 -0
  153. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/examples.md +0 -0
  154. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/governance.md +0 -0
  155. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/help.md +0 -0
  156. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/c-to-the-k-white.svg +0 -0
  157. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/calkit-fragmentation-compendium.png +0 -0
  158. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/calkit-no-bg.png +0 -0
  159. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/connect-zenodo.png +0 -0
  160. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab/all-green.png +0 -0
  161. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab/collect-data-stale.png +0 -0
  162. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab/new-env.png +0 -0
  163. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab/new-notebook.png +0 -0
  164. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab/pipeline-badge.png +0 -0
  165. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/jupyterlab-params.png +0 -0
  166. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/plos-osi-code-2024-03.png +0 -0
  167. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/img/vscode-nb-params.png +0 -0
  168. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/index.md +0 -0
  169. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/jupyterlab.md +0 -0
  170. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/local-server.md +0 -0
  171. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/notebooks.md +0 -0
  172. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/overleaf.md +0 -0
  173. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/pipeline/index.md +0 -0
  174. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/pipeline/manual-steps.md +0 -0
  175. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/pipeline/running-and-logging.md +0 -0
  176. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/pipeline/slurm.md +0 -0
  177. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/quickstart.md +0 -0
  178. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/references.md +0 -0
  179. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/releases.md +0 -0
  180. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/reproducibility.md +0 -0
  181. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/adding-latex-pub-docker.md +0 -0
  182. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/conda-envs.md +0 -0
  183. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/existing-project.md +0 -0
  184. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/first-project.md +0 -0
  185. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/github-actions.md +0 -0
  186. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/actions-repo-secrets.png +0 -0
  187. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
  188. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
  189. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
  190. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
  191. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
  192. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
  193. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
  194. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
  195. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
  196. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
  197. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/push.png +0 -0
  198. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
  199. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
  200. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/chart-more-rows.png +0 -0
  201. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/create-project.png +0 -0
  202. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
  203. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/excel-chart.png +0 -0
  204. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/excel-data.png +0 -0
  205. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
  206. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/needs-clone.png +0 -0
  207. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/new-stage.png +0 -0
  208. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
  209. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
  210. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/status-more-rows.png +0 -0
  211. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
  212. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/untracked-data.png +0 -0
  213. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/updated-publication.png +0 -0
  214. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
  215. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/office/workflow-page.png +0 -0
  216. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/clone.png +0 -0
  217. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/create-project.png +0 -0
  218. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
  219. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
  220. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
  221. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/new-token.png +0 -0
  222. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/reclone.png +0 -0
  223. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
  224. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/quick-actions.png +0 -0
  225. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/run-proc.png +0 -0
  226. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
  227. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
  228. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
  229. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
  230. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
  231. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
  232. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
  233. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/index.md +0 -0
  234. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/jupyterlab.md +0 -0
  235. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/latex-codespaces.md +0 -0
  236. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/matlab.md +0 -0
  237. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/notebook-pipeline.md +0 -0
  238. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/office.md +0 -0
  239. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/openfoam.md +0 -0
  240. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/procedures.md +0 -0
  241. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/tutorials/vscode-slurm-notebook.md +0 -0
  242. {calkit_python-0.37.6 → calkit_python-0.38.0}/docs/version-control.md +0 -0
  243. {calkit_python-0.37.6 → calkit_python-0.38.0}/install.json +0 -0
  244. {calkit_python-0.37.6 → calkit_python-0.38.0}/jest.config.js +0 -0
  245. {calkit_python-0.37.6 → calkit_python-0.38.0}/jupyter-config/server-config/calkit.json +0 -0
  246. {calkit_python-0.37.6 → calkit_python-0.38.0}/mkdocs.yml +0 -0
  247. {calkit_python-0.37.6 → calkit_python-0.38.0}/package.json +0 -0
  248. {calkit_python-0.37.6 → calkit_python-0.38.0}/pyproject.toml +0 -0
  249. {calkit_python-0.37.6 → calkit_python-0.38.0}/schema/plugin.json +0 -0
  250. {calkit_python-0.37.6 → calkit_python-0.38.0}/scripts/generate-docs-references.py +0 -0
  251. {calkit_python-0.37.6 → calkit_python-0.38.0}/scripts/install.ps1 +0 -0
  252. {calkit_python-0.37.6 → calkit_python-0.38.0}/scripts/install.sh +0 -0
  253. {calkit_python-0.37.6 → calkit_python-0.38.0}/scripts/make-calk9.sh +0 -0
  254. {calkit_python-0.37.6 → calkit_python-0.38.0}/scripts/sync-docs.py +0 -0
  255. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/__tests__/useQueries.spec.ts +0 -0
  256. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/calkit-config.ts +0 -0
  257. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/cell-output-marker.ts +0 -0
  258. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/commit-dialog.tsx +0 -0
  259. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/environment-editor.tsx +0 -0
  260. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/notebook-registration.tsx +0 -0
  261. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/notebook-toolbar.tsx +0 -0
  262. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/pipeline-status-bar.tsx +0 -0
  263. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/project-info-editor.tsx +0 -0
  264. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/sidebar-settings.tsx +0 -0
  265. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/sidebar.tsx +0 -0
  266. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/components/stage-editor.tsx +0 -0
  267. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/feature-flags.ts +0 -0
  268. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/file-browser-menu.ts +0 -0
  269. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/hooks/__tests__/useQueries.test.tsx +0 -0
  270. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/hooks/useQueries.ts +0 -0
  271. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/icons.ts +0 -0
  272. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/index.ts +0 -0
  273. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/io-tracker.ts +0 -0
  274. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/pipeline-state.ts +0 -0
  275. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/queryClient.ts +0 -0
  276. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/request.ts +0 -0
  277. {calkit_python-0.37.6 → calkit_python-0.38.0}/src/shims-mainmenu.d.ts +0 -0
  278. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/base.css +0 -0
  279. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/cell-output-marker.css +0 -0
  280. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/environment-editor.css +0 -0
  281. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/environment-selector.css +0 -0
  282. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/img/calkit-no-bg.png +0 -0
  283. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/index.css +0 -0
  284. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/index.js +0 -0
  285. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/notebook-toolbar.css +0 -0
  286. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/pipeline-status-bar.css +0 -0
  287. {calkit_python-0.37.6 → calkit_python-0.38.0}/style/sidebar.css +0 -0
  288. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/dvc-md5-dir/osx-arm64.txt +0 -0
  289. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/nb-julia.ipynb +0 -0
  290. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/nb-params.ipynb +0 -0
  291. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/nb-subdir.ipynb +0 -0
  292. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/pipeline.ipynb +0 -0
  293. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/script.py +0 -0
  294. {calkit_python-0.37.6 → calkit_python-0.38.0}/test/test-log.log +0 -0
  295. {calkit_python-0.37.6 → calkit_python-0.38.0}/tsconfig.json +0 -0
  296. {calkit_python-0.37.6 → calkit_python-0.38.0}/tsconfig.test.json +0 -0
  297. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/.gitignore +0 -0
  298. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/README.md +0 -0
  299. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/jupyter_server_test_config.py +0 -0
  300. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/package.json +0 -0
  301. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/playwright.config.js +0 -0
  302. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/tests/calkit.spec.ts +0 -0
  303. {calkit_python-0.37.6 → calkit_python-0.38.0}/ui-tests/yarn.lock +0 -0
  304. {calkit_python-0.37.6 → calkit_python-0.38.0}/uv.lock +0 -0
  305. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/.gitignore +0 -0
  306. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/.vscodeignore +0 -0
  307. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/CHANGELOG.md +0 -0
  308. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/LICENSE +0 -0
  309. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/README.md +0 -0
  310. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/images/calkit-no-bg.png +0 -0
  311. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/package-lock.json +0 -0
  312. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/package.json +0 -0
  313. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/scripts/set-proposed-api.js +0 -0
  314. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/environments.ts +0 -0
  315. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/extension.ts +0 -0
  316. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/notebooks.ts +0 -0
  317. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/test/environments.test.ts +0 -0
  318. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/test/notebooks.test.ts +0 -0
  319. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/src/types.ts +0 -0
  320. {calkit_python-0.37.6 → calkit_python-0.38.0}/vscode-ext/tsconfig.json +0 -0
  321. {calkit_python-0.37.6 → calkit_python-0.38.0}/yarn.lock +0 -0
@@ -0,0 +1,13 @@
1
+ # Agent instructions for working on Calkit
2
+
3
+ ## Repo structure
4
+
5
+ - The main Python package/CLI lives in `calkit`
6
+ - The JupyterLab extension lives in `src`
7
+ - The VS Code extension lives in `vscode-ext`
8
+
9
+ ## Working
10
+
11
+ See `CONTRIBUTING.md` for tool usage, style guidelines, etc.
12
+
13
+ To run tests, use `uv run pytest`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.37.6
3
+ Version: 0.38.0
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -174,7 +174,11 @@ You may also want to install [Docker](https://docker.com),
174
174
  since that is the default method by which LaTeX environments are created.
175
175
  If you want to use the [Calkit Cloud](https://calkit.io)
176
176
  for collaboration and backup as a DVC remote,
177
- you can [set up cloud integration](https://docs.calkit.org/cloud-integration).
177
+ you can [set up cloud integration](https://docs.calkit.org/cloud-integration) with:
178
+
179
+ ```sh
180
+ calkit cloud login
181
+ ```
178
182
 
179
183
  ### Use without installing
180
184
 
@@ -115,7 +115,11 @@ You may also want to install [Docker](https://docker.com),
115
115
  since that is the default method by which LaTeX environments are created.
116
116
  If you want to use the [Calkit Cloud](https://calkit.io)
117
117
  for collaboration and backup as a DVC remote,
118
- you can [set up cloud integration](https://docs.calkit.org/cloud-integration).
118
+ you can [set up cloud integration](https://docs.calkit.org/cloud-integration) with:
119
+
120
+ ```sh
121
+ calkit cloud login
122
+ ```
119
123
 
120
124
  ### Use without installing
121
125
 
@@ -250,7 +250,7 @@ def check_repro(
250
250
  ) -> None:
251
251
  """Check the reproducibility of a project."""
252
252
  res = check_reproducibility(wdir=wdir, log_func=typer.echo)
253
- typer.echo(res.to_pretty().encode("utf-8", errors="replace"))
253
+ calkit.echo(res.to_pretty())
254
254
 
255
255
 
256
256
  @check_app.command(
@@ -1251,7 +1251,7 @@ def check_dependencies(
1251
1251
  except Exception as e:
1252
1252
  raise_error(str(e))
1253
1253
  message = "✅ All set!"
1254
- typer.echo(message.encode("utf-8", errors="replace"))
1254
+ calkit.echo(message)
1255
1255
 
1256
1256
 
1257
1257
  @check_app.command(name="env-vars")
@@ -1296,7 +1296,7 @@ def check_env_vars(
1296
1296
  with open(".gitignore", "a") as f:
1297
1297
  f.write("\n.env\n")
1298
1298
  message = "✅ All set!"
1299
- typer.echo(message.encode("utf-8", errors="replace"))
1299
+ calkit.echo(message)
1300
1300
 
1301
1301
 
1302
1302
  @check_app.command(name="pipeline")
@@ -1326,7 +1326,7 @@ def check_pipeline(
1326
1326
  if stage_name.startswith("_"):
1327
1327
  raise_error("Stage names cannot start with an underscore")
1328
1328
  message = "✅ This project's pipeline is defined correctly!"
1329
- typer.echo(message.encode("utf-8", errors="replace"))
1329
+ calkit.echo(message)
1330
1330
  if compile_to_dvc:
1331
1331
  typer.echo("Attempting to compile to DVC stages")
1332
1332
  try:
@@ -0,0 +1,125 @@
1
+ """CLI for interacting with Calkit Cloud instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import socket
6
+ import time
7
+ import webbrowser
8
+ from typing import Annotated
9
+
10
+ import typer
11
+ from requests.exceptions import HTTPError
12
+
13
+ import calkit
14
+ from calkit.cli import raise_error
15
+
16
+ cloud_app = typer.Typer(no_args_is_help=True)
17
+
18
+
19
+ @cloud_app.command(name="get")
20
+ def get(endpoint: Annotated[str, typer.Argument(help="API endpoint")]):
21
+ """Get a resource from the Cloud API."""
22
+ if not endpoint.startswith("/"):
23
+ endpoint = "/" + endpoint
24
+ try:
25
+ resp = calkit.cloud.get(endpoint)
26
+ typer.echo(resp)
27
+ except Exception as e:
28
+ raise_error(str(e))
29
+
30
+
31
+ @cloud_app.command(name="login")
32
+ def login(
33
+ force: Annotated[
34
+ bool,
35
+ typer.Option(
36
+ "--force",
37
+ "-f",
38
+ help=(
39
+ "Force logging in again even if already authenticated. "
40
+ "Will store a new token in your local config."
41
+ ),
42
+ ),
43
+ ] = False,
44
+ ):
45
+ """Login to the Calkit Cloud.
46
+
47
+ First try a GET request to the /user endpoint to check if the user is
48
+ already logged in. If not, perform OAuth device flow.
49
+ """
50
+ try:
51
+ calkit.cloud.get("/user")
52
+ calkit.echo("Authenticated successfully ✅")
53
+ if not force:
54
+ return
55
+ except (ValueError, HTTPError) as e:
56
+ if isinstance(e, HTTPError) and "401" not in str(e):
57
+ raise_error(str(e))
58
+ # Now perform the OAuth device flow
59
+ try:
60
+ hostname = socket.gethostname()
61
+ except Exception:
62
+ hostname = None
63
+ try:
64
+ calkit.echo("Initiating device login flow")
65
+ resp = calkit.cloud.post(
66
+ "/login/device",
67
+ json={"hostname": hostname},
68
+ auth=False,
69
+ )
70
+ device_code = resp["device_code"]
71
+ verification_uri = resp["verification_uri"]
72
+ expires_in = int(resp["expires_in"])
73
+ interval = int(resp["interval"])
74
+ except Exception as e:
75
+ raise_error(f"Failed to initiate device login flow: {e}")
76
+ calkit.echo("Authorize this device by opening this URL:")
77
+ calkit.echo(verification_uri)
78
+ calkit.echo("Waiting for authorization")
79
+ try:
80
+ webbrowser.open(verification_uri)
81
+ except Exception:
82
+ # If auto-open fails, user can still copy-paste the URL.
83
+ pass
84
+ deadline = time.monotonic() + expires_in
85
+ while time.monotonic() < deadline:
86
+ try:
87
+ token_resp = calkit.cloud.post(
88
+ "/login/device/token",
89
+ json={"device_code": device_code},
90
+ auth=False,
91
+ )
92
+ except Exception as e:
93
+ txt = str(e)
94
+ if "Device code has expired" in txt:
95
+ raise_error(
96
+ "Device code has expired; Run 'calkit cloud login' again"
97
+ )
98
+ if "Device code not found" in txt:
99
+ raise_error(
100
+ "Device code not found; Run 'calkit cloud login' again"
101
+ )
102
+ raise_error(f"Error while polling for device authorization: {e}")
103
+ access_token = token_resp.get("access_token")
104
+ if access_token:
105
+ refresh_token = token_resp.get("refresh_token")
106
+ try:
107
+ cfg = calkit.config.read()
108
+ cfg.access_token = access_token
109
+ if refresh_token:
110
+ cfg.refresh_token = refresh_token
111
+ cfg.write()
112
+ calkit.cloud._tokens[calkit.cloud.get_base_url()] = (
113
+ access_token
114
+ )
115
+ except Exception as e:
116
+ raise_error(f"Failed to save token in config: {e}")
117
+ calkit.echo("Logged in successfully ✅")
118
+ return
119
+ sleep_seconds = min(interval, max(0.0, deadline - time.monotonic()))
120
+ if sleep_seconds > 0:
121
+ time.sleep(sleep_seconds)
122
+ raise_error(
123
+ "Timed out waiting for device authorization; "
124
+ "Run 'calkit cloud login' again"
125
+ )
@@ -142,7 +142,10 @@ def config_github_ssh():
142
142
  # First check if we can already connect to GitHub
143
143
  ssh_test_cmd = ["ssh", "-T", "git@github.com"]
144
144
  p = subprocess.run(ssh_test_cmd, capture_output=True, text=True)
145
- if "successfully authenticated" in p.stderr:
145
+ if (
146
+ "successfully authenticated" in p.stdout
147
+ or "successfully authenticated" in p.stderr
148
+ ):
146
149
  typer.echo("You can already connect to GitHub via SSH")
147
150
  go_on = typer.confirm("Do you want to add a new SSH key anyway?")
148
151
  if not go_on:
@@ -1564,12 +1564,7 @@ def run(
1564
1564
  if failed:
1565
1565
  raise_error("Pipeline failed")
1566
1566
  else:
1567
- _enc = sys.stdout.encoding or "utf-8"
1568
- typer.echo(
1569
- "Pipeline completed successfully ✅".encode(
1570
- _enc, errors="replace"
1571
- ).decode(_enc)
1572
- )
1567
+ calkit.echo("Pipeline completed successfully ")
1573
1568
  if save_after_run or save_message is not None:
1574
1569
  if save_message is None:
1575
1570
  save_message = "Run pipeline"
@@ -0,0 +1,232 @@
1
+ """The REST API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import logging
8
+ import threading
9
+ import time
10
+ from functools import partial
11
+ from typing import Literal
12
+
13
+ import requests
14
+ from requests.exceptions import HTTPError
15
+
16
+ from . import config
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # A dictionary of tokens keyed by base URL
21
+ _tokens = {}
22
+
23
+ # Single lock guarding all token-refresh operations to prevent thundering herds
24
+ # (e.g., many concurrent fsspec threads all attempting to refresh at once).
25
+ _refresh_lock = threading.Lock()
26
+
27
+ # Seconds before JWT expiry at which we proactively refresh.
28
+ _REFRESH_BUFFER_SECONDS = 60
29
+
30
+
31
+ def get_base_url() -> str:
32
+ """Get the API base URL."""
33
+ urls = {
34
+ "local": "http://api.localhost",
35
+ "staging": "https://api.staging.calkit.io",
36
+ "production": "https://api.calkit.io",
37
+ "test": "http://api.localhost",
38
+ }
39
+ return urls[config.get_env()]
40
+
41
+
42
+ def _jwt_exp(token: str) -> float | None:
43
+ """Return the ``exp`` claim of a JWT as a UTC timestamp, or ``None``.
44
+
45
+ Does not verify the signature — only used for proactive expiry checks.
46
+ """
47
+ try:
48
+ # JWTs are <header>.<payload>.<sig>, all base64url-encoded
49
+ payload_b64 = token.split(".")[1]
50
+ # Pad to a multiple of 4 for standard base64 decoding
51
+ padding = 4 - len(payload_b64) % 4
52
+ if padding != 4:
53
+ payload_b64 += "=" * padding
54
+ payload = json.loads(base64.urlsafe_b64decode(payload_b64))
55
+ return float(payload["exp"])
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def _token_is_expiring(token: str) -> bool:
61
+ """Return True if ``token`` is a JWT that expires within the refresh
62
+ buffer.
63
+ """
64
+ exp = _jwt_exp(token)
65
+ if exp is None:
66
+ return False # Not a JWT (e.g. a PAT) — never proactively refresh
67
+ return time.time() >= exp - _REFRESH_BUFFER_SECONDS
68
+
69
+
70
+ def get_token() -> str:
71
+ """Return a valid bearer token.
72
+
73
+ Priority: in-memory cache (refreshed proactively if expiring), then PAT
74
+ (``token`` / ``CALKIT_TOKEN`` env var), then short-lived ``access_token``
75
+ from device login.
76
+
77
+ For access tokens, expiry is read from the JWT ``exp`` claim and a refresh
78
+ is attempted before the token expires. Only one thread performs the
79
+ refresh at a time; all others wait behind ``_refresh_lock`` and then use
80
+ the new token that was stored in the cache.
81
+ """
82
+ base_url = get_base_url()
83
+ cached = _tokens.get(base_url)
84
+ if cached is not None and not _token_is_expiring(cached):
85
+ return cached
86
+ # Token missing or expiring — acquire the lock so only one thread refreshes
87
+ with _refresh_lock:
88
+ # Re-check after acquiring lock; another thread may have refreshed
89
+ cached = _tokens.get(base_url)
90
+ if cached is not None and not _token_is_expiring(cached):
91
+ return cached
92
+ # If we have a refresh token, try to get a new access token now
93
+ if cached is not None and _token_is_expiring(cached):
94
+ new_token = _do_refresh()
95
+ if new_token is not None:
96
+ return new_token
97
+ # No usable cached token — load from config
98
+ cfg = config.read()
99
+ # Prefer long-lived PAT if present (env var CALKIT_TOKEN or config)
100
+ if cfg.token is not None:
101
+ _tokens[base_url] = cfg.token
102
+ return cfg.token
103
+ # Fall back to short-lived access token from device login
104
+ if cfg.access_token is not None:
105
+ if not _token_is_expiring(cfg.access_token):
106
+ _tokens[base_url] = cfg.access_token
107
+ return cfg.access_token
108
+ # Config token is also expiring — attempt refresh
109
+ new_token = _do_refresh()
110
+ if new_token is not None:
111
+ return new_token
112
+ raise ValueError(
113
+ "No token found; Run 'calkit cloud login' to authenticate"
114
+ )
115
+
116
+
117
+ def _do_refresh() -> str | None:
118
+ """Perform a token refresh request.
119
+
120
+ Must be called with ``_refresh_lock`` already held (or from within
121
+ ``_try_refresh`` which acquires it). Returns the new access token on
122
+ success or ``None`` on failure.
123
+ """
124
+ cfg = config.read()
125
+ if cfg.refresh_token is None:
126
+ return None
127
+ base_url = get_base_url()
128
+ try:
129
+ resp = requests.post(
130
+ base_url + "/login/refresh",
131
+ json={"refresh_token": cfg.refresh_token},
132
+ )
133
+ resp.raise_for_status()
134
+ data = resp.json()
135
+ new_access = data["access_token"]
136
+ new_refresh = data.get("refresh_token", cfg.refresh_token)
137
+ cfg.access_token = new_access
138
+ cfg.refresh_token = new_refresh
139
+ cfg.write()
140
+ _tokens[base_url] = new_access
141
+ return new_access
142
+ except Exception as exc:
143
+ logger.debug("Token refresh failed: %s", exc)
144
+ return None
145
+
146
+
147
+ def _try_refresh() -> str | None:
148
+ """Attempt a token refresh from outside the normal request cycle.
149
+
150
+ Acquires ``_refresh_lock`` to prevent concurrent refresh storms.
151
+ Returns the new access token on success, or ``None``.
152
+ """
153
+ with _refresh_lock:
154
+ return _do_refresh()
155
+
156
+
157
+ def get_headers(headers: dict | None = None, auth: bool = True) -> dict:
158
+ if auth:
159
+ base_headers = {"Authorization": f"Bearer {get_token()}"}
160
+ else:
161
+ base_headers = {}
162
+ if headers is not None:
163
+ return base_headers | headers
164
+ else:
165
+ return base_headers
166
+
167
+
168
+ def _request(
169
+ kind: Literal["get", "post", "put", "patch", "delete"],
170
+ path: str,
171
+ params: dict | None = None,
172
+ json: dict | None = None,
173
+ data: dict | None = None,
174
+ headers: dict | None = None,
175
+ as_json=True,
176
+ auth: bool = True,
177
+ base_url: str | None = None,
178
+ **kwargs,
179
+ ):
180
+ max_retries = 10
181
+ base_delay_seconds = 0.25
182
+ max_delay_seconds = 30
183
+ func = getattr(requests, kind)
184
+ if base_url is None:
185
+ base_url = get_base_url()
186
+ refresh_attempted = False
187
+ for retry_num in range(max_retries + 1):
188
+ resp = func(
189
+ base_url + path,
190
+ params=params,
191
+ json=json,
192
+ data=data,
193
+ headers=get_headers(headers, auth=auth),
194
+ **kwargs,
195
+ )
196
+ if resp.status_code == 502 and retry_num < max_retries:
197
+ wait = min(base_delay_seconds * (2**retry_num), max_delay_seconds)
198
+ time.sleep(wait)
199
+ continue
200
+ # On 401, attempt a token refresh once (only for access_token sessions,
201
+ # not PATs — _try_refresh returns None when no refresh_token is stored).
202
+ # get_headers() re-calls get_token() on the next iteration, so it will
203
+ # automatically pick up the new token stored in _tokens by _try_refresh.
204
+ if resp.status_code == 401 and auth and not refresh_attempted:
205
+ refresh_attempted = True
206
+ new_token = _try_refresh()
207
+ if new_token is not None:
208
+ continue
209
+ try:
210
+ resp.raise_for_status()
211
+ except HTTPError as e:
212
+ try:
213
+ detail = resp.json()["detail"]
214
+ except Exception:
215
+ raise e
216
+ raise HTTPError(f"{resp.status_code}: {detail}")
217
+ break
218
+ if as_json:
219
+ return resp.json()
220
+ else:
221
+ return resp
222
+
223
+
224
+ get = partial(_request, "get")
225
+ post = partial(_request, "post")
226
+ patch = partial(_request, "patch")
227
+ put = partial(_request, "put")
228
+ delete = partial(_request, "delete")
229
+
230
+
231
+ def get_current_user() -> dict:
232
+ return get("/user")
@@ -165,10 +165,10 @@ class Settings(BaseSettings):
165
165
  env_file=".env",
166
166
  env_file_encoding="utf-8",
167
167
  )
168
- username: str | None = None
169
168
  email: str | None = None
170
- password: KeyringOptionalSecret | None = None
171
169
  token: KeyringOptionalSecret | None = None
170
+ access_token: KeyringOptionalSecret | None = None
171
+ refresh_token: KeyringOptionalSecret | None = None
172
172
  dvc_token: KeyringOptionalSecret | None = None
173
173
  dataframe_engine: Literal["pandas", "polars"] = "pandas"
174
174
  github_token: KeyringOptionalSecret | None = None
@@ -14,6 +14,7 @@ import platform
14
14
  import re
15
15
  import socket
16
16
  import subprocess
17
+ import sys
17
18
  import uuid
18
19
  import warnings
19
20
  from os import PathLike
@@ -93,6 +94,16 @@ DVC_EXTENSIONS = [
93
94
  DVC_SIZE_THRESH_BYTES = 5_000_000
94
95
 
95
96
 
97
+ def echo(message: str) -> None:
98
+ """Print a message safely, replacing unencodable characters
99
+ (e.g., emoji).
100
+ """
101
+ import typer
102
+
103
+ enc = sys.stdout.encoding or "utf-8"
104
+ typer.echo(message.encode(enc, errors="replace").decode(enc))
105
+
106
+
96
107
  def find_project_dirs(relative=False, max_depth=3) -> list[str]:
97
108
  """Find all Calkit project directories."""
98
109
  if relative:
@@ -0,0 +1,170 @@
1
+ """Tests for the cloud CLI."""
2
+
3
+ import pytest
4
+ import typer
5
+ from requests.exceptions import HTTPError
6
+
7
+ import calkit.cli.cloud as cloud_cli
8
+
9
+
10
+ def test_cloud_login_already_logged_in(monkeypatch, capsys):
11
+ def _get(path):
12
+ assert path == "/user"
13
+ return {"email": "user@example.com"}
14
+
15
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "get", _get)
16
+ cloud_cli.login()
17
+ out = capsys.readouterr().out
18
+ assert "Authenticated successfully" in out
19
+
20
+
21
+ def test_cloud_login_device_flow_success(monkeypatch, capsys):
22
+ call_counts = {"token_polls": 0}
23
+ post_calls = []
24
+
25
+ class DummyConfig:
26
+ def __init__(self):
27
+ self.token = None
28
+ self.access_token = None
29
+ self.refresh_token = None
30
+ self.written = False
31
+
32
+ def write(self):
33
+ self.written = True
34
+
35
+ cfg = DummyConfig()
36
+
37
+ def _get(path):
38
+ assert path == "/user"
39
+ raise HTTPError("401: Not authenticated")
40
+
41
+ def _post(path, **kwargs):
42
+ post_calls.append((path, kwargs))
43
+ if path == "/login/device":
44
+ return {
45
+ "device_code": "dev-123",
46
+ "verification_uri": (
47
+ "https://app.example.com/cli-auth?device_code=dev-123"
48
+ ),
49
+ "expires_in": 60,
50
+ "interval": 1,
51
+ }
52
+ if path == "/login/device/token":
53
+ call_counts["token_polls"] += 1
54
+ if call_counts["token_polls"] < 2:
55
+ return {"detail": "Authorization pending"}
56
+ return {
57
+ "access_token": "ckp_test_access",
58
+ "refresh_token": "ckp_test_refresh",
59
+ }
60
+ raise AssertionError(f"Unexpected path: {path}")
61
+
62
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "get", _get)
63
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "post", _post)
64
+ monkeypatch.setattr(cloud_cli.calkit.config, "read", lambda: cfg)
65
+ monkeypatch.setattr(cloud_cli.webbrowser, "open", lambda _url: True)
66
+ monkeypatch.setattr(cloud_cli.time, "sleep", lambda _seconds: None)
67
+ cloud_cli.login()
68
+ out = capsys.readouterr().out
69
+ assert "Authorize this device by opening this URL:" in out
70
+ assert "Waiting for authorization" in out
71
+ assert "Logged in successfully" in out
72
+ assert cfg.access_token == "ckp_test_access"
73
+ assert cfg.refresh_token == "ckp_test_refresh"
74
+ assert cfg.token is None # PAT field must not be touched by device login
75
+ assert cfg.written is True
76
+ assert post_calls[0][0] == "/login/device"
77
+ assert post_calls[0][1].get("auth") is False
78
+ assert post_calls[1][0] == "/login/device/token"
79
+ assert post_calls[1][1].get("auth") is False
80
+
81
+
82
+ def test_cloud_login_force_re_authenticates(monkeypatch, capsys):
83
+ """--force should start the device flow even when already authenticated."""
84
+
85
+ class DummyConfig:
86
+ def __init__(self):
87
+ self.token = None
88
+ self.access_token = None
89
+ self.refresh_token = None
90
+
91
+ def write(self):
92
+ pass
93
+
94
+ cfg = DummyConfig()
95
+
96
+ def _get(path):
97
+ return {"email": "user@example.com"}
98
+
99
+ def _post(path, **kwargs):
100
+ if path == "/login/device":
101
+ return {
102
+ "device_code": "dev-force",
103
+ "verification_uri": "https://example.com/auth",
104
+ "expires_in": 60,
105
+ "interval": 1,
106
+ }
107
+ if path == "/login/device/token":
108
+ return {
109
+ "access_token": "ckp_new_access",
110
+ "refresh_token": "ckp_new_refresh",
111
+ }
112
+ raise AssertionError(f"Unexpected path: {path}")
113
+
114
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "get", _get)
115
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "post", _post)
116
+ monkeypatch.setattr(cloud_cli.calkit.config, "read", lambda: cfg)
117
+ monkeypatch.setattr(cloud_cli.webbrowser, "open", lambda _url: True)
118
+ monkeypatch.setattr(cloud_cli.time, "sleep", lambda _s: None)
119
+ cloud_cli.login(force=True)
120
+ out = capsys.readouterr().out
121
+ assert "Logged in successfully" in out
122
+ assert cfg.access_token == "ckp_new_access"
123
+
124
+
125
+ def test_cloud_login_device_code_expired(monkeypatch):
126
+ """Expired device code during polling should raise Exit."""
127
+
128
+ def _get(path):
129
+ raise HTTPError("401: Not authenticated")
130
+
131
+ def _post(path, **kwargs):
132
+ if path == "/login/device":
133
+ return {
134
+ "device_code": "dev-exp",
135
+ "verification_uri": "https://example.com/auth",
136
+ "expires_in": 60,
137
+ "interval": 1,
138
+ }
139
+ raise Exception("401: Device code has expired")
140
+
141
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "get", _get)
142
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "post", _post)
143
+ monkeypatch.setattr(cloud_cli.webbrowser, "open", lambda _url: True)
144
+ monkeypatch.setattr(cloud_cli.time, "sleep", lambda _s: None)
145
+ with pytest.raises(typer.Exit):
146
+ cloud_cli.login()
147
+
148
+
149
+ def test_cloud_login_device_code_not_found(monkeypatch):
150
+ """Not-found device code during polling should raise Exit."""
151
+
152
+ def _get(path):
153
+ raise HTTPError("401: Not authenticated")
154
+
155
+ def _post(path, **kwargs):
156
+ if path == "/login/device":
157
+ return {
158
+ "device_code": "dev-nf",
159
+ "verification_uri": "https://example.com/auth",
160
+ "expires_in": 60,
161
+ "interval": 1,
162
+ }
163
+ raise Exception("404: Device code not found")
164
+
165
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "get", _get)
166
+ monkeypatch.setattr(cloud_cli.calkit.cloud, "post", _post)
167
+ monkeypatch.setattr(cloud_cli.webbrowser, "open", lambda _url: True)
168
+ monkeypatch.setattr(cloud_cli.time, "sleep", lambda _s: None)
169
+ with pytest.raises(typer.Exit):
170
+ cloud_cli.login()