calkit-python 0.0.6__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/main.py +358 -0
- calkit/cli/new.py +107 -0
- calkit/cli/notebooks.py +76 -0
- {calkit_python-0.0.6.dist-info → calkit_python-0.0.8.dist-info}/METADATA +1 -1
- {calkit_python-0.0.6.dist-info → calkit_python-0.0.8.dist-info}/RECORD +12 -7
- calkit/cli.py +0 -392
- {calkit_python-0.0.6.dist-info → calkit_python-0.0.8.dist-info}/WHEEL +0 -0
- {calkit_python-0.0.6.dist-info → calkit_python-0.0.8.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.0.6.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/main.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Main CLI app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from typing_extensions import Annotated, Optional
|
|
10
|
+
|
|
11
|
+
import calkit
|
|
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
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
invoke_without_command=True,
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
context_settings=dict(help_option_names=["-h", "--help"]),
|
|
21
|
+
pretty_exceptions_show_locals=False,
|
|
22
|
+
)
|
|
23
|
+
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
24
|
+
app.add_typer(
|
|
25
|
+
new_app, name="new", help="Add new Calkit object (to calkit.yaml)."
|
|
26
|
+
)
|
|
27
|
+
app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback()
|
|
31
|
+
def main(
|
|
32
|
+
version: Annotated[
|
|
33
|
+
bool,
|
|
34
|
+
typer.Option("--version", help="Show version and exit."),
|
|
35
|
+
] = False,
|
|
36
|
+
):
|
|
37
|
+
if version:
|
|
38
|
+
typer.echo(calkit.__version__)
|
|
39
|
+
raise typer.Exit()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command(name="status")
|
|
43
|
+
def get_status():
|
|
44
|
+
"""Get a unified Git and DVC status."""
|
|
45
|
+
print_sep("Code (Git)")
|
|
46
|
+
run_cmd(["git", "status"])
|
|
47
|
+
typer.echo()
|
|
48
|
+
print_sep("Data (DVC)")
|
|
49
|
+
run_cmd(["dvc", "data", "status"])
|
|
50
|
+
typer.echo()
|
|
51
|
+
print_sep("Pipeline (DVC)")
|
|
52
|
+
run_cmd(["dvc", "status"])
|
|
53
|
+
|
|
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
|
+
|
|
64
|
+
@app.command(name="add")
|
|
65
|
+
def add(
|
|
66
|
+
paths: list[str],
|
|
67
|
+
commit_message: Annotated[
|
|
68
|
+
str,
|
|
69
|
+
typer.Option(
|
|
70
|
+
"-m",
|
|
71
|
+
"--commit-message",
|
|
72
|
+
help="Automatically commit and use this as a message.",
|
|
73
|
+
),
|
|
74
|
+
] = None,
|
|
75
|
+
push_commit: Annotated[
|
|
76
|
+
bool, typer.Option("--push", help="Push after committing.")
|
|
77
|
+
] = False,
|
|
78
|
+
to: Annotated[
|
|
79
|
+
str,
|
|
80
|
+
typer.Option(
|
|
81
|
+
"--to", "-t", help="System with which to add (git or dvc)."
|
|
82
|
+
),
|
|
83
|
+
] = None,
|
|
84
|
+
):
|
|
85
|
+
"""Add paths to the repo.
|
|
86
|
+
|
|
87
|
+
Code will be added to Git and data will be added to DVC.
|
|
88
|
+
|
|
89
|
+
Note: This will enable the 'autostage' feature of DVC, automatically
|
|
90
|
+
adding any .dvc files to Git when adding to DVC.
|
|
91
|
+
"""
|
|
92
|
+
if to is not None and to not in ["git", "dvc"]:
|
|
93
|
+
typer.echo(f"Invalid option for 'to': {to}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
# Ensure autostage is enabled for DVC
|
|
96
|
+
subprocess.call(["dvc", "config", "core.autostage", "true"])
|
|
97
|
+
subprocess.call(["git", "add", ".dvc/config"])
|
|
98
|
+
if to is not None:
|
|
99
|
+
subprocess.call([to, "add"] + paths)
|
|
100
|
+
else:
|
|
101
|
+
dvc_extensions = [
|
|
102
|
+
".png",
|
|
103
|
+
".h5",
|
|
104
|
+
".parquet",
|
|
105
|
+
".pickle",
|
|
106
|
+
".mp4",
|
|
107
|
+
".avi",
|
|
108
|
+
".webm",
|
|
109
|
+
".pdf",
|
|
110
|
+
]
|
|
111
|
+
dvc_size_thresh_bytes = 1_000_000
|
|
112
|
+
if "." in paths and to is None:
|
|
113
|
+
typer.echo("Cannot add '.' with calkit; use git or dvc")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
if to is None:
|
|
116
|
+
for path in paths:
|
|
117
|
+
if os.path.isdir(path):
|
|
118
|
+
typer.echo("Cannot auto-add directories; use git or dvc")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
for path in paths:
|
|
121
|
+
# Detect if this file should be tracked with Git or DVC
|
|
122
|
+
if os.path.splitext(path)[-1] in dvc_extensions:
|
|
123
|
+
typer.echo(f"Adding {path} to DVC per its extension")
|
|
124
|
+
subprocess.call(["dvc", "add", path])
|
|
125
|
+
continue
|
|
126
|
+
if os.path.getsize(path) > dvc_size_thresh_bytes:
|
|
127
|
+
typer.echo(
|
|
128
|
+
f"Adding {path} to DVC since it's greater than 1 MB"
|
|
129
|
+
)
|
|
130
|
+
subprocess.call(["dvc", "add", path])
|
|
131
|
+
continue
|
|
132
|
+
typer.echo(f"Adding {path} to Git")
|
|
133
|
+
subprocess.call(["git", "add", path])
|
|
134
|
+
if commit_message is not None:
|
|
135
|
+
subprocess.call(["git", "commit", "-m", commit_message])
|
|
136
|
+
if push_commit:
|
|
137
|
+
push()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command(name="commit")
|
|
141
|
+
def commit(
|
|
142
|
+
all: Annotated[
|
|
143
|
+
Optional[bool],
|
|
144
|
+
typer.Option(
|
|
145
|
+
"--all", "-a", help="Automatically stage all changed files."
|
|
146
|
+
),
|
|
147
|
+
] = False,
|
|
148
|
+
message: Annotated[
|
|
149
|
+
Optional[str], typer.Option("--message", "-m", help="Commit message.")
|
|
150
|
+
] = None,
|
|
151
|
+
push_commit: Annotated[
|
|
152
|
+
bool,
|
|
153
|
+
typer.Option(
|
|
154
|
+
"--push", help="Push to both Git and DVC after committing."
|
|
155
|
+
),
|
|
156
|
+
] = False,
|
|
157
|
+
):
|
|
158
|
+
"""Commit a change to the repo."""
|
|
159
|
+
cmd = ["git", "commit"]
|
|
160
|
+
if all:
|
|
161
|
+
cmd.append("-a")
|
|
162
|
+
if message:
|
|
163
|
+
cmd += ["-m", message]
|
|
164
|
+
subprocess.call(cmd)
|
|
165
|
+
if push_commit:
|
|
166
|
+
push()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command(name="pull", help="Pull with both Git and DVC.")
|
|
170
|
+
def pull():
|
|
171
|
+
typer.echo("Git pulling")
|
|
172
|
+
subprocess.call(["git", "pull"])
|
|
173
|
+
typer.echo("DVC pulling")
|
|
174
|
+
subprocess.call(["dvc", "pull"])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command(name="push", help="Push with both Git and DVC.")
|
|
178
|
+
def push():
|
|
179
|
+
typer.echo("Pushing to Git remote")
|
|
180
|
+
subprocess.call(["git", "push"])
|
|
181
|
+
typer.echo("Pushing to DVC remote")
|
|
182
|
+
subprocess.call(["dvc", "push"])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command(name="server", help="Run the local server.")
|
|
186
|
+
def run_server():
|
|
187
|
+
import uvicorn
|
|
188
|
+
|
|
189
|
+
uvicorn.run(
|
|
190
|
+
"calkit.server:app",
|
|
191
|
+
port=8866,
|
|
192
|
+
host="localhost",
|
|
193
|
+
reload=True,
|
|
194
|
+
reload_dirs=[os.path.dirname(__file__)],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command(
|
|
199
|
+
name="run",
|
|
200
|
+
add_help_option=False,
|
|
201
|
+
help="Run DVC pipeline (a wrapper for `dvc repro`).",
|
|
202
|
+
)
|
|
203
|
+
def run_dvc_repro(
|
|
204
|
+
targets: Optional[list[str]] = typer.Argument(default=None),
|
|
205
|
+
help: Annotated[bool, typer.Option("-h", "--help")] = False,
|
|
206
|
+
quiet: Annotated[bool, typer.Option("-q", "--quiet")] = False,
|
|
207
|
+
verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
|
|
208
|
+
force: Annotated[bool, typer.Option("-f", "--force")] = False,
|
|
209
|
+
interactive: Annotated[bool, typer.Option("-i", "--interactive")] = False,
|
|
210
|
+
single_item: Annotated[bool, typer.Option("-s", "--single-item")] = False,
|
|
211
|
+
pipeline: Annotated[
|
|
212
|
+
Optional[str], typer.Option("-p", "--pipeline")
|
|
213
|
+
] = None,
|
|
214
|
+
all_pipelines: Annotated[
|
|
215
|
+
bool, typer.Option("-P", "--all-pipelines")
|
|
216
|
+
] = False,
|
|
217
|
+
recursive: Annotated[bool, typer.Option("-R", "--recursive")] = False,
|
|
218
|
+
downstream: Annotated[
|
|
219
|
+
Optional[list[str]], typer.Option("--downstream")
|
|
220
|
+
] = None,
|
|
221
|
+
force_downstream: Annotated[
|
|
222
|
+
bool, typer.Option("--force-downstream")
|
|
223
|
+
] = False,
|
|
224
|
+
pull: Annotated[bool, typer.Option("--pull")] = False,
|
|
225
|
+
allow_missing: Annotated[bool, typer.Option("--allow-missing")] = False,
|
|
226
|
+
dry: Annotated[bool, typer.Option("--dry")] = False,
|
|
227
|
+
keep_going: Annotated[bool, typer.Option("--keep-going", "-k")] = False,
|
|
228
|
+
ignore_errors: Annotated[bool, typer.Option("--ignore-errors")] = False,
|
|
229
|
+
glob: Annotated[bool, typer.Option("--glob")] = False,
|
|
230
|
+
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
231
|
+
no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
|
|
232
|
+
):
|
|
233
|
+
"""A simple wrapper for ``dvc repro`` that will automatically create any
|
|
234
|
+
necessary Calkit objects from stage metadata.
|
|
235
|
+
"""
|
|
236
|
+
if targets is None:
|
|
237
|
+
targets = []
|
|
238
|
+
args = targets
|
|
239
|
+
# Extract any boolean args
|
|
240
|
+
for name in [
|
|
241
|
+
"help",
|
|
242
|
+
"quiet",
|
|
243
|
+
"verbose",
|
|
244
|
+
"force",
|
|
245
|
+
"interactive",
|
|
246
|
+
"single-item",
|
|
247
|
+
"all-pipelines",
|
|
248
|
+
"recursive",
|
|
249
|
+
"pull",
|
|
250
|
+
"allow-missing",
|
|
251
|
+
"dry",
|
|
252
|
+
"keep-going",
|
|
253
|
+
"force-downstream",
|
|
254
|
+
"glob",
|
|
255
|
+
"no-commit",
|
|
256
|
+
"no-run-cache",
|
|
257
|
+
]:
|
|
258
|
+
if locals()[name.replace("-", "_")]:
|
|
259
|
+
args.append("--" + name)
|
|
260
|
+
if pipeline is not None:
|
|
261
|
+
args += ["--pipeline", pipeline]
|
|
262
|
+
if downstream is not None:
|
|
263
|
+
args += downstream
|
|
264
|
+
subprocess.call(["dvc", "repro"] + args)
|
|
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=Ko3ntXUTqYeiLv00PQz68SWRp_vY1hruQgjmgwXnuzc,11788
|
|
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,,
|
calkit/cli.py
DELETED
|
@@ -1,392 +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 git
|
|
11
|
-
import typer
|
|
12
|
-
from typing_extensions import Annotated, Optional
|
|
13
|
-
|
|
14
|
-
import calkit
|
|
15
|
-
from calkit.core import ryaml
|
|
16
|
-
|
|
17
|
-
from . import config
|
|
18
|
-
from .dvc import configure_remote, set_remote_auth
|
|
19
|
-
|
|
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)
|
|
28
|
-
app.add_typer(config_app, name="config", help="Configure Calkit.")
|
|
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()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@config_app.command(name="set")
|
|
48
|
-
def set_config_value(key: str, value: str):
|
|
49
|
-
try:
|
|
50
|
-
cfg = config.read()
|
|
51
|
-
cfg = config.Settings.model_validate(cfg.model_dump() | {key: value})
|
|
52
|
-
# Kind of a hack for setting the password computed field
|
|
53
|
-
# Types have been validated above, so this won't hurt to do again
|
|
54
|
-
setattr(cfg, key, value)
|
|
55
|
-
except FileNotFoundError:
|
|
56
|
-
# TODO: This fails if we try to set password before any config has
|
|
57
|
-
# been written
|
|
58
|
-
# Username is fine
|
|
59
|
-
cfg = config.Settings.model_validate({key: value})
|
|
60
|
-
cfg.write()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@config_app.command(name="get")
|
|
64
|
-
def get_config_value(key: str) -> None:
|
|
65
|
-
cfg = config.read()
|
|
66
|
-
val = getattr(cfg, key)
|
|
67
|
-
if val is not None:
|
|
68
|
-
print(val)
|
|
69
|
-
else:
|
|
70
|
-
print()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@config_app.command(name="setup-remote")
|
|
74
|
-
def setup_remote():
|
|
75
|
-
configure_remote()
|
|
76
|
-
set_remote_auth()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@app.command(name="status")
|
|
80
|
-
def get_status():
|
|
81
|
-
"""Get a unified Git and DVC status."""
|
|
82
|
-
|
|
83
|
-
def print_sep(name: str):
|
|
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}")
|
|
89
|
-
|
|
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"])
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@app.command(name="add")
|
|
107
|
-
def add(paths: list[str]):
|
|
108
|
-
"""Add paths to the repo.
|
|
109
|
-
|
|
110
|
-
Code will be added to Git and data will be added to DVC.
|
|
111
|
-
"""
|
|
112
|
-
dvc_extensions = [".png", ".h5", ".parquet", ".pickle"]
|
|
113
|
-
dvc_size_thresh_bytes = 1_000_000
|
|
114
|
-
dvc_folders = ["data", "figures"]
|
|
115
|
-
if "." in paths:
|
|
116
|
-
print("ERROR: Cannot add '.' with calkit; use git or dvc")
|
|
117
|
-
sys.exit(1)
|
|
118
|
-
for path in paths:
|
|
119
|
-
if os.path.isdir(path):
|
|
120
|
-
print("ERROR: Cannot add directories with calkit; use git or dvc")
|
|
121
|
-
sys.exit(1)
|
|
122
|
-
# Detect if this file should be tracked with Git or DVC
|
|
123
|
-
# TODO: Add to whatever
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@app.command(name="commit")
|
|
127
|
-
def commit(
|
|
128
|
-
all: Annotated[Optional[bool], typer.Option("--all", "-a")] = False,
|
|
129
|
-
message: Annotated[Optional[str], typer.Option("--message", "-m")] = None,
|
|
130
|
-
):
|
|
131
|
-
"""Commit a change to the repo."""
|
|
132
|
-
print(all, message)
|
|
133
|
-
raise NotImplementedError
|
|
134
|
-
|
|
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
|
-
|
|
152
|
-
@new_app.command(name="figure")
|
|
153
|
-
def new_figure(
|
|
154
|
-
path: str,
|
|
155
|
-
title: Annotated[str, typer.Option("--title")],
|
|
156
|
-
description: Annotated[str, typer.Option("--desc")] = None,
|
|
157
|
-
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
158
|
-
):
|
|
159
|
-
"""Add a new figure."""
|
|
160
|
-
if os.path.isfile("calkit.yaml"):
|
|
161
|
-
with open("calkit.yaml") as f:
|
|
162
|
-
ck_info = ryaml.load(f)
|
|
163
|
-
else:
|
|
164
|
-
ck_info = {}
|
|
165
|
-
figures = ck_info.get("figures", [])
|
|
166
|
-
paths = [f.get("path") for f in figures]
|
|
167
|
-
if path in paths:
|
|
168
|
-
raise ValueError(f"Figure at path {path} already exists")
|
|
169
|
-
obj = dict(path=path, title=title)
|
|
170
|
-
if description is not None:
|
|
171
|
-
obj["description"] = description
|
|
172
|
-
figures.append(obj)
|
|
173
|
-
ck_info["figures"] = figures
|
|
174
|
-
with open("calkit.yaml", "w") as f:
|
|
175
|
-
ryaml.dump(ck_info, f)
|
|
176
|
-
if commit:
|
|
177
|
-
repo = git.Repo()
|
|
178
|
-
repo.git.add("calkit.yaml")
|
|
179
|
-
repo.git.commit(["-m", f"Add figure {path}"])
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@new_app.command("question")
|
|
183
|
-
def new_question(
|
|
184
|
-
question: str,
|
|
185
|
-
commit: Annotated[bool, typer.Option("--commit")] = False,
|
|
186
|
-
):
|
|
187
|
-
"""Add a new question."""
|
|
188
|
-
if os.path.isfile("calkit.yaml"):
|
|
189
|
-
with open("calkit.yaml") as f:
|
|
190
|
-
ck_info = ryaml.load(f)
|
|
191
|
-
else:
|
|
192
|
-
ck_info = {}
|
|
193
|
-
questions = ck_info.get("questions", [])
|
|
194
|
-
if question in questions:
|
|
195
|
-
raise ValueError("Question already exists")
|
|
196
|
-
if not question.endswith("?"):
|
|
197
|
-
raise ValueError("Questions must end with a question mark")
|
|
198
|
-
questions.append(question)
|
|
199
|
-
ck_info["questions"] = questions
|
|
200
|
-
with open("calkit.yaml", "w") as f:
|
|
201
|
-
ryaml.dump(ck_info, f)
|
|
202
|
-
if commit:
|
|
203
|
-
repo = git.Repo()
|
|
204
|
-
repo.git.add("calkit.yaml")
|
|
205
|
-
repo.git.commit(["-m", "Add question"])
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
274
|
-
path: str,
|
|
275
|
-
to: Annotated[
|
|
276
|
-
str, typer.Option("--to", help="Output format ('html' or 'notebook').")
|
|
277
|
-
] = "notebook",
|
|
278
|
-
):
|
|
279
|
-
"""Execute notebook and place a copy in the relevant directory.
|
|
280
|
-
|
|
281
|
-
This can be useful to use as a preprocessing DVC stage to use a clean
|
|
282
|
-
notebook as a dependency for a stage that caches and executed notebook.
|
|
283
|
-
"""
|
|
284
|
-
if os.path.isabs(path):
|
|
285
|
-
raise ValueError("Path must be relative")
|
|
286
|
-
if to == "html":
|
|
287
|
-
subdir = "html"
|
|
288
|
-
fname_out = path.removesuffix(".ipynb") + ".html"
|
|
289
|
-
elif to == "notebook":
|
|
290
|
-
subdir = "executed"
|
|
291
|
-
fname_out = path
|
|
292
|
-
else:
|
|
293
|
-
raise ValueError(f"Invalid output format: '{to}'")
|
|
294
|
-
fpath_out = os.path.join(".calkit", "notebooks", subdir, fname_out)
|
|
295
|
-
folder = os.path.dirname(fpath_out)
|
|
296
|
-
os.makedirs(folder, exist_ok=True)
|
|
297
|
-
subprocess.call(
|
|
298
|
-
[
|
|
299
|
-
"jupyter",
|
|
300
|
-
"nbconvert",
|
|
301
|
-
path,
|
|
302
|
-
"--execute",
|
|
303
|
-
"--to",
|
|
304
|
-
to,
|
|
305
|
-
"--output",
|
|
306
|
-
fpath_out,
|
|
307
|
-
]
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
@app.command(name="server", help="Run the local server.")
|
|
312
|
-
def run_server():
|
|
313
|
-
import uvicorn
|
|
314
|
-
|
|
315
|
-
uvicorn.run(
|
|
316
|
-
"calkit.server:app",
|
|
317
|
-
port=8866,
|
|
318
|
-
host="localhost",
|
|
319
|
-
reload=True,
|
|
320
|
-
reload_dirs=[os.path.dirname(__file__)],
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
@app.command(
|
|
325
|
-
name="run",
|
|
326
|
-
add_help_option=False,
|
|
327
|
-
help="Run DVC pipeline (a wrapper for `dvc repro`).",
|
|
328
|
-
)
|
|
329
|
-
def run_dvc_repro(
|
|
330
|
-
targets: Optional[list[str]] = typer.Argument(default=None),
|
|
331
|
-
help: Annotated[bool, typer.Option("-h", "--help")] = False,
|
|
332
|
-
quiet: Annotated[bool, typer.Option("-q", "--quiet")] = False,
|
|
333
|
-
verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
|
|
334
|
-
force: Annotated[bool, typer.Option("-f", "--force")] = False,
|
|
335
|
-
interactive: Annotated[bool, typer.Option("-i", "--interactive")] = False,
|
|
336
|
-
single_item: Annotated[bool, typer.Option("-s", "--single-item")] = False,
|
|
337
|
-
pipeline: Annotated[
|
|
338
|
-
Optional[str], typer.Option("-p", "--pipeline")
|
|
339
|
-
] = None,
|
|
340
|
-
all_pipelines: Annotated[
|
|
341
|
-
bool, typer.Option("-P", "--all-pipelines")
|
|
342
|
-
] = False,
|
|
343
|
-
recursive: Annotated[bool, typer.Option("-R", "--recursive")] = False,
|
|
344
|
-
downstream: Annotated[
|
|
345
|
-
Optional[list[str]], typer.Option("--downstream")
|
|
346
|
-
] = None,
|
|
347
|
-
force_downstream: Annotated[
|
|
348
|
-
bool, typer.Option("--force-downstream")
|
|
349
|
-
] = False,
|
|
350
|
-
pull: Annotated[bool, typer.Option("--pull")] = False,
|
|
351
|
-
allow_missing: Annotated[bool, typer.Option("--allow-missing")] = False,
|
|
352
|
-
dry: Annotated[bool, typer.Option("--dry")] = False,
|
|
353
|
-
keep_going: Annotated[bool, typer.Option("--keep-going", "-k")] = False,
|
|
354
|
-
ignore_errors: Annotated[bool, typer.Option("--ignore-errors")] = False,
|
|
355
|
-
glob: Annotated[bool, typer.Option("--glob")] = False,
|
|
356
|
-
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
357
|
-
no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
|
|
358
|
-
):
|
|
359
|
-
"""A simple wrapper for ``dvc repro``."""
|
|
360
|
-
if targets is None:
|
|
361
|
-
targets = []
|
|
362
|
-
args = targets
|
|
363
|
-
# Extract any boolean args
|
|
364
|
-
for name in [
|
|
365
|
-
"help",
|
|
366
|
-
"quiet",
|
|
367
|
-
"verbose",
|
|
368
|
-
"force",
|
|
369
|
-
"interactive",
|
|
370
|
-
"single-item",
|
|
371
|
-
"all-pipelines",
|
|
372
|
-
"recursive",
|
|
373
|
-
"pull",
|
|
374
|
-
"allow-missing",
|
|
375
|
-
"dry",
|
|
376
|
-
"keep-going",
|
|
377
|
-
"force-downstream",
|
|
378
|
-
"glob",
|
|
379
|
-
"no-commit",
|
|
380
|
-
"no-run-cache",
|
|
381
|
-
]:
|
|
382
|
-
if locals()[name.replace("-", "_")]:
|
|
383
|
-
args.append("--" + name)
|
|
384
|
-
if pipeline is not None:
|
|
385
|
-
args += ["--pipeline", pipeline]
|
|
386
|
-
if downstream is not None:
|
|
387
|
-
args += downstream
|
|
388
|
-
subprocess.call(["dvc", "repro"] + args)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def run() -> None:
|
|
392
|
-
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|