calkit-python 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl
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/__init__.py +1 -1
- calkit/cli.py +220 -18
- calkit/models.py +7 -0
- {calkit_python-0.0.3.dist-info → calkit_python-0.0.5.dist-info}/METADATA +36 -4
- {calkit_python-0.0.3.dist-info → calkit_python-0.0.5.dist-info}/RECORD +8 -8
- {calkit_python-0.0.3.dist-info → calkit_python-0.0.5.dist-info}/WHEEL +0 -0
- {calkit_python-0.0.3.dist-info → calkit_python-0.0.5.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.0.3.dist-info → calkit_python-0.0.5.dist-info}/licenses/LICENSE +0 -0
calkit/__init__.py
CHANGED
calkit/cli.py
CHANGED
|
@@ -11,16 +11,37 @@ import git
|
|
|
11
11
|
import typer
|
|
12
12
|
from typing_extensions import Annotated, Optional
|
|
13
13
|
|
|
14
|
+
import calkit
|
|
14
15
|
from calkit.core import ryaml
|
|
15
16
|
|
|
16
17
|
from . import config
|
|
17
18
|
from .dvc import configure_remote, set_remote_auth
|
|
18
19
|
|
|
19
|
-
app = typer.Typer(
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
invoke_without_command=True,
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
context_settings=dict(help_option_names=["-h", "--help"]),
|
|
24
|
+
)
|
|
25
|
+
config_app = typer.Typer(no_args_is_help=True)
|
|
26
|
+
new_app = typer.Typer(no_args_is_help=True)
|
|
27
|
+
notebooks_app = typer.Typer(no_args_is_help=True)
|
|
22
28
|
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
23
|
-
app.add_typer(
|
|
29
|
+
app.add_typer(
|
|
30
|
+
new_app, name="new", help="Add new Calkit object (to calkit.yaml)."
|
|
31
|
+
)
|
|
32
|
+
app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.callback()
|
|
36
|
+
def main(
|
|
37
|
+
version: Annotated[
|
|
38
|
+
bool,
|
|
39
|
+
typer.Option("--version", help="Show version and exit."),
|
|
40
|
+
] = False,
|
|
41
|
+
):
|
|
42
|
+
if version:
|
|
43
|
+
typer.echo(calkit.__version__)
|
|
44
|
+
raise typer.Exit()
|
|
24
45
|
|
|
25
46
|
|
|
26
47
|
@config_app.command(name="set")
|
|
@@ -60,19 +81,26 @@ def get_status():
|
|
|
60
81
|
"""Get a unified Git and DVC status."""
|
|
61
82
|
|
|
62
83
|
def print_sep(name: str):
|
|
63
|
-
|
|
84
|
+
width = 66
|
|
85
|
+
txt_width = len(name) + 2
|
|
86
|
+
buffer_width = (width - txt_width) // 2
|
|
87
|
+
buffer = "-" * buffer_width
|
|
88
|
+
typer.echo(f"{buffer} {name} {buffer}")
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
def run_cmd(cmd: list[str]):
|
|
91
|
+
if os.name == "nt":
|
|
92
|
+
subprocess.call(cmd)
|
|
93
|
+
else:
|
|
94
|
+
pty.spawn(cmd, lambda fd: os.read(fd, 1024))
|
|
95
|
+
|
|
96
|
+
print_sep("Code (Git)")
|
|
97
|
+
run_cmd(["git", "status"])
|
|
98
|
+
typer.echo()
|
|
99
|
+
print_sep("Data (DVC)")
|
|
100
|
+
run_cmd(["dvc", "data", "status"])
|
|
101
|
+
typer.echo()
|
|
102
|
+
print_sep("Pipeline (DVC)")
|
|
103
|
+
run_cmd(["dvc", "status"])
|
|
76
104
|
|
|
77
105
|
|
|
78
106
|
@app.command(name="add")
|
|
@@ -105,6 +133,22 @@ def commit(
|
|
|
105
133
|
raise NotImplementedError
|
|
106
134
|
|
|
107
135
|
|
|
136
|
+
@app.command(name="pull", help="Pull with both Git and DVC.")
|
|
137
|
+
def pull():
|
|
138
|
+
typer.echo("Git pulling")
|
|
139
|
+
subprocess.call(["git", "pull"])
|
|
140
|
+
typer.echo("DVC pulling")
|
|
141
|
+
subprocess.call(["dvc", "pull"])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command(name="push", help="Push with both Git and DVC.")
|
|
145
|
+
def push():
|
|
146
|
+
typer.echo("Pushing to Git remote")
|
|
147
|
+
subprocess.call(["git", "push"])
|
|
148
|
+
typer.echo("Pushing to DVC remote")
|
|
149
|
+
subprocess.call(["dvc", "push"])
|
|
150
|
+
|
|
151
|
+
|
|
108
152
|
@new_app.command(name="figure")
|
|
109
153
|
def new_figure(
|
|
110
154
|
path: str,
|
|
@@ -112,7 +156,7 @@ def new_figure(
|
|
|
112
156
|
description: Annotated[str, typer.Option("--desc")] = None,
|
|
113
157
|
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
114
158
|
):
|
|
115
|
-
"""Add a new figure
|
|
159
|
+
"""Add a new figure."""
|
|
116
160
|
if os.path.isfile("calkit.yaml"):
|
|
117
161
|
with open("calkit.yaml") as f:
|
|
118
162
|
ck_info = ryaml.load(f)
|
|
@@ -140,6 +184,7 @@ def new_question(
|
|
|
140
184
|
question: str,
|
|
141
185
|
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
142
186
|
):
|
|
187
|
+
"""Add a new question."""
|
|
143
188
|
if os.path.isfile("calkit.yaml"):
|
|
144
189
|
with open("calkit.yaml") as f:
|
|
145
190
|
ck_info = ryaml.load(f)
|
|
@@ -160,7 +205,97 @@ def new_question(
|
|
|
160
205
|
repo.git.commit(["-m", "Add question"])
|
|
161
206
|
|
|
162
207
|
|
|
163
|
-
@
|
|
208
|
+
@new_app.command("notebook")
|
|
209
|
+
def new_notebook(
|
|
210
|
+
path: Annotated[str, typer.Argument(help="Notebook path (relative)")],
|
|
211
|
+
title: Annotated[str, typer.Option("--title")],
|
|
212
|
+
description: Annotated[str, typer.Option("--desc")] = None,
|
|
213
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
214
|
+
):
|
|
215
|
+
"""Add a new notebook."""
|
|
216
|
+
if os.path.isabs(path):
|
|
217
|
+
raise ValueError("Path must be relative")
|
|
218
|
+
if not os.path.isfile(path):
|
|
219
|
+
raise ValueError("Path is not a file")
|
|
220
|
+
if not path.endswith(".ipynb"):
|
|
221
|
+
raise ValueError("Path does not have .ipynb extension")
|
|
222
|
+
# TODO: Add option to create stages that run `calkit nb clean` and
|
|
223
|
+
# `calkit nb execute`
|
|
224
|
+
if os.path.isfile("calkit.yaml"):
|
|
225
|
+
with open("calkit.yaml") as f:
|
|
226
|
+
ck_info = ryaml.load(f)
|
|
227
|
+
else:
|
|
228
|
+
ck_info = {}
|
|
229
|
+
notebooks = ck_info.get("notebooks", [])
|
|
230
|
+
paths = [f.get("path") for f in notebooks]
|
|
231
|
+
if path in paths:
|
|
232
|
+
raise ValueError(f"Notebook at path {path} already exists")
|
|
233
|
+
obj = dict(path=path, title=title)
|
|
234
|
+
if description is not None:
|
|
235
|
+
obj["description"] = description
|
|
236
|
+
notebooks.append(obj)
|
|
237
|
+
ck_info["notebooks"] = notebooks
|
|
238
|
+
with open("calkit.yaml", "w") as f:
|
|
239
|
+
ryaml.dump(ck_info, f)
|
|
240
|
+
if commit:
|
|
241
|
+
repo = git.Repo()
|
|
242
|
+
repo.git.add("calkit.yaml")
|
|
243
|
+
repo.git.commit(["-m", f"Add notebook {path}"])
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@notebooks_app.command("clean")
|
|
247
|
+
def clean_notebook_outputs(path: str):
|
|
248
|
+
"""Clean notebook and place a copy in the cleaned notebooks directory.
|
|
249
|
+
|
|
250
|
+
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
251
|
+
notebook as a dependency for a stage that caches and executed notebook.
|
|
252
|
+
"""
|
|
253
|
+
if os.path.isabs(path):
|
|
254
|
+
raise ValueError("Path must be relative")
|
|
255
|
+
fpath_out = os.path.join(".calkit", "notebooks", "cleaned", path)
|
|
256
|
+
folder = os.path.dirname(fpath_out)
|
|
257
|
+
os.makedirs(folder, exist_ok=True)
|
|
258
|
+
subprocess.call(
|
|
259
|
+
[
|
|
260
|
+
"jupyter",
|
|
261
|
+
"nbconvert",
|
|
262
|
+
path,
|
|
263
|
+
"--clear-output",
|
|
264
|
+
"--to",
|
|
265
|
+
"notebook",
|
|
266
|
+
"--output",
|
|
267
|
+
fpath_out,
|
|
268
|
+
]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@notebooks_app.command("execute")
|
|
273
|
+
def execute_notebook(path: str):
|
|
274
|
+
"""Execute notebook and place a copy in the executed notebooks directory.
|
|
275
|
+
|
|
276
|
+
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
277
|
+
notebook as a dependency for a stage that caches and executed notebook.
|
|
278
|
+
"""
|
|
279
|
+
if os.path.isabs(path):
|
|
280
|
+
raise ValueError("Path must be relative")
|
|
281
|
+
fpath_out = os.path.join(".calkit", "notebooks", "executed", path)
|
|
282
|
+
folder = os.path.dirname(fpath_out)
|
|
283
|
+
os.makedirs(folder, exist_ok=True)
|
|
284
|
+
subprocess.call(
|
|
285
|
+
[
|
|
286
|
+
"jupyter",
|
|
287
|
+
"nbconvert",
|
|
288
|
+
path,
|
|
289
|
+
"--execute",
|
|
290
|
+
"--to",
|
|
291
|
+
"notebook",
|
|
292
|
+
"--output",
|
|
293
|
+
fpath_out,
|
|
294
|
+
]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.command(name="server", help="Run the local server.")
|
|
164
299
|
def run_server():
|
|
165
300
|
import uvicorn
|
|
166
301
|
|
|
@@ -173,5 +308,72 @@ def run_server():
|
|
|
173
308
|
)
|
|
174
309
|
|
|
175
310
|
|
|
311
|
+
@app.command(
|
|
312
|
+
name="run",
|
|
313
|
+
add_help_option=False,
|
|
314
|
+
help="Run DVC pipeline (a wrapper for `dvc repro`).",
|
|
315
|
+
)
|
|
316
|
+
def run_dvc_repro(
|
|
317
|
+
targets: Optional[list[str]] = typer.Argument(default=None),
|
|
318
|
+
help: Annotated[bool, typer.Option("-h", "--help")] = False,
|
|
319
|
+
quiet: Annotated[bool, typer.Option("-q", "--quiet")] = False,
|
|
320
|
+
verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
|
|
321
|
+
force: Annotated[bool, typer.Option("-f", "--force")] = False,
|
|
322
|
+
interactive: Annotated[bool, typer.Option("-i", "--interactive")] = False,
|
|
323
|
+
single_item: Annotated[bool, typer.Option("-s", "--single-item")] = False,
|
|
324
|
+
pipeline: Annotated[
|
|
325
|
+
Optional[str], typer.Option("-p", "--pipeline")
|
|
326
|
+
] = None,
|
|
327
|
+
all_pipelines: Annotated[
|
|
328
|
+
bool, typer.Option("-P", "--all-pipelines")
|
|
329
|
+
] = False,
|
|
330
|
+
recursive: Annotated[bool, typer.Option("-R", "--recursive")] = False,
|
|
331
|
+
downstream: Annotated[
|
|
332
|
+
Optional[list[str]], typer.Option("--downstream")
|
|
333
|
+
] = None,
|
|
334
|
+
force_downstream: Annotated[
|
|
335
|
+
bool, typer.Option("--force-downstream")
|
|
336
|
+
] = False,
|
|
337
|
+
pull: Annotated[bool, typer.Option("--pull")] = False,
|
|
338
|
+
allow_missing: Annotated[bool, typer.Option("--allow-missing")] = False,
|
|
339
|
+
dry: Annotated[bool, typer.Option("--dry")] = False,
|
|
340
|
+
keep_going: Annotated[bool, typer.Option("--keep-going", "-k")] = False,
|
|
341
|
+
ignore_errors: Annotated[bool, typer.Option("--ignore-errors")] = False,
|
|
342
|
+
glob: Annotated[bool, typer.Option("--glob")] = False,
|
|
343
|
+
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
344
|
+
no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
|
|
345
|
+
):
|
|
346
|
+
"""A simple wrapper for ``dvc repro``."""
|
|
347
|
+
if targets is None:
|
|
348
|
+
targets = []
|
|
349
|
+
args = targets
|
|
350
|
+
# Extract any boolean args
|
|
351
|
+
for name in [
|
|
352
|
+
"help",
|
|
353
|
+
"quiet",
|
|
354
|
+
"verbose",
|
|
355
|
+
"force",
|
|
356
|
+
"interactive",
|
|
357
|
+
"single-item",
|
|
358
|
+
"all-pipelines",
|
|
359
|
+
"recursive",
|
|
360
|
+
"pull",
|
|
361
|
+
"allow-missing",
|
|
362
|
+
"dry",
|
|
363
|
+
"keep-going",
|
|
364
|
+
"force-downstream",
|
|
365
|
+
"glob",
|
|
366
|
+
"no-commit",
|
|
367
|
+
"no-run-cache",
|
|
368
|
+
]:
|
|
369
|
+
if locals()[name.replace("-", "_")]:
|
|
370
|
+
args.append("--" + name)
|
|
371
|
+
if pipeline is not None:
|
|
372
|
+
args += ["--pipeline", pipeline]
|
|
373
|
+
if downstream is not None:
|
|
374
|
+
args += downstream
|
|
375
|
+
subprocess.call(["dvc", "repro"] + args)
|
|
376
|
+
|
|
377
|
+
|
|
176
378
|
def run() -> None:
|
|
177
379
|
app()
|
calkit/models.py
CHANGED
|
@@ -60,6 +60,12 @@ class Software(BaseModel):
|
|
|
60
60
|
description: str
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
class Notebook(_CalkitObject):
|
|
64
|
+
"""A Jupyter notebook."""
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
63
69
|
class ProjectInfo(BaseModel):
|
|
64
70
|
"""All of the project's information or metadata, written to the
|
|
65
71
|
``calkit.yaml`` file.
|
|
@@ -72,3 +78,4 @@ class ProjectInfo(BaseModel):
|
|
|
72
78
|
references: list[ReferenceCollection] = []
|
|
73
79
|
environments: list[Environment] = []
|
|
74
80
|
software: list[Software] = []
|
|
81
|
+
notebooks: list[Notebook] = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
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
|
|
@@ -15,6 +15,7 @@ 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: nbconvert
|
|
18
19
|
Requires-Dist: pydantic-settings
|
|
19
20
|
Requires-Dist: pydantic[email]
|
|
20
21
|
Requires-Dist: pyjwt
|
|
@@ -27,12 +28,43 @@ Description-Content-Type: text/markdown
|
|
|
27
28
|
|
|
28
29
|
# Calkit
|
|
29
30
|
|
|
30
|
-
[Calkit](https://calkit.io)
|
|
31
|
+
[Calkit](https://calkit.io) simplifies reproducibility,
|
|
31
32
|
acting as a layer on top of
|
|
32
|
-
[Git](https://git-scm.com/)
|
|
33
|
-
|
|
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
|
|
34
36
|
single repository.
|
|
35
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
|
+
|
|
36
68
|
## Installation
|
|
37
69
|
|
|
38
70
|
Simply run
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
calkit/__init__.py,sha256=
|
|
2
|
-
calkit/cli.py,sha256=
|
|
1
|
+
calkit/__init__.py,sha256=msMn6fygXybwu7UaIyTE7aSHGggrzrO1oPGG8SC8EbQ,142
|
|
2
|
+
calkit/cli.py,sha256=akfF8Qk_okiVNS37fUjjMMjDQaqIVi7NTam96XjVGQE,11427
|
|
3
3
|
calkit/cloud.py,sha256=CnQKms4mBQo4X8jVLIa-70-3jPDl38zRNFyK5TjiaHM,2209
|
|
4
4
|
calkit/config.py,sha256=NPFRk5inn66Wy44pyx8WppyqvIzkW1sO6VjXwIcCacE,2044
|
|
5
5
|
calkit/core.py,sha256=qLNjBwlECzMitL85pMGIHzne4j7FpQAR9-ukYtdIMig,1225
|
|
@@ -8,13 +8,13 @@ calkit/dvc.py,sha256=tX5172Ahw0mHcuJIrMenz1oGYxCrMzMURJJMHEHenbg,1506
|
|
|
8
8
|
calkit/git.py,sha256=NLd7A32dPbfVnZLa8xbmviJ-ChGZcdkcd6wcOOnsSKQ,350
|
|
9
9
|
calkit/gui.py,sha256=2UCrMyosc5v4CLlDcHO7oDwW9eLn5_WbNaLaG5bjMTc,23
|
|
10
10
|
calkit/jupyter.py,sha256=S6HUOVafaLQJ7_ZfC1ddBwF3fT5q3GcbouLHdlH-oSo,1034
|
|
11
|
-
calkit/models.py,sha256=
|
|
11
|
+
calkit/models.py,sha256=ejl032XIrbaCFkO-4VbCaKVQyHqNl7N4WWU20RjeAfc,1447
|
|
12
12
|
calkit/server.py,sha256=gq3643ABKTQu1eVR_OqJ-SdQh2dPLHSFsipmmAK5ttA,5959
|
|
13
13
|
calkit/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
calkit/tests/test_core.py,sha256=CdRqvYnq3MidyTknvfIbkmmhMfydpyKtTW9hakAvzsc,167
|
|
15
15
|
calkit/tests/test_jupyter.py,sha256=YTL6zI740UM2KUjskSipzvSKxsyQ8rVPFQIX5cb2tMQ,114
|
|
16
|
-
calkit_python-0.0.
|
|
17
|
-
calkit_python-0.0.
|
|
18
|
-
calkit_python-0.0.
|
|
19
|
-
calkit_python-0.0.
|
|
20
|
-
calkit_python-0.0.
|
|
16
|
+
calkit_python-0.0.5.dist-info/METADATA,sha256=tTUrA0R_i6RffDEd4VmMHbWvN0NR_4ifj53-SauHuVI,4004
|
|
17
|
+
calkit_python-0.0.5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
18
|
+
calkit_python-0.0.5.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
|
|
19
|
+
calkit_python-0.0.5.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
20
|
+
calkit_python-0.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|