calkit-python 0.0.9__tar.gz → 0.1.0__tar.gz

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.
Files changed (43) hide show
  1. {calkit_python-0.0.9 → calkit_python-0.1.0}/PKG-INFO +32 -1
  2. {calkit_python-0.0.9 → calkit_python-0.1.0}/README.md +31 -0
  3. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/__init__.py +2 -1
  4. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/core.py +2 -1
  5. calkit_python-0.1.0/calkit/cli/import_.py +122 -0
  6. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/list.py +9 -0
  7. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/main.py +132 -1
  8. calkit_python-0.1.0/calkit/cli/new.py +354 -0
  9. calkit_python-0.1.0/calkit/docker.py +50 -0
  10. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/dvc.py +25 -4
  11. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/models.py +26 -6
  12. calkit_python-0.1.0/calkit/tests/cli/__init__.py +0 -0
  13. calkit_python-0.1.0/calkit/tests/cli/test_list.py +8 -0
  14. calkit_python-0.1.0/calkit/tests/cli/test_main.py +91 -0
  15. calkit_python-0.1.0/calkit/tests/cli/test_new.py +128 -0
  16. calkit_python-0.0.9/calkit/cli/new.py +0 -96
  17. {calkit_python-0.0.9 → calkit_python-0.1.0}/.github/FUNDING.yml +0 -0
  18. {calkit_python-0.0.9 → calkit_python-0.1.0}/.github/workflows/publish-test.yml +0 -0
  19. {calkit_python-0.0.9 → calkit_python-0.1.0}/.github/workflows/publish.yml +0 -0
  20. {calkit_python-0.0.9 → calkit_python-0.1.0}/.gitignore +0 -0
  21. {calkit_python-0.0.9 → calkit_python-0.1.0}/LICENSE +0 -0
  22. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/__init__.py +0 -0
  23. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/config.py +0 -0
  24. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cli/notebooks.py +0 -0
  25. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/cloud.py +0 -0
  26. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/config.py +0 -0
  27. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/core.py +0 -0
  28. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/data.py +0 -0
  29. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/git.py +0 -0
  30. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/gui.py +0 -0
  31. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/jupyter.py +0 -0
  32. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/server.py +0 -0
  33. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/tests/__init__.py +0 -0
  34. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/tests/test_core.py +0 -0
  35. {calkit_python-0.0.9 → calkit_python-0.1.0}/calkit/tests/test_jupyter.py +0 -0
  36. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/cfd-study/README.md +0 -0
  37. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/cfd-study/calkit.yaml +0 -0
  38. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/cfd-study/config/simulations/runs.csv +0 -0
  39. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/cfd-study/notebook.ipynb +0 -0
  40. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/ms-office/.gitignore +0 -0
  41. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/ms-office/README.md +0 -0
  42. {calkit_python-0.0.9 → calkit_python-0.1.0}/examples/ms-office/calkit.yaml +0 -0
  43. {calkit_python-0.0.9 → calkit_python-0.1.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: calkit-python
3
- Version: 0.0.9
3
+ Version: 0.1.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
@@ -107,3 +107,34 @@ what files should be considered datasets, figures, publications, etc.
107
107
  The Calkit cloud reads this database and registers the various entities
108
108
  as part of the entire ecosystem such that if a project is made public,
109
109
  other researchers can find and reuse your work to accelerate their own.
110
+
111
+ ## Design/UX principles
112
+
113
+ 1. Be opinionated. Users should not be forced to make unimportant decisions.
114
+ However, if they disagree, they should have the ability to change the
115
+ default behavior. The most common use case should be default.
116
+ Commands that are commonly executed as groups should be combined, but
117
+ still available to be run individually if desired.
118
+ 1. Commits should ideally be made automatically as part of actions that make
119
+ changes to the project repo. For
120
+ example, if a new object is added via the CLI, a commit should be made
121
+ right then unless otherwise specified. This saves the trouble of running
122
+ multiple commands and encourages atomic commits.
123
+ 1. Pushes should require explicit input from the user.
124
+ It is still TBD whether or not a pull should automatically be
125
+ made, though in general we want to encourage trunk-based development, i.e.,
126
+ only working on a single branch. One exception might be for local
127
+ experimentation that has a high likelihood of failure, in which case a
128
+ branch can be a nice way to throw those changes away.
129
+ Multiple branches should probably not live in the cloud, however, except
130
+ for small, quickly merged pull requests.
131
+ 1. Idempotency is always a good thing. Unnecessary state is bad. For example,
132
+ we should not encourage caching pipeline outputs for operations that are
133
+ cheap. Caching should happen either for state that is valuable on its
134
+ own, like a figure, or for an intermediate result that is expensive to
135
+ generate.
136
+ 1. There should be the smallest number of
137
+ frequently used commands as possible, and they should require at little
138
+ memorization as possible to know how to execute, e.g., a user should be
139
+ able to keep running `calkit run` and that's all they really need to do
140
+ to make sure the project is up-to-date.
@@ -79,3 +79,34 @@ what files should be considered datasets, figures, publications, etc.
79
79
  The Calkit cloud reads this database and registers the various entities
80
80
  as part of the entire ecosystem such that if a project is made public,
81
81
  other researchers can find and reuse your work to accelerate their own.
82
+
83
+ ## Design/UX principles
84
+
85
+ 1. Be opinionated. Users should not be forced to make unimportant decisions.
86
+ However, if they disagree, they should have the ability to change the
87
+ default behavior. The most common use case should be default.
88
+ Commands that are commonly executed as groups should be combined, but
89
+ still available to be run individually if desired.
90
+ 1. Commits should ideally be made automatically as part of actions that make
91
+ changes to the project repo. For
92
+ example, if a new object is added via the CLI, a commit should be made
93
+ right then unless otherwise specified. This saves the trouble of running
94
+ multiple commands and encourages atomic commits.
95
+ 1. Pushes should require explicit input from the user.
96
+ It is still TBD whether or not a pull should automatically be
97
+ made, though in general we want to encourage trunk-based development, i.e.,
98
+ only working on a single branch. One exception might be for local
99
+ experimentation that has a high likelihood of failure, in which case a
100
+ branch can be a nice way to throw those changes away.
101
+ Multiple branches should probably not live in the cloud, however, except
102
+ for small, quickly merged pull requests.
103
+ 1. Idempotency is always a good thing. Unnecessary state is bad. For example,
104
+ we should not encourage caching pipeline outputs for operations that are
105
+ cheap. Caching should happen either for state that is valuable on its
106
+ own, like a figure, or for an intermediate result that is expensive to
107
+ generate.
108
+ 1. There should be the smallest number of
109
+ frequently used commands as possible, and they should require at little
110
+ memorization as possible to know how to execute, e.g., a user should be
111
+ able to keep running `calkit run` and that's all they really need to do
112
+ to make sure the project is up-to-date.
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.9"
1
+ __version__ = "0.1.0"
2
2
 
3
3
  from .core import *
4
4
  from . import git
@@ -6,3 +6,4 @@ from . import dvc
6
6
  from . import cloud
7
7
  from . import jupyter
8
8
  from . import config
9
+ from . import models
@@ -1,7 +1,6 @@
1
1
  """Core CLI functionality."""
2
2
 
3
3
  import os
4
- import pty
5
4
  import subprocess
6
5
 
7
6
  import typer
@@ -19,4 +18,6 @@ def run_cmd(cmd: list[str]):
19
18
  if os.name == "nt":
20
19
  subprocess.call(cmd)
21
20
  else:
21
+ import pty
22
+
22
23
  pty.spawn(cmd, lambda fd: os.read(fd, 1024))
@@ -0,0 +1,122 @@
1
+ """CLI for importing objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from typing import Annotated
8
+
9
+ import git
10
+ import typer
11
+
12
+ import calkit
13
+
14
+ import_app = typer.Typer(no_args_is_help=True)
15
+
16
+
17
+ @import_app.command(name="dataset")
18
+ def import_dataset(
19
+ src_path: Annotated[
20
+ str,
21
+ typer.Argument(
22
+ help=(
23
+ "Location of dataset, including project owner and name, e.g., "
24
+ "someone/some-project/data/some-data.csv"
25
+ )
26
+ ),
27
+ ],
28
+ dest_path: Annotated[
29
+ str,
30
+ typer.Argument(help="Output path at which to save."),
31
+ ] = None,
32
+ overwrite: Annotated[
33
+ bool,
34
+ typer.Option(
35
+ "--overwrite",
36
+ "-f",
37
+ help="Force adding the dataset even if it already exists.",
38
+ ),
39
+ ] = False,
40
+ ):
41
+ """Import a dataset.
42
+
43
+ Currently only supports datasets kept in DVC, not Git.
44
+ """
45
+ # Ensure we don't already have a dataset at this path
46
+ path_split = src_path.split("/")
47
+ owner_name = path_split[0]
48
+ project_name = path_split[1]
49
+ path = "/".join(path_split[2:])
50
+ if dest_path is None:
51
+ dest_path = path
52
+ ck_info = calkit.load_calkit_info()
53
+ datasets = ck_info.get("datasets", [])
54
+ ds_paths = [ds["path"] for ds in datasets]
55
+ if not overwrite and dest_path in ds_paths:
56
+ raise ValueError(
57
+ "A dataset already exists in this project at this path"
58
+ )
59
+ elif overwrite and dest_path in ds_paths:
60
+ datasets = [ds for ds in datasets if ds["path"] != dest_path]
61
+ repo = git.Repo()
62
+ # Obtain, save, and commit the .dvc file for the dataset
63
+ typer.echo("Fetching import info")
64
+ resp = calkit.cloud.get(
65
+ f"/projects/{owner_name}/{project_name}/datasets/{path}"
66
+ )
67
+ if not "dvc_import" in resp:
68
+ raise ValueError("This file is not available to import with DVC")
69
+ dvc_fpath = dest_path + ".dvc"
70
+ dvc_dir = os.path.dirname(dvc_fpath)
71
+ os.makedirs(dvc_dir, exist_ok=True)
72
+ # Update path in .dvc file if necessary
73
+ dvc_import = resp["dvc_import"]
74
+ dvc_import["outs"][0]["path"] = os.path.basename(dest_path)
75
+ typer.echo("Saving .dvc file")
76
+ with open(dvc_fpath, "w") as f:
77
+ calkit.ryaml.dump(dvc_import, f)
78
+ repo.git.add(dvc_fpath)
79
+ # Ensure we have a DVC remote corresponding to this project, and that we
80
+ # have a token set for that remote
81
+ typer.echo("Adding new DVC remote")
82
+ calkit.dvc.add_external_remote(
83
+ owner_name=owner_name, project_name=project_name
84
+ )
85
+ repo.git.add(".dvc/config")
86
+ # Add to .gitignore
87
+ typer.echo("Checking .gitignore")
88
+ if os.path.isfile(".gitignore"):
89
+ with open(".gitignore") as f:
90
+ gitignore = f.read()
91
+ else:
92
+ gitignore = ""
93
+ if dest_path not in gitignore.split("\n"):
94
+ typer.echo(f"Adding {dest_path} to .gitignore")
95
+ gitignore = gitignore.rstrip() + "\n" + dest_path + "\n"
96
+ with open(".gitignore", "w") as f:
97
+ f.write(gitignore)
98
+ repo.git.add(".gitignore")
99
+ # Add to datasets in calkit.yaml
100
+ typer.echo("Adding dataset to calkit.yaml")
101
+ new_ds = calkit.models.ImportedDataset(
102
+ path=dest_path,
103
+ title=resp.get("title"),
104
+ description=resp.get("description"),
105
+ stage=None,
106
+ imported_from=calkit.models._ImportedFromProject(
107
+ project=f"{owner_name}/{project_name}",
108
+ path=path,
109
+ git_rev=None, # TODO?
110
+ ),
111
+ )
112
+ datasets.append(new_ds.model_dump())
113
+ ck_info["datasets"] = datasets
114
+ with open("calkit.yaml", "w") as f:
115
+ calkit.ryaml.dump(ck_info, f)
116
+ repo.git.add("calkit.yaml")
117
+ # Commit any necessary changes
118
+ typer.echo("Committing changes")
119
+ repo.git.commit(["-m", f"Import dataset {src_path}"])
120
+ # Run dvc pull
121
+ typer.echo("Running dvc pull")
122
+ subprocess.call(["dvc", "pull", dest_path])
@@ -68,3 +68,12 @@ def list_publications():
68
68
  @list_app.command(name="references")
69
69
  def list_references():
70
70
  _list_objects("references")
71
+
72
+
73
+ @list_app.command(name="environments")
74
+ def list_environments():
75
+ envs = calkit.load_calkit_info().get("environments", {})
76
+ for name, env in envs.items():
77
+ typer.echo(name + ":")
78
+ for k, v in env.items():
79
+ typer.echo(f" {k}: {v}")
@@ -4,13 +4,16 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import subprocess
7
+ import sys
7
8
 
9
+ import git
8
10
  import typer
9
11
  from typing_extensions import Annotated, Optional
10
12
 
11
13
  import calkit
12
14
  from calkit.cli import print_sep, run_cmd
13
15
  from calkit.cli.config import config_app
16
+ from calkit.cli.import_ import import_app
14
17
  from calkit.cli.list import list_app
15
18
  from calkit.cli.new import new_app
16
19
  from calkit.cli.notebooks import notebooks_app
@@ -27,6 +30,7 @@ app.add_typer(
27
30
  )
28
31
  app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
29
32
  app.add_typer(list_app, name="list", help="List Calkit objects.")
33
+ app.add_typer(import_app, name="import", help="Import objects.")
30
34
 
31
35
 
32
36
  @app.callback()
@@ -37,7 +41,7 @@ def main(
37
41
  ] = False,
38
42
  ):
39
43
  if version:
40
- typer.echo(calkit.__version__)
44
+ typer.echo(f"Calkit {calkit.__version__}")
41
45
  raise typer.Exit()
42
46
 
43
47
 
@@ -119,8 +123,16 @@ def add(
119
123
  if os.path.isdir(path):
120
124
  typer.echo("Cannot auto-add directories; use git or dvc")
121
125
  raise typer.Exit(1)
126
+ repo = git.Repo()
122
127
  for path in paths:
123
128
  # Detect if this file should be tracked with Git or DVC
129
+ # First see if it's in Git
130
+ if repo.git.ls_files(path):
131
+ typer.echo(
132
+ f"Adding {path} to Git since it's already in the repo"
133
+ )
134
+ subprocess.call(["git", "add", path])
135
+ continue
124
136
  if os.path.splitext(path)[-1] in dvc_extensions:
125
137
  typer.echo(f"Adding {path} to DVC per its extension")
126
138
  subprocess.call(["dvc", "add", path])
@@ -168,6 +180,50 @@ def commit(
168
180
  push()
169
181
 
170
182
 
183
+ @app.command(name="save")
184
+ def save(
185
+ paths: Annotated[
186
+ Optional[list[str]],
187
+ typer.Argument(
188
+ help=(
189
+ "Paths to add and commit. If not provided, will default to "
190
+ "any changed files that have been added previously."
191
+ ),
192
+ ),
193
+ ] = None,
194
+ all: Annotated[
195
+ Optional[bool],
196
+ typer.Option(
197
+ "--all", "-a", help="Automatically stage all changed files."
198
+ ),
199
+ ] = False,
200
+ message: Annotated[
201
+ Optional[str], typer.Option("--message", "-m", help="Commit message.")
202
+ ] = None,
203
+ to: Annotated[
204
+ str,
205
+ typer.Option(
206
+ "--to", "-t", help="System with which to add (git or dvc)."
207
+ ),
208
+ ] = None,
209
+ no_push: Annotated[
210
+ bool,
211
+ typer.Option(
212
+ "--no-push", help="Do not push to Git and DVC after committing."
213
+ ),
214
+ ] = False,
215
+ ):
216
+ """Save paths by committing and pushing.
217
+
218
+ This is essentially git/dvc add, commit, and push in one step.
219
+ """
220
+ if paths is not None:
221
+ add(paths, to=to)
222
+ commit(all=True if paths is None else False, message=message)
223
+ if not no_push:
224
+ push()
225
+
226
+
171
227
  @app.command(name="pull", help="Pull with both Git and DVC.")
172
228
  def pull():
173
229
  typer.echo("Git pulling")
@@ -354,3 +410,78 @@ def manual_step(
354
410
  )
355
411
  input(message + " (press enter to confirm): ")
356
412
  typer.echo("Done")
413
+
414
+
415
+ @app.command(
416
+ name="run-env",
417
+ help="Run a command in an environment.",
418
+ context_settings={"ignore_unknown_options": True},
419
+ )
420
+ def run_in_env(
421
+ cmd: Annotated[
422
+ list[str], typer.Argument(help="Command to run in the environment.")
423
+ ],
424
+ env_name: Annotated[
425
+ str,
426
+ typer.Option(
427
+ "--name",
428
+ "-n",
429
+ help=(
430
+ "Environment name in which to run. "
431
+ "Only necessary if there are multiple in this project."
432
+ ),
433
+ ),
434
+ ] = None,
435
+ verbose: Annotated[
436
+ bool, typer.Option("--verbose", "-v", help="Print verbose output.")
437
+ ] = False,
438
+ ):
439
+ ck_info = calkit.load_calkit_info()
440
+ envs = ck_info.get("environments", {})
441
+ if not envs:
442
+ typer.echo("No environments defined in calkit.yaml", err=True)
443
+ raise typer.Exit(1)
444
+ if isinstance(envs, list):
445
+ typer.echo(
446
+ "Error: Environments should be a dict, not a list", err=True
447
+ )
448
+ raise typer.Exit(1)
449
+ if len(envs) > 1 and env_name is None:
450
+ typer.echo(
451
+ "Environment must be specified if there are multiple",
452
+ err=True,
453
+ )
454
+ raise typer.Exit(1)
455
+ if env_name is None:
456
+ env_name = list(envs.keys())[0]
457
+ env = envs[env_name]
458
+ cwd = os.getcwd()
459
+ image_name = env.get("image", env_name)
460
+ wdir = env.get("wdir", "/work")
461
+ if env["kind"] == "docker":
462
+ cmd = " ".join(cmd)
463
+ cmd = [
464
+ "docker",
465
+ "run",
466
+ "-it" if sys.stdin.isatty() else "-i",
467
+ "--rm",
468
+ "-w",
469
+ wdir,
470
+ "-v",
471
+ f"{cwd}:{wdir}",
472
+ image_name,
473
+ "bash",
474
+ "-c",
475
+ f"{cmd}",
476
+ ]
477
+ if verbose:
478
+ typer.echo(f"Running command: {cmd}")
479
+ subprocess.call(cmd)
480
+ elif env["kind"] == "conda":
481
+ cmd = ["conda", "run", "-n", env_name] + cmd
482
+ if verbose:
483
+ typer.echo(f"Running command: {cmd}")
484
+ subprocess.call(cmd)
485
+ else:
486
+ typer.echo("Environment kind not supported", err=True)
487
+ raise typer.Exit(1)