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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.3"
1
+ __version__ = "0.0.5"
2
2
 
3
3
  from .core import *
4
4
  from . import git
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
- config_app = typer.Typer()
21
- new_app = typer.Typer()
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(new_app, name="new", help="Add new Calkit object.")
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
- print(f"------------ {name} ------------")
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
- print_sep("Code")
66
- if os.name == "nt":
67
- subprocess.call(["git", "status"])
68
- print()
69
- print_sep("data")
70
- subprocess.call(["dvc", "status"])
71
- else:
72
- pty.spawn(["git", "status"], lambda fd: os.read(fd, 1024))
73
- print()
74
- print_sep("Data")
75
- pty.spawn(["dvc", "status"], lambda fd: os.read(fd, 1024))
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 to ``calkit.yaml``."""
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
- @app.command(name="server")
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
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) makes reproducible research easy,
31
+ [Calkit](https://calkit.io) simplifies reproducibility,
31
32
  acting as a layer on top of
32
- [Git](https://git-scm.com/) and [DVC](https://dvc.org/), such that all
33
- all materials involved in the research process can be fully described in a
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=WCAC5nhZD6Wzwgm3VMpKFCm3aGvtYoHqy3pt7kIgI9k,142
2
- calkit/cli.py,sha256=DpYo_EKLBbLZfbFFihGo3L3uXlKURWwv_QUOgSkCUoo,4901
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=TYllDbTutcODMUCLK2oCKivbJrtjwDu9t-5GyON9dq0,1339
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.3.dist-info/METADATA,sha256=NDjQb1WNaMJcEx-s4RTJyB-hNkGS3O7xy2Q6eCCn67Q,2449
17
- calkit_python-0.0.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
- calkit_python-0.0.3.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
19
- calkit_python-0.0.3.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
20
- calkit_python-0.0.3.dist-info/RECORD,,
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,,