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.
- calkit_python-0.41.5/.gitattributes +1 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/AGENTS.md +2 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/PKG-INFO +1 -1
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/check.py +1 -1
- calkit_python-0.41.5/calkit/cli/cloud.py +64 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/core.py +6 -4
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/new.py +3 -3
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/scheduler.py +16 -7
- calkit_python-0.41.5/calkit/cloud.py +420 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/detect.py +18 -11
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/core.py +70 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/zip.py +7 -2
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/environments.py +17 -9
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/git.py +4 -4
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/overleaf.py +4 -1
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/pipeline.py +16 -39
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/releases.py +2 -2
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_core.py +25 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_xr.py +5 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_check.py +9 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_cloud.py +18 -8
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_latex.py +15 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_new.py +5 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_notebooks.py +9 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_overleaf.py +5 -1
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_scheduler.py +7 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_update.py +5 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/test_core.py +88 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_cloud.py +76 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_conda.py +17 -2
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_git.py +14 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_install.py +13 -0
- calkit_python-0.41.3/calkit/cli/cloud.py +0 -126
- calkit_python-0.41.3/calkit/cloud.py +0 -236
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.claude-plugin/marketplace.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.gitignore +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.pre-commit-config.yaml +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.prettierignore +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.python-version +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.vscode/launch.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.vscode/tasks.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/.yarnrc.yml +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/CITATION.cff +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/CODE_OF_CONDUCT.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/CONTRIBUTING.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/LICENSE +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/Makefile +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/README.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/.claude-plugin/plugin.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/add-pipeline-stage/SKILL.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/conventions/SKILL.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/agent-plugin/skills/create-pipeline/SKILL.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/babel.config.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/__main__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/calc.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/check.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/config.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/describe.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/dev.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/import_.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/latex.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/list.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/main/xr.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/notebooks.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/office.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/overleaf.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/cli/update.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/conda.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/config.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/datasets.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dependencies.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/docker.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/dvc/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/fs.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/github.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/gui.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/install.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/invenio.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/julia.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyter.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyterlab/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/jupyterlab/routes.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/package.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/909.e3f9cc3408834a7fdcc3.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/remoteEntry.65469af996e7a96aa983.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/style.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/labextension/static/third-party-licenses.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/licenses.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/magics.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/matlab.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/io.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/iteration.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/models/pipeline.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/notebooks.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/office.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/ops.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/server.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/article/paper.tex +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/jfm.bst +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/jfm.cls +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/lineno-FLM.sty +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/paper.tex +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/templates/latex/jfm/upmath.sty +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/main/test_subprojects.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_config.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_import.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/cli/test_list.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/dvc/test_zip.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/jupyterlab/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/jupyterlab/test_routes.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/__init__.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/test_iteration.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/models/test_pipeline.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_calc.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_check.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_dependencies.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_detect.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_docker.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_environments.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_fs.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_invenio.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_julia.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_magics.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_matlab.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_notebooks.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_pipeline.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_releases.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/calkit/tests/test_templates.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/conftest.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/CNAME +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/ai-tools.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/apps.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/calculations.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/calkit-yaml.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/cli-reference.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/cloud-integration.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/datasets.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/dependencies.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/environments.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/examples.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/governance.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/help.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/hpc.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/c-to-the-k-white.svg +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/calkit-fragmentation-compendium.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/calkit-no-bg.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/connect-zenodo.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/all-green.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/collect-data-stale.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/new-env.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/new-notebook.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab/pipeline-badge.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/jupyterlab-params.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/plos-osi-code-2024-03.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/img/vscode-nb-params.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/index.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/installation.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/jupyterlab.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/local-server.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/notebooks.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/overleaf.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/index.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/manual-steps.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/pipeline/running-and-logging.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/quickstart.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/references.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/releases.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/reproducibility.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/subprojects.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/adding-latex-pub-docker.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/conda-envs.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/existing-project.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/first-project.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/github-actions.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/actions-repo-secrets.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/building-codespace.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/codespaces-secrets-2.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/editor-split.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/go-to-linked-code.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/issue-from-selection.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-project.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-pub-2.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/new-token.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/paper.tex.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/project-home-3.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/push.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/latex-codespaces/stage.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/anakin-excel.jpg +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/chart-more-rows.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/create-project.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/elsevier-research-data-guidelines.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/excel-chart.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/excel-data.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/insert-link-to-file.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/needs-clone.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/new-stage.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/phd-comics-version-control.webp +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/pipeline-out-of-date.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/status-more-rows.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/uncommitted-changes.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/untracked-data.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/updated-publication.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/word-to-pdf-stage-2.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/office/workflow-page.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/clone.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/create-project.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/datasets-page.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/figure-on-website-updated.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/figure-on-website.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/new-token.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/reclone.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/openfoam/status-after-import-dataset.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/quick-actions.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/run-proc.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-calkit-env.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-inner-env.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/create-new-calkit-env.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/select-calkit-env.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/slurm-job-running.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/slurm-launch-options.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/img/vscode-slurm-notebook/starting-slurm-job.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/index.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/jupyterlab.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/latex-codespaces.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/matlab.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/notebook-pipeline.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/office.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/openfoam.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/procedures.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/tutorials/vscode-slurm-notebook.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/docs/version-control.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/flake.lock +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/flake.nix +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/install.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/jest.config.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/jupyter-config/server-config/calkit.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/mkdocs.yml +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/package.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/pyproject.toml +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/schema/plugin.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/generate-docs-references.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/install.ps1 +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/install.sh +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/make-calk9.sh +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/scripts/sync-docs.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/__tests__/useQueries.spec.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/calkit-config.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/cell-output-marker.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/commit-dialog.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/environment-editor.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/notebook-registration.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/notebook-toolbar.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/pipeline-status-bar.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/project-info-editor.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/sidebar-settings.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/sidebar.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/components/stage-editor.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/feature-flags.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/file-browser-menu.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/hooks/__tests__/useQueries.test.tsx +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/hooks/useQueries.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/icons.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/index.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/io-tracker.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/pipeline-state.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/queryClient.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/request.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/src/shims-mainmenu.d.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/base.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/cell-output-marker.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/environment-editor.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/environment-selector.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/img/calkit-no-bg.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/index.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/index.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/notebook-toolbar.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/pipeline-status-bar.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/style/sidebar.css +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/dvc-md5-dir/osx-arm64.txt +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-julia.ipynb +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-params.ipynb +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/nb-subdir.ipynb +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/pipeline.ipynb +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/script.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/test/test-log.log +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/tsconfig.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/tsconfig.test.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/.gitignore +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/README.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/jupyter_server_test_config.py +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/package.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/playwright.config.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/tests/calkit.spec.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/ui-tests/yarn.lock +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/uv.lock +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/.gitignore +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/.vscodeignore +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/CHANGELOG.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/LICENSE +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/README.md +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/images/calkit-no-bg.png +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/package-lock.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/package.json +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/scripts/set-proposed-api.js +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/environments.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/extension.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/notebooks.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/environments.test.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/kernel-selection.test.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/test/notebooks.test.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/src/types.ts +0 -0
- {calkit_python-0.41.3 → calkit_python-0.41.5}/vscode-ext/tsconfig.json +0 -0
- {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.
|
|
@@ -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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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 =
|
|
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=
|
|
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
|
|
96
|
-
if
|
|
97
|
-
raise_error("CALKIT_MOCK_SCHEDULER
|
|
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
|
-
|
|
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.
|
|
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")
|