calkit-python 0.41.11__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 (23) hide show
  1. calkit/cli/list.py +94 -28
  2. calkit/detect.py +433 -75
  3. calkit/tests/test_detect.py +116 -0
  4. {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/METADATA +1 -1
  5. {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/RECORD +23 -23
  6. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  7. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
  8. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  9. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  10. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  11. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
  12. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
  13. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
  14. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  15. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
  16. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  17. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
  18. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +0 -0
  19. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  20. {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  21. {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/WHEEL +0 -0
  22. {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/entry_points.txt +0 -0
  23. {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/licenses/LICENSE +0 -0
calkit/cli/list.py CHANGED
@@ -2,6 +2,7 @@
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
@@ -12,6 +13,32 @@ from calkit.cli import AliasGroup, raise_error, warn
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
+
25
56
 
26
- TODO: This should probably just use some library to dump YAML to string.
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.
63
+
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")
calkit/detect.py CHANGED
@@ -74,6 +74,7 @@ def detect_r_script_io(
74
74
  content,
75
75
  script_dir=script_dir,
76
76
  wdir=effective_wdir,
77
+ project_root=effective_wdir,
77
78
  )
78
79
 
79
80
 
@@ -995,7 +996,11 @@ def _resolve_python_import(
995
996
 
996
997
 
997
998
  def _detect_r_code_io(
998
- code: str, script_dir: str = ".", wdir: str = "."
999
+ code: str,
1000
+ script_dir: str = ".",
1001
+ wdir: str = ".",
1002
+ project_root: str = ".",
1003
+ _seen: set[str] | None = None,
999
1004
  ) -> dict[str, list[str]]:
1000
1005
  """Detect I/O from R code string (used for scripts and notebook cells).
1001
1006
 
@@ -1007,6 +1012,12 @@ def _detect_r_code_io(
1007
1012
  Directory containing the script (for resolving source() includes).
1008
1013
  wdir : str
1009
1014
  Working directory from which the script is executed (for data file paths).
1015
+ project_root : str
1016
+ Directory that ``here::here()`` resolves against, i.e. the project
1017
+ root where the pipeline runs.
1018
+ _seen : set[str] | None
1019
+ Absolute paths of scripts already analyzed, used to guard against
1020
+ cycles when recursing into ``source()``d scripts.
1010
1021
 
1011
1022
  Returns
1012
1023
  -------
@@ -1015,78 +1026,154 @@ def _detect_r_code_io(
1015
1026
  """
1016
1027
  inputs = []
1017
1028
  outputs = []
1029
+ if _seen is None:
1030
+ _seen = set()
1018
1031
  code = re.sub(r"#.*$", "", code, flags=re.MULTILINE)
1019
1032
  r_vars = _extract_r_string_assignments(code)
1020
1033
  fig_dir = r_vars.get("fig_dir")
1021
- # Detect source() calls for R script includes
1022
- source_pattern = r'source\s*\(\s*["\']([^"\']+\.R)["\']'
1023
- source_matches = re.findall(source_pattern, code, flags=re.IGNORECASE)
1024
- for match in source_matches:
1025
- if not os.path.isabs(match):
1026
- full_path = os.path.join(script_dir, match)
1027
- if os.path.exists(full_path):
1028
- inputs.append(Path(os.path.relpath(full_path)).as_posix())
1029
- elif _is_valid_project_path(match):
1030
- inputs.append(match)
1031
- # Read patterns that capture variables, file.path() expressions, or literals
1032
- read_patterns = [
1033
- r'read\.(?:csv|table|delim|tsv)\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1034
- r'readRDS\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1035
- r'load\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1036
- r'read_csv\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1037
- r'read_excel\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1038
- r'fread\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1034
+ # A path argument may be a here::here()/file.path() call, a quoted literal,
1035
+ # or a variable name that resolves to one of those
1036
+ path_expr = (
1037
+ r'(here::here\([^)]*\)|file\.path\([^)]*\)|"[^"]+"|\'[^\']+\'|'
1038
+ r"[A-Za-z_][A-Za-z0-9_]*)"
1039
+ )
1040
+ # Detect source() calls for R script includes, add them as inputs, and
1041
+ # recurse into them so their I/O is attributed to this stage
1042
+ for raw in re.findall(r"source\s*\(\s*" + path_expr, code):
1043
+ resolved = _resolve_r_path_expr(raw, r_vars)
1044
+ if not resolved or not resolved.lower().endswith(".r"):
1045
+ continue
1046
+ if os.path.isabs(resolved):
1047
+ continue
1048
+ # here::here() paths are relative to the project root; everything else
1049
+ # is resolved relative to the sourcing script's directory
1050
+ if raw.strip().startswith("here::here"):
1051
+ fs_path = os.path.join(project_root, resolved)
1052
+ else:
1053
+ fs_path = os.path.join(script_dir, resolved)
1054
+ if os.path.exists(fs_path):
1055
+ inputs.append(Path(os.path.relpath(fs_path)).as_posix())
1056
+ elif _is_valid_project_path(resolved):
1057
+ inputs.append(resolved)
1058
+ # Recurse into the sourced script if we can read it
1059
+ abs_path = os.path.abspath(fs_path)
1060
+ if os.path.exists(fs_path) and abs_path not in _seen:
1061
+ _seen.add(abs_path)
1062
+ try:
1063
+ with open(fs_path, "r", encoding="utf-8") as f:
1064
+ sub_code = f.read()
1065
+ except (OSError, UnicodeDecodeError):
1066
+ sub_code = None
1067
+ if sub_code is not None:
1068
+ sub_dir = (
1069
+ os.path.dirname(Path(os.path.relpath(fs_path)).as_posix())
1070
+ or "."
1071
+ )
1072
+ sub_io = _detect_r_code_io(
1073
+ sub_code,
1074
+ script_dir=sub_dir,
1075
+ wdir=wdir,
1076
+ project_root=project_root,
1077
+ _seen=_seen,
1078
+ )
1079
+ inputs.extend(sub_io["inputs"])
1080
+ outputs.extend(sub_io["outputs"])
1081
+ # Read patterns that capture variables, here::here()/file.path()
1082
+ # expressions, or literals
1083
+ read_funcs = [
1084
+ r"read\.(?:csv|table|delim|tsv)",
1085
+ r"readRDS",
1086
+ r"readLines",
1087
+ r"load",
1088
+ r"read_csv",
1089
+ r"read_tsv",
1090
+ r"read_delim",
1091
+ r"read_excel",
1092
+ r"fread",
1039
1093
  ]
1040
- # Process read patterns with variable resolution
1041
- for pattern in read_patterns:
1042
- matches = re.findall(pattern, code)
1043
- for match in matches:
1094
+ for func in read_funcs:
1095
+ for match in re.findall(func + r"\s*\(\s*" + path_expr, code):
1044
1096
  resolved = _resolve_r_path_expr(match, r_vars)
1045
1097
  if resolved:
1046
1098
  inputs.append(resolved)
1047
- # Write patterns for simple cases (literal strings in 2nd argument)
1048
- simple_write_patterns = [
1049
- r'write\.(?:csv|table)\s*\(\s*[^,]+,\s*["\']([^"\']+)["\']',
1050
- r'saveRDS\s*\(\s*[^,]+,\s*["\']([^"\']+)["\']',
1051
- r'save\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1052
- r'write_csv\s*\(\s*[^,]+,\s*["\']([^"\']+)["\']',
1053
- r'write_excel\s*\(\s*[^,]+,\s*["\']([^"\']+)["\']',
1054
- r'pdf\s*\(\s*["\']([^"\']+)["\']',
1055
- r'png\s*\(\s*["\']([^"\']+)["\']',
1056
- r'pdf\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1057
- r'png\s*\([^)]*filename\s*=\s*["\']([^"\']+)["\']',
1058
- r'svg\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1059
- r'svg\s*\([^)]*filename\s*=\s*["\']([^"\']+)["\']',
1060
- r'jpeg\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1061
- r'jpeg\s*\([^)]*filename\s*=\s*["\']([^"\']+)["\']',
1062
- r'tiff\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1063
- r'tiff\s*\([^)]*filename\s*=\s*["\']([^"\']+)["\']',
1064
- r'bmp\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1065
- r'bmp\s*\([^)]*filename\s*=\s*["\']([^"\']+)["\']',
1066
- r'CairoPNG\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1067
- r'CairoPDF\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1068
- r'svglite\s*\([^)]*file\s*=\s*["\']([^"\']+)["\']',
1099
+ # Write functions where the path is the second positional argument
1100
+ second_arg_write_funcs = [
1101
+ r"write\.(?:csv|table)",
1102
+ r"saveRDS",
1103
+ r"write_csv",
1104
+ r"write_tsv",
1105
+ r"write_delim",
1106
+ r"write_excel\w*",
1107
+ r"writeLines",
1108
+ r"fwrite",
1069
1109
  ]
1070
- for pattern in simple_write_patterns:
1071
- outputs.extend(re.findall(pattern, code))
1072
- # ggsave patterns that handle file.path(), variables, or literals
1073
- ggsave_patterns = [
1074
- r'ggsave\s*\(\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1075
- r'ggsave\s*\([^)]*filename\s*=\s*(file\.path\([^\)]*\)|"[^"]+"|\'[^\']+\'|[A-Za-z_][A-Za-z0-9_]*)',
1110
+ for func in second_arg_write_funcs:
1111
+ pattern = func + r"\s*\(\s*[^,]+,\s*" + path_expr
1112
+ for match in re.findall(pattern, code):
1113
+ resolved = _resolve_r_path_expr(match, r_vars)
1114
+ if resolved:
1115
+ outputs.append(resolved)
1116
+ # Write functions where the path is the (only) first positional argument
1117
+ first_arg_write_funcs = [
1118
+ r"ggsave",
1119
+ r"sink",
1120
+ r"pdf",
1121
+ r"png",
1122
+ r"svg",
1123
+ r"svglite",
1124
+ r"jpeg",
1125
+ r"tiff",
1126
+ r"bmp",
1127
+ r"cairo_pdf",
1128
+ r"CairoPNG",
1129
+ r"CairoPDF",
1076
1130
  ]
1077
- for pattern in ggsave_patterns:
1078
- matches = re.findall(pattern, code)
1079
- for match in matches:
1131
+ for func in first_arg_write_funcs:
1132
+ pattern = func + r"\s*\(\s*" + path_expr
1133
+ for match in re.findall(pattern, code):
1134
+ resolved = _resolve_r_path_expr(match, r_vars)
1135
+ if resolved:
1136
+ outputs.append(resolved)
1137
+ # Write functions whose path is passed as a named argument, regardless of
1138
+ # position (e.g. saveRDS(x, file = ...), pdf(file = ...), save(file = ...))
1139
+ named_arg_write_funcs = [
1140
+ r"write\.(?:csv|table)",
1141
+ r"saveRDS",
1142
+ r"save",
1143
+ r"write_csv",
1144
+ r"write_tsv",
1145
+ r"write_delim",
1146
+ r"write_excel\w*",
1147
+ r"writeLines",
1148
+ r"fwrite",
1149
+ r"ggsave",
1150
+ r"sink",
1151
+ r"pdf",
1152
+ r"png",
1153
+ r"svg",
1154
+ r"svglite",
1155
+ r"jpeg",
1156
+ r"tiff",
1157
+ r"bmp",
1158
+ r"cairo_pdf",
1159
+ r"CairoPNG",
1160
+ r"CairoPDF",
1161
+ ]
1162
+ for func in named_arg_write_funcs:
1163
+ pattern = (
1164
+ func + r"\s*\([^)]*\b(?:file|filename|path|con)\s*=\s*" + path_expr
1165
+ )
1166
+ for match in re.findall(pattern, code):
1080
1167
  resolved = _resolve_r_path_expr(match, r_vars)
1081
1168
  if resolved:
1082
1169
  outputs.append(resolved)
1170
+ # save_fig() writes into fig_dir when the path isn't already qualified
1083
1171
  save_fig_patterns = [
1084
- r"save_fig\s*\(\s*[^,]+,\s*(file\.path\([^\)]*\)|\"[^\"]+\"|'[^']+'|[A-Za-z_][A-Za-z0-9_]*)",
1085
- r"save_fig\s*\([^\)]*filename\s*=\s*(file\.path\([^\)]*\)|\"[^\"]+\"|'[^']+'|[A-Za-z_][A-Za-z0-9_]*)",
1172
+ r"save_fig\s*\(\s*[^,]+,\s*" + path_expr,
1173
+ r"save_fig\s*\([^)]*filename\s*=\s*" + path_expr,
1086
1174
  ]
1087
1175
  for pattern in save_fig_patterns:
1088
- matches = re.findall(pattern, code)
1089
- for match in matches:
1176
+ for match in re.findall(pattern, code):
1090
1177
  resolved = _resolve_r_path_expr(match, r_vars)
1091
1178
  if resolved:
1092
1179
  if fig_dir and not os.path.isabs(resolved):
@@ -1101,13 +1188,27 @@ def _detect_r_code_io(
1101
1188
  # Resolve paths relative to working directory
1102
1189
  inputs = _resolve_paths_to_wdir(inputs, script_dir, wdir)
1103
1190
  outputs = _resolve_paths_to_wdir(outputs, script_dir, wdir)
1191
+ # Normalize to POSIX separators for persistence/display, since paths built
1192
+ # from here::here()/file.path() use the host OS separator
1193
+ inputs = [Path(p).as_posix() for p in inputs]
1194
+ outputs = [Path(p).as_posix() for p in outputs]
1104
1195
  inputs = list(dict.fromkeys(inputs))
1105
1196
  outputs = list(dict.fromkeys(outputs))
1197
+ # A file produced within this stage is not an external input, even if it is
1198
+ # also read back (e.g. an intermediate written by one sourced script and
1199
+ # consumed by another)
1200
+ output_set = set(outputs)
1201
+ inputs = [p for p in inputs if p not in output_set]
1106
1202
  return {"inputs": inputs, "outputs": outputs}
1107
1203
 
1108
1204
 
1109
1205
  def _extract_r_string_assignments(code: str) -> dict[str, str]:
1110
- """Extract simple string assignments in R code."""
1206
+ """Extract simple string assignments in R code.
1207
+
1208
+ Captures both string-literal assignments (``x <- "path"``) and
1209
+ ``here::here()``/``file.path()`` assignments whose arguments are literals
1210
+ or previously assigned variables (``x <- here::here("a", "b")``).
1211
+ """
1111
1212
  assignments = {}
1112
1213
  pattern = re.compile(
1113
1214
  r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)\s*[\"']([^\"']+)[\"']",
@@ -1115,6 +1216,16 @@ def _extract_r_string_assignments(code: str) -> dict[str, str]:
1115
1216
  )
1116
1217
  for name, value in pattern.findall(code):
1117
1218
  assignments[name] = value
1219
+ # Resolve here::here()/file.path() assignments using the literals above
1220
+ call_pattern = re.compile(
1221
+ r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)\s*"
1222
+ r"(here::here\([^)]*\)|file\.path\([^)]*\))",
1223
+ flags=re.MULTILINE,
1224
+ )
1225
+ for name, expr in call_pattern.findall(code):
1226
+ resolved = _resolve_r_path_expr(expr, assignments)
1227
+ if resolved:
1228
+ assignments[name] = resolved
1118
1229
  return assignments
1119
1230
 
1120
1231
 
@@ -1146,7 +1257,11 @@ def _resolve_julia_path_expr(
1146
1257
 
1147
1258
 
1148
1259
  def _resolve_r_path_expr(expr: str, variables: dict[str, str]) -> str | None:
1149
- """Resolve simple R path expressions like "x", var, or file.path(...)."""
1260
+ """Resolve simple R path expressions.
1261
+
1262
+ Handles quoted literals, variables, and ``file.path()``/``here::here()``
1263
+ calls whose arguments are literals or known variables.
1264
+ """
1150
1265
  expr = expr.strip()
1151
1266
  if not expr:
1152
1267
  return None
@@ -1154,22 +1269,23 @@ def _resolve_r_path_expr(expr: str, variables: dict[str, str]) -> str | None:
1154
1269
  return expr[1:-1]
1155
1270
  if expr in variables:
1156
1271
  return variables[expr]
1157
- if expr.startswith("file.path"):
1158
- match = re.match(r"file\.path\((.*)\)", expr)
1159
- if not match:
1160
- return None
1161
- args = [a.strip() for a in match.group(1).split(",") if a.strip()]
1162
- if not args:
1163
- return None
1164
- parts = []
1165
- for arg in args:
1166
- if arg[0] in ['"', "'"] and arg[-1] == arg[0]:
1167
- parts.append(arg[1:-1])
1168
- elif arg in variables:
1169
- parts.append(variables[arg])
1170
- else:
1272
+ for prefix in ("file.path", "here::here"):
1273
+ if expr.startswith(prefix):
1274
+ match = re.match(re.escape(prefix) + r"\((.*)\)", expr)
1275
+ if not match:
1276
+ return None
1277
+ args = [a.strip() for a in match.group(1).split(",") if a.strip()]
1278
+ if not args:
1171
1279
  return None
1172
- return os.path.join(*parts)
1280
+ parts = []
1281
+ for arg in args:
1282
+ if arg[0] in ['"', "'"] and arg[-1] == arg[0]:
1283
+ parts.append(arg[1:-1])
1284
+ elif arg in variables:
1285
+ parts.append(variables[arg])
1286
+ else:
1287
+ return None
1288
+ return os.path.join(*parts)
1173
1289
  return None
1174
1290
 
1175
1291
 
@@ -1687,3 +1803,245 @@ def create_r_description_file(
1687
1803
  os.makedirs(output_dir, exist_ok=True)
1688
1804
  with open(output_path, "w") as f:
1689
1805
  f.write(create_r_description_content(dependencies))
1806
+
1807
+
1808
+ # Figure/dataset auto-detection
1809
+ # A file is only treated as an auto-detected figure or dataset when it lives in
1810
+ # a directory whose name signals its kind. These sets are intentionally narrow
1811
+ # (and kept in sync with Calkit Cloud) so we don't flag arbitrary images or
1812
+ # data files scattered around a repository.
1813
+ FIGURE_EXTENSIONS = {
1814
+ ".png",
1815
+ ".jpg",
1816
+ ".jpeg",
1817
+ ".gif",
1818
+ ".svg",
1819
+ ".pdf",
1820
+ ".eps",
1821
+ ".tiff",
1822
+ ".tif",
1823
+ }
1824
+ FIGURE_DIRS = {"figures", "figure", "figs", "fig", "plots", "plot", "images"}
1825
+ DATASET_EXTENSIONS = {
1826
+ ".csv",
1827
+ ".h5",
1828
+ ".hdf5",
1829
+ ".parquet",
1830
+ ".nc",
1831
+ ".zarr",
1832
+ ".feather",
1833
+ ".arrow",
1834
+ ".avro",
1835
+ ".json",
1836
+ ".jsonl",
1837
+ ".ndjson",
1838
+ }
1839
+ DATA_DIRS = {"data", "datasets", "dataset"}
1840
+
1841
+
1842
+ def _ancestor_dir_names(rel_path: str) -> set[str]:
1843
+ """Lower-cased names of the ancestor directories of a "/"-separated path."""
1844
+ return {p.lower() for p in rel_path.split("/")[:-1]}
1845
+
1846
+
1847
+ def _path_ext(rel_path: str) -> str:
1848
+ name = rel_path.rsplit("/", 1)[-1]
1849
+ return ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ""
1850
+
1851
+
1852
+ def is_figure_path(rel_path: str) -> bool:
1853
+ """Whether a repo-relative path looks like an auto-detectable figure."""
1854
+ ancestors = _ancestor_dir_names(rel_path)
1855
+ ext = _path_ext(rel_path)
1856
+ if ext in FIGURE_EXTENSIONS and ancestors & FIGURE_DIRS:
1857
+ return True
1858
+ # A Plotly figure is stored as JSON; treat .json under a figure-only
1859
+ # directory (one that isn't also a data directory) as a figure.
1860
+ if ext == ".json" and ancestors & (FIGURE_DIRS - DATA_DIRS):
1861
+ return True
1862
+ return False
1863
+
1864
+
1865
+ def is_dataset_path(rel_path: str) -> bool:
1866
+ """Whether a repo-relative path looks like an auto-detectable dataset."""
1867
+ return (
1868
+ _path_ext(rel_path) in DATASET_EXTENSIONS
1869
+ and bool(_ancestor_dir_names(rel_path) & DATA_DIRS)
1870
+ and not is_figure_path(rel_path)
1871
+ )
1872
+
1873
+
1874
+ def _is_hidden_path(rel_path: str) -> bool:
1875
+ return any(part.startswith(".") for part in rel_path.split("/"))
1876
+
1877
+
1878
+ def _is_under_any_dir(rel_path: str, dirs: list[str]) -> bool:
1879
+ """Whether ``rel_path`` equals or lives inside any of ``dirs``."""
1880
+ return any(rel_path == d or rel_path.startswith(d + "/") for d in dirs)
1881
+
1882
+
1883
+ def _collapse_dataset_folders(paths: list[str]) -> list[str]:
1884
+ """Collapse data files into their containing folder.
1885
+
1886
+ A folder holding two or more data files is reported as a single dataset
1887
+ rather than file-by-file. Folders nested inside another collapsed folder
1888
+ are dropped so only the outermost dataset folder remains.
1889
+ """
1890
+ by_dir: dict[str, list[str]] = {}
1891
+ for p in paths:
1892
+ directory = p.rsplit("/", 1)[0] if "/" in p else ""
1893
+ by_dir.setdefault(directory, []).append(p)
1894
+ collapsed: set[str] = set()
1895
+ for directory, group in by_dir.items():
1896
+ if directory and len(group) >= 2:
1897
+ collapsed.add(directory)
1898
+ else:
1899
+ collapsed.update(group)
1900
+ result = sorted(collapsed)
1901
+ return [
1902
+ p
1903
+ for p in result
1904
+ if not any(o != p and p.startswith(o + "/") for o in result)
1905
+ ]
1906
+
1907
+
1908
+ def detect_figures(
1909
+ candidate_paths: list[str],
1910
+ reserved_paths: list[str] | tuple[str, ...] = (),
1911
+ ) -> list[str]:
1912
+ """Auto-detected figure paths among ``candidate_paths``.
1913
+
1914
+ Paths under a dot-directory or under one of ``reserved_paths`` (declared
1915
+ artifacts and pipeline outputs) are skipped.
1916
+ """
1917
+ reserved = list(reserved_paths)
1918
+ return sorted(
1919
+ {
1920
+ p
1921
+ for p in candidate_paths
1922
+ if not _is_hidden_path(p)
1923
+ and not _is_under_any_dir(p, reserved)
1924
+ and is_figure_path(p)
1925
+ }
1926
+ )
1927
+
1928
+
1929
+ def detect_datasets(
1930
+ candidate_paths: list[str],
1931
+ reserved_paths: list[str] | tuple[str, ...] = (),
1932
+ figure_paths: list[str] | tuple[str, ...] = (),
1933
+ ) -> list[str]:
1934
+ """Auto-detected dataset paths among ``candidate_paths``.
1935
+
1936
+ Anything already detected as a figure (``figure_paths``) is excluded, and a
1937
+ folder full of data files collapses to a single dataset entry.
1938
+ """
1939
+ reserved = list(reserved_paths)
1940
+ figset = set(figure_paths)
1941
+ files = {
1942
+ p
1943
+ for p in candidate_paths
1944
+ if not _is_hidden_path(p)
1945
+ and not _is_under_any_dir(p, reserved)
1946
+ and p not in figset
1947
+ and is_dataset_path(p)
1948
+ }
1949
+ return _collapse_dataset_folders(sorted(files))
1950
+
1951
+
1952
+ def list_repo_files(wdir: str | None = None) -> list[str]:
1953
+ """Repo-relative paths of files Git does not ignore.
1954
+
1955
+ Combines tracked and untracked files while honoring ``.gitignore``, so
1956
+ detection skips virtualenvs, ``node_modules``, and other ignored content.
1957
+ Paths are returned relative to ``wdir`` (or the current directory), using
1958
+ POSIX separators; files outside ``wdir`` are omitted.
1959
+ """
1960
+ import calkit.git
1961
+
1962
+ try:
1963
+ repo = calkit.git.get_repo(wdir)
1964
+ except Exception:
1965
+ return []
1966
+ files = calkit.git.ls_files(
1967
+ repo, "--cached", "--others", "--exclude-standard"
1968
+ )
1969
+ repo_root = repo.working_dir
1970
+ base = os.path.abspath(wdir if wdir is not None else ".")
1971
+ rel_files = []
1972
+ for f in files:
1973
+ rel = os.path.relpath(os.path.join(repo_root, f), base)
1974
+ if rel.startswith(".."):
1975
+ continue
1976
+ rel_files.append(Path(rel).as_posix())
1977
+ return rel_files
1978
+
1979
+
1980
+ def list_dvc_tracked_files(wdir: str | None = None) -> list[str]:
1981
+ """Repo-relative paths of files tracked by DVC.
1982
+
1983
+ DVC-tracked artifacts (added via ``dvc add`` or produced as pipeline
1984
+ outputs) are Git-ignored, so they don't appear in :func:`list_repo_files`;
1985
+ include them so figures and datasets stored with DVC are still detected.
1986
+ Returns an empty list when DVC isn't available or the project isn't a DVC
1987
+ repo.
1988
+ """
1989
+ from calkit.dvc.core import list_paths
1990
+
1991
+ try:
1992
+ return [
1993
+ p.replace("\\", "/")
1994
+ for p in list_paths(wdir=wdir, recursive=True)
1995
+ if p
1996
+ ]
1997
+ except Exception:
1998
+ return []
1999
+
2000
+
2001
+ def _reserved_artifact_paths(
2002
+ wdir: str | None = None, ck_info: dict | None = None
2003
+ ) -> list[str]:
2004
+ """Paths of artifacts declared in ``calkit.yaml``.
2005
+
2006
+ These are excluded from auto-detection so a declared figure/dataset isn't
2007
+ reported twice and individual files inside a declared artifact folder aren't
2008
+ listed separately. Pipeline/DVC stage outputs are intentionally *not*
2009
+ reserved: a figure or dataset produced by the pipeline should still be
2010
+ detected (and shown with its producing stage as provenance).
2011
+ """
2012
+ import calkit
2013
+
2014
+ if ck_info is None:
2015
+ ck_info = calkit.load_calkit_info(wdir=wdir)
2016
+ paths: list[str] = []
2017
+ for kind in ("figures", "datasets"):
2018
+ for obj in ck_info.get(kind, []) or []:
2019
+ if isinstance(obj, dict) and isinstance(obj.get("path"), str):
2020
+ paths.append(obj["path"])
2021
+ return [p.replace("\\", "/") for p in paths]
2022
+
2023
+
2024
+ def detect_project_artifacts(
2025
+ wdir: str | None = None, ck_info: dict | None = None
2026
+ ) -> dict[str, list[str]]:
2027
+ """Auto-detect figure and dataset paths in the project at ``wdir``.
2028
+
2029
+ Candidates are the files Git does not ignore plus any files tracked by DVC
2030
+ (which are Git-ignored); declared artifacts are reserved so they aren't
2031
+ detected again.
2032
+ """
2033
+ import calkit
2034
+
2035
+ if ck_info is None:
2036
+ ck_info = calkit.load_calkit_info(wdir=wdir)
2037
+ reserved = _reserved_artifact_paths(wdir=wdir, ck_info=ck_info)
2038
+ candidates = list(
2039
+ dict.fromkeys(
2040
+ [*list_repo_files(wdir=wdir), *list_dvc_tracked_files(wdir=wdir)]
2041
+ )
2042
+ )
2043
+ figures = detect_figures(candidates, reserved_paths=reserved)
2044
+ datasets = detect_datasets(
2045
+ candidates, reserved_paths=reserved, figure_paths=figures
2046
+ )
2047
+ return {"figures": figures, "datasets": datasets}
@@ -209,6 +209,48 @@ pdf("figure.pdf")
209
209
  assert "plot.png" in result["outputs"]
210
210
  assert "clean_data.csv" in result["outputs"]
211
211
  assert "figure.pdf" in result["outputs"]
212
+ # An orchestrator script that sources other scripts via here::here()
213
+ # should recursively attribute their I/O to the stage
214
+ os.makedirs("code", exist_ok=True)
215
+ master_content = """
216
+ here::i_am("code/master.R")
217
+ log_file <- here::here("output", "run.log")
218
+ sink(log_file, split = TRUE)
219
+ source(here::here("code", "clean.R"))
220
+ source(here::here("code", "model.R"))
221
+ sink()
222
+ """
223
+ clean_content = """
224
+ raw <- read_excel("data/raw.xlsx", sheet = "Master")
225
+ saveRDS(raw, file = "data/clean.rds")
226
+ """
227
+ model_content = """
228
+ df <- readRDS("data/clean.rds")
229
+ write.csv(df, file = "output/tables/summary.csv")
230
+ ggsave("output/figures/plot.pdf", p)
231
+ sink("output/tables/model.tex")
232
+ """
233
+ with open("code/master.R", "w") as f:
234
+ f.write(master_content)
235
+ with open("code/clean.R", "w") as f:
236
+ f.write(clean_content)
237
+ with open("code/model.R", "w") as f:
238
+ f.write(model_content)
239
+ master = detect_r_script_io("code/master.R")
240
+ # Sourced scripts are detected as inputs even when wrapped in here::here()
241
+ assert "code/clean.R" in master["inputs"]
242
+ assert "code/model.R" in master["inputs"]
243
+ # I/O from sourced scripts is attributed to the orchestrator
244
+ assert "data/raw.xlsx" in master["inputs"]
245
+ # Named file=/filename= write arguments are detected
246
+ assert "data/clean.rds" in master["outputs"]
247
+ assert "output/tables/summary.csv" in master["outputs"]
248
+ # sink(), here::here()-assigned variables, and ggsave() literals
249
+ assert "output/run.log" in master["outputs"]
250
+ assert "output/tables/model.tex" in master["outputs"]
251
+ assert "output/figures/plot.pdf" in master["outputs"]
252
+ # An intermediate produced and consumed within the stage is not an input
253
+ assert "data/clean.rds" not in master["inputs"]
212
254
 
213
255
 
214
256
  def test_detect_shell_script_io(tmp_dir):
@@ -1033,3 +1075,77 @@ def test_create_files_in_subdirectory(tmp_dir):
1033
1075
 
1034
1076
  assert os.path.exists(output_path)
1035
1077
  assert os.path.exists(".calkit/envs/test-env")
1078
+
1079
+
1080
+ def test_is_figure_path():
1081
+ """Figures must live in a figure-named directory."""
1082
+ from calkit.detect import is_figure_path
1083
+
1084
+ # Detected: image-like extension under a figure directory, at any depth.
1085
+ assert is_figure_path("figures/result.png")
1086
+ assert is_figure_path("paper/figs/plot.pdf")
1087
+ assert is_figure_path("Plots/Fig1.SVG")
1088
+ # Plotly JSON under a figure-only directory counts as a figure.
1089
+ assert is_figure_path("figures/interactive.json")
1090
+ # Not detected: right extension, wrong (or no) directory.
1091
+ assert not is_figure_path("result.png")
1092
+ assert not is_figure_path("results/result.png")
1093
+ assert not is_figure_path("output/plot.pdf")
1094
+ assert not is_figure_path("img/logo.png")
1095
+ # JSON under a data directory is not a figure.
1096
+ assert not is_figure_path("data/records.json")
1097
+ # Non-figure extension under a figure directory.
1098
+ assert not is_figure_path("figures/notes.txt")
1099
+
1100
+
1101
+ def test_is_dataset_path():
1102
+ """Datasets must live in a data-named directory."""
1103
+ from calkit.detect import is_dataset_path
1104
+
1105
+ assert is_dataset_path("data/raw.csv")
1106
+ assert is_dataset_path("datasets/measurements.parquet")
1107
+ assert is_dataset_path("project/Data/records.json")
1108
+ # Wrong directory.
1109
+ assert not is_dataset_path("results/raw.csv")
1110
+ assert not is_dataset_path("raw.csv")
1111
+ # A .json under a figure-only dir is a figure, not a dataset.
1112
+ assert not is_dataset_path("figures/plot.json")
1113
+ # Non-dataset extension under a data directory.
1114
+ assert not is_dataset_path("data/readme.md")
1115
+
1116
+
1117
+ def test_detect_figures():
1118
+ """detect_figures filters hidden and reserved paths."""
1119
+ from calkit.detect import detect_figures
1120
+
1121
+ candidates = [
1122
+ "figures/a.png",
1123
+ "figs/b.pdf",
1124
+ "result.png",
1125
+ ".cache/figures/c.png",
1126
+ "figures/reserved.png",
1127
+ ]
1128
+ out = detect_figures(candidates, reserved_paths=["figures/reserved.png"])
1129
+ assert out == ["figs/b.pdf", "figures/a.png"]
1130
+ # Files inside a reserved directory are excluded too.
1131
+ out = detect_figures(
1132
+ ["figures/sub/x.png", "figures/y.png"], reserved_paths=["figures/sub"]
1133
+ )
1134
+ assert out == ["figures/y.png"]
1135
+
1136
+
1137
+ def test_detect_datasets():
1138
+ """detect_datasets excludes figures and collapses folders."""
1139
+ from calkit.detect import detect_datasets
1140
+
1141
+ candidates = [
1142
+ "data/a.csv",
1143
+ "data/sub/b.csv",
1144
+ "data/sub/c.csv",
1145
+ "figures/plot.json",
1146
+ ".venv/data/d.csv",
1147
+ ]
1148
+ out = detect_datasets(candidates, figure_paths=["figures/plot.json"])
1149
+ # data/sub holds two files, so it collapses to the folder; data/a.csv has
1150
+ # only one file in its folder, so it stays a file.
1151
+ assert out == ["data/a.csv", "data/sub"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calkit-python
3
- Version: 0.41.11
3
+ Version: 0.41.12
4
4
  Summary: Reproducibility simplified.
5
5
  Project-URL: Homepage, https://calkit.org
6
6
  Project-URL: Issues, https://github.com/calkit/calkit/issues
@@ -8,7 +8,7 @@ calkit/config.py,sha256=tkrdAQU1m3__B9efpYgK6IhtLtS3WWFcVpY2zEQpBio,7573
8
8
  calkit/core.py,sha256=On8UGUuR0vKBJHdhEvsPJAk5QD9Lzf3LuWl_gmdfUrI,28179
9
9
  calkit/datasets.py,sha256=9lOlOreey2aJGMw6MiV9MDzSHp-fYkCf1k1AZbWTdQE,2235
10
10
  calkit/dependencies.py,sha256=y5N_4ctwp9rFZVg82Si9_2x8Yh64Ex-WqP0iiN5NNBE,13276
11
- calkit/detect.py,sha256=w8H8r2p0R38645TUWFz9xtmeinKic_T8E6eTuldN6OM,62418
11
+ calkit/detect.py,sha256=fK-x3uBE2i1VaKu8vO_6Llj0tvLilaM3fGyeI6XbxPg,73436
12
12
  calkit/docker.py,sha256=EEcgbXjs-1ttixAZ-G-hmDlvnqSxYbIZmbzeAza37TI,15706
13
13
  calkit/environments.py,sha256=y30tKE0KSRM9ANQwrI15ej2BaXe4s0kUeDm1Vr_BhXA,77230
14
14
  calkit/fs.py,sha256=tVyiSBfWNZJv5qudR-KzzxajPYR2rf-uTNEJSQjMHfI,43818
@@ -38,7 +38,7 @@ calkit/cli/describe.py,sha256=g6WWqa4nHSDda1Jt4D1g-VUm1k4bMskBfgKwDAhUokU,1817
38
38
  calkit/cli/dev.py,sha256=cu4QsfDiA_KurGx4WJcgA2qgfghMoZutE8kl4IZbUcM,1070
39
39
  calkit/cli/import_.py,sha256=5j2ANLq6v7HOku9kJUWylwvQnJfkb_OUKCNOdv1BQXU,18264
40
40
  calkit/cli/latex.py,sha256=dhcbVNcR3YjVSD5mo-OBtZN1FhH4mv4mlGnuP19EbjU,6220
41
- calkit/cli/list.py,sha256=jsSq-6_6iikbhHgU2-zgQgeXm_1FbfigQ6Pww__10o4,6812
41
+ calkit/cli/list.py,sha256=gAkcvF4K59cxrtWQZtBK-i3LhcDlO1r6bGqyfXOMOMw,8947
42
42
  calkit/cli/new.py,sha256=RmQbHtdlMrO6-4mZANq75RPpjvRqzhZUIi1xo724EUU,126211
43
43
  calkit/cli/notebooks.py,sha256=W2EHNLMOx6EuqE1dSFirud6puwayXlg4T8DcC_lSEos,21423
44
44
  calkit/cli/office.py,sha256=jVSTvbl91TCzlglST5O2b8hdApZA_QHw_UiEQFJqxdc,1754
@@ -75,7 +75,7 @@ calkit/tests/test_cloud.py,sha256=583oy2zH0fLexBBB_cmrxCgu7qsouBe7tKY-kkbzhXE,10
75
75
  calkit/tests/test_conda.py,sha256=t-Oy1mL3Vb5njj_t3LQ8HwMhsmLN64HAzSlebnN1A9s,14204
76
76
  calkit/tests/test_core.py,sha256=qZ090GbIu-gqu-0qJqwPj47t7k1Q_N_5mv0sT2VAeZc,5496
77
77
  calkit/tests/test_dependencies.py,sha256=HskXLzbvvRJz2TXnKPpPAo9lPhoZGYQFy4BmEF31nvk,11139
78
- calkit/tests/test_detect.py,sha256=1QLei1SjLdxy3IWwFZi3PIz8WHpKmG50Hjygc_HxYxg,32907
78
+ calkit/tests/test_detect.py,sha256=_dVIMalauvpSFlJQcEuMF33krSsrO9EK2Yelqs0KWpA,37401
79
79
  calkit/tests/test_docker.py,sha256=27d0K1GOWiCNv_5TkZSob6EaC0EtQ9DSBIDfkEAEzGk,2077
80
80
  calkit/tests/test_environments.py,sha256=ms8avkZy70QPD4GSWAFa04O5rhd2oePaBeo0CBgnXAo,40300
81
81
  calkit/tests/test_fs.py,sha256=8XFlC6bsw_aGodhmDq87OnI60L2licz459yUlDcAieY,9448
@@ -118,23 +118,23 @@ calkit/tests/models/test_pipeline.py,sha256=AJsMAX4928U3_Omt0_8K96U5LU_KoNErgpHe
118
118
  calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
119
119
  calkit/agent_skills/conventions/SKILL.md,sha256=nAM2FjSMk6ED56Dr5zh2bL_dhfzDra-h2ZgzHU7ruS4,9524
120
120
  calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
121
- calkit_python-0.41.11.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
122
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/package.json,sha256=INxx8QhvSl7KP6yL7A7-m2-FIsnB__mYEWG_Eq5RP-c,6651
123
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=6w0b5y8LztEv2tErajJ5d39yBryld02crX0CG3zZFNs,6509
124
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
125
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js,sha256=mixXcqFUZukj7_jQVxHTavsYl7cdZv83sEe4phJgkh0,59893
126
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js,sha256=LEEAOkUtQ9KzWEHn-QFv5yGi70B-sBOnrvztzHr0Shw,223
127
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js,sha256=pCoEaqUQj1T4-zAc9SUd__9ZGm7uL2wHQve2xwL89NI,8156
128
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js,sha256=4_nMNAiDSn_cw7UjIHEwone4fizrvqrqSgbwUWvjmoU,114571
129
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js,sha256=n_a0yu_gE7l6fauyoXXv9BZ7ZEYt2387mTfs7CbLcs4,51939
130
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt,sha256=eNJ8gc9n9IF8nW1d9sI9niuHstYzjNz5vqXx9UgWSPc,249
131
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
132
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js,sha256=ZUaa-ZbnqWqpgxIpcQcRX7auvhOBkPH2YHNID5udGQs,8737
133
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=r89Jlk5v1drcwhCpv9FnC3Jig_JH4k24kZNIvxh6Htk,149
134
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
135
- calkit_python-0.41.11.data/data/share/jupyter/labextensions/calkit/install.json,sha256=DK9d8G-q-rMVlcT3rAeGIyo3REWKw6FySBZceLU9yaw,187
136
- calkit_python-0.41.11.dist-info/METADATA,sha256=GJrq8kJ3EmCVwtkdkfWi3bYksvVr1yAg4U4OJTkGNiE,12071
137
- calkit_python-0.41.11.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
138
- calkit_python-0.41.11.dist-info/entry_points.txt,sha256=2iQBBzjTAOdk66CwS-f3ecXXW5DjUqkG1KBRj8mHI1I,133
139
- calkit_python-0.41.11.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
140
- calkit_python-0.41.11.dist-info/RECORD,,
121
+ calkit_python-0.41.12.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
122
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/package.json,sha256=INxx8QhvSl7KP6yL7A7-m2-FIsnB__mYEWG_Eq5RP-c,6651
123
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=6w0b5y8LztEv2tErajJ5d39yBryld02crX0CG3zZFNs,6509
124
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
125
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js,sha256=mixXcqFUZukj7_jQVxHTavsYl7cdZv83sEe4phJgkh0,59893
126
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js,sha256=LEEAOkUtQ9KzWEHn-QFv5yGi70B-sBOnrvztzHr0Shw,223
127
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js,sha256=pCoEaqUQj1T4-zAc9SUd__9ZGm7uL2wHQve2xwL89NI,8156
128
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js,sha256=4_nMNAiDSn_cw7UjIHEwone4fizrvqrqSgbwUWvjmoU,114571
129
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js,sha256=n_a0yu_gE7l6fauyoXXv9BZ7ZEYt2387mTfs7CbLcs4,51939
130
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt,sha256=eNJ8gc9n9IF8nW1d9sI9niuHstYzjNz5vqXx9UgWSPc,249
131
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
132
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js,sha256=ZUaa-ZbnqWqpgxIpcQcRX7auvhOBkPH2YHNID5udGQs,8737
133
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=r89Jlk5v1drcwhCpv9FnC3Jig_JH4k24kZNIvxh6Htk,149
134
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
135
+ calkit_python-0.41.12.data/data/share/jupyter/labextensions/calkit/install.json,sha256=DK9d8G-q-rMVlcT3rAeGIyo3REWKw6FySBZceLU9yaw,187
136
+ calkit_python-0.41.12.dist-info/METADATA,sha256=SAnLxjNtC4KA3GcwLlonzb98yGDPjHyheSdiK3UuzG0,12071
137
+ calkit_python-0.41.12.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
138
+ calkit_python-0.41.12.dist-info/entry_points.txt,sha256=2iQBBzjTAOdk66CwS-f3ecXXW5DjUqkG1KBRj8mHI1I,133
139
+ calkit_python-0.41.12.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
140
+ calkit_python-0.41.12.dist-info/RECORD,,