calkit-python 0.0.7__py3-none-any.whl → 0.0.8__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/__init__.py +6 -0
- calkit/cli/config.py +42 -0
- calkit/cli/core.py +22 -0
- calkit/{cli.py → cli/main.py} +111 -221
- calkit/cli/new.py +107 -0
- calkit/cli/notebooks.py +76 -0
- {calkit_python-0.0.7.dist-info → calkit_python-0.0.8.dist-info}/METADATA +1 -1
- {calkit_python-0.0.7.dist-info → calkit_python-0.0.8.dist-info}/RECORD +12 -7
- {calkit_python-0.0.7.dist-info → calkit_python-0.0.8.dist-info}/WHEEL +0 -0
- {calkit_python-0.0.7.dist-info → calkit_python-0.0.8.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.0.7.dist-info → calkit_python-0.0.8.dist-info}/licenses/LICENSE +0 -0
calkit/__init__.py
CHANGED
calkit/cli/__init__.py
ADDED
calkit/cli/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Config CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from calkit import config
|
|
8
|
+
from calkit.dvc import configure_remote, set_remote_auth
|
|
9
|
+
|
|
10
|
+
config_app = typer.Typer(no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@config_app.command(name="set")
|
|
14
|
+
def set_config_value(key: str, value: str):
|
|
15
|
+
try:
|
|
16
|
+
cfg = config.read()
|
|
17
|
+
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
18
|
+
# Kind of a hack for setting the password computed field
|
|
19
|
+
# Types have been validated above, so this won't hurt to do again
|
|
20
|
+
setattr(cfg, key, value)
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
# TODO: This fails if we try to set password before any config has
|
|
23
|
+
# been written
|
|
24
|
+
# Username is fine
|
|
25
|
+
cfg = config.Settings.model_validate({key: value})
|
|
26
|
+
cfg.write()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@config_app.command(name="get")
|
|
30
|
+
def get_config_value(key: str) -> None:
|
|
31
|
+
cfg = config.read()
|
|
32
|
+
val = getattr(cfg, key)
|
|
33
|
+
if val is not None:
|
|
34
|
+
print(val)
|
|
35
|
+
else:
|
|
36
|
+
print()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@config_app.command(name="setup-remote")
|
|
40
|
+
def setup_remote():
|
|
41
|
+
configure_remote()
|
|
42
|
+
set_remote_auth()
|
calkit/cli/core.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Core CLI functionality."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pty
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_sep(name: str):
|
|
11
|
+
width = 66
|
|
12
|
+
txt_width = len(name) + 2
|
|
13
|
+
buffer_width = (width - txt_width) // 2
|
|
14
|
+
buffer = "-" * buffer_width
|
|
15
|
+
typer.echo(f"{buffer} {name} {buffer}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_cmd(cmd: list[str]):
|
|
19
|
+
if os.name == "nt":
|
|
20
|
+
subprocess.call(cmd)
|
|
21
|
+
else:
|
|
22
|
+
pty.spawn(cmd, lambda fd: os.read(fd, 1024))
|
calkit/{cli.py → cli/main.py}
RENAMED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Main CLI app."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
import pty
|
|
7
6
|
import subprocess
|
|
8
|
-
import sys
|
|
9
7
|
|
|
10
|
-
import git
|
|
11
8
|
import typer
|
|
12
9
|
from typing_extensions import Annotated, Optional
|
|
13
10
|
|
|
14
11
|
import calkit
|
|
15
|
-
from calkit.
|
|
16
|
-
|
|
17
|
-
from . import
|
|
18
|
-
from .
|
|
12
|
+
from calkit.cli import print_sep, run_cmd
|
|
13
|
+
from calkit.cli.config import config_app
|
|
14
|
+
from calkit.cli.new import new_app
|
|
15
|
+
from calkit.cli.notebooks import notebooks_app
|
|
19
16
|
|
|
20
17
|
app = typer.Typer(
|
|
21
18
|
invoke_without_command=True,
|
|
@@ -23,9 +20,6 @@ app = typer.Typer(
|
|
|
23
20
|
context_settings=dict(help_option_names=["-h", "--help"]),
|
|
24
21
|
pretty_exceptions_show_locals=False,
|
|
25
22
|
)
|
|
26
|
-
config_app = typer.Typer(no_args_is_help=True)
|
|
27
|
-
new_app = typer.Typer(no_args_is_help=True)
|
|
28
|
-
notebooks_app = typer.Typer(no_args_is_help=True)
|
|
29
23
|
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
30
24
|
app.add_typer(
|
|
31
25
|
new_app, name="new", help="Add new Calkit object (to calkit.yaml)."
|
|
@@ -45,55 +39,9 @@ def main(
|
|
|
45
39
|
raise typer.Exit()
|
|
46
40
|
|
|
47
41
|
|
|
48
|
-
@config_app.command(name="set")
|
|
49
|
-
def set_config_value(key: str, value: str):
|
|
50
|
-
try:
|
|
51
|
-
cfg = config.read()
|
|
52
|
-
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
53
|
-
# Kind of a hack for setting the password computed field
|
|
54
|
-
# Types have been validated above, so this won't hurt to do again
|
|
55
|
-
setattr(cfg, key, value)
|
|
56
|
-
except FileNotFoundError:
|
|
57
|
-
# TODO: This fails if we try to set password before any config has
|
|
58
|
-
# been written
|
|
59
|
-
# Username is fine
|
|
60
|
-
cfg = config.Settings.model_validate({key: value})
|
|
61
|
-
cfg.write()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@config_app.command(name="get")
|
|
65
|
-
def get_config_value(key: str) -> None:
|
|
66
|
-
cfg = config.read()
|
|
67
|
-
val = getattr(cfg, key)
|
|
68
|
-
if val is not None:
|
|
69
|
-
print(val)
|
|
70
|
-
else:
|
|
71
|
-
print()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@config_app.command(name="setup-remote")
|
|
75
|
-
def setup_remote():
|
|
76
|
-
configure_remote()
|
|
77
|
-
set_remote_auth()
|
|
78
|
-
|
|
79
|
-
|
|
80
42
|
@app.command(name="status")
|
|
81
43
|
def get_status():
|
|
82
44
|
"""Get a unified Git and DVC status."""
|
|
83
|
-
|
|
84
|
-
def print_sep(name: str):
|
|
85
|
-
width = 66
|
|
86
|
-
txt_width = len(name) + 2
|
|
87
|
-
buffer_width = (width - txt_width) // 2
|
|
88
|
-
buffer = "-" * buffer_width
|
|
89
|
-
typer.echo(f"{buffer} {name} {buffer}")
|
|
90
|
-
|
|
91
|
-
def run_cmd(cmd: list[str]):
|
|
92
|
-
if os.name == "nt":
|
|
93
|
-
subprocess.call(cmd)
|
|
94
|
-
else:
|
|
95
|
-
pty.spawn(cmd, lambda fd: os.read(fd, 1024))
|
|
96
|
-
|
|
97
45
|
print_sep("Code (Git)")
|
|
98
46
|
run_cmd(["git", "status"])
|
|
99
47
|
typer.echo()
|
|
@@ -104,6 +52,15 @@ def get_status():
|
|
|
104
52
|
run_cmd(["dvc", "status"])
|
|
105
53
|
|
|
106
54
|
|
|
55
|
+
@app.command(name="diff")
|
|
56
|
+
def diff():
|
|
57
|
+
"""Get a unified Git and DVC diff."""
|
|
58
|
+
print_sep("Code (Git)")
|
|
59
|
+
run_cmd(["git", "diff"])
|
|
60
|
+
print_sep("Pipeline (DVC)")
|
|
61
|
+
run_cmd(["dvc", "diff"])
|
|
62
|
+
|
|
63
|
+
|
|
107
64
|
@app.command(name="add")
|
|
108
65
|
def add(
|
|
109
66
|
paths: list[str],
|
|
@@ -225,165 +182,6 @@ def push():
|
|
|
225
182
|
subprocess.call(["dvc", "push"])
|
|
226
183
|
|
|
227
184
|
|
|
228
|
-
@new_app.command(name="figure")
|
|
229
|
-
def new_figure(
|
|
230
|
-
path: str,
|
|
231
|
-
title: Annotated[str, typer.Option("--title")],
|
|
232
|
-
description: Annotated[str, typer.Option("--desc")] = None,
|
|
233
|
-
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
234
|
-
):
|
|
235
|
-
"""Add a new figure."""
|
|
236
|
-
if os.path.isfile("calkit.yaml"):
|
|
237
|
-
with open("calkit.yaml") as f:
|
|
238
|
-
ck_info = ryaml.load(f)
|
|
239
|
-
else:
|
|
240
|
-
ck_info = {}
|
|
241
|
-
figures = ck_info.get("figures", [])
|
|
242
|
-
paths = [f.get("path") for f in figures]
|
|
243
|
-
if path in paths:
|
|
244
|
-
raise ValueError(f"Figure at path {path} already exists")
|
|
245
|
-
obj = dict(path=path, title=title)
|
|
246
|
-
if description is not None:
|
|
247
|
-
obj["description"] = description
|
|
248
|
-
figures.append(obj)
|
|
249
|
-
ck_info["figures"] = figures
|
|
250
|
-
with open("calkit.yaml", "w") as f:
|
|
251
|
-
ryaml.dump(ck_info, f)
|
|
252
|
-
if commit:
|
|
253
|
-
repo = git.Repo()
|
|
254
|
-
repo.git.add("calkit.yaml")
|
|
255
|
-
repo.git.commit(["-m", f"Add figure {path}"])
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
@new_app.command("question")
|
|
259
|
-
def new_question(
|
|
260
|
-
question: str,
|
|
261
|
-
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
262
|
-
):
|
|
263
|
-
"""Add a new question."""
|
|
264
|
-
if os.path.isfile("calkit.yaml"):
|
|
265
|
-
with open("calkit.yaml") as f:
|
|
266
|
-
ck_info = ryaml.load(f)
|
|
267
|
-
else:
|
|
268
|
-
ck_info = {}
|
|
269
|
-
questions = ck_info.get("questions", [])
|
|
270
|
-
if question in questions:
|
|
271
|
-
raise ValueError("Question already exists")
|
|
272
|
-
if not question.endswith("?"):
|
|
273
|
-
raise ValueError("Questions must end with a question mark")
|
|
274
|
-
questions.append(question)
|
|
275
|
-
ck_info["questions"] = questions
|
|
276
|
-
with open("calkit.yaml", "w") as f:
|
|
277
|
-
ryaml.dump(ck_info, f)
|
|
278
|
-
if commit:
|
|
279
|
-
repo = git.Repo()
|
|
280
|
-
repo.git.add("calkit.yaml")
|
|
281
|
-
repo.git.commit(["-m", "Add question"])
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
@new_app.command("notebook")
|
|
285
|
-
def new_notebook(
|
|
286
|
-
path: Annotated[str, typer.Argument(help="Notebook path (relative)")],
|
|
287
|
-
title: Annotated[str, typer.Option("--title")],
|
|
288
|
-
description: Annotated[str, typer.Option("--desc")] = None,
|
|
289
|
-
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
290
|
-
):
|
|
291
|
-
"""Add a new notebook."""
|
|
292
|
-
if os.path.isabs(path):
|
|
293
|
-
raise ValueError("Path must be relative")
|
|
294
|
-
if not os.path.isfile(path):
|
|
295
|
-
raise ValueError("Path is not a file")
|
|
296
|
-
if not path.endswith(".ipynb"):
|
|
297
|
-
raise ValueError("Path does not have .ipynb extension")
|
|
298
|
-
# TODO: Add option to create stages that run `calkit nb clean` and
|
|
299
|
-
# `calkit nb execute`
|
|
300
|
-
if os.path.isfile("calkit.yaml"):
|
|
301
|
-
with open("calkit.yaml") as f:
|
|
302
|
-
ck_info = ryaml.load(f)
|
|
303
|
-
else:
|
|
304
|
-
ck_info = {}
|
|
305
|
-
notebooks = ck_info.get("notebooks", [])
|
|
306
|
-
paths = [f.get("path") for f in notebooks]
|
|
307
|
-
if path in paths:
|
|
308
|
-
raise ValueError(f"Notebook at path {path} already exists")
|
|
309
|
-
obj = dict(path=path, title=title)
|
|
310
|
-
if description is not None:
|
|
311
|
-
obj["description"] = description
|
|
312
|
-
notebooks.append(obj)
|
|
313
|
-
ck_info["notebooks"] = notebooks
|
|
314
|
-
with open("calkit.yaml", "w") as f:
|
|
315
|
-
ryaml.dump(ck_info, f)
|
|
316
|
-
if commit:
|
|
317
|
-
repo = git.Repo()
|
|
318
|
-
repo.git.add("calkit.yaml")
|
|
319
|
-
repo.git.commit(["-m", f"Add notebook {path}"])
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
@notebooks_app.command("clean")
|
|
323
|
-
def clean_notebook_outputs(path: str):
|
|
324
|
-
"""Clean notebook and place a copy in the cleaned notebooks directory.
|
|
325
|
-
|
|
326
|
-
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
327
|
-
notebook as a dependency for a stage that caches and executed notebook.
|
|
328
|
-
"""
|
|
329
|
-
if os.path.isabs(path):
|
|
330
|
-
raise ValueError("Path must be relative")
|
|
331
|
-
fpath_out = os.path.join(".calkit", "notebooks", "cleaned", path)
|
|
332
|
-
folder = os.path.dirname(fpath_out)
|
|
333
|
-
os.makedirs(folder, exist_ok=True)
|
|
334
|
-
subprocess.call(
|
|
335
|
-
[
|
|
336
|
-
"jupyter",
|
|
337
|
-
"nbconvert",
|
|
338
|
-
path,
|
|
339
|
-
"--clear-output",
|
|
340
|
-
"--to",
|
|
341
|
-
"notebook",
|
|
342
|
-
"--output",
|
|
343
|
-
fpath_out,
|
|
344
|
-
]
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
@notebooks_app.command("execute")
|
|
349
|
-
def execute_notebook(
|
|
350
|
-
path: str,
|
|
351
|
-
to: Annotated[
|
|
352
|
-
str, typer.Option("--to", help="Output format ('html' or 'notebook').")
|
|
353
|
-
] = "notebook",
|
|
354
|
-
):
|
|
355
|
-
"""Execute notebook and place a copy in the relevant directory.
|
|
356
|
-
|
|
357
|
-
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
358
|
-
notebook as a dependency for a stage that caches and executed notebook.
|
|
359
|
-
"""
|
|
360
|
-
if os.path.isabs(path):
|
|
361
|
-
raise ValueError("Path must be relative")
|
|
362
|
-
if to == "html":
|
|
363
|
-
subdir = "html"
|
|
364
|
-
fname_out = path.removesuffix(".ipynb") + ".html"
|
|
365
|
-
elif to == "notebook":
|
|
366
|
-
subdir = "executed"
|
|
367
|
-
fname_out = path
|
|
368
|
-
else:
|
|
369
|
-
raise ValueError(f"Invalid output format: '{to}'")
|
|
370
|
-
fpath_out = os.path.join(".calkit", "notebooks", subdir, fname_out)
|
|
371
|
-
folder = os.path.dirname(fpath_out)
|
|
372
|
-
os.makedirs(folder, exist_ok=True)
|
|
373
|
-
subprocess.call(
|
|
374
|
-
[
|
|
375
|
-
"jupyter",
|
|
376
|
-
"nbconvert",
|
|
377
|
-
path,
|
|
378
|
-
"--execute",
|
|
379
|
-
"--to",
|
|
380
|
-
to,
|
|
381
|
-
"--output",
|
|
382
|
-
fpath_out,
|
|
383
|
-
]
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
|
|
387
185
|
@app.command(name="server", help="Run the local server.")
|
|
388
186
|
def run_server():
|
|
389
187
|
import uvicorn
|
|
@@ -432,7 +230,9 @@ def run_dvc_repro(
|
|
|
432
230
|
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
433
231
|
no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
|
|
434
232
|
):
|
|
435
|
-
"""A simple wrapper for ``dvc repro
|
|
233
|
+
"""A simple wrapper for ``dvc repro`` that will automatically create any
|
|
234
|
+
necessary Calkit objects from stage metadata.
|
|
235
|
+
"""
|
|
436
236
|
if targets is None:
|
|
437
237
|
targets = []
|
|
438
238
|
args = targets
|
|
@@ -462,7 +262,97 @@ def run_dvc_repro(
|
|
|
462
262
|
if downstream is not None:
|
|
463
263
|
args += downstream
|
|
464
264
|
subprocess.call(["dvc", "repro"] + args)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
265
|
+
# Now parse stage metadata for calkit objects
|
|
266
|
+
if not os.path.isfile("dvc.yaml"):
|
|
267
|
+
typer.echo("No dvc.yaml file found")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
objects = []
|
|
270
|
+
with open("dvc.yaml") as f:
|
|
271
|
+
pipeline = calkit.ryaml.load(f)
|
|
272
|
+
for stage_name, stage_info in pipeline.get("stages", {}).items():
|
|
273
|
+
ckmeta = stage_info.get("meta", {}).get("calkit")
|
|
274
|
+
if ckmeta is not None:
|
|
275
|
+
if not isinstance(ckmeta, dict):
|
|
276
|
+
typer.echo(
|
|
277
|
+
f"Calkit metadata for {stage_name} is not a dictionary"
|
|
278
|
+
)
|
|
279
|
+
typer.Exit(1)
|
|
280
|
+
# Stage must have a single output
|
|
281
|
+
outs = stage_info.get("outs", [])
|
|
282
|
+
if len(outs) != 1:
|
|
283
|
+
typer.echo(
|
|
284
|
+
f"Stage {stage_name} does not have exactly one output"
|
|
285
|
+
)
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
cktype = ckmeta.get("type")
|
|
288
|
+
if cktype not in ["figure", "dataset", "publication"]:
|
|
289
|
+
typer.echo(f"Invalid Calkit output type '{cktype}'")
|
|
290
|
+
raise typer.Exit(1)
|
|
291
|
+
objects.append(
|
|
292
|
+
dict(path=outs[0]) | ckmeta | dict(stage=stage_name)
|
|
293
|
+
)
|
|
294
|
+
# Now that we've extracted Calkit objects from stage metadata, we can put
|
|
295
|
+
# them into the calkit.yaml file, overwriting objects with the same path
|
|
296
|
+
if os.path.isfile("calkit.yaml"):
|
|
297
|
+
with open("calkit.yaml") as f:
|
|
298
|
+
ck_info = calkit.ryaml.load(f)
|
|
299
|
+
else:
|
|
300
|
+
ck_info = {}
|
|
301
|
+
for obj in objects:
|
|
302
|
+
cktype = obj.pop("type")
|
|
303
|
+
cktype_plural = cktype + "s"
|
|
304
|
+
existing = ck_info.get(cktype_plural, [])
|
|
305
|
+
new = []
|
|
306
|
+
added = False
|
|
307
|
+
for ex_obj in existing:
|
|
308
|
+
if ex_obj.get("path") == obj["path"]:
|
|
309
|
+
typer.echo(f"Updating {cktype} {ex_obj['path']}")
|
|
310
|
+
new.append(obj)
|
|
311
|
+
added = True
|
|
312
|
+
else:
|
|
313
|
+
new.append(ex_obj)
|
|
314
|
+
if not added:
|
|
315
|
+
typer.echo(f"Adding new {cktype} {obj['path']}")
|
|
316
|
+
new.append(obj)
|
|
317
|
+
ck_info[cktype_plural] = new
|
|
318
|
+
if not dry:
|
|
319
|
+
with open("calkit.yaml", "w") as f:
|
|
320
|
+
calkit.ryaml.dump(ck_info, f)
|
|
321
|
+
run_cmd(["git", "add", "calkit.yaml"])
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@app.command(name="manual-step", help="Execute a manual step.")
|
|
325
|
+
def manual_step(
|
|
326
|
+
message: Annotated[
|
|
327
|
+
str,
|
|
328
|
+
typer.Option(
|
|
329
|
+
"--message",
|
|
330
|
+
"-m",
|
|
331
|
+
help="Message to display as a prompt.",
|
|
332
|
+
),
|
|
333
|
+
],
|
|
334
|
+
cmd: Annotated[str, typer.Option("--cmd", help="Command to run.")] = None,
|
|
335
|
+
shell: Annotated[
|
|
336
|
+
bool,
|
|
337
|
+
typer.Option(
|
|
338
|
+
"--shell",
|
|
339
|
+
help="Whether or not to execute the command in shell mode.",
|
|
340
|
+
),
|
|
341
|
+
] = False,
|
|
342
|
+
show_stdout: Annotated[
|
|
343
|
+
bool, typer.Option("--show-stdout", help="Show stdout.")
|
|
344
|
+
] = False,
|
|
345
|
+
show_stderr: Annotated[
|
|
346
|
+
bool, typer.Option("--show-stderr", help="Show stderr.")
|
|
347
|
+
] = False,
|
|
348
|
+
) -> None:
|
|
349
|
+
if cmd is not None:
|
|
350
|
+
typer.echo(f"Running command: {cmd}")
|
|
351
|
+
subprocess.Popen(
|
|
352
|
+
cmd.split() if not shell else cmd,
|
|
353
|
+
stderr=subprocess.PIPE if not show_stderr else None,
|
|
354
|
+
stdout=subprocess.PIPE if not show_stdout else None,
|
|
355
|
+
shell=shell,
|
|
356
|
+
)
|
|
357
|
+
input(message + " (press enter to confirm): ")
|
|
358
|
+
typer.echo("Done")
|
calkit/cli/new.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""CLI for creating new objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import git
|
|
8
|
+
import typer
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from calkit.core import ryaml
|
|
12
|
+
|
|
13
|
+
new_app = typer.Typer(no_args_is_help=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@new_app.command(name="figure")
|
|
17
|
+
def new_figure(
|
|
18
|
+
path: str,
|
|
19
|
+
title: Annotated[str, typer.Option("--title")],
|
|
20
|
+
description: Annotated[str, typer.Option("--desc")] = None,
|
|
21
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
22
|
+
):
|
|
23
|
+
"""Add a new figure."""
|
|
24
|
+
if os.path.isfile("calkit.yaml"):
|
|
25
|
+
with open("calkit.yaml") as f:
|
|
26
|
+
ck_info = ryaml.load(f)
|
|
27
|
+
else:
|
|
28
|
+
ck_info = {}
|
|
29
|
+
figures = ck_info.get("figures", [])
|
|
30
|
+
paths = [f.get("path") for f in figures]
|
|
31
|
+
if path in paths:
|
|
32
|
+
raise ValueError(f"Figure at path {path} already exists")
|
|
33
|
+
obj = dict(path=path, title=title)
|
|
34
|
+
if description is not None:
|
|
35
|
+
obj["description"] = description
|
|
36
|
+
figures.append(obj)
|
|
37
|
+
ck_info["figures"] = figures
|
|
38
|
+
with open("calkit.yaml", "w") as f:
|
|
39
|
+
ryaml.dump(ck_info, f)
|
|
40
|
+
if commit:
|
|
41
|
+
repo = git.Repo()
|
|
42
|
+
repo.git.add("calkit.yaml")
|
|
43
|
+
repo.git.commit(["-m", f"Add figure {path}"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@new_app.command("question")
|
|
47
|
+
def new_question(
|
|
48
|
+
question: str,
|
|
49
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
50
|
+
):
|
|
51
|
+
"""Add a new question."""
|
|
52
|
+
if os.path.isfile("calkit.yaml"):
|
|
53
|
+
with open("calkit.yaml") as f:
|
|
54
|
+
ck_info = ryaml.load(f)
|
|
55
|
+
else:
|
|
56
|
+
ck_info = {}
|
|
57
|
+
questions = ck_info.get("questions", [])
|
|
58
|
+
if question in questions:
|
|
59
|
+
raise ValueError("Question already exists")
|
|
60
|
+
if not question.endswith("?"):
|
|
61
|
+
raise ValueError("Questions must end with a question mark")
|
|
62
|
+
questions.append(question)
|
|
63
|
+
ck_info["questions"] = questions
|
|
64
|
+
with open("calkit.yaml", "w") as f:
|
|
65
|
+
ryaml.dump(ck_info, f)
|
|
66
|
+
if commit:
|
|
67
|
+
repo = git.Repo()
|
|
68
|
+
repo.git.add("calkit.yaml")
|
|
69
|
+
repo.git.commit(["-m", "Add question"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@new_app.command("notebook")
|
|
73
|
+
def new_notebook(
|
|
74
|
+
path: Annotated[str, typer.Argument(help="Notebook path (relative)")],
|
|
75
|
+
title: Annotated[str, typer.Option("--title")],
|
|
76
|
+
description: Annotated[str, typer.Option("--desc")] = None,
|
|
77
|
+
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
78
|
+
):
|
|
79
|
+
"""Add a new notebook."""
|
|
80
|
+
if os.path.isabs(path):
|
|
81
|
+
raise ValueError("Path must be relative")
|
|
82
|
+
if not os.path.isfile(path):
|
|
83
|
+
raise ValueError("Path is not a file")
|
|
84
|
+
if not path.endswith(".ipynb"):
|
|
85
|
+
raise ValueError("Path does not have .ipynb extension")
|
|
86
|
+
# TODO: Add option to create stages that run `calkit nb clean` and
|
|
87
|
+
# `calkit nb execute`
|
|
88
|
+
if os.path.isfile("calkit.yaml"):
|
|
89
|
+
with open("calkit.yaml") as f:
|
|
90
|
+
ck_info = ryaml.load(f)
|
|
91
|
+
else:
|
|
92
|
+
ck_info = {}
|
|
93
|
+
notebooks = ck_info.get("notebooks", [])
|
|
94
|
+
paths = [f.get("path") for f in notebooks]
|
|
95
|
+
if path in paths:
|
|
96
|
+
raise ValueError(f"Notebook at path {path} already exists")
|
|
97
|
+
obj = dict(path=path, title=title)
|
|
98
|
+
if description is not None:
|
|
99
|
+
obj["description"] = description
|
|
100
|
+
notebooks.append(obj)
|
|
101
|
+
ck_info["notebooks"] = notebooks
|
|
102
|
+
with open("calkit.yaml", "w") as f:
|
|
103
|
+
ryaml.dump(ck_info, f)
|
|
104
|
+
if commit:
|
|
105
|
+
repo = git.Repo()
|
|
106
|
+
repo.git.add("calkit.yaml")
|
|
107
|
+
repo.git.commit(["-m", f"Add notebook {path}"])
|
calkit/cli/notebooks.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Notebooks CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
notebooks_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@notebooks_app.command("clean")
|
|
15
|
+
def clean_notebook_outputs(path: str):
|
|
16
|
+
"""Clean notebook and place a copy in the cleaned notebooks directory.
|
|
17
|
+
|
|
18
|
+
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
19
|
+
notebook as a dependency for a stage that caches and executed notebook.
|
|
20
|
+
"""
|
|
21
|
+
if os.path.isabs(path):
|
|
22
|
+
raise ValueError("Path must be relative")
|
|
23
|
+
fpath_out = os.path.join(".calkit", "notebooks", "cleaned", path)
|
|
24
|
+
folder = os.path.dirname(fpath_out)
|
|
25
|
+
os.makedirs(folder, exist_ok=True)
|
|
26
|
+
subprocess.call(
|
|
27
|
+
[
|
|
28
|
+
"jupyter",
|
|
29
|
+
"nbconvert",
|
|
30
|
+
path,
|
|
31
|
+
"--clear-output",
|
|
32
|
+
"--to",
|
|
33
|
+
"notebook",
|
|
34
|
+
"--output",
|
|
35
|
+
fpath_out,
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@notebooks_app.command("execute")
|
|
41
|
+
def execute_notebook(
|
|
42
|
+
path: str,
|
|
43
|
+
to: Annotated[
|
|
44
|
+
str, typer.Option("--to", help="Output format ('html' or 'notebook').")
|
|
45
|
+
] = "notebook",
|
|
46
|
+
):
|
|
47
|
+
"""Execute notebook and place a copy in the relevant directory.
|
|
48
|
+
|
|
49
|
+
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
50
|
+
notebook as a dependency for a stage that caches and executed notebook.
|
|
51
|
+
"""
|
|
52
|
+
if os.path.isabs(path):
|
|
53
|
+
raise ValueError("Path must be relative")
|
|
54
|
+
if to == "html":
|
|
55
|
+
subdir = "html"
|
|
56
|
+
fname_out = path.removesuffix(".ipynb") + ".html"
|
|
57
|
+
elif to == "notebook":
|
|
58
|
+
subdir = "executed"
|
|
59
|
+
fname_out = path
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"Invalid output format: '{to}'")
|
|
62
|
+
fpath_out = os.path.join(".calkit", "notebooks", subdir, fname_out)
|
|
63
|
+
folder = os.path.dirname(fpath_out)
|
|
64
|
+
os.makedirs(folder, exist_ok=True)
|
|
65
|
+
subprocess.call(
|
|
66
|
+
[
|
|
67
|
+
"jupyter",
|
|
68
|
+
"nbconvert",
|
|
69
|
+
path,
|
|
70
|
+
"--execute",
|
|
71
|
+
"--to",
|
|
72
|
+
to,
|
|
73
|
+
"--output",
|
|
74
|
+
fpath_out,
|
|
75
|
+
]
|
|
76
|
+
)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
calkit/__init__.py,sha256=
|
|
2
|
-
calkit/cli.py,sha256=IxF2P_sO2J9MiO8mIxYedVfjWWTTKlLYFGZqYXjPIxI,14112
|
|
1
|
+
calkit/__init__.py,sha256=IQMt4pG5Q-NtyYJJWLzR88yFOvC8riMn-DQ52wqYi8w,142
|
|
3
2
|
calkit/cloud.py,sha256=CnQKms4mBQo4X8jVLIa-70-3jPDl38zRNFyK5TjiaHM,2209
|
|
4
3
|
calkit/config.py,sha256=NPFRk5inn66Wy44pyx8WppyqvIzkW1sO6VjXwIcCacE,2044
|
|
5
4
|
calkit/core.py,sha256=qLNjBwlECzMitL85pMGIHzne4j7FpQAR9-ukYtdIMig,1225
|
|
@@ -10,11 +9,17 @@ calkit/gui.py,sha256=2UCrMyosc5v4CLlDcHO7oDwW9eLn5_WbNaLaG5bjMTc,23
|
|
|
10
9
|
calkit/jupyter.py,sha256=S6HUOVafaLQJ7_ZfC1ddBwF3fT5q3GcbouLHdlH-oSo,1034
|
|
11
10
|
calkit/models.py,sha256=ejl032XIrbaCFkO-4VbCaKVQyHqNl7N4WWU20RjeAfc,1447
|
|
12
11
|
calkit/server.py,sha256=gq3643ABKTQu1eVR_OqJ-SdQh2dPLHSFsipmmAK5ttA,5959
|
|
12
|
+
calkit/cli/__init__.py,sha256=2Id-_5dsHbL29qqScLkRUhWXzTgxsZ_A_lMPs9rf5uY,77
|
|
13
|
+
calkit/cli/config.py,sha256=y-TWv150qWvUtlMfySgCyMyk8n5SNL-8hNRm9NycJ98,1098
|
|
14
|
+
calkit/cli/core.py,sha256=N-I2Yw1STgEMN5xLmfOuVq1od1EJx2oMCyVQtNa8Nwg,424
|
|
15
|
+
calkit/cli/main.py,sha256=_zrl3nYszxDwpvis5rd0Qa5jP78XjLvril1pvn9BnhA,11378
|
|
16
|
+
calkit/cli/new.py,sha256=Kn80X_z47uJ27Nkl8WJVWYqdwXyuaKpIy19_C1A26Q4,3386
|
|
17
|
+
calkit/cli/notebooks.py,sha256=GsNmm8pdFkbs_KyZxClfIUThD78GCdu0R5udCXhXc9E,2083
|
|
13
18
|
calkit/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
19
|
calkit/tests/test_core.py,sha256=CdRqvYnq3MidyTknvfIbkmmhMfydpyKtTW9hakAvzsc,167
|
|
15
20
|
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.
|
|
21
|
+
calkit_python-0.0.8.dist-info/METADATA,sha256=I1R0dOcHch17Q1K3vbqQ83CWU95Eu0ygri_rWfBsZbc,4004
|
|
22
|
+
calkit_python-0.0.8.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
23
|
+
calkit_python-0.0.8.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
|
|
24
|
+
calkit_python-0.0.8.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
25
|
+
calkit_python-0.0.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|