calkit-python 0.1.1__tar.gz → 0.2.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.1.1 → calkit_python-0.2.0}/PKG-INFO +1 -1
  2. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/__init__.py +1 -1
  3. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/core.py +5 -0
  4. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/main.py +77 -33
  5. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/new.py +12 -25
  6. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/dvc.py +15 -3
  7. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/models.py +17 -0
  8. calkit_python-0.2.0/calkit/tests/test_dvc.py +15 -0
  9. {calkit_python-0.1.1 → calkit_python-0.2.0}/.github/FUNDING.yml +0 -0
  10. {calkit_python-0.1.1 → calkit_python-0.2.0}/.github/workflows/publish-test.yml +0 -0
  11. {calkit_python-0.1.1 → calkit_python-0.2.0}/.github/workflows/publish.yml +0 -0
  12. {calkit_python-0.1.1 → calkit_python-0.2.0}/.gitignore +0 -0
  13. {calkit_python-0.1.1 → calkit_python-0.2.0}/LICENSE +0 -0
  14. {calkit_python-0.1.1 → calkit_python-0.2.0}/README.md +0 -0
  15. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/__init__.py +0 -0
  16. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/config.py +0 -0
  17. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/import_.py +0 -0
  18. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/list.py +0 -0
  19. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cli/notebooks.py +0 -0
  20. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/cloud.py +0 -0
  21. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/config.py +0 -0
  22. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/core.py +0 -0
  23. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/data.py +0 -0
  24. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/docker.py +0 -0
  25. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/git.py +0 -0
  26. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/gui.py +0 -0
  27. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/jupyter.py +0 -0
  28. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/server.py +0 -0
  29. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/__init__.py +0 -0
  30. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/cli/__init__.py +0 -0
  31. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/cli/test_list.py +0 -0
  32. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/cli/test_main.py +0 -0
  33. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/cli/test_new.py +0 -0
  34. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/test_core.py +0 -0
  35. {calkit_python-0.1.1 → calkit_python-0.2.0}/calkit/tests/test_jupyter.py +0 -0
  36. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/cfd-study/README.md +0 -0
  37. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/cfd-study/calkit.yaml +0 -0
  38. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/cfd-study/config/simulations/runs.csv +0 -0
  39. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/cfd-study/notebook.ipynb +0 -0
  40. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/ms-office/.gitignore +0 -0
  41. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/ms-office/README.md +0 -0
  42. {calkit_python-0.1.1 → calkit_python-0.2.0}/examples/ms-office/calkit.yaml +0 -0
  43. {calkit_python-0.1.1 → calkit_python-0.2.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: calkit-python
3
- Version: 0.1.1
3
+ Version: 0.2.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
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.1"
1
+ __version__ = "0.2.0"
2
2
 
3
3
  from .core import *
4
4
  from . import git
@@ -21,3 +21,8 @@ def run_cmd(cmd: list[str]):
21
21
  import pty
22
22
 
23
23
  pty.spawn(cmd, lambda fd: os.read(fd, 1024))
24
+
25
+
26
+ def raise_error(txt):
27
+ typer.echo(txt, err=txt)
28
+ raise typer.Exit(1)
@@ -12,7 +12,7 @@ import typer
12
12
  from typing_extensions import Annotated, Optional
13
13
 
14
14
  import calkit
15
- from calkit.cli import print_sep, run_cmd
15
+ from calkit.cli import print_sep, raise_error, run_cmd
16
16
  from calkit.cli.config import config_app
17
17
  from calkit.cli.import_ import import_app
18
18
  from calkit.cli.list import list_app
@@ -26,8 +26,11 @@ app = typer.Typer(
26
26
  pretty_exceptions_show_locals=False,
27
27
  )
28
28
  app.add_typer(config_app, name="config", help="Configure Calkit.")
29
+ app.add_typer(new_app, name="new", help="Create a new Calkit object.")
29
30
  app.add_typer(
30
- new_app, name="new", help="Add new Calkit object (to calkit.yaml)."
31
+ new_app,
32
+ name="create",
33
+ help="Create a new Calkit object (alias for 'new').",
31
34
  )
32
35
  app.add_typer(notebooks_app, name="nb", help="Work with Jupyter notebooks.")
33
36
  app.add_typer(list_app, name="list", help="List Calkit objects.")
@@ -46,6 +49,53 @@ def main(
46
49
  raise typer.Exit()
47
50
 
48
51
 
52
+ @app.command(name="clone")
53
+ def clone(
54
+ url: Annotated[str, typer.Argument(help="Repo URL.")],
55
+ location: Annotated[
56
+ str,
57
+ typer.Argument(
58
+ help="Location to clone to (default will be ./{repo_name})"
59
+ ),
60
+ ] = None,
61
+ no_config_remote: Annotated[
62
+ bool,
63
+ typer.Option(
64
+ "--no-config-remote",
65
+ help="Do not automatically configure Calkit DVC remote.",
66
+ ),
67
+ ] = False,
68
+ no_dvc_pull: Annotated[
69
+ bool, typer.Option("--no-dvc-pull", help="Do not pull DVC objects.")
70
+ ] = False,
71
+ ):
72
+ """Clone a Git repo and by default configure and pull from the DVC
73
+ remote.
74
+ """
75
+ # Git clone
76
+ cmd = ["git", "clone", url]
77
+ if location is not None:
78
+ cmd.append(location)
79
+ try:
80
+ subprocess.call(cmd)
81
+ except Exception as e:
82
+ raise_error(str(e))
83
+ if location is None:
84
+ location = url.split("/")[-1].removesuffix(".git")
85
+ typer.echo(f"Moving into repo dir: {location}")
86
+ os.chdir(location)
87
+ # Setup auth for any Calkit remotes
88
+ if not no_config_remote:
89
+ remotes = calkit.dvc.get_remotes()
90
+ for name, url in remotes.items():
91
+ if name == "calkit" or name.startswith("calkit:"):
92
+ typer.echo(f"Setting up authentication for DVC remote: {name}")
93
+ calkit.dvc.set_remote_auth(remote_name=name)
94
+ # DVC pull
95
+ if not no_dvc_pull:
96
+ subprocess.call(["dvc", "pull"])
97
+
98
+
49
99
  @app.command(name="status")
50
100
  def get_status():
51
101
  """Get a unified Git and DVC status."""
@@ -97,8 +147,7 @@ def add(
97
147
  adding any .dvc files to Git when adding to DVC.
98
148
  """
99
149
  if to is not None and to not in ["git", "dvc"]:
100
- typer.echo(f"Invalid option for 'to': {to}")
101
- raise typer.Exit(1)
150
+ raise_error(f"Invalid option for 'to': {to}")
102
151
  # Ensure autostage is enabled for DVC
103
152
  subprocess.call(["dvc", "config", "core.autostage", "true"])
104
153
  subprocess.call(["git", "add", ".dvc/config"])
@@ -117,13 +166,11 @@ def add(
117
166
  ]
118
167
  dvc_size_thresh_bytes = 1_000_000
119
168
  if "." in paths and to is None:
120
- typer.echo("Cannot add '.' with calkit; use git or dvc")
121
- raise typer.Exit(1)
169
+ raise_error("Cannot add '.' with calkit; use git or dvc")
122
170
  if to is None:
123
171
  for path in paths:
124
172
  if os.path.isdir(path):
125
- typer.echo("Cannot auto-add directories; use git or dvc")
126
- raise typer.Exit(1)
173
+ raise_error("Cannot auto-add directories; use git or dvc")
127
174
  repo = git.Repo()
128
175
  for path in paths:
129
176
  # Detect if this file should be tracked with Git or DVC
@@ -323,8 +370,7 @@ def run_dvc_repro(
323
370
  subprocess.call(["dvc", "repro"] + args)
324
371
  # Now parse stage metadata for calkit objects
325
372
  if not os.path.isfile("dvc.yaml"):
326
- typer.echo("No dvc.yaml file found")
327
- raise typer.Exit(1)
373
+ raise_error("No dvc.yaml file found")
328
374
  objects = []
329
375
  with open("dvc.yaml") as f:
330
376
  pipeline = calkit.ryaml.load(f)
@@ -332,21 +378,18 @@ def run_dvc_repro(
332
378
  ckmeta = stage_info.get("meta", {}).get("calkit")
333
379
  if ckmeta is not None:
334
380
  if not isinstance(ckmeta, dict):
335
- typer.echo(
381
+ raise_error(
336
382
  f"Calkit metadata for {stage_name} is not a dictionary"
337
383
  )
338
- typer.Exit(1)
339
384
  # Stage must have a single output
340
385
  outs = stage_info.get("outs", [])
341
386
  if len(outs) != 1:
342
- typer.echo(
387
+ raise_error(
343
388
  f"Stage {stage_name} does not have exactly one output"
344
389
  )
345
- raise typer.Exit(1)
346
390
  cktype = ckmeta.get("type")
347
391
  if cktype not in ["figure", "dataset", "publication"]:
348
- typer.echo(f"Invalid Calkit output type '{cktype}'")
349
- raise typer.Exit(1)
392
+ raise_error(f"Invalid Calkit output type '{cktype}'")
350
393
  objects.append(
351
394
  dict(path=outs[0]) | ckmeta | dict(stage=stage_name)
352
395
  )
@@ -433,21 +476,24 @@ def run_in_env(
433
476
  ck_info = calkit.load_calkit_info()
434
477
  envs = ck_info.get("environments", {})
435
478
  if not envs:
436
- typer.echo("No environments defined in calkit.yaml", err=True)
437
- raise typer.Exit(1)
479
+ raise_error("No environments defined in calkit.yaml")
438
480
  if isinstance(envs, list):
439
- typer.echo(
440
- "Error: Environments should be a dict, not a list", err=True
441
- )
442
- raise typer.Exit(1)
443
- if len(envs) > 1 and env_name is None:
444
- typer.echo(
445
- "Environment must be specified if there are multiple",
446
- err=True,
447
- )
448
- raise typer.Exit(1)
481
+ raise_error("Error: Environments should be a dict, not a list")
482
+ if env_name is None:
483
+ # See if there's a default env, or only one env defined
484
+ default_env_name = None
485
+ for n, e in envs.items():
486
+ if e.get("default"):
487
+ if default_env_name is not None:
488
+ raise_error(
489
+ "Only one default environment can be specified"
490
+ )
491
+ default_env_name = n
492
+ if default_env_name is None and len(envs) == 1:
493
+ default_env_name = list(envs.keys())[0]
494
+ env_name = default_env_name
449
495
  if env_name is None:
450
- env_name = list(envs.keys())[0]
496
+ raise_error("Environment must be specified if there are multiple")
451
497
  env = envs[env_name]
452
498
  cwd = os.getcwd()
453
499
  image_name = env.get("image", env_name)
@@ -477,8 +523,7 @@ def run_in_env(
477
523
  typer.echo(f"Running command: {cmd}")
478
524
  subprocess.call(cmd)
479
525
  else:
480
- typer.echo("Environment kind not supported", err=True)
481
- raise typer.Exit(1)
526
+ raise_error("Environment kind not supported")
482
527
 
483
528
 
484
529
  @app.command(
@@ -507,8 +552,7 @@ def check_call(
507
552
  subprocess.check_call(if_error, shell=True)
508
553
  typer.echo("Fallback call succeeded")
509
554
  except subprocess.CalledProcessError:
510
- typer.echo("Fallback call failed", err=True)
511
- raise typer.Exit(1)
555
+ raise_error("Fallback call failed")
512
556
 
513
557
 
514
558
  @app.command(
@@ -10,17 +10,13 @@ import typer
10
10
  from typing_extensions import Annotated
11
11
 
12
12
  import calkit
13
+ from calkit.cli import raise_error
13
14
  from calkit.core import ryaml
14
15
  from calkit.docker import LAYERS
15
16
 
16
17
  new_app = typer.Typer(no_args_is_help=True)
17
18
 
18
19
 
19
- def _error(txt):
20
- typer.echo(txt, err=txt)
21
- raise typer.Exit(1)
22
-
23
-
24
20
  @new_app.command(name="figure")
25
21
  def new_figure(
26
22
  path: str,
@@ -69,18 +65,18 @@ def new_figure(
69
65
  ),
70
66
  ] = False,
71
67
  ):
72
- """Add a new figure."""
68
+ """Create a new figure."""
73
69
  ck_info = calkit.load_calkit_info()
74
70
  figures = ck_info.get("figures", [])
75
71
  paths = [f.get("path") for f in figures]
76
72
  if not overwrite and path in paths:
77
- _error(f"Figure at path {path} already exists")
73
+ raise_error(f"Figure at path {path} already exists")
78
74
  if cmd is not None and stage_name is None:
79
- _error("Stage name must be provided if command is specified")
75
+ raise_error("Stage name must be provided if command is specified")
80
76
  if (deps or outs or outs_from_stage) and not cmd:
81
- _error("Command must be provided")
77
+ raise_error("Command must be provided")
82
78
  if (deps or outs or outs_from_stage) and not stage_name:
83
- _error("Stage name must be provided")
79
+ raise_error("Stage name must be provided")
84
80
  obj = dict(path=path, title=title)
85
81
  if description is not None:
86
82
  obj["description"] = description
@@ -91,7 +87,7 @@ def new_figure(
91
87
  pipeline = calkit.dvc.read_pipeline()
92
88
  stages = pipeline.get("stages", {})
93
89
  if outs_from_stage not in stages:
94
- _error(f"Stage {outs_from_stage} does not exist")
90
+ raise_error(f"Stage {outs_from_stage} does not exist")
95
91
  deps += stages[outs_from_stage].get("outs", [])
96
92
  if path not in outs:
97
93
  outs.append(path)
@@ -232,15 +228,9 @@ def new_docker_env(
232
228
  ):
233
229
  """Create a new Docker environment."""
234
230
  if base and os.path.isfile(path) and not overwrite:
235
- typer.echo(
236
- "Output path already exists (use -f to overwrite)", err=True
237
- )
238
- raise typer.Exit(1)
231
+ raise_error("Output path already exists (use -f to overwrite)")
239
232
  if stage and not base:
240
- typer.echo(
241
- "--from must be specified when creating a build stage", err=True
242
- )
243
- raise typer.Exit(1)
233
+ raise_error("--from must be specified when creating a build stage")
244
234
  if image_name is None:
245
235
  typer.echo("No image name specified; using environment name")
246
236
  image_name = name
@@ -249,8 +239,7 @@ def new_docker_env(
249
239
  txt = "FROM " + base + "\n\n"
250
240
  for layer in layers:
251
241
  if layer not in LAYERS:
252
- typer.echo(f"Unknown layer type '{layer}'")
253
- raise typer.Exit(1)
242
+ raise_error(f"Unknown layer type '{layer}'")
254
243
  txt += LAYERS[layer] + "\n\n"
255
244
  txt += f"RUN mkdir {wdir}\n"
256
245
  txt += f"WORKDIR {wdir}\n"
@@ -264,11 +253,10 @@ def new_docker_env(
264
253
  typer.echo("Converting environments from list to dict")
265
254
  envs = {env.pop("name"): env for env in envs}
266
255
  if name in envs and not overwrite:
267
- typer.echo(
256
+ raise_error(
268
257
  f"Environment with name {name} already exists "
269
258
  "(use -f to overwrite)"
270
259
  )
271
- raise typer.Exit(1)
272
260
  if base:
273
261
  repo.git.add(path)
274
262
  typer.echo("Adding environment to calkit.yaml")
@@ -348,8 +336,7 @@ def new_foreach_stage(
348
336
  """
349
337
  pipeline = calkit.dvc.read_pipeline()
350
338
  if name in pipeline and not overwrite:
351
- typer.echo("Stage already exists; use -f to overwrite", err=True)
352
- raise typer.Exit(1)
339
+ raise_error("Stage already exists; use -f to overwrite")
353
340
  if "stages" not in pipeline:
354
341
  pipeline["stages"] = {}
355
342
  pipeline["stages"][name] = dict(
@@ -67,9 +67,7 @@ def add_external_remote(owner_name: str, project_name: str):
67
67
  base_url = calkit.cloud.get_base_url()
68
68
  remote_url = f"{base_url}/projects/{owner_name}/{project_name}/dvc"
69
69
  remote_name = f"{get_app_name()}:{owner_name}/{project_name}"
70
- subprocess.call(
71
- ["dvc", "remote", "add", "-f", remote_name, remote_url]
72
- )
70
+ subprocess.call(["dvc", "remote", "add", "-f", remote_name, remote_url])
73
71
  subprocess.call(["dvc", "remote", "modify", remote_name, "auth", "custom"])
74
72
  set_remote_auth(remote_name)
75
73
 
@@ -79,3 +77,17 @@ def read_pipeline() -> dict:
79
77
  return {}
80
78
  with open("dvc.yaml") as f:
81
79
  return calkit.ryaml.load(f)
80
+
81
+
82
+ def get_remotes() -> dict[str, str]:
83
+ """Get a dictionary of DVC remotes, keyed by name, with URL as the
84
+ value.
85
+ """
86
+ out = subprocess.check_output(["dvc", "remote", "list"]).decode().strip()
87
+ if not out:
88
+ return {}
89
+ resp = {}
90
+ for line in out.split("\n"):
91
+ name, url = line.split("\t")
92
+ resp[name] = url
93
+ return resp
@@ -66,6 +66,7 @@ class Environment(BaseModel):
66
66
  path: str | None = None
67
67
  description: str | None = None
68
68
  stage: str | None = None
69
+ default: bool | None = None
69
70
 
70
71
 
71
72
  class DockerEnvironment(Environment):
@@ -89,8 +90,24 @@ class Notebook(_CalkitObject):
89
90
  class ProjectInfo(BaseModel):
90
91
  """All of the project's information or metadata, written to the
91
92
  ``calkit.yaml`` file.
93
+
94
+ Attributes
95
+ ----------
96
+ parent : str
97
+ The project's parent project, if applicable. This should be set if
98
+ the project was created as a copy of another. This is similar to the
99
+ concept of forking, but unlike a fork, a child project's changes
100
+ are not meant to be merged back into the parent.
101
+ The format of this field should be something like
102
+ {owner_name}/{project_name}, e.g., 'someuser/some-project-name'.
103
+ Note that individual objects can be imported from other projects, but
104
+ that doesn't necessarily make them parent projects.
105
+ This is probably not that important of a distinction.
106
+ The real use case is being able to trace where things came from and
107
+ distinguish what has been newly created here.
92
108
  """
93
109
 
110
+ parent: str | None = None
94
111
  questions: list[str] = []
95
112
  datasets: list[Dataset] = []
96
113
  figures: list[Figure] = []
@@ -0,0 +1,15 @@
1
+ """Tests for the ``dvc`` module."""
2
+
3
+ import subprocess
4
+
5
+ import calkit
6
+
7
+
8
+ def test_get_remotes(tmp_dir):
9
+ subprocess.call(["git", "init"])
10
+ assert not calkit.dvc.get_remotes()
11
+ subprocess.call(["dvc", "init"])
12
+ assert not calkit.dvc.get_remotes()
13
+ subprocess.call(["dvc", "remote", "add", "something", "https://sup.com"])
14
+ resp = calkit.dvc.get_remotes()
15
+ assert resp == {"something": "https://sup.com"}
File without changes
File without changes
File without changes