calkit-python 0.0.1__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.0.1/.github/FUNDING.yml +16 -0
- calkit_python-0.0.1/.github/workflows/publish-test.yml +49 -0
- calkit_python-0.0.1/.github/workflows/publish.yml +48 -0
- calkit_python-0.0.1/.gitignore +3 -0
- calkit_python-0.0.1/LICENSE +19 -0
- calkit_python-0.0.1/PKG-INFO +75 -0
- calkit_python-0.0.1/README.md +50 -0
- calkit_python-0.0.1/calkit/__init__.py +8 -0
- calkit_python-0.0.1/calkit/cli.py +117 -0
- calkit_python-0.0.1/calkit/cloud.py +95 -0
- calkit_python-0.0.1/calkit/config.py +71 -0
- calkit_python-0.0.1/calkit/core.py +34 -0
- calkit_python-0.0.1/calkit/data.py +53 -0
- calkit_python-0.0.1/calkit/dvc.py +58 -0
- calkit_python-0.0.1/calkit/git.py +12 -0
- calkit_python-0.0.1/calkit/gui.py +1 -0
- calkit_python-0.0.1/calkit/jupyter.py +46 -0
- calkit_python-0.0.1/calkit/server.py +201 -0
- calkit_python-0.0.1/calkit/tests/__init__.py +0 -0
- calkit_python-0.0.1/calkit/tests/test_core.py +8 -0
- calkit_python-0.0.1/calkit/tests/test_jupyter.py +7 -0
- calkit_python-0.0.1/examples/cfd-study/README.md +3 -0
- calkit_python-0.0.1/examples/cfd-study/calkit.yaml +9 -0
- calkit_python-0.0.1/examples/cfd-study/config/simulations/runs.csv +2 -0
- calkit_python-0.0.1/examples/cfd-study/notebook.ipynb +37 -0
- calkit_python-0.0.1/examples/ms-office/.gitignore +6 -0
- calkit_python-0.0.1/examples/ms-office/README.md +11 -0
- calkit_python-0.0.1/examples/ms-office/calkit.yaml +2 -0
- calkit_python-0.0.1/pyproject.toml +44 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github:
|
|
4
|
+
- petebachant
|
|
5
|
+
patreon: # Replace with a single Patreon username
|
|
6
|
+
open_collective: # Replace with a single Open Collective username
|
|
7
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
8
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
9
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
10
|
+
liberapay: # Replace with a single Liberapay username
|
|
11
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
12
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
13
|
+
polar: # Replace with a single Polar username
|
|
14
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
15
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
16
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution 📦
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.x"
|
|
17
|
+
- name: Install pypa/build
|
|
18
|
+
run: >-
|
|
19
|
+
python3 -m
|
|
20
|
+
pip install
|
|
21
|
+
build
|
|
22
|
+
--user
|
|
23
|
+
- name: Build a binary wheel and a source tarball
|
|
24
|
+
run: python3 -m build
|
|
25
|
+
- name: Store the distribution packages
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: python-package-distributions
|
|
29
|
+
path: dist/
|
|
30
|
+
publish-to-testpypi:
|
|
31
|
+
name: Publish Python 🐍 distribution 📦 to TestPyPI
|
|
32
|
+
needs:
|
|
33
|
+
- build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment:
|
|
36
|
+
name: testpypi
|
|
37
|
+
url: https://test.pypi.org/p/calkit-python
|
|
38
|
+
permissions:
|
|
39
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
40
|
+
steps:
|
|
41
|
+
- name: Download all the dists
|
|
42
|
+
uses: actions/download-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: python-package-distributions
|
|
45
|
+
path: dist/
|
|
46
|
+
- name: Publish distribution 📦 to TestPyPI
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
48
|
+
with:
|
|
49
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution 📦
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.x"
|
|
17
|
+
- name: Install pypa/build
|
|
18
|
+
run: >-
|
|
19
|
+
python3 -m
|
|
20
|
+
pip install
|
|
21
|
+
build
|
|
22
|
+
--user
|
|
23
|
+
- name: Build a binary wheel and a source tarball
|
|
24
|
+
run: python3 -m build
|
|
25
|
+
- name: Store the distribution packages
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: python-package-distributions
|
|
29
|
+
path: dist/
|
|
30
|
+
publish-to-pypi:
|
|
31
|
+
name: >-
|
|
32
|
+
Publish Python 🐍 distribution 📦 to PyPI
|
|
33
|
+
needs:
|
|
34
|
+
- build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment:
|
|
37
|
+
name: pypi
|
|
38
|
+
url: https://pypi.org/p/calkit
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
41
|
+
steps:
|
|
42
|
+
- name: Download all the dists
|
|
43
|
+
uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: python-package-distributions
|
|
46
|
+
path: dist/
|
|
47
|
+
- name: Publish distribution 📦 to PyPI
|
|
48
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 Pete Bachant
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: calkit-python
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Reproducibility made easy.
|
|
5
|
+
Project-URL: Homepage, https://github.com/calkit/calkit
|
|
6
|
+
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
7
|
+
Author-email: Pete Bachant <petebachant@gmail.com>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Requires-Dist: dvc
|
|
14
|
+
Requires-Dist: fastapi
|
|
15
|
+
Requires-Dist: gitpython
|
|
16
|
+
Requires-Dist: keyring
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: polars
|
|
19
|
+
Requires-Dist: pydantic-settings
|
|
20
|
+
Requires-Dist: pydantic[email]
|
|
21
|
+
Requires-Dist: pyjwt
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: typer
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Calkit
|
|
27
|
+
|
|
28
|
+
[Calkit](https://calkit.io) makes reproducible research easy,
|
|
29
|
+
acting as a layer on top of
|
|
30
|
+
[Git](https://git-scm.com/) and [DVC](https://dvc.org/), such that all
|
|
31
|
+
all materials involved in the research process can be fully described in a
|
|
32
|
+
single repository.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Simply run
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install calkit-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Cloud integration
|
|
43
|
+
|
|
44
|
+
The Calkit cloud platform (https://calkit.io) serves as a project
|
|
45
|
+
management interface and a DVC remote for easily storing all versions of your
|
|
46
|
+
data/code/figures/publications, interacting with your collaborators,
|
|
47
|
+
reusing others' research artifacts, etc.
|
|
48
|
+
|
|
49
|
+
After signing up, visit the [settings](https://calkit.io/settings) page
|
|
50
|
+
and create a token.
|
|
51
|
+
Then run
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
calkit config set token ${YOUR_TOKEN_HERE}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then, inside a project repo you'd like to connect to the cloud, run
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
calkit config setup-remote
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This will setup the Calkit DVC remote, such that commands like `dvc push` will
|
|
64
|
+
allow you to push versions of your data or pipeline outputs to the cloud
|
|
65
|
+
for safe storage and sharing with your collaborators.
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
Calkit creates a simple human-readable "database" inside the `calkit.yaml`
|
|
70
|
+
file, which serves as a way to store important information about the project,
|
|
71
|
+
e.g., what question(s) it seeks to answer,
|
|
72
|
+
what files should be considered datasets, figures, publications, etc.
|
|
73
|
+
The Calkit cloud reads this database and registers the various entities
|
|
74
|
+
as part of the entire ecosystem such that if a project is made public,
|
|
75
|
+
other researchers can find and reuse your work to accelerate their own.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Calkit
|
|
2
|
+
|
|
3
|
+
[Calkit](https://calkit.io) makes reproducible research easy,
|
|
4
|
+
acting as a layer on top of
|
|
5
|
+
[Git](https://git-scm.com/) and [DVC](https://dvc.org/), such that all
|
|
6
|
+
all materials involved in the research process can be fully described in a
|
|
7
|
+
single repository.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Simply run
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install calkit-python
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Cloud integration
|
|
18
|
+
|
|
19
|
+
The Calkit cloud platform (https://calkit.io) serves as a project
|
|
20
|
+
management interface and a DVC remote for easily storing all versions of your
|
|
21
|
+
data/code/figures/publications, interacting with your collaborators,
|
|
22
|
+
reusing others' research artifacts, etc.
|
|
23
|
+
|
|
24
|
+
After signing up, visit the [settings](https://calkit.io/settings) page
|
|
25
|
+
and create a token.
|
|
26
|
+
Then run
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
calkit config set token ${YOUR_TOKEN_HERE}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then, inside a project repo you'd like to connect to the cloud, run
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
calkit config setup-remote
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This will setup the Calkit DVC remote, such that commands like `dvc push` will
|
|
39
|
+
allow you to push versions of your data or pipeline outputs to the cloud
|
|
40
|
+
for safe storage and sharing with your collaborators.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
Calkit creates a simple human-readable "database" inside the `calkit.yaml`
|
|
45
|
+
file, which serves as a way to store important information about the project,
|
|
46
|
+
e.g., what question(s) it seeks to answer,
|
|
47
|
+
what files should be considered datasets, figures, publications, etc.
|
|
48
|
+
The Calkit cloud reads this database and registers the various entities
|
|
49
|
+
as part of the entire ecosystem such that if a project is made public,
|
|
50
|
+
other researchers can find and reuse your work to accelerate their own.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The command line interface."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pty
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from typing_extensions import Annotated, Optional
|
|
10
|
+
|
|
11
|
+
import calkit
|
|
12
|
+
|
|
13
|
+
from . import config
|
|
14
|
+
from .dvc import configure_remote, set_remote_auth
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
config_app = typer.Typer()
|
|
18
|
+
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@config_app.command(name="set")
|
|
22
|
+
def set_config_value(key: str, value: str):
|
|
23
|
+
try:
|
|
24
|
+
cfg = config.read()
|
|
25
|
+
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
26
|
+
# Kind of a hack for setting the password computed field
|
|
27
|
+
# Types have been validated above, so this won't hurt to do again
|
|
28
|
+
setattr(cfg, key, value)
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
# TODO: This fails if we try to set password before any config has
|
|
31
|
+
# been written
|
|
32
|
+
# Username is fine
|
|
33
|
+
cfg = config.Settings.model_validate({key: value})
|
|
34
|
+
cfg.write()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@config_app.command(name="get")
|
|
38
|
+
def get_config_value(key: str) -> None:
|
|
39
|
+
cfg = config.read()
|
|
40
|
+
val = getattr(cfg, key)
|
|
41
|
+
if val is not None:
|
|
42
|
+
print(val)
|
|
43
|
+
else:
|
|
44
|
+
print()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@config_app.command(name="setup-remote")
|
|
48
|
+
def setup_remote():
|
|
49
|
+
configure_remote()
|
|
50
|
+
set_remote_auth()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command(name="status")
|
|
54
|
+
def get_status():
|
|
55
|
+
"""Get a unified Git and DVC status."""
|
|
56
|
+
|
|
57
|
+
def print_sep(name: str):
|
|
58
|
+
print(f"------------ {name} ------------")
|
|
59
|
+
|
|
60
|
+
print_sep("Code")
|
|
61
|
+
if os.name == "nt":
|
|
62
|
+
subprocess.call(["git", "status"])
|
|
63
|
+
print()
|
|
64
|
+
print_sep("data")
|
|
65
|
+
subprocess.call(["dvc", "status"])
|
|
66
|
+
else:
|
|
67
|
+
pty.spawn(["git", "status"], lambda fd: os.read(fd, 1024))
|
|
68
|
+
print()
|
|
69
|
+
print_sep("Data")
|
|
70
|
+
pty.spawn(["dvc", "status"], lambda fd: os.read(fd, 1024))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command(name="add")
|
|
74
|
+
def add(paths: list[str]):
|
|
75
|
+
"""Add paths to the repo.
|
|
76
|
+
|
|
77
|
+
Code will be added to Git and data will be added to DVC.
|
|
78
|
+
"""
|
|
79
|
+
dvc_extensions = [".png", ".h5", ".parquet", ".pickle"]
|
|
80
|
+
dvc_size_thresh_bytes = 1_000_000
|
|
81
|
+
dvc_folders = ["data", "figures"]
|
|
82
|
+
if "." in paths:
|
|
83
|
+
print("ERROR: Cannot add '.' with calkit; use git or dvc")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
for path in paths:
|
|
86
|
+
if os.path.isdir(path):
|
|
87
|
+
print("ERROR: Cannot add directories with calkit; use git or dvc")
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
# Detect if this file should be tracked with Git or DVC
|
|
90
|
+
# TODO: Add to whatever
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command(name="commit")
|
|
94
|
+
def commit(
|
|
95
|
+
all: Annotated[Optional[bool], typer.Option("--all", "-a")] = False,
|
|
96
|
+
message: Annotated[Optional[str], typer.Option("--message", "-m")] = None,
|
|
97
|
+
):
|
|
98
|
+
"""Commit a change to the repo."""
|
|
99
|
+
print(all, message)
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command(name="server")
|
|
104
|
+
def run_server():
|
|
105
|
+
import uvicorn
|
|
106
|
+
|
|
107
|
+
uvicorn.run(
|
|
108
|
+
"calkit.server:app",
|
|
109
|
+
port=8866,
|
|
110
|
+
host="localhost",
|
|
111
|
+
reload=True,
|
|
112
|
+
reload_dirs=[os.path.dirname(__file__)],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run() -> None:
|
|
117
|
+
app()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""The REST API client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from . import config
|
|
10
|
+
|
|
11
|
+
# A dictionary of tokens keyed by base URL
|
|
12
|
+
_tokens = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_base_url() -> str:
|
|
16
|
+
"""Get the API base URL."""
|
|
17
|
+
urls = {
|
|
18
|
+
"local": "http://api.localhost",
|
|
19
|
+
"staging": "https://api.staging.calkit.io",
|
|
20
|
+
"production": "https://api.calkit.io",
|
|
21
|
+
}
|
|
22
|
+
return urls[config.get_env()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_token() -> str:
|
|
26
|
+
"""Get a token.
|
|
27
|
+
|
|
28
|
+
Automatically reauthenticate if the token doesn't exist or has expired.
|
|
29
|
+
"""
|
|
30
|
+
token = _tokens.get(get_base_url())
|
|
31
|
+
if token is None:
|
|
32
|
+
token = config.read().token
|
|
33
|
+
_tokens[get_base_url()] = token
|
|
34
|
+
# TODO: Check for expiration
|
|
35
|
+
if token is None:
|
|
36
|
+
return auth()
|
|
37
|
+
return token
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_headers(headers: dict | None = None) -> dict:
|
|
41
|
+
base_headers = {"Authorization": f"Bearer {get_token()}"}
|
|
42
|
+
if headers is not None:
|
|
43
|
+
return base_headers | headers
|
|
44
|
+
else:
|
|
45
|
+
return base_headers
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def auth() -> str:
|
|
49
|
+
"""Authenticate with the server and save a token."""
|
|
50
|
+
cfg = config.read()
|
|
51
|
+
base_url = get_base_url()
|
|
52
|
+
resp = requests.post(
|
|
53
|
+
base_url + "/login/access-token",
|
|
54
|
+
data=dict(username=cfg.username, password=cfg.password),
|
|
55
|
+
)
|
|
56
|
+
token = resp.json()["access_token"]
|
|
57
|
+
_tokens[base_url] = token
|
|
58
|
+
return token
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _request(
|
|
62
|
+
kind: Literal["get", "post", "put", "patch", "delete"],
|
|
63
|
+
path: str,
|
|
64
|
+
params: dict | None = None,
|
|
65
|
+
json: dict | None = None,
|
|
66
|
+
data: dict | None = None,
|
|
67
|
+
headers: dict | None = None,
|
|
68
|
+
as_json=True,
|
|
69
|
+
**kwargs,
|
|
70
|
+
):
|
|
71
|
+
func = getattr(requests, kind)
|
|
72
|
+
resp = func(
|
|
73
|
+
get_base_url() + path,
|
|
74
|
+
params=params,
|
|
75
|
+
json=json,
|
|
76
|
+
data=data,
|
|
77
|
+
headers=get_headers(headers),
|
|
78
|
+
**kwargs,
|
|
79
|
+
)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
if as_json:
|
|
82
|
+
return resp.json()
|
|
83
|
+
else:
|
|
84
|
+
return resp
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
get = partial(_request, "get")
|
|
88
|
+
post = partial(_request, "post")
|
|
89
|
+
patch = partial(_request, "patch")
|
|
90
|
+
put = partial(_request, "put")
|
|
91
|
+
delete = partial(_request, "delete")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_current_user() -> dict:
|
|
95
|
+
return get("/user")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import keyring
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import EmailStr, computed_field
|
|
9
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_env() -> Literal["local", "staging", "production"]:
|
|
13
|
+
return os.getenv(f"{__package__.upper()}_ENV", "production")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_env(name: Literal["local", "staging", "production"]) -> None:
|
|
17
|
+
if name not in ["local", "staging", "production"]:
|
|
18
|
+
raise ValueError(f"{name} is not a valid environment name")
|
|
19
|
+
os.environ[f"{__package__.upper()}_ENV"] = name
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_env_suffix() -> str:
|
|
23
|
+
if get_env() != "production":
|
|
24
|
+
return "-" + get_env()
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_app_name() -> str:
|
|
29
|
+
return __package__ + get_env_suffix()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Settings(BaseSettings):
|
|
33
|
+
model_config = SettingsConfigDict(
|
|
34
|
+
yaml_file=os.path.join(
|
|
35
|
+
os.path.expanduser("~"),
|
|
36
|
+
"." + __package__,
|
|
37
|
+
f"config{get_env_suffix()}.yaml",
|
|
38
|
+
),
|
|
39
|
+
extra="ignore",
|
|
40
|
+
)
|
|
41
|
+
username: EmailStr | None = None
|
|
42
|
+
token: str | None = None
|
|
43
|
+
dvc_token: str | None = None
|
|
44
|
+
dataframe_engine: Literal["pandas", "polars"] = "pandas"
|
|
45
|
+
|
|
46
|
+
@computed_field
|
|
47
|
+
@property
|
|
48
|
+
def password(self) -> str:
|
|
49
|
+
return keyring.get_password(get_app_name(), self.username)
|
|
50
|
+
|
|
51
|
+
@password.setter
|
|
52
|
+
def password(self, value: str) -> None:
|
|
53
|
+
keyring.set_password(get_app_name(), self.username, value)
|
|
54
|
+
|
|
55
|
+
def write(self) -> None:
|
|
56
|
+
base_dir = os.path.dirname(self.model_config["yaml_file"])
|
|
57
|
+
os.makedirs(base_dir, exist_ok=True)
|
|
58
|
+
cfg = self.model_dump()
|
|
59
|
+
# Remove password
|
|
60
|
+
_ = cfg.pop("password")
|
|
61
|
+
with open(self.model_config["yaml_file"], "w") as f:
|
|
62
|
+
yaml.safe_dump(cfg, f)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def read() -> Settings:
|
|
66
|
+
"""Read the config."""
|
|
67
|
+
fpath = Settings.model_config["yaml_file"]
|
|
68
|
+
if not os.path.isfile(fpath):
|
|
69
|
+
return Settings()
|
|
70
|
+
with open(fpath) as f:
|
|
71
|
+
return Settings.model_validate(yaml.safe_load(f))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Core functionality."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from git import Repo
|
|
7
|
+
from git.exc import InvalidGitRepositoryError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def find_project_dirs(relative=False, max_depth=3) -> list[str]:
|
|
11
|
+
"""Find all Calkit project directories."""
|
|
12
|
+
if relative:
|
|
13
|
+
start = ""
|
|
14
|
+
else:
|
|
15
|
+
start = os.path.expanduser("~")
|
|
16
|
+
res = []
|
|
17
|
+
for i in range(max_depth):
|
|
18
|
+
pattern = os.path.join(start, *["*"] * (i + 1), "calkit.yaml")
|
|
19
|
+
res += glob.glob(pattern)
|
|
20
|
+
# Check GitHub documents for users who use GitHub Desktop
|
|
21
|
+
pattern = os.path.join(
|
|
22
|
+
start, "*", "GitHub", *["*"] * (i + 1), "calkit.yaml"
|
|
23
|
+
)
|
|
24
|
+
res += glob.glob(pattern)
|
|
25
|
+
final_res = []
|
|
26
|
+
for ck_fpath in res:
|
|
27
|
+
path = os.path.dirname(ck_fpath)
|
|
28
|
+
# Make sure this path is a Git repo
|
|
29
|
+
try:
|
|
30
|
+
Repo(path)
|
|
31
|
+
except InvalidGitRepositoryError:
|
|
32
|
+
continue
|
|
33
|
+
final_res.append(path)
|
|
34
|
+
return final_res
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Functionality for working with datasets."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Union
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import polars as pl
|
|
7
|
+
|
|
8
|
+
import calkit.config
|
|
9
|
+
|
|
10
|
+
DEFAULT_ENGINE = calkit.config.read().dataframe_engine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_data():
|
|
14
|
+
"""Read the Calkit metadata file and list out our datasets."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_data(
|
|
19
|
+
path: str, engine: Literal["pandas", "polars"] = DEFAULT_ENGINE
|
|
20
|
+
) -> Union[pd.DataFrame, pl.DataFrame]:
|
|
21
|
+
"""Read (tabular) data from dataset with path ``path`` and return a
|
|
22
|
+
DataFrame.
|
|
23
|
+
|
|
24
|
+
If the dataset doesn't exist locally, but is a DVC object, download it
|
|
25
|
+
first.
|
|
26
|
+
|
|
27
|
+
If the dataset path includes a user and project name, we add it to the
|
|
28
|
+
project as an imported dataset, and therefore DVC import it?
|
|
29
|
+
|
|
30
|
+
For example: someuser/someproject:data/somefile.parquet
|
|
31
|
+
|
|
32
|
+
We can run a DVC import command if it needs to be imported. We will need to
|
|
33
|
+
find the Git repo and path within it? Maybe we should require an explicit
|
|
34
|
+
import of the data.
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def write_data(
|
|
40
|
+
data: Union[pd.DataFrame, pl.DataFrame],
|
|
41
|
+
path: str,
|
|
42
|
+
filename: str | None = None,
|
|
43
|
+
commit=False,
|
|
44
|
+
):
|
|
45
|
+
"""Write ``data`` to the dataset with path ``path``.
|
|
46
|
+
|
|
47
|
+
If the dataset path is a directory, the filename must be specified.
|
|
48
|
+
|
|
49
|
+
If the path is not a Calkit dataset, it will be created.
|
|
50
|
+
|
|
51
|
+
If ``commit`` is specified, create a commit for the dataset update.
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Working with DVC."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import calkit
|
|
7
|
+
from calkit.config import get_app_name
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__package__)
|
|
10
|
+
logger.setLevel(logging.INFO)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def configure_remote():
|
|
14
|
+
project_name = calkit.git.detect_project_name()
|
|
15
|
+
base_url = calkit.cloud.get_base_url()
|
|
16
|
+
remote_url = f"{base_url}/projects/{project_name}/dvc"
|
|
17
|
+
subprocess.call(
|
|
18
|
+
["dvc", "remote", "add", "-d", "-f", get_app_name(), remote_url]
|
|
19
|
+
)
|
|
20
|
+
subprocess.call(
|
|
21
|
+
["dvc", "remote", "modify", get_app_name(), "auth", "custom"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_remote_auth():
|
|
26
|
+
"""Get a token and set it in the local DVC config so we can interact with
|
|
27
|
+
the cloud as an HTTP remote.
|
|
28
|
+
"""
|
|
29
|
+
settings = calkit.config.read()
|
|
30
|
+
if settings.dvc_token is None:
|
|
31
|
+
logger.info("Creating token for DVC scope")
|
|
32
|
+
token = calkit.cloud.post(
|
|
33
|
+
"/user/tokens", json=dict(expires_days=365, scope="dvc")
|
|
34
|
+
)["access_token"]
|
|
35
|
+
settings.dvc_token = token
|
|
36
|
+
settings.write()
|
|
37
|
+
subprocess.call(
|
|
38
|
+
[
|
|
39
|
+
"dvc",
|
|
40
|
+
"remote",
|
|
41
|
+
"modify",
|
|
42
|
+
"--local",
|
|
43
|
+
get_app_name(),
|
|
44
|
+
"custom_auth_header",
|
|
45
|
+
"Authorization",
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
subprocess.call(
|
|
49
|
+
[
|
|
50
|
+
"dvc",
|
|
51
|
+
"remote",
|
|
52
|
+
"modify",
|
|
53
|
+
"--local",
|
|
54
|
+
get_app_name(),
|
|
55
|
+
"password",
|
|
56
|
+
f"Bearer {settings.dvc_token}",
|
|
57
|
+
]
|
|
58
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Git-related functionality."""
|
|
2
|
+
|
|
3
|
+
import git
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def detect_project_name(path=None) -> str:
|
|
7
|
+
"""Read the project owner and name from the remote.
|
|
8
|
+
|
|
9
|
+
TODO: Currently only works with GitHub remotes.
|
|
10
|
+
"""
|
|
11
|
+
url = git.Repo(path=path).remote().url
|
|
12
|
+
return url.split("github.com")[-1][1:].removesuffix(".git")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""An optional GUI."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Functionality for working with Jupyter."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Server(BaseModel):
|
|
9
|
+
url: str
|
|
10
|
+
wdir: str
|
|
11
|
+
token: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_servers() -> list[Server]:
|
|
15
|
+
out = (
|
|
16
|
+
subprocess.check_output(["jupyter", "server", "list"])
|
|
17
|
+
.decode()
|
|
18
|
+
.strip()
|
|
19
|
+
.split("\n")
|
|
20
|
+
)
|
|
21
|
+
resp = []
|
|
22
|
+
for line in out:
|
|
23
|
+
if line.startswith("http://"):
|
|
24
|
+
url, wdir = line.split(" :: ")
|
|
25
|
+
token = url.split("?token=")[-1]
|
|
26
|
+
resp.append(Server(url=url, wdir=wdir, token=token))
|
|
27
|
+
return resp
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def start_server(wdir=None, no_browser=False):
|
|
31
|
+
"""Start a Jupyter server."""
|
|
32
|
+
subprocess.Popen(
|
|
33
|
+
[
|
|
34
|
+
"jupyter",
|
|
35
|
+
"lab",
|
|
36
|
+
"-y",
|
|
37
|
+
"--no-browser" if no_browser else "",
|
|
38
|
+
],
|
|
39
|
+
cwd=wdir,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stop_server(url: str):
|
|
44
|
+
# Extract the port from the URL
|
|
45
|
+
port = url.split(":")[2][:4]
|
|
46
|
+
subprocess.call(["jupyter", "server", "stop", port])
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""A local server for interacting with project repos."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import dvc
|
|
6
|
+
import dvc.repo
|
|
7
|
+
import git
|
|
8
|
+
from fastapi import FastAPI, HTTPException
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
11
|
+
|
|
12
|
+
import calkit
|
|
13
|
+
import calkit.jupyter
|
|
14
|
+
|
|
15
|
+
app = FastAPI(title="calkit-server")
|
|
16
|
+
|
|
17
|
+
app.add_middleware(
|
|
18
|
+
CORSMiddleware,
|
|
19
|
+
allow_origins=[
|
|
20
|
+
"http://localhost:5173",
|
|
21
|
+
"http://localhost",
|
|
22
|
+
"https://calkit.io",
|
|
23
|
+
"https://staging.calkit.io",
|
|
24
|
+
],
|
|
25
|
+
allow_credentials=True,
|
|
26
|
+
allow_methods=["*"],
|
|
27
|
+
allow_headers=["*"],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.get("/health")
|
|
32
|
+
def get_health() -> str:
|
|
33
|
+
return "All good!"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LocalProject(BaseModel):
|
|
37
|
+
owner_name: str
|
|
38
|
+
project_name: str
|
|
39
|
+
wdir: str
|
|
40
|
+
jupyter_url: str | None
|
|
41
|
+
jupyter_token: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.get("/")
|
|
45
|
+
def get_root() -> list[LocalProject]:
|
|
46
|
+
"""Return information about the current running server.
|
|
47
|
+
|
|
48
|
+
- The project owner
|
|
49
|
+
- The project name
|
|
50
|
+
- The current working directory
|
|
51
|
+
- A Jupyter server running here, if applicable
|
|
52
|
+
"""
|
|
53
|
+
resp = []
|
|
54
|
+
project_dirs = calkit.find_project_dirs()
|
|
55
|
+
servers = calkit.jupyter.get_servers()
|
|
56
|
+
for pdir in project_dirs:
|
|
57
|
+
project = calkit.git.detect_project_name(path=pdir)
|
|
58
|
+
owner, name = project.split("/")
|
|
59
|
+
url = token = None
|
|
60
|
+
for server in servers:
|
|
61
|
+
if server.wdir == pdir:
|
|
62
|
+
url = server.url
|
|
63
|
+
token = server.token
|
|
64
|
+
break
|
|
65
|
+
resp.append(
|
|
66
|
+
LocalProject(
|
|
67
|
+
owner_name=owner,
|
|
68
|
+
project_name=name,
|
|
69
|
+
wdir=os.path.abspath(pdir),
|
|
70
|
+
jupyter_token=token,
|
|
71
|
+
jupyter_url=url,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return resp
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.get("/projects/{owner_name}/{project_name}")
|
|
78
|
+
def get_local_project(owner_name: str, project_name: str) -> LocalProject:
|
|
79
|
+
all_projects = get_root()
|
|
80
|
+
for project in all_projects:
|
|
81
|
+
if (
|
|
82
|
+
project.owner_name == owner_name
|
|
83
|
+
and project.project_name == project_name
|
|
84
|
+
):
|
|
85
|
+
return project
|
|
86
|
+
raise HTTPException(404)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.get("/projects/{owner_name}/{project_name}/jupyter-server")
|
|
90
|
+
def get_project_jupyter_server(
|
|
91
|
+
owner_name: str,
|
|
92
|
+
project_name: str,
|
|
93
|
+
autostart=False,
|
|
94
|
+
no_browser=False,
|
|
95
|
+
) -> calkit.jupyter.Server | None:
|
|
96
|
+
project = get_local_project(
|
|
97
|
+
owner_name=owner_name, project_name=project_name
|
|
98
|
+
)
|
|
99
|
+
if project is None:
|
|
100
|
+
return
|
|
101
|
+
if project.jupyter_url is not None:
|
|
102
|
+
return calkit.jupyter.Server(
|
|
103
|
+
url=project.jupyter_url,
|
|
104
|
+
token=project.jupyter_token,
|
|
105
|
+
wdir=project.wdir,
|
|
106
|
+
)
|
|
107
|
+
if autostart:
|
|
108
|
+
calkit.jupyter.start_server(project.wdir, no_browser=no_browser)
|
|
109
|
+
servers = calkit.jupyter.get_servers()
|
|
110
|
+
for server in servers:
|
|
111
|
+
if server.wdir == project.wdir:
|
|
112
|
+
return server
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.delete("/projects/{owner_name}/{project_name}/jupyter-server")
|
|
116
|
+
def stop_project_jupyter_server(owner_name: str, project_name: str) -> None:
|
|
117
|
+
project = get_local_project(
|
|
118
|
+
owner_name=owner_name, project_name=project_name
|
|
119
|
+
)
|
|
120
|
+
if project is None:
|
|
121
|
+
return
|
|
122
|
+
if project.jupyter_url is not None:
|
|
123
|
+
calkit.jupyter.stop_server(url=project.jupyter_url)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.get("/cwd")
|
|
127
|
+
def get_cwd() -> str:
|
|
128
|
+
return os.getcwd()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.get("/projects/{owner_name}/{project_name}/ls")
|
|
132
|
+
def get_ls(owner_name: str, project_name: str) -> list[dict]:
|
|
133
|
+
project = get_local_project(owner_name, project_name)
|
|
134
|
+
repo = git.Repo(project.wdir)
|
|
135
|
+
contents = os.listdir(project.wdir)
|
|
136
|
+
resp = []
|
|
137
|
+
for item in contents:
|
|
138
|
+
if item == ".git" or repo.ignored(item):
|
|
139
|
+
continue
|
|
140
|
+
if os.path.isfile(os.path.join(project.wdir, item)):
|
|
141
|
+
kind = "file"
|
|
142
|
+
else:
|
|
143
|
+
kind = "dir"
|
|
144
|
+
resp.append(dict(name=item, type=kind))
|
|
145
|
+
return sorted(resp, key=lambda item: (item["type"], item["name"]))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.post("/projects/{owner_name}/{project_name}/git/{command}")
|
|
149
|
+
def run_git_command(
|
|
150
|
+
owner_name: str, project_name: str, command: str, params: dict = {}
|
|
151
|
+
):
|
|
152
|
+
project = get_local_project(owner_name, project_name)
|
|
153
|
+
func = getattr(git.Repo(project.wdir).git, command)
|
|
154
|
+
return func(**params)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.get("/diff")
|
|
158
|
+
def get_diff():
|
|
159
|
+
"""Get differences in working directory, from both Git and DVC."""
|
|
160
|
+
raise HTTPException(501)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@app.get("/projects/{owner_name}/{project_name}/status")
|
|
164
|
+
def get_status(owner_name: str, project_name: str):
|
|
165
|
+
"""Get status in working directory, from both Git and DVC."""
|
|
166
|
+
project = get_local_project(owner_name, project_name)
|
|
167
|
+
git_repo = git.Repo(project.wdir)
|
|
168
|
+
untracked_git_files = git_repo.untracked_files
|
|
169
|
+
# Get a list of diffs of the working tree to the index
|
|
170
|
+
git_diff = git_repo.index.diff(None)
|
|
171
|
+
git_diff_files = [d.a_path for d in git_diff]
|
|
172
|
+
dvc_repo = dvc.repo.Repo(project.wdir)
|
|
173
|
+
# Get a dictionary of DVC artifacts that have changed, keyed by the DVC
|
|
174
|
+
# file, where values are a list, which may include a dict with a
|
|
175
|
+
# 'changed outs' key, e.g.,
|
|
176
|
+
# {
|
|
177
|
+
# "data/jhtdb-transitional-bl/all-stats.h5.dvc": [
|
|
178
|
+
# {
|
|
179
|
+
# "changed outs": {
|
|
180
|
+
# "data/jhtdb-transitional-bl/all-stats.h5": "modified"
|
|
181
|
+
# }
|
|
182
|
+
# }
|
|
183
|
+
# ]
|
|
184
|
+
# }
|
|
185
|
+
dvc_status = dvc.repo.status.status(dvc_repo)
|
|
186
|
+
# TODO: Structure this in an intelligent way
|
|
187
|
+
return {
|
|
188
|
+
"dvc": dvc_status,
|
|
189
|
+
"git": {"untracked": untracked_git_files, "diff": git_diff_files},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.post("/projects/{owner_name}/{project_name}/open/vscode")
|
|
194
|
+
def open_vscode(owner_name: str, project_name: str) -> int:
|
|
195
|
+
project = get_local_project(owner_name, project_name)
|
|
196
|
+
return os.system(f"code {project.wdir}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.get("/jupyter/servers")
|
|
200
|
+
def get_jupyter_servers() -> list[calkit.jupyter.Server]:
|
|
201
|
+
return calkit.jupyter.get_servers()
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "code",
|
|
5
|
+
"execution_count": null,
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"outputs": [],
|
|
8
|
+
"source": [
|
|
9
|
+
"import calkit\n",
|
|
10
|
+
"\n",
|
|
11
|
+
"# Print out the plan of this study\n",
|
|
12
|
+
"plan = calkit.get_plan()\n",
|
|
13
|
+
"\n",
|
|
14
|
+
"# Describe this project\n",
|
|
15
|
+
"project = calkit.get_project()\n",
|
|
16
|
+
"\n",
|
|
17
|
+
"# What are the questions?\n",
|
|
18
|
+
"print(project.questions)\n",
|
|
19
|
+
"\n",
|
|
20
|
+
"# Get the status of the project, i.e., how many more simulations are there\n",
|
|
21
|
+
"# left to run\n",
|
|
22
|
+
"# is everything post-processed?\n",
|
|
23
|
+
"status = calkit.get_status()\n",
|
|
24
|
+
"\n",
|
|
25
|
+
"# By default this will run any non-finished simulations locally\n",
|
|
26
|
+
"calkit.run()"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"metadata": {
|
|
31
|
+
"language_info": {
|
|
32
|
+
"name": "python"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"nbformat": 4,
|
|
36
|
+
"nbformat_minor": 2
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# MS Office user
|
|
2
|
+
|
|
3
|
+
This example reflects the "non-hacker" researcher who uses Microsoft
|
|
4
|
+
Office (or similar) for a lot of their work.
|
|
5
|
+
Note that there is nothing wrong with this workflow,
|
|
6
|
+
but it is inherently harder for collaboration and reproducibility,
|
|
7
|
+
since version control for MS Office files is tougher.
|
|
8
|
+
|
|
9
|
+
Our goal with this example is to allow someone who likes Word and Excel
|
|
10
|
+
to keep using those tools, but to still help with organization
|
|
11
|
+
and reproducibility.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "calkit-python"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Pete Bachant", email="petebachant@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Reproducibility made easy."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"dvc",
|
|
21
|
+
"fastapi",
|
|
22
|
+
"gitpython",
|
|
23
|
+
"keyring",
|
|
24
|
+
"pandas",
|
|
25
|
+
"polars",
|
|
26
|
+
"pydantic[email]",
|
|
27
|
+
"pydantic-settings",
|
|
28
|
+
"pyjwt",
|
|
29
|
+
"requests",
|
|
30
|
+
"typer",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/calkit/calkit"
|
|
35
|
+
Issues = "https://github.com/calkit/calkit/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
calkit = "calkit.cli:run"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["calkit"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.version]
|
|
44
|
+
path = "calkit/__init__.py"
|