calkit-python 0.0.2__tar.gz → 0.0.4__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.2 → calkit_python-0.0.4}/PKG-INFO +40 -7
- calkit_python-0.0.4/README.md +81 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/__init__.py +1 -1
- calkit_python-0.0.4/calkit/cli.py +267 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/core.py +10 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/data.py +6 -1
- calkit_python-0.0.4/calkit/models.py +81 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/pyproject.toml +27 -22
- calkit_python-0.0.2/README.md +0 -50
- calkit_python-0.0.2/calkit/cli.py +0 -119
- {calkit_python-0.0.2 → calkit_python-0.0.4}/.github/FUNDING.yml +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/.github/workflows/publish-test.yml +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/.github/workflows/publish.yml +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/.gitignore +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/LICENSE +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/cloud.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/config.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/dvc.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/git.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/gui.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/jupyter.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/server.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/tests/__init__.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/tests/test_core.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/calkit/tests/test_jupyter.py +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/cfd-study/README.md +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/cfd-study/calkit.yaml +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/cfd-study/config/simulations/runs.csv +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/cfd-study/notebook.ipynb +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/ms-office/.gitignore +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/ms-office/README.md +0 -0
- {calkit_python-0.0.2 → calkit_python-0.0.4}/examples/ms-office/calkit.yaml +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: Reproducibility
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Reproducibility simplified.
|
|
5
5
|
Project-URL: Homepage, https://github.com/calkit/calkit
|
|
6
6
|
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
7
7
|
Author-email: Pete Bachant <petebachant@gmail.com>
|
|
@@ -15,23 +15,56 @@ Requires-Dist: eval-type-backport; python_version < '3.10'
|
|
|
15
15
|
Requires-Dist: fastapi
|
|
16
16
|
Requires-Dist: gitpython
|
|
17
17
|
Requires-Dist: keyring
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist: polars
|
|
18
|
+
Requires-Dist: nbconvert
|
|
20
19
|
Requires-Dist: pydantic-settings
|
|
21
20
|
Requires-Dist: pydantic[email]
|
|
22
21
|
Requires-Dist: pyjwt
|
|
23
22
|
Requires-Dist: requests
|
|
24
23
|
Requires-Dist: typer
|
|
24
|
+
Provides-Extra: data
|
|
25
|
+
Requires-Dist: pandas; extra == 'data'
|
|
26
|
+
Requires-Dist: polars; extra == 'data'
|
|
25
27
|
Description-Content-Type: text/markdown
|
|
26
28
|
|
|
27
29
|
# Calkit
|
|
28
30
|
|
|
29
|
-
[Calkit](https://calkit.io)
|
|
31
|
+
[Calkit](https://calkit.io) simplifies reproducibility,
|
|
30
32
|
acting as a layer on top of
|
|
31
|
-
[Git](https://git-scm.com/)
|
|
32
|
-
|
|
33
|
+
[Git](https://git-scm.com/), [DVC](https://dvc.org/),
|
|
34
|
+
[Zenodo](https://zenodo.org), and more,
|
|
35
|
+
such that all all aspects of the research process can be fully described in a
|
|
33
36
|
single repository.
|
|
34
37
|
|
|
38
|
+
## Why does reproducibility matter?
|
|
39
|
+
|
|
40
|
+
If your work is reproducible, that means that someone else can "run" it and
|
|
41
|
+
get the same results or outputs.
|
|
42
|
+
This is a major step towards addressing
|
|
43
|
+
[the replication crisis](https://en.wikipedia.org/wiki/Replication_crisis)
|
|
44
|
+
and has some major benefits for both you as an individual and the research
|
|
45
|
+
community:
|
|
46
|
+
|
|
47
|
+
1. You will avoid mistakes caused by, e.g., running an old version of a script
|
|
48
|
+
and including a figure that wasn't created after fixing a bug in the data
|
|
49
|
+
processing pipeline.
|
|
50
|
+
2. Since your project is "runnable," it's more likely that someone else will be
|
|
51
|
+
able to reuse part of your work to run it in a different context, thereby
|
|
52
|
+
producing a bigger impact and accelerating the pace of discovery.
|
|
53
|
+
If someone can take what you've done and use it to calculate a
|
|
54
|
+
prediction, you have just produced truly useful knowledge.
|
|
55
|
+
|
|
56
|
+
## Why another tool/platform?
|
|
57
|
+
|
|
58
|
+
Git, GitHub, DVC, Zenodo et al. are amazing tools/platforms, but their
|
|
59
|
+
use involves multiple fairly difficult learning curves.
|
|
60
|
+
Our goal is to provide a single tool and platform to unify all of these so
|
|
61
|
+
that there is a single, gentle learning curve.
|
|
62
|
+
However, it is not our goal to hide or replace these underlying components.
|
|
63
|
+
Advanced users can use them directly, but new users aren't forced to, which
|
|
64
|
+
helps them get up and running with less effort and training.
|
|
65
|
+
Calkit should help users understand what is going on under the hood without
|
|
66
|
+
forcing them to work at that lower level of abstraction.
|
|
67
|
+
|
|
35
68
|
## Installation
|
|
36
69
|
|
|
37
70
|
Simply run
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Calkit
|
|
2
|
+
|
|
3
|
+
[Calkit](https://calkit.io) simplifies reproducibility,
|
|
4
|
+
acting as a layer on top of
|
|
5
|
+
[Git](https://git-scm.com/), [DVC](https://dvc.org/),
|
|
6
|
+
[Zenodo](https://zenodo.org), and more,
|
|
7
|
+
such that all all aspects of the research process can be fully described in a
|
|
8
|
+
single repository.
|
|
9
|
+
|
|
10
|
+
## Why does reproducibility matter?
|
|
11
|
+
|
|
12
|
+
If your work is reproducible, that means that someone else can "run" it and
|
|
13
|
+
get the same results or outputs.
|
|
14
|
+
This is a major step towards addressing
|
|
15
|
+
[the replication crisis](https://en.wikipedia.org/wiki/Replication_crisis)
|
|
16
|
+
and has some major benefits for both you as an individual and the research
|
|
17
|
+
community:
|
|
18
|
+
|
|
19
|
+
1. You will avoid mistakes caused by, e.g., running an old version of a script
|
|
20
|
+
and including a figure that wasn't created after fixing a bug in the data
|
|
21
|
+
processing pipeline.
|
|
22
|
+
2. Since your project is "runnable," it's more likely that someone else will be
|
|
23
|
+
able to reuse part of your work to run it in a different context, thereby
|
|
24
|
+
producing a bigger impact and accelerating the pace of discovery.
|
|
25
|
+
If someone can take what you've done and use it to calculate a
|
|
26
|
+
prediction, you have just produced truly useful knowledge.
|
|
27
|
+
|
|
28
|
+
## Why another tool/platform?
|
|
29
|
+
|
|
30
|
+
Git, GitHub, DVC, Zenodo et al. are amazing tools/platforms, but their
|
|
31
|
+
use involves multiple fairly difficult learning curves.
|
|
32
|
+
Our goal is to provide a single tool and platform to unify all of these so
|
|
33
|
+
that there is a single, gentle learning curve.
|
|
34
|
+
However, it is not our goal to hide or replace these underlying components.
|
|
35
|
+
Advanced users can use them directly, but new users aren't forced to, which
|
|
36
|
+
helps them get up and running with less effort and training.
|
|
37
|
+
Calkit should help users understand what is going on under the hood without
|
|
38
|
+
forcing them to work at that lower level of abstraction.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Simply run
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install calkit-python
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Cloud integration
|
|
49
|
+
|
|
50
|
+
The Calkit cloud platform (https://calkit.io) serves as a project
|
|
51
|
+
management interface and a DVC remote for easily storing all versions of your
|
|
52
|
+
data/code/figures/publications, interacting with your collaborators,
|
|
53
|
+
reusing others' research artifacts, etc.
|
|
54
|
+
|
|
55
|
+
After signing up, visit the [settings](https://calkit.io/settings) page
|
|
56
|
+
and create a token.
|
|
57
|
+
Then run
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
calkit config set token ${YOUR_TOKEN_HERE}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then, inside a project repo you'd like to connect to the cloud, run
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
calkit config setup-remote
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This will setup the Calkit DVC remote, such that commands like `dvc push` will
|
|
70
|
+
allow you to push versions of your data or pipeline outputs to the cloud
|
|
71
|
+
for safe storage and sharing with your collaborators.
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
Calkit creates a simple human-readable "database" inside the `calkit.yaml`
|
|
76
|
+
file, which serves as a way to store important information about the project,
|
|
77
|
+
e.g., what question(s) it seeks to answer,
|
|
78
|
+
what files should be considered datasets, figures, publications, etc.
|
|
79
|
+
The Calkit cloud reads this database and registers the various entities
|
|
80
|
+
as part of the entire ecosystem such that if a project is made public,
|
|
81
|
+
other researchers can find and reuse your work to accelerate their own.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""The command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import git
|
|
11
|
+
import typer
|
|
12
|
+
from typing_extensions import Annotated, Optional
|
|
13
|
+
|
|
14
|
+
from calkit.core import ryaml
|
|
15
|
+
|
|
16
|
+
from . import config
|
|
17
|
+
from .dvc import configure_remote, set_remote_auth
|
|
18
|
+
|
|
19
|
+
app = typer.Typer()
|
|
20
|
+
config_app = typer.Typer()
|
|
21
|
+
new_app = typer.Typer()
|
|
22
|
+
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
23
|
+
app.add_typer(new_app, name="new", help="Add new Calkit object.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@config_app.command(name="set")
|
|
27
|
+
def set_config_value(key: str, value: str):
|
|
28
|
+
try:
|
|
29
|
+
cfg = config.read()
|
|
30
|
+
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
31
|
+
# Kind of a hack for setting the password computed field
|
|
32
|
+
# Types have been validated above, so this won't hurt to do again
|
|
33
|
+
setattr(cfg, key, value)
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
# TODO: This fails if we try to set password before any config has
|
|
36
|
+
# been written
|
|
37
|
+
# Username is fine
|
|
38
|
+
cfg = config.Settings.model_validate({key: value})
|
|
39
|
+
cfg.write()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@config_app.command(name="get")
|
|
43
|
+
def get_config_value(key: str) -> None:
|
|
44
|
+
cfg = config.read()
|
|
45
|
+
val = getattr(cfg, key)
|
|
46
|
+
if val is not None:
|
|
47
|
+
print(val)
|
|
48
|
+
else:
|
|
49
|
+
print()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@config_app.command(name="setup-remote")
|
|
53
|
+
def setup_remote():
|
|
54
|
+
configure_remote()
|
|
55
|
+
set_remote_auth()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command(name="status")
|
|
59
|
+
def get_status():
|
|
60
|
+
"""Get a unified Git and DVC status."""
|
|
61
|
+
|
|
62
|
+
def print_sep(name: str):
|
|
63
|
+
width = 66
|
|
64
|
+
txt_width = len(name) + 2
|
|
65
|
+
buffer_width = (width - txt_width) // 2
|
|
66
|
+
buffer = "-" * buffer_width
|
|
67
|
+
typer.echo(f"{buffer} {name} {buffer}")
|
|
68
|
+
|
|
69
|
+
def run_cmd(cmd: list[str]):
|
|
70
|
+
if os.name == "nt":
|
|
71
|
+
subprocess.call(cmd)
|
|
72
|
+
else:
|
|
73
|
+
pty.spawn(cmd, lambda fd: os.read(fd, 1024))
|
|
74
|
+
|
|
75
|
+
print_sep("Code (Git)")
|
|
76
|
+
run_cmd(["git", "status"])
|
|
77
|
+
typer.echo()
|
|
78
|
+
print_sep("Data (DVC)")
|
|
79
|
+
run_cmd(["dvc", "data", "status"])
|
|
80
|
+
typer.echo()
|
|
81
|
+
print_sep("Pipeline (DVC)")
|
|
82
|
+
run_cmd(["dvc", "status"])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command(name="add")
|
|
86
|
+
def add(paths: list[str]):
|
|
87
|
+
"""Add paths to the repo.
|
|
88
|
+
|
|
89
|
+
Code will be added to Git and data will be added to DVC.
|
|
90
|
+
"""
|
|
91
|
+
dvc_extensions = [".png", ".h5", ".parquet", ".pickle"]
|
|
92
|
+
dvc_size_thresh_bytes = 1_000_000
|
|
93
|
+
dvc_folders = ["data", "figures"]
|
|
94
|
+
if "." in paths:
|
|
95
|
+
print("ERROR: Cannot add '.' with calkit; use git or dvc")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
for path in paths:
|
|
98
|
+
if os.path.isdir(path):
|
|
99
|
+
print("ERROR: Cannot add directories with calkit; use git or dvc")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
# Detect if this file should be tracked with Git or DVC
|
|
102
|
+
# TODO: Add to whatever
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command(name="commit")
|
|
106
|
+
def commit(
|
|
107
|
+
all: Annotated[Optional[bool], typer.Option("--all", "-a")] = False,
|
|
108
|
+
message: Annotated[Optional[str], typer.Option("--message", "-m")] = None,
|
|
109
|
+
):
|
|
110
|
+
"""Commit a change to the repo."""
|
|
111
|
+
print(all, message)
|
|
112
|
+
raise NotImplementedError
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command(name="pull", help="Pull with both Git and DVC.")
|
|
116
|
+
def pull():
|
|
117
|
+
typer.echo("Git pulling")
|
|
118
|
+
subprocess.call(["git", "pull"])
|
|
119
|
+
typer.echo("DVC pulling")
|
|
120
|
+
subprocess.call(["dvc", "pull"])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command(name="push", help="Push with both Git and DVC.")
|
|
124
|
+
def push():
|
|
125
|
+
typer.echo("Pushing to Git remote")
|
|
126
|
+
subprocess.call(["git", "push"])
|
|
127
|
+
typer.echo("Pushing to DVC remote")
|
|
128
|
+
subprocess.call(["dvc", "push"])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@new_app.command(name="figure")
|
|
132
|
+
def new_figure(
|
|
133
|
+
path: str,
|
|
134
|
+
title: Annotated[str, typer.Option("--title")],
|
|
135
|
+
description: Annotated[str, typer.Option("--desc")] = None,
|
|
136
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
137
|
+
):
|
|
138
|
+
"""Add a new figure to ``calkit.yaml``."""
|
|
139
|
+
if os.path.isfile("calkit.yaml"):
|
|
140
|
+
with open("calkit.yaml") as f:
|
|
141
|
+
ck_info = ryaml.load(f)
|
|
142
|
+
else:
|
|
143
|
+
ck_info = {}
|
|
144
|
+
figures = ck_info.get("figures", [])
|
|
145
|
+
paths = [f.get("path") for f in figures]
|
|
146
|
+
if path in paths:
|
|
147
|
+
raise ValueError(f"Figure at path {path} already exists")
|
|
148
|
+
obj = dict(path=path, title=title)
|
|
149
|
+
if description is not None:
|
|
150
|
+
obj["description"] = description
|
|
151
|
+
figures.append(obj)
|
|
152
|
+
ck_info["figures"] = figures
|
|
153
|
+
with open("calkit.yaml", "w") as f:
|
|
154
|
+
ryaml.dump(ck_info, f)
|
|
155
|
+
if commit:
|
|
156
|
+
repo = git.Repo()
|
|
157
|
+
repo.git.add("calkit.yaml")
|
|
158
|
+
repo.git.commit(["-m", f"Add figure {path}"])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@new_app.command("question")
|
|
162
|
+
def new_question(
|
|
163
|
+
question: str,
|
|
164
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
165
|
+
):
|
|
166
|
+
if os.path.isfile("calkit.yaml"):
|
|
167
|
+
with open("calkit.yaml") as f:
|
|
168
|
+
ck_info = ryaml.load(f)
|
|
169
|
+
else:
|
|
170
|
+
ck_info = {}
|
|
171
|
+
questions = ck_info.get("questions", [])
|
|
172
|
+
if question in questions:
|
|
173
|
+
raise ValueError("Question already exists")
|
|
174
|
+
if not question.endswith("?"):
|
|
175
|
+
raise ValueError("Questions must end with a question mark")
|
|
176
|
+
questions.append(question)
|
|
177
|
+
ck_info["questions"] = questions
|
|
178
|
+
with open("calkit.yaml", "w") as f:
|
|
179
|
+
ryaml.dump(ck_info, f)
|
|
180
|
+
if commit:
|
|
181
|
+
repo = git.Repo()
|
|
182
|
+
repo.git.add("calkit.yaml")
|
|
183
|
+
repo.git.commit(["-m", "Add question"])
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command(name="server", help="Run the local server.")
|
|
187
|
+
def run_server():
|
|
188
|
+
import uvicorn
|
|
189
|
+
|
|
190
|
+
uvicorn.run(
|
|
191
|
+
"calkit.server:app",
|
|
192
|
+
port=8866,
|
|
193
|
+
host="localhost",
|
|
194
|
+
reload=True,
|
|
195
|
+
reload_dirs=[os.path.dirname(__file__)],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command(
|
|
200
|
+
name="run",
|
|
201
|
+
add_help_option=False,
|
|
202
|
+
help="Run DVC pipeline (a wrapper for `dvc repro`).",
|
|
203
|
+
)
|
|
204
|
+
def run_dvc_repro(
|
|
205
|
+
targets: Optional[list[str]] = typer.Argument(default=None),
|
|
206
|
+
help: Annotated[bool, typer.Option("-h", "--help")] = False,
|
|
207
|
+
quiet: Annotated[bool, typer.Option("-q", "--quiet")] = False,
|
|
208
|
+
verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
|
|
209
|
+
force: Annotated[bool, typer.Option("-f", "--force")] = False,
|
|
210
|
+
interactive: Annotated[bool, typer.Option("-i", "--interactive")] = False,
|
|
211
|
+
single_item: Annotated[bool, typer.Option("-s", "--single-item")] = False,
|
|
212
|
+
pipeline: Annotated[
|
|
213
|
+
Optional[str], typer.Option("-p", "--pipeline")
|
|
214
|
+
] = None,
|
|
215
|
+
all_pipelines: Annotated[
|
|
216
|
+
bool, typer.Option("-P", "--all-pipelines")
|
|
217
|
+
] = False,
|
|
218
|
+
recursive: Annotated[bool, typer.Option("-R", "--recursive")] = False,
|
|
219
|
+
downstream: Annotated[
|
|
220
|
+
Optional[list[str]], typer.Option("--downstream")
|
|
221
|
+
] = None,
|
|
222
|
+
force_downstream: Annotated[
|
|
223
|
+
bool, typer.Option("--force-downstream")
|
|
224
|
+
] = False,
|
|
225
|
+
pull: Annotated[bool, typer.Option("--pull")] = False,
|
|
226
|
+
allow_missing: Annotated[bool, typer.Option("--allow-missing")] = False,
|
|
227
|
+
dry: Annotated[bool, typer.Option("--dry")] = False,
|
|
228
|
+
keep_going: Annotated[bool, typer.Option("--keep-going", "-k")] = False,
|
|
229
|
+
ignore_errors: Annotated[bool, typer.Option("--ignore-errors")] = False,
|
|
230
|
+
glob: Annotated[bool, typer.Option("--glob")] = False,
|
|
231
|
+
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
232
|
+
no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
|
|
233
|
+
):
|
|
234
|
+
"""A simple wrapper for ``dvc repro``."""
|
|
235
|
+
if targets is None:
|
|
236
|
+
targets = []
|
|
237
|
+
args = targets
|
|
238
|
+
# Extract any boolean args
|
|
239
|
+
for name in [
|
|
240
|
+
"help",
|
|
241
|
+
"quiet",
|
|
242
|
+
"verbose",
|
|
243
|
+
"force",
|
|
244
|
+
"interactive",
|
|
245
|
+
"single-item",
|
|
246
|
+
"all-pipelines",
|
|
247
|
+
"recursive",
|
|
248
|
+
"pull",
|
|
249
|
+
"allow-missing",
|
|
250
|
+
"dry",
|
|
251
|
+
"keep-going",
|
|
252
|
+
"force-downstream",
|
|
253
|
+
"glob",
|
|
254
|
+
"no-commit",
|
|
255
|
+
"no-run-cache",
|
|
256
|
+
]:
|
|
257
|
+
if locals()[name.replace("-", "_")]:
|
|
258
|
+
args.append("--" + name)
|
|
259
|
+
if pipeline is not None:
|
|
260
|
+
args += ["--pipeline", pipeline]
|
|
261
|
+
if downstream is not None:
|
|
262
|
+
args += downstream
|
|
263
|
+
subprocess.call(["dvc", "repro"] + args)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def run() -> None:
|
|
267
|
+
app()
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import glob
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
7
8
|
|
|
9
|
+
import ruamel.yaml
|
|
8
10
|
from git import Repo
|
|
9
11
|
from git.exc import InvalidGitRepositoryError
|
|
10
12
|
|
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
|
14
|
+
logger = logging.getLogger(__package__)
|
|
15
|
+
|
|
16
|
+
ryaml = ruamel.yaml.YAML()
|
|
17
|
+
ryaml.indent(mapping=2, sequence=4, offset=2)
|
|
18
|
+
ryaml.preserve_quotes = True
|
|
19
|
+
ryaml.width = 70
|
|
20
|
+
|
|
11
21
|
|
|
12
22
|
def find_project_dirs(relative=False, max_depth=3) -> list[str]:
|
|
13
23
|
"""Find all Calkit project directories."""
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"""Functionality for working with datasets.
|
|
1
|
+
"""Functionality for working with datasets.
|
|
2
|
+
|
|
3
|
+
Since the dependencies here are optional, we need to ensure this isn't imported
|
|
4
|
+
by default, or otherwise ensure ``import calkit`` works when the data
|
|
5
|
+
dependencies are not installed.
|
|
6
|
+
"""
|
|
2
7
|
|
|
3
8
|
from __future__ import annotations
|
|
4
9
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Data models."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _ImportedFrom(BaseModel):
|
|
9
|
+
project: str
|
|
10
|
+
path: str = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _CalkitObject(BaseModel):
|
|
14
|
+
path: str
|
|
15
|
+
title: str
|
|
16
|
+
description: str
|
|
17
|
+
stage: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Dataset(_CalkitObject):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Figure(_CalkitObject):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Publication(_CalkitObject):
|
|
29
|
+
kind: Literal[
|
|
30
|
+
"journal-article",
|
|
31
|
+
"conference-paper",
|
|
32
|
+
"presentation",
|
|
33
|
+
"poster",
|
|
34
|
+
"report",
|
|
35
|
+
"blog",
|
|
36
|
+
]
|
|
37
|
+
is_published: bool = False
|
|
38
|
+
doi: str = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ReferenceFile(BaseModel):
|
|
42
|
+
path: str
|
|
43
|
+
key: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReferenceCollection(BaseModel):
|
|
47
|
+
path: str
|
|
48
|
+
files: list[ReferenceFile] = []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Environment(BaseModel):
|
|
52
|
+
name: str
|
|
53
|
+
kind: Literal["conda", "docker", "pip", "poetry", "npm", "yarn"]
|
|
54
|
+
path: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Software(BaseModel):
|
|
58
|
+
title: str
|
|
59
|
+
path: str
|
|
60
|
+
description: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Notebook(_CalkitObject):
|
|
64
|
+
"""A Jupyter notebook."""
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ProjectInfo(BaseModel):
|
|
70
|
+
"""All of the project's information or metadata, written to the
|
|
71
|
+
``calkit.yaml`` file.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
questions: list[str] = []
|
|
75
|
+
datasets: list[Dataset] = []
|
|
76
|
+
figures: list[Figure] = []
|
|
77
|
+
publications: list[Publication] = []
|
|
78
|
+
references: list[ReferenceCollection] = []
|
|
79
|
+
environments: list[Environment] = []
|
|
80
|
+
software: list[Software] = []
|
|
81
|
+
notebooks: list[Notebook] = []
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
2
|
build-backend = "hatchling.build"
|
|
3
|
+
requires = ["hatchling"]
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
|
-
name = "calkit-python"
|
|
7
|
-
dynamic = ["version"]
|
|
8
6
|
authors = [
|
|
9
|
-
{
|
|
7
|
+
{name = "Pete Bachant", email = "petebachant@gmail.com"},
|
|
10
8
|
]
|
|
11
|
-
description = "Reproducibility made easy."
|
|
12
|
-
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.8"
|
|
14
9
|
classifiers = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
18
13
|
]
|
|
19
14
|
dependencies = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
"dvc",
|
|
16
|
+
"eval-type-backport; python_version < '3.10'",
|
|
17
|
+
"fastapi",
|
|
18
|
+
"gitpython",
|
|
19
|
+
"keyring",
|
|
20
|
+
"nbconvert",
|
|
21
|
+
"pydantic[email]",
|
|
22
|
+
"pydantic-settings",
|
|
23
|
+
"pyjwt",
|
|
24
|
+
"requests",
|
|
25
|
+
"typer",
|
|
26
|
+
]
|
|
27
|
+
description = "Reproducibility simplified."
|
|
28
|
+
dynamic = ["version"]
|
|
29
|
+
name = "calkit-python"
|
|
30
|
+
readme = "README.md"
|
|
31
|
+
requires-python = ">=3.8"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
data = [
|
|
35
|
+
"pandas",
|
|
36
|
+
"polars",
|
|
32
37
|
]
|
|
33
38
|
|
|
34
39
|
[project.urls]
|
calkit_python-0.0.2/README.md
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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.
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""The command line interface."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import pty
|
|
7
|
-
import subprocess
|
|
8
|
-
import sys
|
|
9
|
-
|
|
10
|
-
import typer
|
|
11
|
-
from typing_extensions import Annotated, Optional
|
|
12
|
-
|
|
13
|
-
import calkit
|
|
14
|
-
|
|
15
|
-
from . import config
|
|
16
|
-
from .dvc import configure_remote, set_remote_auth
|
|
17
|
-
|
|
18
|
-
app = typer.Typer()
|
|
19
|
-
config_app = typer.Typer()
|
|
20
|
-
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@config_app.command(name="set")
|
|
24
|
-
def set_config_value(key: str, value: str):
|
|
25
|
-
try:
|
|
26
|
-
cfg = config.read()
|
|
27
|
-
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
28
|
-
# Kind of a hack for setting the password computed field
|
|
29
|
-
# Types have been validated above, so this won't hurt to do again
|
|
30
|
-
setattr(cfg, key, value)
|
|
31
|
-
except FileNotFoundError:
|
|
32
|
-
# TODO: This fails if we try to set password before any config has
|
|
33
|
-
# been written
|
|
34
|
-
# Username is fine
|
|
35
|
-
cfg = config.Settings.model_validate({key: value})
|
|
36
|
-
cfg.write()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@config_app.command(name="get")
|
|
40
|
-
def get_config_value(key: str) -> None:
|
|
41
|
-
cfg = config.read()
|
|
42
|
-
val = getattr(cfg, key)
|
|
43
|
-
if val is not None:
|
|
44
|
-
print(val)
|
|
45
|
-
else:
|
|
46
|
-
print()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@config_app.command(name="setup-remote")
|
|
50
|
-
def setup_remote():
|
|
51
|
-
configure_remote()
|
|
52
|
-
set_remote_auth()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@app.command(name="status")
|
|
56
|
-
def get_status():
|
|
57
|
-
"""Get a unified Git and DVC status."""
|
|
58
|
-
|
|
59
|
-
def print_sep(name: str):
|
|
60
|
-
print(f"------------ {name} ------------")
|
|
61
|
-
|
|
62
|
-
print_sep("Code")
|
|
63
|
-
if os.name == "nt":
|
|
64
|
-
subprocess.call(["git", "status"])
|
|
65
|
-
print()
|
|
66
|
-
print_sep("data")
|
|
67
|
-
subprocess.call(["dvc", "status"])
|
|
68
|
-
else:
|
|
69
|
-
pty.spawn(["git", "status"], lambda fd: os.read(fd, 1024))
|
|
70
|
-
print()
|
|
71
|
-
print_sep("Data")
|
|
72
|
-
pty.spawn(["dvc", "status"], lambda fd: os.read(fd, 1024))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@app.command(name="add")
|
|
76
|
-
def add(paths: list[str]):
|
|
77
|
-
"""Add paths to the repo.
|
|
78
|
-
|
|
79
|
-
Code will be added to Git and data will be added to DVC.
|
|
80
|
-
"""
|
|
81
|
-
dvc_extensions = [".png", ".h5", ".parquet", ".pickle"]
|
|
82
|
-
dvc_size_thresh_bytes = 1_000_000
|
|
83
|
-
dvc_folders = ["data", "figures"]
|
|
84
|
-
if "." in paths:
|
|
85
|
-
print("ERROR: Cannot add '.' with calkit; use git or dvc")
|
|
86
|
-
sys.exit(1)
|
|
87
|
-
for path in paths:
|
|
88
|
-
if os.path.isdir(path):
|
|
89
|
-
print("ERROR: Cannot add directories with calkit; use git or dvc")
|
|
90
|
-
sys.exit(1)
|
|
91
|
-
# Detect if this file should be tracked with Git or DVC
|
|
92
|
-
# TODO: Add to whatever
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@app.command(name="commit")
|
|
96
|
-
def commit(
|
|
97
|
-
all: Annotated[Optional[bool], typer.Option("--all", "-a")] = False,
|
|
98
|
-
message: Annotated[Optional[str], typer.Option("--message", "-m")] = None,
|
|
99
|
-
):
|
|
100
|
-
"""Commit a change to the repo."""
|
|
101
|
-
print(all, message)
|
|
102
|
-
raise NotImplementedError
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@app.command(name="server")
|
|
106
|
-
def run_server():
|
|
107
|
-
import uvicorn
|
|
108
|
-
|
|
109
|
-
uvicorn.run(
|
|
110
|
-
"calkit.server:app",
|
|
111
|
-
port=8866,
|
|
112
|
-
host="localhost",
|
|
113
|
-
reload=True,
|
|
114
|
-
reload_dirs=[os.path.dirname(__file__)],
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def run() -> None:
|
|
119
|
-
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|