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.
- calkit/cli/list.py +94 -28
- calkit/detect.py +433 -75
- calkit/tests/test_detect.py +116 -0
- {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/METADATA +1 -1
- {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/RECORD +23 -23
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {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
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.41.11.data → calkit_python-0.41.12.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/WHEEL +0 -0
- {calkit_python-0.41.11.dist-info → calkit_python-0.41.12.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
30
|
-
for
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
#
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
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
|
|
1048
|
-
|
|
1049
|
-
r
|
|
1050
|
-
r
|
|
1051
|
-
r
|
|
1052
|
-
r
|
|
1053
|
-
r
|
|
1054
|
-
r
|
|
1055
|
-
r
|
|
1056
|
-
r
|
|
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
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
|
1078
|
-
|
|
1079
|
-
for match in
|
|
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*
|
|
1085
|
-
r"save_fig\s*\([
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
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}
|
calkit/tests/test_detect.py
CHANGED
|
@@ -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"]
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
122
|
-
calkit_python-0.41.
|
|
123
|
-
calkit_python-0.41.
|
|
124
|
-
calkit_python-0.41.
|
|
125
|
-
calkit_python-0.41.
|
|
126
|
-
calkit_python-0.41.
|
|
127
|
-
calkit_python-0.41.
|
|
128
|
-
calkit_python-0.41.
|
|
129
|
-
calkit_python-0.41.
|
|
130
|
-
calkit_python-0.41.
|
|
131
|
-
calkit_python-0.41.
|
|
132
|
-
calkit_python-0.41.
|
|
133
|
-
calkit_python-0.41.
|
|
134
|
-
calkit_python-0.41.
|
|
135
|
-
calkit_python-0.41.
|
|
136
|
-
calkit_python-0.41.
|
|
137
|
-
calkit_python-0.41.
|
|
138
|
-
calkit_python-0.41.
|
|
139
|
-
calkit_python-0.41.
|
|
140
|
-
calkit_python-0.41.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|