calkit-python 0.0.2__py3-none-any.whl → 0.0.4__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.2"
1
+ __version__ = "0.0.4"
2
2
 
3
3
  from .core import *
4
4
  from . import git
calkit/cli.py CHANGED
@@ -7,17 +7,20 @@ import pty
7
7
  import subprocess
8
8
  import sys
9
9
 
10
+ import git
10
11
  import typer
11
12
  from typing_extensions import Annotated, Optional
12
13
 
13
- import calkit
14
+ from calkit.core import ryaml
14
15
 
15
16
  from . import config
16
17
  from .dvc import configure_remote, set_remote_auth
17
18
 
18
19
  app = typer.Typer()
19
20
  config_app = typer.Typer()
21
+ new_app = typer.Typer()
20
22
  app.add_typer(config_app, name="config", help="Configure Calkit.")
23
+ app.add_typer(new_app, name="new", help="Add new Calkit object.")
21
24
 
22
25
 
23
26
  @config_app.command(name="set")
@@ -57,19 +60,26 @@ def get_status():
57
60
  """Get a unified Git and DVC status."""
58
61
 
59
62
  def print_sep(name: str):
60
- print(f"------------ {name} ------------")
63
+ width = 66
64
+ txt_width = len(name) + 2
65
+ buffer_width = (width - txt_width) // 2
66
+ buffer = "-" * buffer_width
67
+ typer.echo(f"{buffer} {name} {buffer}")
61
68
 
62
- print_sep("Code")
63
- if os.name == "nt":
64
- subprocess.call(["git", "status"])
65
- print()
66
- print_sep("data")
67
- subprocess.call(["dvc", "status"])
68
- else:
69
- pty.spawn(["git", "status"], lambda fd: os.read(fd, 1024))
70
- print()
71
- print_sep("Data")
72
- pty.spawn(["dvc", "status"], lambda fd: os.read(fd, 1024))
69
+ def run_cmd(cmd: list[str]):
70
+ if os.name == "nt":
71
+ subprocess.call(cmd)
72
+ else:
73
+ pty.spawn(cmd, lambda fd: os.read(fd, 1024))
74
+
75
+ print_sep("Code (Git)")
76
+ run_cmd(["git", "status"])
77
+ typer.echo()
78
+ print_sep("Data (DVC)")
79
+ run_cmd(["dvc", "data", "status"])
80
+ typer.echo()
81
+ print_sep("Pipeline (DVC)")
82
+ run_cmd(["dvc", "status"])
73
83
 
74
84
 
75
85
  @app.command(name="add")
@@ -102,7 +112,78 @@ def commit(
102
112
  raise NotImplementedError
103
113
 
104
114
 
105
- @app.command(name="server")
115
+ @app.command(name="pull", help="Pull with both Git and DVC.")
116
+ def pull():
117
+ typer.echo("Git pulling")
118
+ subprocess.call(["git", "pull"])
119
+ typer.echo("DVC pulling")
120
+ subprocess.call(["dvc", "pull"])
121
+
122
+
123
+ @app.command(name="push", help="Push with both Git and DVC.")
124
+ def push():
125
+ typer.echo("Pushing to Git remote")
126
+ subprocess.call(["git", "push"])
127
+ typer.echo("Pushing to DVC remote")
128
+ subprocess.call(["dvc", "push"])
129
+
130
+
131
+ @new_app.command(name="figure")
132
+ def new_figure(
133
+ path: str,
134
+ title: Annotated[str, typer.Option("--title")],
135
+ description: Annotated[str, typer.Option("--desc")] = None,
136
+ commit: Annotated[bool, typer.Option("--commit")] = False,
137
+ ):
138
+ """Add a new figure to ``calkit.yaml``."""
139
+ if os.path.isfile("calkit.yaml"):
140
+ with open("calkit.yaml") as f:
141
+ ck_info = ryaml.load(f)
142
+ else:
143
+ ck_info = {}
144
+ figures = ck_info.get("figures", [])
145
+ paths = [f.get("path") for f in figures]
146
+ if path in paths:
147
+ raise ValueError(f"Figure at path {path} already exists")
148
+ obj = dict(path=path, title=title)
149
+ if description is not None:
150
+ obj["description"] = description
151
+ figures.append(obj)
152
+ ck_info["figures"] = figures
153
+ with open("calkit.yaml", "w") as f:
154
+ ryaml.dump(ck_info, f)
155
+ if commit:
156
+ repo = git.Repo()
157
+ repo.git.add("calkit.yaml")
158
+ repo.git.commit(["-m", f"Add figure {path}"])
159
+
160
+
161
+ @new_app.command("question")
162
+ def new_question(
163
+ question: str,
164
+ commit: Annotated[bool, typer.Option("--commit")] = False,
165
+ ):
166
+ if os.path.isfile("calkit.yaml"):
167
+ with open("calkit.yaml") as f:
168
+ ck_info = ryaml.load(f)
169
+ else:
170
+ ck_info = {}
171
+ questions = ck_info.get("questions", [])
172
+ if question in questions:
173
+ raise ValueError("Question already exists")
174
+ if not question.endswith("?"):
175
+ raise ValueError("Questions must end with a question mark")
176
+ questions.append(question)
177
+ ck_info["questions"] = questions
178
+ with open("calkit.yaml", "w") as f:
179
+ ryaml.dump(ck_info, f)
180
+ if commit:
181
+ repo = git.Repo()
182
+ repo.git.add("calkit.yaml")
183
+ repo.git.commit(["-m", "Add question"])
184
+
185
+
186
+ @app.command(name="server", help="Run the local server.")
106
187
  def run_server():
107
188
  import uvicorn
108
189
 
@@ -115,5 +196,72 @@ def run_server():
115
196
  )
116
197
 
117
198
 
199
+ @app.command(
200
+ name="run",
201
+ add_help_option=False,
202
+ help="Run DVC pipeline (a wrapper for `dvc repro`).",
203
+ )
204
+ def run_dvc_repro(
205
+ targets: Optional[list[str]] = typer.Argument(default=None),
206
+ help: Annotated[bool, typer.Option("-h", "--help")] = False,
207
+ quiet: Annotated[bool, typer.Option("-q", "--quiet")] = False,
208
+ verbose: Annotated[bool, typer.Option("-v", "--verbose")] = False,
209
+ force: Annotated[bool, typer.Option("-f", "--force")] = False,
210
+ interactive: Annotated[bool, typer.Option("-i", "--interactive")] = False,
211
+ single_item: Annotated[bool, typer.Option("-s", "--single-item")] = False,
212
+ pipeline: Annotated[
213
+ Optional[str], typer.Option("-p", "--pipeline")
214
+ ] = None,
215
+ all_pipelines: Annotated[
216
+ bool, typer.Option("-P", "--all-pipelines")
217
+ ] = False,
218
+ recursive: Annotated[bool, typer.Option("-R", "--recursive")] = False,
219
+ downstream: Annotated[
220
+ Optional[list[str]], typer.Option("--downstream")
221
+ ] = None,
222
+ force_downstream: Annotated[
223
+ bool, typer.Option("--force-downstream")
224
+ ] = False,
225
+ pull: Annotated[bool, typer.Option("--pull")] = False,
226
+ allow_missing: Annotated[bool, typer.Option("--allow-missing")] = False,
227
+ dry: Annotated[bool, typer.Option("--dry")] = False,
228
+ keep_going: Annotated[bool, typer.Option("--keep-going", "-k")] = False,
229
+ ignore_errors: Annotated[bool, typer.Option("--ignore-errors")] = False,
230
+ glob: Annotated[bool, typer.Option("--glob")] = False,
231
+ no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
232
+ no_run_cache: Annotated[bool, typer.Option("--no-run-cache")] = False,
233
+ ):
234
+ """A simple wrapper for ``dvc repro``."""
235
+ if targets is None:
236
+ targets = []
237
+ args = targets
238
+ # Extract any boolean args
239
+ for name in [
240
+ "help",
241
+ "quiet",
242
+ "verbose",
243
+ "force",
244
+ "interactive",
245
+ "single-item",
246
+ "all-pipelines",
247
+ "recursive",
248
+ "pull",
249
+ "allow-missing",
250
+ "dry",
251
+ "keep-going",
252
+ "force-downstream",
253
+ "glob",
254
+ "no-commit",
255
+ "no-run-cache",
256
+ ]:
257
+ if locals()[name.replace("-", "_")]:
258
+ args.append("--" + name)
259
+ if pipeline is not None:
260
+ args += ["--pipeline", pipeline]
261
+ if downstream is not None:
262
+ args += downstream
263
+ subprocess.call(["dvc", "repro"] + args)
264
+
265
+
118
266
  def run() -> None:
119
267
  app()
calkit/core.py CHANGED
@@ -3,11 +3,21 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import glob
6
+ import logging
6
7
  import os
7
8
 
9
+ import ruamel.yaml
8
10
  from git import Repo
9
11
  from git.exc import InvalidGitRepositoryError
10
12
 
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__package__)
15
+
16
+ ryaml = ruamel.yaml.YAML()
17
+ ryaml.indent(mapping=2, sequence=4, offset=2)
18
+ ryaml.preserve_quotes = True
19
+ ryaml.width = 70
20
+
11
21
 
12
22
  def find_project_dirs(relative=False, max_depth=3) -> list[str]:
13
23
  """Find all Calkit project directories."""
calkit/data.py CHANGED
@@ -1,4 +1,9 @@
1
- """Functionality for working with datasets."""
1
+ """Functionality for working with datasets.
2
+
3
+ Since the dependencies here are optional, we need to ensure this isn't imported
4
+ by default, or otherwise ensure ``import calkit`` works when the data
5
+ dependencies are not installed.
6
+ """
2
7
 
3
8
  from __future__ import annotations
4
9
 
calkit/models.py ADDED
@@ -0,0 +1,81 @@
1
+ """Data models."""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class _ImportedFrom(BaseModel):
9
+ project: str
10
+ path: str = None
11
+
12
+
13
+ class _CalkitObject(BaseModel):
14
+ path: str
15
+ title: str
16
+ description: str
17
+ stage: str | None = None
18
+
19
+
20
+ class Dataset(_CalkitObject):
21
+ pass
22
+
23
+
24
+ class Figure(_CalkitObject):
25
+ pass
26
+
27
+
28
+ class Publication(_CalkitObject):
29
+ kind: Literal[
30
+ "journal-article",
31
+ "conference-paper",
32
+ "presentation",
33
+ "poster",
34
+ "report",
35
+ "blog",
36
+ ]
37
+ is_published: bool = False
38
+ doi: str = None
39
+
40
+
41
+ class ReferenceFile(BaseModel):
42
+ path: str
43
+ key: str
44
+
45
+
46
+ class ReferenceCollection(BaseModel):
47
+ path: str
48
+ files: list[ReferenceFile] = []
49
+
50
+
51
+ class Environment(BaseModel):
52
+ name: str
53
+ kind: Literal["conda", "docker", "pip", "poetry", "npm", "yarn"]
54
+ path: str
55
+
56
+
57
+ class Software(BaseModel):
58
+ title: str
59
+ path: str
60
+ description: str
61
+
62
+
63
+ class Notebook(_CalkitObject):
64
+ """A Jupyter notebook."""
65
+
66
+ pass
67
+
68
+
69
+ class ProjectInfo(BaseModel):
70
+ """All of the project's information or metadata, written to the
71
+ ``calkit.yaml`` file.
72
+ """
73
+
74
+ questions: list[str] = []
75
+ datasets: list[Dataset] = []
76
+ figures: list[Figure] = []
77
+ publications: list[Publication] = []
78
+ references: list[ReferenceCollection] = []
79
+ environments: list[Environment] = []
80
+ software: list[Software] = []
81
+ notebooks: list[Notebook] = []
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: calkit-python
3
- Version: 0.0.2
4
- Summary: Reproducibility made easy.
3
+ Version: 0.0.4
4
+ Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://github.com/calkit/calkit
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
7
7
  Author-email: Pete Bachant <petebachant@gmail.com>
@@ -15,23 +15,56 @@ Requires-Dist: eval-type-backport; python_version < '3.10'
15
15
  Requires-Dist: fastapi
16
16
  Requires-Dist: gitpython
17
17
  Requires-Dist: keyring
18
- Requires-Dist: pandas
19
- Requires-Dist: polars
18
+ Requires-Dist: nbconvert
20
19
  Requires-Dist: pydantic-settings
21
20
  Requires-Dist: pydantic[email]
22
21
  Requires-Dist: pyjwt
23
22
  Requires-Dist: requests
24
23
  Requires-Dist: typer
24
+ Provides-Extra: data
25
+ Requires-Dist: pandas; extra == 'data'
26
+ Requires-Dist: polars; extra == 'data'
25
27
  Description-Content-Type: text/markdown
26
28
 
27
29
  # Calkit
28
30
 
29
- [Calkit](https://calkit.io) makes reproducible research easy,
31
+ [Calkit](https://calkit.io) simplifies reproducibility,
30
32
  acting as a layer on top of
31
- [Git](https://git-scm.com/) and [DVC](https://dvc.org/), such that all
32
- 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
33
36
  single repository.
34
37
 
38
+ ## Why does reproducibility matter?
39
+
40
+ If your work is reproducible, that means that someone else can "run" it and
41
+ get the same results or outputs.
42
+ This is a major step towards addressing
43
+ [the replication crisis](https://en.wikipedia.org/wiki/Replication_crisis)
44
+ and has some major benefits for both you as an individual and the research
45
+ community:
46
+
47
+ 1. You will avoid mistakes caused by, e.g., running an old version of a script
48
+ and including a figure that wasn't created after fixing a bug in the data
49
+ processing pipeline.
50
+ 2. Since your project is "runnable," it's more likely that someone else will be
51
+ able to reuse part of your work to run it in a different context, thereby
52
+ producing a bigger impact and accelerating the pace of discovery.
53
+ If someone can take what you've done and use it to calculate a
54
+ prediction, you have just produced truly useful knowledge.
55
+
56
+ ## Why another tool/platform?
57
+
58
+ Git, GitHub, DVC, Zenodo et al. are amazing tools/platforms, but their
59
+ use involves multiple fairly difficult learning curves.
60
+ Our goal is to provide a single tool and platform to unify all of these so
61
+ that there is a single, gentle learning curve.
62
+ However, it is not our goal to hide or replace these underlying components.
63
+ Advanced users can use them directly, but new users aren't forced to, which
64
+ helps them get up and running with less effort and training.
65
+ Calkit should help users understand what is going on under the hood without
66
+ forcing them to work at that lower level of abstraction.
67
+
35
68
  ## Installation
36
69
 
37
70
  Simply run
@@ -1,19 +1,20 @@
1
- calkit/__init__.py,sha256=5ZyOW3PHimYwnmImfuA_Yqh2Vh1daNTXojMUlAPXeog,142
2
- calkit/cli.py,sha256=biHONCwIlYFp8Gcj8kl_XPafD0F8jnKQ6m_LUyk07VA,3029
1
+ calkit/__init__.py,sha256=ZhTSlSvcKFHVv921t4q9zBBPNWHaT4h43s3G26KldNo,142
2
+ calkit/cli.py,sha256=FkZGfQNSjWKRjWDX10GMDTh3NM9E7cW9DJch2h_F75Q,7930
3
3
  calkit/cloud.py,sha256=CnQKms4mBQo4X8jVLIa-70-3jPDl38zRNFyK5TjiaHM,2209
4
4
  calkit/config.py,sha256=NPFRk5inn66Wy44pyx8WppyqvIzkW1sO6VjXwIcCacE,2044
5
- calkit/core.py,sha256=fB5HAGJg9yDqJESFisFyKImvMB8wO2CpurGg6hfr7qE,990
6
- calkit/data.py,sha256=DqgrvRj9eI8eidmHZFC-UYHmWc-xmBb_GFYDJP1R0DI,1447
5
+ calkit/core.py,sha256=qLNjBwlECzMitL85pMGIHzne4j7FpQAR9-ukYtdIMig,1225
6
+ calkit/data.py,sha256=DZDkj-BLFWXz56R-A9XZeSKSgXpm3aKtiqD6JstfmnY,1631
7
7
  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=ejl032XIrbaCFkO-4VbCaKVQyHqNl7N4WWU20RjeAfc,1447
11
12
  calkit/server.py,sha256=gq3643ABKTQu1eVR_OqJ-SdQh2dPLHSFsipmmAK5ttA,5959
12
13
  calkit/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  calkit/tests/test_core.py,sha256=CdRqvYnq3MidyTknvfIbkmmhMfydpyKtTW9hakAvzsc,167
14
15
  calkit/tests/test_jupyter.py,sha256=YTL6zI740UM2KUjskSipzvSKxsyQ8rVPFQIX5cb2tMQ,114
15
- calkit_python-0.0.2.dist-info/METADATA,sha256=QaKWH12fJ1QZ04UT7bgBC8VOW5oVYzsuDk8l8uKgUKs,2393
16
- calkit_python-0.0.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
17
- calkit_python-0.0.2.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
18
- calkit_python-0.0.2.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
19
- calkit_python-0.0.2.dist-info/RECORD,,
16
+ calkit_python-0.0.4.dist-info/METADATA,sha256=yAlvKleBuX6xFXFH3u7UHZVJILjlC-RIOSeQEXcNx8A,4004
17
+ calkit_python-0.0.4.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
+ calkit_python-0.0.4.dist-info/entry_points.txt,sha256=tiPfbDDZNB-YStw5YVO_4Gww0W3Dcdy8Mh6muX_GWnY,42
19
+ calkit_python-0.0.4.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
20
+ calkit_python-0.0.4.dist-info/RECORD,,