calkit-python 0.41.10__py3-none-any.whl → 0.41.12__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.
Files changed (32) hide show
  1. calkit/cli/describe.py +45 -0
  2. calkit/cli/list.py +112 -30
  3. calkit/cli/main/core.py +6 -1
  4. calkit/cli/update.py +558 -4
  5. calkit/core.py +12 -0
  6. calkit/detect.py +433 -75
  7. calkit/pipeline.py +25 -10
  8. calkit/tests/cli/main/test_core.py +16 -1
  9. calkit/tests/cli/test_list.py +19 -2
  10. calkit/tests/cli/test_update.py +136 -0
  11. calkit/tests/test_detect.py +116 -0
  12. calkit/tests/test_pipeline.py +2 -0
  13. {calkit_python-0.41.10.dist-info → calkit_python-0.41.12.dist-info}/METADATA +1 -1
  14. {calkit_python-0.41.10.dist-info → calkit_python-0.41.12.dist-info}/RECORD +32 -32
  15. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  16. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
  17. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  18. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  19. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  20. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
  21. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
  22. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
  23. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  24. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
  25. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  26. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
  27. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  28. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  29. {calkit_python-0.41.10.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  30. {calkit_python-0.41.10.dist-info → calkit_python-0.41.12.dist-info}/WHEEL +0 -0
  31. {calkit_python-0.41.10.dist-info → calkit_python-0.41.12.dist-info}/entry_points.txt +0 -0
  32. {calkit_python-0.41.10.dist-info → calkit_python-0.41.12.dist-info}/licenses/LICENSE +0 -0
calkit/cli/describe.py CHANGED
@@ -3,10 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from typing import Annotated
6
7
 
7
8
  import typer
8
9
 
9
10
  import calkit
11
+ from calkit.cli import raise_error
12
+ from calkit.environments import get_env_lock_fpath
10
13
 
11
14
  describe_app = typer.Typer(no_args_is_help=True)
12
15
 
@@ -16,3 +19,45 @@ def describe_system():
16
19
  """Describe the system."""
17
20
  system_info = calkit.get_system_info()
18
21
  typer.echo(json.dumps(system_info, indent=2))
22
+
23
+
24
+ @describe_app.command(name="env")
25
+ def describe_env(
26
+ name: Annotated[
27
+ str,
28
+ typer.Option("--name", "-n", help="Environment name."),
29
+ ],
30
+ ):
31
+ """Describe a single environment, including spec and lock file paths."""
32
+ ck_info = calkit.load_calkit_info()
33
+ envs: dict = ck_info.get("environments", {})
34
+ if name not in envs:
35
+ raise_error(f"Environment '{name}' not found.")
36
+ env = envs[name]
37
+ lock_path = get_env_lock_fpath(env=env, env_name=name)
38
+ result = {
39
+ "kind": env.get("kind"),
40
+ "spec_path": env.get("path"),
41
+ "lock_path": lock_path,
42
+ "prefix": env.get("prefix"),
43
+ "python": env.get("python"),
44
+ }
45
+ typer.echo(json.dumps(result, indent=2))
46
+
47
+
48
+ @describe_app.command(name="envs")
49
+ def describe_envs():
50
+ """Describe all environments, including spec and lock file paths."""
51
+ ck_info = calkit.load_calkit_info()
52
+ envs: dict = ck_info.get("environments", {})
53
+ result = {}
54
+ for env_name, env in envs.items():
55
+ lock_path = get_env_lock_fpath(env=env, env_name=env_name)
56
+ result[env_name] = {
57
+ "kind": env.get("kind"),
58
+ "spec_path": env.get("path"),
59
+ "lock_path": lock_path,
60
+ "prefix": env.get("prefix"),
61
+ "python": env.get("python"),
62
+ }
63
+ typer.echo(json.dumps(result, indent=2))
calkit/cli/list.py CHANGED
@@ -2,16 +2,43 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  from typing import Annotated, Literal
6
7
 
7
8
  import typer
8
9
 
9
10
  import calkit
10
- from calkit.cli import AliasGroup, warn
11
+ from calkit.cli import AliasGroup, raise_error, warn
11
12
 
12
13
  list_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
13
14
 
14
15
 
16
+ def _echo_object(obj: dict) -> None:
17
+ """Print a single object in the human-readable YAML-ish listing format."""
18
+ # Copy so popping 'path' doesn't mutate the caller's dict.
19
+ obj = dict(obj)
20
+ path = obj.pop("path", None)
21
+ typer.echo(f"- path: {path}")
22
+ for k, v in obj.items():
23
+ if isinstance(v, dict):
24
+ typer.echo(f" {k}:")
25
+ for k1, v1 in v.items():
26
+ typer.echo(f" {k1}: {v1}")
27
+ elif isinstance(v, list):
28
+ typer.echo(f" {k}:")
29
+ for item in v:
30
+ if isinstance(item, dict):
31
+ for n, (k1, v1) in enumerate(item.items()):
32
+ if n == 0:
33
+ typer.echo(f" - {k1}: {v1}")
34
+ else:
35
+ typer.echo(f" {k1}: {v1}")
36
+ else:
37
+ typer.echo(f" - {item}")
38
+ else:
39
+ typer.echo(f" {k}: {v}")
40
+
41
+
15
42
  def _list_objects(
16
43
  kind: Literal[
17
44
  "notebooks",
@@ -21,33 +48,44 @@ def _list_objects(
21
48
  "publications",
22
49
  ],
23
50
  ):
24
- """List objects.
51
+ """List objects."""
52
+ ck_info = calkit.load_calkit_info()
53
+ for obj in ck_info.get(kind, []) or []:
54
+ _echo_object(obj)
55
+
56
+
57
+ def _list_artifacts(
58
+ kind: Literal["figures", "datasets"],
59
+ json_output: bool,
60
+ declared_only: bool,
61
+ ):
62
+ """List figures or datasets, optionally including auto-detected ones.
25
63
 
26
- TODO: This should probably just use some library to dump YAML to string.
64
+ By default, artifacts declared in ``calkit.yaml`` are merged with any
65
+ auto-detected from the project's files; ``--declared-only`` returns just the
66
+ declared ones. Each entry carries a ``detected`` flag so callers can tell
67
+ declared and auto-detected artifacts apart.
27
68
  """
28
69
  ck_info = calkit.load_calkit_info()
29
- objects = ck_info.get(kind, [])
30
- for obj in objects:
31
- path = obj.pop("path")
32
- typer.echo(f"- path: {path}")
33
- for k, v in obj.items():
34
- if isinstance(v, dict):
35
- typer.echo(f" {k}:")
36
- for k1, v1 in v.items():
37
- typer.echo(f" {k1}: {v1}")
38
- elif isinstance(v, list):
39
- typer.echo(f" {k}:")
40
- for item in v:
41
- if isinstance(item, dict):
42
- for n, (k1, v1) in enumerate(item.items()):
43
- if n == 0:
44
- typer.echo(f" - {k1}: {v1}")
45
- else:
46
- typer.echo(f" {k1}: {v1}")
47
- else:
48
- typer.echo(f" - {item}")
49
- else:
50
- typer.echo(f" {k}: {v}")
70
+ declared = ck_info.get(kind, []) or []
71
+ declared_paths = {o.get("path") for o in declared if isinstance(o, dict)}
72
+ detected: list[dict] = []
73
+ if not declared_only:
74
+ found = calkit.detect.detect_project_artifacts(ck_info=ck_info)
75
+ for path in found.get(kind, []):
76
+ if path not in declared_paths:
77
+ detected.append({"path": path})
78
+ if json_output:
79
+ result = [
80
+ {**o, "detected": False} for o in declared if isinstance(o, dict)
81
+ ]
82
+ result += [{**o, "detected": True} for o in detected]
83
+ typer.echo(json.dumps(result))
84
+ return
85
+ for obj in declared:
86
+ _echo_object(obj)
87
+ for obj in detected:
88
+ _echo_object({**obj, "detected": True})
51
89
 
52
90
 
53
91
  @list_app.command(name="notebooks|nb")
@@ -57,15 +95,43 @@ def list_notebooks():
57
95
 
58
96
 
59
97
  @list_app.command(name="figures|figs")
60
- def list_figures():
98
+ def list_figures(
99
+ json_output: Annotated[
100
+ bool, typer.Option("--json", help="Output result as JSON.")
101
+ ] = False,
102
+ declared_only: Annotated[
103
+ bool,
104
+ typer.Option(
105
+ "--declared-only",
106
+ help=(
107
+ "Only list figures declared in calkit.yaml; "
108
+ "skip auto-detection."
109
+ ),
110
+ ),
111
+ ] = False,
112
+ ):
61
113
  """List figures in the project."""
62
- _list_objects("figures")
114
+ _list_artifacts("figures", json_output, declared_only)
63
115
 
64
116
 
65
117
  @list_app.command(name="datasets")
66
- def list_datasets():
118
+ def list_datasets(
119
+ json_output: Annotated[
120
+ bool, typer.Option("--json", help="Output result as JSON.")
121
+ ] = False,
122
+ declared_only: Annotated[
123
+ bool,
124
+ typer.Option(
125
+ "--declared-only",
126
+ help=(
127
+ "Only list datasets declared in calkit.yaml; "
128
+ "skip auto-detection."
129
+ ),
130
+ ),
131
+ ] = False,
132
+ ):
67
133
  """List datasets in the project."""
68
- _list_objects("datasets")
134
+ _list_artifacts("datasets", json_output, declared_only)
69
135
 
70
136
 
71
137
  @list_app.command(name="publications|pubs")
@@ -167,12 +233,28 @@ def list_stages(
167
233
  list[str] | None,
168
234
  typer.Option("--kind", "-k", help="Filter stages by kind."),
169
235
  ] = None,
236
+ stale_only: Annotated[
237
+ bool, typer.Option("--stale", help="Show only stale stages.")
238
+ ] = False,
170
239
  ):
171
240
  """List pipeline stages."""
172
- stages = calkit.load_calkit_info().get("pipeline", {}).get("stages", {})
241
+ ck_info = calkit.load_calkit_info()
242
+ stages = ck_info.get("pipeline", {}).get("stages", {})
243
+ # If we only want stale stages, we need to get the status first.
244
+ # This compiles the pipeline, cleans notebooks, and checks environments,
245
+ # all of which can affect whether a stage is stale, so we don't skip them.
246
+ if stale_only:
247
+ status = calkit.pipeline.get_status(ck_info=ck_info)
248
+ if status.errors:
249
+ raise_error(
250
+ "Failed to determine stale stages: " + "; ".join(status.errors)
251
+ )
252
+ stale_stage_names = status.stale_stage_names
173
253
  for name, stage in stages.items():
174
254
  if kinds is not None and stage.get("kind") not in kinds:
175
255
  continue
256
+ if stale_only and name not in stale_stage_names:
257
+ continue
176
258
  typer.echo(name)
177
259
 
178
260
 
calkit/cli/main/core.py CHANGED
@@ -196,7 +196,12 @@ def init(
196
196
  repo = calkit.git.get_repo()
197
197
  repo.git.add(".dvc")
198
198
  repo.git.commit("-m", "Initialize DVC")
199
- # TODO: Initialize `calkit.yaml`
199
+ # Create an empty calkit.yaml if one doesn't already exist
200
+ if not os.path.isfile("calkit.yaml"):
201
+ with open("calkit.yaml", "w"):
202
+ pass
203
+ repo.git.add("calkit.yaml")
204
+ repo.git.commit("-m", "Initialize Calkit")
200
205
  # TODO: Initialize `dvc.yaml`
201
206
  # TODO: Add a sane .gitignore file
202
207
  # TODO: Add a sane LICENSE file?