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