calkit-python 0.41.19__py3-none-any.whl → 0.41.21__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 +35 -1
- calkit/cli/main/core.py +33 -3
- calkit/cli/new.py +4 -8
- calkit/cli/overleaf.py +12 -2
- calkit/detect.py +102 -25
- calkit/dvc/zip.py +17 -0
- calkit/git.py +29 -0
- calkit/models/core.py +11 -1
- calkit/tests/cli/main/test_core.py +90 -0
- calkit/tests/cli/test_list.py +41 -1
- calkit/tests/cli/test_new.py +14 -19
- calkit/tests/cli/test_overleaf.py +47 -0
- calkit/tests/dvc/test_zip.py +25 -0
- calkit/tests/models/test_core.py +22 -1
- calkit/tests/test_detect.py +59 -2
- {calkit_python-0.41.19.dist-info → calkit_python-0.41.21.dist-info}/METADATA +9 -8
- {calkit_python-0.41.19.dist-info → calkit_python-0.41.21.dist-info}/RECORD +34 -35
- {calkit_python-0.41.19.dist-info → calkit_python-0.41.21.dist-info}/WHEEL +1 -1
- calkit_python-0.41.19.data/data/share/jupyter/labextensions/calkit/install.json +0 -5
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.ac9035764d5b2adbb542.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.41.19.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.41.19.dist-info → calkit_python-0.41.21.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.41.19.dist-info → calkit_python-0.41.21.dist-info}/licenses/LICENSE +0 -0
calkit/cli/list.py
CHANGED
|
@@ -174,6 +174,40 @@ def list_presentations(
|
|
|
174
174
|
_list_artifacts("presentations", json_output, declared_only)
|
|
175
175
|
|
|
176
176
|
|
|
177
|
+
def _echo_question(n: int, question: str | dict) -> None:
|
|
178
|
+
"""Print a single question in the human-readable YAML-ish listing format.
|
|
179
|
+
|
|
180
|
+
String questions are shown as a numbered line. Rich (dict) questions are
|
|
181
|
+
shown with their text on the first line followed by hypothesis, answer,
|
|
182
|
+
evidence, and any other fields in YAML-like indented form.
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(question, str):
|
|
185
|
+
typer.echo(f"{n}. {question}")
|
|
186
|
+
return
|
|
187
|
+
# Copy so popping 'question' doesn't mutate the caller's dict.
|
|
188
|
+
question = dict(question)
|
|
189
|
+
text = question.pop("question", "")
|
|
190
|
+
typer.echo(f"{n}. question: {text}")
|
|
191
|
+
for k, v in question.items():
|
|
192
|
+
if isinstance(v, dict):
|
|
193
|
+
typer.echo(f" {k}:")
|
|
194
|
+
for k1, v1 in v.items():
|
|
195
|
+
typer.echo(f" {k1}: {v1}")
|
|
196
|
+
elif isinstance(v, list):
|
|
197
|
+
typer.echo(f" {k}:")
|
|
198
|
+
for item in v:
|
|
199
|
+
if isinstance(item, dict):
|
|
200
|
+
for n1, (k1, v1) in enumerate(item.items()):
|
|
201
|
+
if n1 == 0:
|
|
202
|
+
typer.echo(f" - {k1}: {v1}")
|
|
203
|
+
else:
|
|
204
|
+
typer.echo(f" {k1}: {v1}")
|
|
205
|
+
else:
|
|
206
|
+
typer.echo(f" - {item}")
|
|
207
|
+
else:
|
|
208
|
+
typer.echo(f" {k}: {v}")
|
|
209
|
+
|
|
210
|
+
|
|
177
211
|
@list_app.command(name="questions")
|
|
178
212
|
def list_questions(
|
|
179
213
|
json_output: Annotated[
|
|
@@ -186,7 +220,7 @@ def list_questions(
|
|
|
186
220
|
typer.echo(json.dumps(questions))
|
|
187
221
|
return
|
|
188
222
|
for n, question in enumerate(questions, start=1):
|
|
189
|
-
|
|
223
|
+
_echo_question(n, question)
|
|
190
224
|
|
|
191
225
|
|
|
192
226
|
@list_app.command(name="publications|pubs")
|
calkit/cli/main/core.py
CHANGED
|
@@ -179,11 +179,26 @@ def init(
|
|
|
179
179
|
typer.Option(
|
|
180
180
|
"--force",
|
|
181
181
|
"-f",
|
|
182
|
-
help="
|
|
182
|
+
help="Re-initialize even if the project is already initialized.",
|
|
183
183
|
),
|
|
184
184
|
] = False,
|
|
185
185
|
):
|
|
186
186
|
"""Initialize the current working directory."""
|
|
187
|
+
import git
|
|
188
|
+
from git.exc import InvalidGitRepositoryError
|
|
189
|
+
|
|
190
|
+
def _project_is_initialized() -> bool:
|
|
191
|
+
try:
|
|
192
|
+
git.Repo(".", search_parent_directories=False)
|
|
193
|
+
except InvalidGitRepositoryError:
|
|
194
|
+
return False
|
|
195
|
+
return os.path.isfile(".dvc/config") and os.path.isfile("calkit.yaml")
|
|
196
|
+
|
|
197
|
+
if _project_is_initialized() and not force:
|
|
198
|
+
raise_error(
|
|
199
|
+
"This project is already initialized. "
|
|
200
|
+
"Use --force to re-initialize."
|
|
201
|
+
)
|
|
187
202
|
subprocess.run(["git", "init"])
|
|
188
203
|
result = calkit.dvc.run_dvc_command(
|
|
189
204
|
["init"] + (["--force"] if force else [])
|
|
@@ -197,13 +212,15 @@ def init(
|
|
|
197
212
|
# Commit the newly created .dvc directory
|
|
198
213
|
repo = calkit.git.get_repo()
|
|
199
214
|
repo.git.add(".dvc")
|
|
200
|
-
|
|
215
|
+
if calkit.git.get_staged_files(repo=repo):
|
|
216
|
+
repo.git.commit("-m", "Initialize DVC")
|
|
201
217
|
# Create an empty calkit.yaml if one doesn't already exist
|
|
202
218
|
if not os.path.isfile("calkit.yaml"):
|
|
203
219
|
with open("calkit.yaml", "w"):
|
|
204
220
|
pass
|
|
205
221
|
repo.git.add("calkit.yaml")
|
|
206
|
-
|
|
222
|
+
if calkit.git.get_staged_files(repo=repo):
|
|
223
|
+
repo.git.commit("-m", "Initialize Calkit")
|
|
207
224
|
# TODO: Initialize `dvc.yaml`
|
|
208
225
|
# TODO: Add a sane .gitignore file
|
|
209
226
|
# TODO: Add a sane LICENSE file?
|
|
@@ -769,6 +786,8 @@ def add(
|
|
|
769
786
|
elif to == "git":
|
|
770
787
|
subprocess.call(["git", "add"] + paths)
|
|
771
788
|
elif to == "dvc":
|
|
789
|
+
for path in paths:
|
|
790
|
+
calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
|
|
772
791
|
calkit.dvc.run_dvc_command(["add"] + paths)
|
|
773
792
|
elif to == "dvc-zip":
|
|
774
793
|
for path in paths:
|
|
@@ -882,6 +901,7 @@ def add(
|
|
|
882
901
|
f"Adding {path} to DVC since it's already tracked "
|
|
883
902
|
"with DVC"
|
|
884
903
|
)
|
|
904
|
+
calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
|
|
885
905
|
calkit.dvc.run_dvc_command(["add", path])
|
|
886
906
|
elif posix_path in pipeline_output_storage:
|
|
887
907
|
# Respect storage explicitly set in the pipeline definition
|
|
@@ -926,6 +946,7 @@ def add(
|
|
|
926
946
|
typer.echo(f"Would add {path} to DVC (per extension)")
|
|
927
947
|
else:
|
|
928
948
|
typer.echo(f"Adding {path} to DVC per its extension")
|
|
949
|
+
calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
|
|
929
950
|
calkit.dvc.run_dvc_command(["add", path])
|
|
930
951
|
elif calkit.dvc.zip.is_zip_candidate(path):
|
|
931
952
|
if dry_run:
|
|
@@ -946,6 +967,7 @@ def add(
|
|
|
946
967
|
typer.echo(
|
|
947
968
|
f"Adding {path} to DVC since it's greater than 1 MB"
|
|
948
969
|
)
|
|
970
|
+
calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
|
|
949
971
|
calkit.dvc.run_dvc_command(["add", path])
|
|
950
972
|
else:
|
|
951
973
|
if dry_run:
|
|
@@ -3201,6 +3223,14 @@ def call_dvc(
|
|
|
3201
3223
|
and DVC is not installed.
|
|
3202
3224
|
"""
|
|
3203
3225
|
result = calkit.dvc.run_dvc_command(sys.argv[2:])
|
|
3226
|
+
if result != 0 and len(sys.argv) > 2 and sys.argv[2] == "add":
|
|
3227
|
+
typer.secho(
|
|
3228
|
+
"Hint: If DVC failed because a .dvc pointer file is git-ignored, "
|
|
3229
|
+
"use `calkit add --to=dvc <path>` instead so Calkit can "
|
|
3230
|
+
"automatically manage the gitignore exception.",
|
|
3231
|
+
fg=typer.colors.YELLOW,
|
|
3232
|
+
err=True,
|
|
3233
|
+
)
|
|
3204
3234
|
sys.exit(result)
|
|
3205
3235
|
|
|
3206
3236
|
|
calkit/cli/new.py
CHANGED
|
@@ -3174,7 +3174,7 @@ def new_release(
|
|
|
3174
3174
|
detected_kind, artifact = calkit.releases.find_artifact(path, ck_info)
|
|
3175
3175
|
if detected_kind is None:
|
|
3176
3176
|
detected_kind = calkit.detect.detect_artifact_kind(
|
|
3177
|
-
pathlib.Path(path).as_posix()
|
|
3177
|
+
pathlib.Path(path).as_posix(),
|
|
3178
3178
|
)
|
|
3179
3179
|
if release_kind is None:
|
|
3180
3180
|
if detected_kind is None:
|
|
@@ -3258,16 +3258,12 @@ def new_release(
|
|
|
3258
3258
|
# stored as-is (renamed); folders and whole-project releases are
|
|
3259
3259
|
# zipped.
|
|
3260
3260
|
project_name = calkit.detect_project_name(prepend_owner=False)
|
|
3261
|
-
if path == ".":
|
|
3261
|
+
if path == "." or os.path.isdir(path):
|
|
3262
3262
|
stored_filename = f"{project_name}-{name}.zip"
|
|
3263
3263
|
is_zip = True
|
|
3264
|
-
elif os.path.isdir(path):
|
|
3265
|
-
folder_name = os.path.basename(os.path.normpath(path))
|
|
3266
|
-
stored_filename = f"{project_name}-{folder_name}-{name}.zip"
|
|
3267
|
-
is_zip = True
|
|
3268
3264
|
elif os.path.isfile(path):
|
|
3269
|
-
|
|
3270
|
-
stored_filename = f"{project_name}-{
|
|
3265
|
+
_, ext = os.path.splitext(os.path.basename(path))
|
|
3266
|
+
stored_filename = f"{project_name}-{name}{ext}"
|
|
3271
3267
|
is_zip = False
|
|
3272
3268
|
else:
|
|
3273
3269
|
raise_error(f"Release path '{path}' does not exist")
|
calkit/cli/overleaf.py
CHANGED
|
@@ -437,7 +437,12 @@ def sync(
|
|
|
437
437
|
raise_error("No Overleaf sync info found")
|
|
438
438
|
overleaf_sync_dirs = list(overleaf_info.keys())
|
|
439
439
|
if paths is not None:
|
|
440
|
-
paths = [
|
|
440
|
+
paths = [
|
|
441
|
+
(os.path.dirname(p) if os.path.isfile(p) else p)
|
|
442
|
+
.strip()
|
|
443
|
+
.rstrip("/\\")
|
|
444
|
+
for p in paths
|
|
445
|
+
]
|
|
441
446
|
for path in paths:
|
|
442
447
|
if path not in overleaf_sync_dirs:
|
|
443
448
|
raise_error(f"Path '{path}' is not synced with Overleaf")
|
|
@@ -611,7 +616,12 @@ def get_status(
|
|
|
611
616
|
raise_error("No Overleaf sync info found")
|
|
612
617
|
overleaf_sync_dirs = list(overleaf_info.keys())
|
|
613
618
|
if paths is not None:
|
|
614
|
-
paths = [
|
|
619
|
+
paths = [
|
|
620
|
+
(os.path.dirname(p) if os.path.isfile(p) else p)
|
|
621
|
+
.strip()
|
|
622
|
+
.rstrip("/\\")
|
|
623
|
+
for p in paths
|
|
624
|
+
]
|
|
615
625
|
for path in paths:
|
|
616
626
|
if path not in overleaf_sync_dirs:
|
|
617
627
|
raise_error(f"Path '{path}' is not synced with Overleaf")
|
calkit/detect.py
CHANGED
|
@@ -13,6 +13,8 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Literal
|
|
15
15
|
|
|
16
|
+
import pathspec
|
|
17
|
+
|
|
16
18
|
NotebookLanguage = Literal["python", "julia", "r"]
|
|
17
19
|
|
|
18
20
|
|
|
@@ -1549,6 +1551,20 @@ def _is_stdlib_module(module_name: str) -> bool:
|
|
|
1549
1551
|
return base_module in common_stdlib
|
|
1550
1552
|
|
|
1551
1553
|
|
|
1554
|
+
_PYTHON_IMPORT_TO_DISTRIBUTION = {
|
|
1555
|
+
"sklearn": "scikit-learn",
|
|
1556
|
+
"skimage": "scikit-image",
|
|
1557
|
+
"cv2": "opencv-python",
|
|
1558
|
+
"PIL": "Pillow",
|
|
1559
|
+
"bs4": "beautifulsoup4",
|
|
1560
|
+
"yaml": "PyYAML",
|
|
1561
|
+
"dateutil": "python-dateutil",
|
|
1562
|
+
"dotenv": "python-dotenv",
|
|
1563
|
+
"serial": "pyserial",
|
|
1564
|
+
"git": "GitPython",
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
|
|
1552
1568
|
def detect_python_dependencies(
|
|
1553
1569
|
script_path: str | None = None,
|
|
1554
1570
|
code: str | None = None,
|
|
@@ -1614,13 +1630,21 @@ def detect_python_dependencies(
|
|
|
1614
1630
|
if not _is_stdlib_module(module_name):
|
|
1615
1631
|
# Get the top-level package name
|
|
1616
1632
|
top_level = module_name.split(".")[0]
|
|
1617
|
-
dependencies.add(
|
|
1633
|
+
dependencies.add(
|
|
1634
|
+
_PYTHON_IMPORT_TO_DISTRIBUTION.get(
|
|
1635
|
+
top_level, top_level
|
|
1636
|
+
)
|
|
1637
|
+
)
|
|
1618
1638
|
elif isinstance(node, ast.ImportFrom):
|
|
1619
1639
|
if node.module and node.level == 0:
|
|
1620
1640
|
module_name = node.module
|
|
1621
1641
|
if not _is_stdlib_module(module_name):
|
|
1622
1642
|
top_level = module_name.split(".")[0]
|
|
1623
|
-
dependencies.add(
|
|
1643
|
+
dependencies.add(
|
|
1644
|
+
_PYTHON_IMPORT_TO_DISTRIBUTION.get(
|
|
1645
|
+
top_level, top_level
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1624
1648
|
return sorted(list(dependencies))
|
|
1625
1649
|
|
|
1626
1650
|
|
|
@@ -1927,6 +1951,21 @@ PRESENTATION_NAMES = {
|
|
|
1927
1951
|
"talk.pdf",
|
|
1928
1952
|
}
|
|
1929
1953
|
|
|
1954
|
+
DETECTION_IGNORE_FPATH = os.path.join(".calkit", "ignore")
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def load_detection_ignore(wdir: str | None = None) -> pathspec.PathSpec | None:
|
|
1958
|
+
"""Load ``.calkit/ignore`` artifact detection exclusions."""
|
|
1959
|
+
import pathspec
|
|
1960
|
+
|
|
1961
|
+
fpath = DETECTION_IGNORE_FPATH
|
|
1962
|
+
if wdir is not None:
|
|
1963
|
+
fpath = os.path.join(wdir, fpath)
|
|
1964
|
+
if not os.path.isfile(fpath):
|
|
1965
|
+
return None
|
|
1966
|
+
with open(fpath, encoding="utf-8") as f:
|
|
1967
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", f)
|
|
1968
|
+
|
|
1930
1969
|
|
|
1931
1970
|
def _ancestor_dir_names(rel_path: str) -> set[str]:
|
|
1932
1971
|
"""Lower-cased names of the ancestor directories of a "/"-separated path."""
|
|
@@ -1938,8 +1977,12 @@ def _path_ext(rel_path: str) -> str:
|
|
|
1938
1977
|
return ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ""
|
|
1939
1978
|
|
|
1940
1979
|
|
|
1941
|
-
def is_figure_path(
|
|
1980
|
+
def is_figure_path(
|
|
1981
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
1982
|
+
) -> bool:
|
|
1942
1983
|
"""Whether a repo-relative path looks like an auto-detectable figure."""
|
|
1984
|
+
if ignore is not None and ignore.match_file(rel_path):
|
|
1985
|
+
return False
|
|
1943
1986
|
ancestors = _ancestor_dir_names(rel_path)
|
|
1944
1987
|
ext = _path_ext(rel_path)
|
|
1945
1988
|
if ext in FIGURE_EXTENSIONS and ancestors & FIGURE_DIRS:
|
|
@@ -1951,37 +1994,49 @@ def is_figure_path(rel_path: str) -> bool:
|
|
|
1951
1994
|
return False
|
|
1952
1995
|
|
|
1953
1996
|
|
|
1954
|
-
def is_dataset_path(
|
|
1997
|
+
def is_dataset_path(
|
|
1998
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
1999
|
+
) -> bool:
|
|
1955
2000
|
"""Whether a repo-relative path looks like an auto-detectable dataset."""
|
|
2001
|
+
if ignore is not None and ignore.match_file(rel_path):
|
|
2002
|
+
return False
|
|
1956
2003
|
return (
|
|
1957
2004
|
_path_ext(rel_path) in DATASET_EXTENSIONS
|
|
1958
2005
|
and bool(_ancestor_dir_names(rel_path) & DATA_DIRS)
|
|
1959
|
-
and not is_figure_path(rel_path)
|
|
2006
|
+
and not is_figure_path(rel_path, ignore=ignore)
|
|
1960
2007
|
)
|
|
1961
2008
|
|
|
1962
2009
|
|
|
1963
|
-
def is_result_path(
|
|
2010
|
+
def is_result_path(
|
|
2011
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
2012
|
+
) -> bool:
|
|
1964
2013
|
"""Whether a repo-relative path looks like an auto-detectable result.
|
|
1965
2014
|
|
|
1966
2015
|
A result is a data-like file (JSON, CSV, etc.) under a ``results``-style
|
|
1967
2016
|
directory. Anything already detected as a figure or dataset is excluded.
|
|
1968
2017
|
"""
|
|
2018
|
+
if ignore is not None and ignore.match_file(rel_path):
|
|
2019
|
+
return False
|
|
1969
2020
|
return (
|
|
1970
2021
|
_path_ext(rel_path) in RESULT_EXTENSIONS
|
|
1971
2022
|
and bool(_ancestor_dir_names(rel_path) & RESULT_DIRS)
|
|
1972
|
-
and not is_figure_path(rel_path)
|
|
1973
|
-
and not is_dataset_path(rel_path)
|
|
2023
|
+
and not is_figure_path(rel_path, ignore=ignore)
|
|
2024
|
+
and not is_dataset_path(rel_path, ignore=ignore)
|
|
1974
2025
|
)
|
|
1975
2026
|
|
|
1976
2027
|
|
|
1977
|
-
def is_presentation_path(
|
|
2028
|
+
def is_presentation_path(
|
|
2029
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
2030
|
+
) -> bool:
|
|
1978
2031
|
"""Whether a repo-relative path looks like an auto-detectable presentation.
|
|
1979
2032
|
|
|
1980
2033
|
Either a slide-deck file (PDF/PPTX/KEY) under a ``slides``/``presentations``
|
|
1981
2034
|
directory, or a file with a presentation-like name (e.g. ``slides.pdf``,
|
|
1982
2035
|
``presentation.pdf``) anywhere. Figures are excluded.
|
|
1983
2036
|
"""
|
|
1984
|
-
if
|
|
2037
|
+
if ignore is not None and ignore.match_file(rel_path):
|
|
2038
|
+
return False
|
|
2039
|
+
if is_figure_path(rel_path, ignore=ignore):
|
|
1985
2040
|
return False
|
|
1986
2041
|
name = rel_path.rsplit("/", 1)[-1].lower()
|
|
1987
2042
|
if name in PRESENTATION_NAMES:
|
|
@@ -2018,7 +2073,9 @@ PUBLICATION_NAMES = {
|
|
|
2018
2073
|
}
|
|
2019
2074
|
|
|
2020
2075
|
|
|
2021
|
-
def is_publication_path(
|
|
2076
|
+
def is_publication_path(
|
|
2077
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
2078
|
+
) -> bool:
|
|
2022
2079
|
"""Whether a repo-relative path looks like an auto-detectable publication.
|
|
2023
2080
|
|
|
2024
2081
|
Either a document (PDF/TeX/DOCX) under a ``paper``/``publication``/
|
|
@@ -2026,7 +2083,11 @@ def is_publication_path(rel_path: str) -> bool:
|
|
|
2026
2083
|
``manuscript.pdf``, ``main.tex``) anywhere. Figures and presentations are
|
|
2027
2084
|
excluded.
|
|
2028
2085
|
"""
|
|
2029
|
-
if
|
|
2086
|
+
if ignore is not None and ignore.match_file(rel_path):
|
|
2087
|
+
return False
|
|
2088
|
+
if is_figure_path(rel_path, ignore=ignore) or is_presentation_path(
|
|
2089
|
+
rel_path, ignore=ignore
|
|
2090
|
+
):
|
|
2030
2091
|
return False
|
|
2031
2092
|
name = rel_path.rsplit("/", 1)[-1].lower()
|
|
2032
2093
|
if name in PUBLICATION_NAMES:
|
|
@@ -2036,19 +2097,21 @@ def is_publication_path(rel_path: str) -> bool:
|
|
|
2036
2097
|
)
|
|
2037
2098
|
|
|
2038
2099
|
|
|
2039
|
-
def detect_artifact_kind(
|
|
2100
|
+
def detect_artifact_kind(
|
|
2101
|
+
rel_path: str, *, ignore: pathspec.PathSpec | None = None
|
|
2102
|
+
) -> str | None:
|
|
2040
2103
|
"""Infer an artifact's release kind from its repo-relative path.
|
|
2041
2104
|
|
|
2042
2105
|
Returns one of ``"figure"``, ``"presentation"``, ``"publication"``, or
|
|
2043
2106
|
``"dataset"``, or ``None`` if the path doesn't look like any of these.
|
|
2044
2107
|
"""
|
|
2045
|
-
if is_figure_path(rel_path):
|
|
2108
|
+
if is_figure_path(rel_path, ignore=ignore):
|
|
2046
2109
|
return "figure"
|
|
2047
|
-
if is_presentation_path(rel_path):
|
|
2110
|
+
if is_presentation_path(rel_path, ignore=ignore):
|
|
2048
2111
|
return "presentation"
|
|
2049
|
-
if is_publication_path(rel_path):
|
|
2112
|
+
if is_publication_path(rel_path, ignore=ignore):
|
|
2050
2113
|
return "publication"
|
|
2051
|
-
if is_dataset_path(rel_path):
|
|
2114
|
+
if is_dataset_path(rel_path, ignore=ignore):
|
|
2052
2115
|
return "dataset"
|
|
2053
2116
|
return None
|
|
2054
2117
|
|
|
@@ -2090,6 +2153,7 @@ def _collapse_dataset_folders(paths: list[str]) -> list[str]:
|
|
|
2090
2153
|
def detect_figures(
|
|
2091
2154
|
candidate_paths: list[str],
|
|
2092
2155
|
reserved_paths: list[str] | tuple[str, ...] = (),
|
|
2156
|
+
ignore: pathspec.PathSpec | None = None,
|
|
2093
2157
|
) -> list[str]:
|
|
2094
2158
|
"""Auto-detected figure paths among ``candidate_paths``.
|
|
2095
2159
|
|
|
@@ -2103,7 +2167,7 @@ def detect_figures(
|
|
|
2103
2167
|
for p in candidate_paths
|
|
2104
2168
|
if not _is_hidden_path(p)
|
|
2105
2169
|
and not _is_under_any_dir(p, reserved)
|
|
2106
|
-
and is_figure_path(p)
|
|
2170
|
+
and is_figure_path(p, ignore=ignore)
|
|
2107
2171
|
}
|
|
2108
2172
|
)
|
|
2109
2173
|
|
|
@@ -2112,6 +2176,7 @@ def detect_datasets(
|
|
|
2112
2176
|
candidate_paths: list[str],
|
|
2113
2177
|
reserved_paths: list[str] | tuple[str, ...] = (),
|
|
2114
2178
|
figure_paths: list[str] | tuple[str, ...] = (),
|
|
2179
|
+
ignore: pathspec.PathSpec | None = None,
|
|
2115
2180
|
) -> list[str]:
|
|
2116
2181
|
"""Auto-detected dataset paths among ``candidate_paths``.
|
|
2117
2182
|
|
|
@@ -2126,7 +2191,7 @@ def detect_datasets(
|
|
|
2126
2191
|
if not _is_hidden_path(p)
|
|
2127
2192
|
and not _is_under_any_dir(p, reserved)
|
|
2128
2193
|
and p not in figset
|
|
2129
|
-
and is_dataset_path(p)
|
|
2194
|
+
and is_dataset_path(p, ignore=ignore)
|
|
2130
2195
|
}
|
|
2131
2196
|
return _collapse_dataset_folders(sorted(files))
|
|
2132
2197
|
|
|
@@ -2134,6 +2199,7 @@ def detect_datasets(
|
|
|
2134
2199
|
def detect_results(
|
|
2135
2200
|
candidate_paths: list[str],
|
|
2136
2201
|
reserved_paths: list[str] | tuple[str, ...] = (),
|
|
2202
|
+
ignore: pathspec.PathSpec | None = None,
|
|
2137
2203
|
) -> list[str]:
|
|
2138
2204
|
"""Auto-detected result paths among ``candidate_paths``.
|
|
2139
2205
|
|
|
@@ -2147,7 +2213,7 @@ def detect_results(
|
|
|
2147
2213
|
for p in candidate_paths
|
|
2148
2214
|
if not _is_hidden_path(p)
|
|
2149
2215
|
and not _is_under_any_dir(p, reserved)
|
|
2150
|
-
and is_result_path(p)
|
|
2216
|
+
and is_result_path(p, ignore=ignore)
|
|
2151
2217
|
}
|
|
2152
2218
|
)
|
|
2153
2219
|
|
|
@@ -2155,6 +2221,7 @@ def detect_results(
|
|
|
2155
2221
|
def detect_presentations(
|
|
2156
2222
|
candidate_paths: list[str],
|
|
2157
2223
|
reserved_paths: list[str] | tuple[str, ...] = (),
|
|
2224
|
+
ignore: pathspec.PathSpec | None = None,
|
|
2158
2225
|
) -> list[str]:
|
|
2159
2226
|
"""Auto-detected presentation paths among ``candidate_paths``."""
|
|
2160
2227
|
reserved = list(reserved_paths)
|
|
@@ -2164,7 +2231,7 @@ def detect_presentations(
|
|
|
2164
2231
|
for p in candidate_paths
|
|
2165
2232
|
if not _is_hidden_path(p)
|
|
2166
2233
|
and not _is_under_any_dir(p, reserved)
|
|
2167
|
-
and is_presentation_path(p)
|
|
2234
|
+
and is_presentation_path(p, ignore=ignore)
|
|
2168
2235
|
}
|
|
2169
2236
|
)
|
|
2170
2237
|
|
|
@@ -2254,18 +2321,28 @@ def detect_project_artifacts(
|
|
|
2254
2321
|
|
|
2255
2322
|
if ck_info is None:
|
|
2256
2323
|
ck_info = calkit.load_calkit_info(wdir=wdir)
|
|
2324
|
+
ignore = load_detection_ignore(wdir=wdir)
|
|
2257
2325
|
reserved = _reserved_artifact_paths(wdir=wdir, ck_info=ck_info)
|
|
2258
2326
|
candidates = list(
|
|
2259
2327
|
dict.fromkeys(
|
|
2260
2328
|
[*list_repo_files(wdir=wdir), *list_dvc_tracked_files(wdir=wdir)]
|
|
2261
2329
|
)
|
|
2262
2330
|
)
|
|
2263
|
-
figures = detect_figures(
|
|
2331
|
+
figures = detect_figures(
|
|
2332
|
+
candidates, reserved_paths=reserved, ignore=ignore
|
|
2333
|
+
)
|
|
2264
2334
|
datasets = detect_datasets(
|
|
2265
|
-
candidates,
|
|
2335
|
+
candidates,
|
|
2336
|
+
reserved_paths=reserved,
|
|
2337
|
+
figure_paths=figures,
|
|
2338
|
+
ignore=ignore,
|
|
2339
|
+
)
|
|
2340
|
+
results = detect_results(
|
|
2341
|
+
candidates, reserved_paths=reserved, ignore=ignore
|
|
2342
|
+
)
|
|
2343
|
+
presentations = detect_presentations(
|
|
2344
|
+
candidates, reserved_paths=reserved, ignore=ignore
|
|
2266
2345
|
)
|
|
2267
|
-
results = detect_results(candidates, reserved_paths=reserved)
|
|
2268
|
-
presentations = detect_presentations(candidates, reserved_paths=reserved)
|
|
2269
2346
|
return {
|
|
2270
2347
|
"figures": figures,
|
|
2271
2348
|
"datasets": datasets,
|
calkit/dvc/zip.py
CHANGED
|
@@ -499,6 +499,23 @@ def _sync_one(
|
|
|
499
499
|
# pipeline output that hasn't been produced yet on a fresh run)
|
|
500
500
|
if workspace_hash is None and zip_hash is None:
|
|
501
501
|
return None
|
|
502
|
+
# Workspace doesn't exist with no prior sync record while syncing to-zip —
|
|
503
|
+
# there's nothing to push into the zip (e.g., the zip was pulled but
|
|
504
|
+
# never unzipped into the workspace)
|
|
505
|
+
if (
|
|
506
|
+
workspace_hash is None
|
|
507
|
+
and last_sync_record is None
|
|
508
|
+
and direction == "to-zip"
|
|
509
|
+
):
|
|
510
|
+
return None
|
|
511
|
+
# Zip doesn't exist with no prior sync record while syncing to-workspace —
|
|
512
|
+
# there's nothing to pull into the workspace
|
|
513
|
+
if (
|
|
514
|
+
zip_hash is None
|
|
515
|
+
and last_sync_record is None
|
|
516
|
+
and direction == "to-workspace"
|
|
517
|
+
):
|
|
518
|
+
return None
|
|
502
519
|
# Deletion + change on the other side is a conflict
|
|
503
520
|
if workspace_deleted and zip_changed and direction == "both":
|
|
504
521
|
raise RuntimeError(
|
calkit/git.py
CHANGED
|
@@ -337,3 +337,32 @@ def ensure_path_is_not_ignored(
|
|
|
337
337
|
if target_repo.ignored(target_path) and _depth < 10:
|
|
338
338
|
ensure_path_is_not_ignored(target_repo, target_path, _depth + 1)
|
|
339
339
|
return True
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def ensure_dvc_pointer_is_not_ignored(repo, path: str) -> None:
|
|
343
|
+
"""Ensure the .dvc pointer for ``path`` will not be Git-ignored.
|
|
344
|
+
|
|
345
|
+
A broad pattern in a ``.gitignore`` (e.g. ``*.pdf*``) can also match the
|
|
346
|
+
``<path>.dvc`` pointer DVC commits to Git, causing ``dvc add`` to fail with
|
|
347
|
+
"bad DVC file name ... is git-ignored". This appends a ``!*.dvc`` negation
|
|
348
|
+
to the ``.gitignore`` in the pointer's own directory (which wins under Git
|
|
349
|
+
precedence) so pointers stay tracked. Idempotent.
|
|
350
|
+
"""
|
|
351
|
+
path = path.replace("\\", "/").rstrip("/")
|
|
352
|
+
pointer = path + ".dvc"
|
|
353
|
+
if pointer not in repo.ignored(pointer):
|
|
354
|
+
return
|
|
355
|
+
pointer_dir = os.path.dirname(pointer)
|
|
356
|
+
gitignore_path = os.path.join(repo.working_dir, pointer_dir, ".gitignore")
|
|
357
|
+
exception = "!*.dvc"
|
|
358
|
+
existing_lines = []
|
|
359
|
+
if os.path.isfile(gitignore_path):
|
|
360
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
361
|
+
existing_lines = f.read().splitlines()
|
|
362
|
+
if exception in existing_lines:
|
|
363
|
+
return # Already present
|
|
364
|
+
os.makedirs(os.path.dirname(gitignore_path), exist_ok=True)
|
|
365
|
+
with open(gitignore_path, "a", encoding="utf-8") as f:
|
|
366
|
+
if existing_lines and existing_lines[-1] != "":
|
|
367
|
+
f.write("\n")
|
|
368
|
+
f.write(exception + "\n")
|
calkit/models/core.py
CHANGED
|
@@ -351,13 +351,23 @@ class ResultsEvidence(BaseModel):
|
|
|
351
351
|
explanation: str | None = None
|
|
352
352
|
|
|
353
353
|
|
|
354
|
+
class PublicationEvidence(BaseModel):
|
|
355
|
+
"""Evidence in the form of a publication."""
|
|
356
|
+
|
|
357
|
+
kind: Literal["publication"] = "publication"
|
|
358
|
+
path: str
|
|
359
|
+
explanation: str | None = None
|
|
360
|
+
|
|
361
|
+
|
|
354
362
|
class Question(BaseModel):
|
|
355
363
|
"""A question the project hopes to answer."""
|
|
356
364
|
|
|
357
365
|
question: str
|
|
358
366
|
hypothesis: str | None = None
|
|
359
367
|
answer: str | None = None
|
|
360
|
-
evidence:
|
|
368
|
+
evidence: (
|
|
369
|
+
list[FigureEvidence | ResultsEvidence | PublicationEvidence] | None
|
|
370
|
+
) = None
|
|
361
371
|
|
|
362
372
|
|
|
363
373
|
class Dependency(BaseModel):
|
|
@@ -60,6 +60,16 @@ def test_init(tmp_dir):
|
|
|
60
60
|
subprocess.check_call(["calkit", "init"])
|
|
61
61
|
assert os.path.isfile("calkit.yaml")
|
|
62
62
|
assert calkit.load_calkit_info() == {}
|
|
63
|
+
# Already initialized: init without --force fails and does not clobber
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["calkit", "init"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
)
|
|
69
|
+
assert result.returncode != 0
|
|
70
|
+
assert "already initialized" in result.stderr.lower()
|
|
71
|
+
assert "use --force" in result.stderr.lower()
|
|
72
|
+
assert calkit.load_calkit_info() == {}
|
|
63
73
|
# init must not clobber a pre-existing calkit.yaml
|
|
64
74
|
os.makedirs("sub")
|
|
65
75
|
ck_info = {"name": "test-project"}
|
|
@@ -67,6 +77,19 @@ def test_init(tmp_dir):
|
|
|
67
77
|
calkit.ryaml.dump(ck_info, f)
|
|
68
78
|
subprocess.check_call(["calkit", "init"], cwd="sub")
|
|
69
79
|
assert calkit.load_calkit_info(wdir="sub") == ck_info
|
|
80
|
+
# Fully initialized project blocks init without --force
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["calkit", "init"],
|
|
83
|
+
cwd="sub",
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
)
|
|
87
|
+
assert result.returncode != 0
|
|
88
|
+
assert "already initialized" in result.stderr.lower()
|
|
89
|
+
assert calkit.load_calkit_info(wdir="sub") == ck_info
|
|
90
|
+
# --force allows re-initialization without clobbering calkit.yaml
|
|
91
|
+
subprocess.check_call(["calkit", "init", "--force"], cwd="sub")
|
|
92
|
+
assert calkit.load_calkit_info(wdir="sub") == ck_info
|
|
70
93
|
|
|
71
94
|
|
|
72
95
|
@skipif_windows_docker
|
|
@@ -1813,3 +1836,70 @@ def test_run_downstream_does_not_submit_unrelated_sweep(tmp_dir):
|
|
|
1813
1836
|
assert os.path.exists("other.txt")
|
|
1814
1837
|
assert not os.path.exists("out-1.txt")
|
|
1815
1838
|
assert not os.path.exists("out-2.txt")
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def test_add_dvc_pointer_unignored(tmp_dir):
|
|
1842
|
+
"""Test that calkit add --to=dvc ensures the .dvc pointer is unignored."""
|
|
1843
|
+
subprocess.check_call(["git", "init"])
|
|
1844
|
+
subprocess.check_call(["calkit", "init"])
|
|
1845
|
+
os.makedirs("pubs/defense/ppt")
|
|
1846
|
+
with open("pubs/defense/ppt/.gitignore", "w") as f:
|
|
1847
|
+
f.write("*.pdf*\n")
|
|
1848
|
+
pdf_path = "pubs/defense/ppt/McCabe.pdf"
|
|
1849
|
+
with open(pdf_path, "w") as f:
|
|
1850
|
+
f.write("dummy pdf content")
|
|
1851
|
+
subprocess.check_call(["calkit", "add", "--to=dvc", pdf_path])
|
|
1852
|
+
# Assert pointer exists
|
|
1853
|
+
dvc_file = f"{pdf_path}.dvc"
|
|
1854
|
+
assert os.path.exists(dvc_file)
|
|
1855
|
+
# Assert pointer is NOT ignored (should return 1)
|
|
1856
|
+
res = subprocess.run(["git", "check-ignore", "-q", dvc_file])
|
|
1857
|
+
assert res.returncode == 1
|
|
1858
|
+
# Assert data file IS still ignored
|
|
1859
|
+
res_data = subprocess.run(["git", "check-ignore", "-q", pdf_path])
|
|
1860
|
+
assert res_data.returncode == 0
|
|
1861
|
+
# Assert it was staged
|
|
1862
|
+
staged = calkit.git.get_staged_files()
|
|
1863
|
+
assert dvc_file in staged
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def test_add_dvc_pointer_unignored_idempotent(tmp_dir):
|
|
1867
|
+
"""Test that the unignore logic does not duplicate lines."""
|
|
1868
|
+
subprocess.check_call(["git", "init"])
|
|
1869
|
+
subprocess.check_call(["calkit", "init"])
|
|
1870
|
+
os.makedirs("data")
|
|
1871
|
+
with open("data/.gitignore", "w") as f:
|
|
1872
|
+
f.write("*.pdf*\n")
|
|
1873
|
+
pdf_path = "data/doc.pdf"
|
|
1874
|
+
with open(pdf_path, "w") as f:
|
|
1875
|
+
f.write("doc")
|
|
1876
|
+
subprocess.check_call(["calkit", "add", "--to=dvc", pdf_path])
|
|
1877
|
+
with open("data/.gitignore") as f:
|
|
1878
|
+
content1 = f.read()
|
|
1879
|
+
# Second add should not duplicate
|
|
1880
|
+
subprocess.check_call(["calkit", "add", "--to=dvc", pdf_path])
|
|
1881
|
+
with open("data/.gitignore") as f:
|
|
1882
|
+
content2 = f.read()
|
|
1883
|
+
assert content1 == content2
|
|
1884
|
+
assert content1.count("!*.dvc") == 1
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def test_call_dvc_passthrough_hint(tmp_dir):
|
|
1888
|
+
"""Test that calkit dvc add prints a hint if .dvc is ignored."""
|
|
1889
|
+
subprocess.check_call(["git", "init"])
|
|
1890
|
+
subprocess.check_call(["calkit", "init"])
|
|
1891
|
+
os.makedirs("data")
|
|
1892
|
+
with open("data/.gitignore", "w") as f:
|
|
1893
|
+
f.write("*.pdf*\n")
|
|
1894
|
+
pdf_path = "data/doc.pdf"
|
|
1895
|
+
with open(pdf_path, "w") as f:
|
|
1896
|
+
f.write("doc")
|
|
1897
|
+
# Run the passthrough dvc add which will fail because it's ignored
|
|
1898
|
+
res = subprocess.run(
|
|
1899
|
+
["calkit", "dvc", "add", pdf_path], capture_output=True, text=True
|
|
1900
|
+
)
|
|
1901
|
+
assert res.returncode != 0
|
|
1902
|
+
assert (
|
|
1903
|
+
"Hint: If DVC failed because a .dvc pointer file is git-ignored"
|
|
1904
|
+
in res.stderr
|
|
1905
|
+
)
|
calkit/tests/cli/test_list.py
CHANGED
|
@@ -91,12 +91,52 @@ def test_list_presentations(tmp_dir):
|
|
|
91
91
|
def test_list_questions(tmp_dir):
|
|
92
92
|
subprocess.check_call(["calkit", "init"])
|
|
93
93
|
subprocess.check_call(["calkit", "new", "question", "Does it work?"])
|
|
94
|
+
ck_info = calkit.load_calkit_info()
|
|
95
|
+
ck_info["questions"].append(
|
|
96
|
+
{
|
|
97
|
+
"question": "What is the effect?",
|
|
98
|
+
"hypothesis": "It improves performance.",
|
|
99
|
+
"answer": "It improves performance by 10%.",
|
|
100
|
+
"evidence": [
|
|
101
|
+
{"kind": "figure", "path": "figures/performance.png"},
|
|
102
|
+
{
|
|
103
|
+
"kind": "result",
|
|
104
|
+
"path": "results/metrics.json",
|
|
105
|
+
"key": "accuracy",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"kind": "publication",
|
|
109
|
+
"path": "paper/paper.pdf",
|
|
110
|
+
"explanation": "See the results section.",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
with open("calkit.yaml", "w") as f:
|
|
116
|
+
calkit.ryaml.dump(ck_info, f)
|
|
94
117
|
out = subprocess.check_output(
|
|
95
118
|
["calkit", "list", "questions", "--json"], text=True
|
|
96
119
|
)
|
|
97
|
-
|
|
120
|
+
questions = json.loads(out)
|
|
121
|
+
assert questions[0] == "Does it work?"
|
|
122
|
+
assert questions[1]["question"] == "What is the effect?"
|
|
123
|
+
assert questions[1]["hypothesis"] == "It improves performance."
|
|
124
|
+
assert questions[1]["evidence"][2]["kind"] == "publication"
|
|
125
|
+
assert questions[1]["evidence"][2]["path"] == "paper/paper.pdf"
|
|
98
126
|
out = subprocess.check_output(["calkit", "list", "questions"], text=True)
|
|
99
127
|
assert "1. Does it work?" in out
|
|
128
|
+
assert "2. question: What is the effect?" in out
|
|
129
|
+
assert "hypothesis: It improves performance." in out
|
|
130
|
+
assert "answer: It improves performance by 10%." in out
|
|
131
|
+
assert "evidence:" in out
|
|
132
|
+
assert "- kind: figure" in out
|
|
133
|
+
assert "path: figures/performance.png" in out
|
|
134
|
+
assert "- kind: result" in out
|
|
135
|
+
assert "path: results/metrics.json" in out
|
|
136
|
+
assert "key: accuracy" in out
|
|
137
|
+
assert "- kind: publication" in out
|
|
138
|
+
assert "path: paper/paper.pdf" in out
|
|
139
|
+
assert "explanation: See the results section." in out
|
|
100
140
|
|
|
101
141
|
|
|
102
142
|
def test_list_remotes(tmp_dir):
|
calkit/tests/cli/test_new.py
CHANGED
|
@@ -760,7 +760,7 @@ def test_new_nix_env_stages_flake(tmp_dir):
|
|
|
760
760
|
)
|
|
761
761
|
assert os.path.isfile("flake.nix")
|
|
762
762
|
repo = git.Repo(".")
|
|
763
|
-
head_files = [item.path for item in repo.head.commit.tree.traverse()]
|
|
763
|
+
head_files = [item.path for item in repo.head.commit.tree.traverse()] # type: ignore
|
|
764
764
|
assert "flake.nix" in head_files
|
|
765
765
|
assert "calkit.yaml" in head_files
|
|
766
766
|
|
|
@@ -991,26 +991,26 @@ def test_new_internal_release(tmp_dir):
|
|
|
991
991
|
"--to",
|
|
992
992
|
"none",
|
|
993
993
|
"--name",
|
|
994
|
-
"v0",
|
|
994
|
+
"slides-v0",
|
|
995
995
|
"--desc",
|
|
996
|
-
"First version sent
|
|
996
|
+
"First version sent for review.",
|
|
997
997
|
"--no-push",
|
|
998
998
|
]
|
|
999
999
|
)
|
|
1000
1000
|
ck_info = calkit.load_calkit_info()
|
|
1001
|
-
assert "v0" in ck_info["releases"]
|
|
1002
|
-
release = ck_info["releases"]["v0"]
|
|
1001
|
+
assert "slides-v0" in ck_info["releases"]
|
|
1002
|
+
release = ck_info["releases"]["slides-v0"]
|
|
1003
1003
|
assert release["internal"] is True
|
|
1004
1004
|
assert release["kind"] == "presentation"
|
|
1005
1005
|
assert release["doi"] is None
|
|
1006
1006
|
assert release["calkit_version"] == calkit.__version__
|
|
1007
|
-
assert release["description"] == "First version sent
|
|
1007
|
+
assert release["description"] == "First version sent for review."
|
|
1008
1008
|
assert (
|
|
1009
1009
|
release["stored_path"]
|
|
1010
|
-
== ".calkit/releases/v0/test-project-slides-v0.pdf"
|
|
1010
|
+
== ".calkit/releases/slides-v0/test-project-slides-v0.pdf"
|
|
1011
1011
|
)
|
|
1012
1012
|
assert os.path.isfile(release["stored_path"])
|
|
1013
|
-
assert "v0" in [t.name for t in git.Repo().tags]
|
|
1013
|
+
assert "slides-v0" in [t.name for t in git.Repo().tags]
|
|
1014
1014
|
# Internal release of a folder: zipped with a self-describing name
|
|
1015
1015
|
subprocess.check_call(
|
|
1016
1016
|
[
|
|
@@ -1020,14 +1020,15 @@ def test_new_internal_release(tmp_dir):
|
|
|
1020
1020
|
"data/raw",
|
|
1021
1021
|
"--internal",
|
|
1022
1022
|
"--name",
|
|
1023
|
-
"v1",
|
|
1023
|
+
"raw-data-v1",
|
|
1024
1024
|
"--no-push",
|
|
1025
1025
|
]
|
|
1026
1026
|
)
|
|
1027
|
-
release = calkit.load_calkit_info()["releases"]["v1"]
|
|
1027
|
+
release = calkit.load_calkit_info()["releases"]["raw-data-v1"]
|
|
1028
1028
|
assert release["kind"] == "dataset"
|
|
1029
1029
|
assert (
|
|
1030
|
-
release["stored_path"]
|
|
1030
|
+
release["stored_path"]
|
|
1031
|
+
== ".calkit/releases/raw-data-v1/test-project-raw-data-v1.zip"
|
|
1031
1032
|
)
|
|
1032
1033
|
assert os.path.isfile(release["stored_path"])
|
|
1033
1034
|
# Internal release of the whole project: zipped archive
|
|
@@ -1068,10 +1069,7 @@ def test_new_internal_release(tmp_dir):
|
|
|
1068
1069
|
)
|
|
1069
1070
|
release = calkit.load_calkit_info()["releases"]["v3"]
|
|
1070
1071
|
assert release["kind"] == "figure"
|
|
1071
|
-
assert
|
|
1072
|
-
release["stored_path"]
|
|
1073
|
-
== ".calkit/releases/v3/test-project-plot-v3.png"
|
|
1074
|
-
)
|
|
1072
|
+
assert release["stored_path"] == ".calkit/releases/v3/test-project-v3.png"
|
|
1075
1073
|
# A path that isn't a defined artifact should fail kind detection
|
|
1076
1074
|
with open("notes.txt", "w") as f:
|
|
1077
1075
|
f.write("hello\n")
|
|
@@ -1110,10 +1108,7 @@ def test_new_internal_release(tmp_dir):
|
|
|
1110
1108
|
)
|
|
1111
1109
|
release = calkit.load_calkit_info()["releases"]["v5"]
|
|
1112
1110
|
assert release["kind"] == "dataset"
|
|
1113
|
-
assert
|
|
1114
|
-
release["stored_path"]
|
|
1115
|
-
== ".calkit/releases/v5/test-project-notes-v5.txt"
|
|
1116
|
-
)
|
|
1111
|
+
assert release["stored_path"] == ".calkit/releases/v5/test-project-v5.txt"
|
|
1117
1112
|
# Listing releases must not warn about publication status for internal
|
|
1118
1113
|
# releases, which have no publisher or record ID
|
|
1119
1114
|
result = subprocess.run(
|
|
@@ -362,3 +362,50 @@ def test_extract_title_from_tex(tmp_dir):
|
|
|
362
362
|
f.write(tex)
|
|
363
363
|
title = _extract_title_from_tex("test.tex")
|
|
364
364
|
assert title == "My Cool Paper"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_overleaf_sync_trailing_slash_or_space(tmp_dir):
|
|
368
|
+
pid = str(uuid.uuid4())
|
|
369
|
+
ol_repo = _make_temp_overleaf_project(pid)
|
|
370
|
+
ol_repo.git.config(["receive.denyCurrentBranch", "ignore"])
|
|
371
|
+
subprocess.run(["calkit", "init"], check=True)
|
|
372
|
+
repo = git.Repo()
|
|
373
|
+
tmp_remote = (
|
|
374
|
+
Path(tempfile.gettempdir()) / "overleaf-sync-remotes" / pid
|
|
375
|
+
).as_posix()
|
|
376
|
+
os.makedirs(tmp_remote, exist_ok=True)
|
|
377
|
+
remote_repo = git.Repo.init(path=tmp_remote, bare=True)
|
|
378
|
+
remote_repo.git.config(["receive.denyCurrentBranch", "ignore"])
|
|
379
|
+
repo.git.config(["push.autoSetupRemote", "true"])
|
|
380
|
+
repo.git.remote(["add", "origin", tmp_remote])
|
|
381
|
+
ol_url = calkit.overleaf.get_git_remote_url(pid, "no token")
|
|
382
|
+
assert os.environ["CALKIT_ENV"] == "test"
|
|
383
|
+
assert os.environ["CALKIT_TEST_OVERLEAF_TOKEN"] == "none"
|
|
384
|
+
config = calkit.config.read()
|
|
385
|
+
assert config.overleaf_token == "none"
|
|
386
|
+
subprocess.run(
|
|
387
|
+
[
|
|
388
|
+
"calkit",
|
|
389
|
+
"overleaf",
|
|
390
|
+
"import",
|
|
391
|
+
ol_url,
|
|
392
|
+
"pubs/applied-ocean-research",
|
|
393
|
+
"--title",
|
|
394
|
+
"Test Pub",
|
|
395
|
+
],
|
|
396
|
+
check=True,
|
|
397
|
+
)
|
|
398
|
+
# Try syncing with a trailing slash
|
|
399
|
+
res = subprocess.run(
|
|
400
|
+
["calkit", "overleaf", "sync", "pubs/applied-ocean-research/"],
|
|
401
|
+
capture_output=True,
|
|
402
|
+
text=True,
|
|
403
|
+
)
|
|
404
|
+
assert res.returncode == 0, f"Trailing slash sync failed: {res.stderr}"
|
|
405
|
+
# Try syncing with a trailing space
|
|
406
|
+
res2 = subprocess.run(
|
|
407
|
+
["calkit", "overleaf", "sync", "pubs/applied-ocean-research "],
|
|
408
|
+
capture_output=True,
|
|
409
|
+
text=True,
|
|
410
|
+
)
|
|
411
|
+
assert res2.returncode == 0, f"Trailing space sync failed: {res2.stderr}"
|
calkit/tests/dvc/test_zip.py
CHANGED
|
@@ -303,3 +303,28 @@ def test_sync_one(tmp_dir, monkeypatch):
|
|
|
303
303
|
assert result is not None
|
|
304
304
|
assert (src7 / "a.txt").read_text() == "hello"
|
|
305
305
|
assert get_sync_record(str(src7.as_posix())) is not None
|
|
306
|
+
# Zip exists but workspace was never unzipped (no sync record) with
|
|
307
|
+
# direction=to-zip: nothing to push, skip without error
|
|
308
|
+
src8 = tmp_dir / "src8"
|
|
309
|
+
src8.mkdir()
|
|
310
|
+
(src8 / "a.txt").write_text("hello")
|
|
311
|
+
zip_out8 = str(tmp_dir / calkit.dvc.zip.ZIPS_DIR / "src8.zip")
|
|
312
|
+
zip_(str(src8), zip_out8)
|
|
313
|
+
shutil.rmtree(str(src8))
|
|
314
|
+
write_zip_path_map({str(src8.as_posix()): zip_out8})
|
|
315
|
+
result = sync_one(str(src8), zip_out8, direction="to-zip")
|
|
316
|
+
assert result is None
|
|
317
|
+
assert os.path.isfile(zip_out8)
|
|
318
|
+
assert not os.path.exists(str(src8))
|
|
319
|
+
# Workspace exists but zip was never created (no sync record) with
|
|
320
|
+
# direction=to-workspace: nothing to pull, skip without error
|
|
321
|
+
src9 = tmp_dir / "src9"
|
|
322
|
+
src9.mkdir()
|
|
323
|
+
(src9 / "a.txt").write_text("hello")
|
|
324
|
+
zip_out9 = str(tmp_dir / calkit.dvc.zip.ZIPS_DIR / "src9.zip")
|
|
325
|
+
write_zip_path_map({str(src9.as_posix()): zip_out9})
|
|
326
|
+
result = sync_one(str(src9), zip_out9, direction="to-workspace")
|
|
327
|
+
assert result is None
|
|
328
|
+
assert not os.path.exists(zip_out9)
|
|
329
|
+
assert get_sync_record(str(src9.as_posix())) is None
|
|
330
|
+
assert (src9 / "a.txt").read_text() == "hello"
|
calkit/tests/models/test_core.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import pytest
|
|
4
4
|
from pydantic import ValidationError
|
|
5
5
|
|
|
6
|
-
from calkit.models.core import ProjectInfo, Publication
|
|
6
|
+
from calkit.models.core import ProjectInfo, Publication, Question
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def test_publication_kind_no_longer_allows_presentation():
|
|
@@ -23,3 +23,24 @@ def test_project_info_has_results_and_presentations():
|
|
|
23
23
|
)
|
|
24
24
|
assert info.results[0].path == "results/metrics.json"
|
|
25
25
|
assert info.presentations[0].path == "slides/talk.pdf"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_question_accepts_publication_evidence():
|
|
29
|
+
q = Question.model_validate(
|
|
30
|
+
{
|
|
31
|
+
"question": "Does the paper support this?",
|
|
32
|
+
"answer": "Yes.",
|
|
33
|
+
"evidence": [
|
|
34
|
+
{"kind": "publication", "path": "paper/paper.pdf"},
|
|
35
|
+
{
|
|
36
|
+
"kind": "publication",
|
|
37
|
+
"path": "paper/supplement.pdf",
|
|
38
|
+
"explanation": "See Table 1.",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
assert q.evidence is not None
|
|
44
|
+
assert q.evidence[0].kind == "publication"
|
|
45
|
+
assert q.evidence[0].path == "paper/paper.pdf"
|
|
46
|
+
assert q.evidence[1].explanation == "See Table 1."
|
calkit/tests/test_detect.py
CHANGED
|
@@ -821,7 +821,7 @@ data = pd.read_csv("data.csv")
|
|
|
821
821
|
deps = detect_python_dependencies(script_path="script.py")
|
|
822
822
|
assert "numpy" in deps
|
|
823
823
|
assert "pandas" in deps
|
|
824
|
-
assert "
|
|
824
|
+
assert "scikit-learn" in deps
|
|
825
825
|
assert "matplotlib" in deps
|
|
826
826
|
# stdlib modules should not be included
|
|
827
827
|
assert "os" not in deps
|
|
@@ -841,6 +841,19 @@ import json # stdlib
|
|
|
841
841
|
assert "json" not in deps
|
|
842
842
|
|
|
843
843
|
|
|
844
|
+
def test_detect_python_dependencies_distribution_mapping():
|
|
845
|
+
"""Test mapping of Python import names to distribution names."""
|
|
846
|
+
code = """
|
|
847
|
+
import sklearn
|
|
848
|
+
import cv2
|
|
849
|
+
"""
|
|
850
|
+
deps = detect_python_dependencies(code=code)
|
|
851
|
+
assert "scikit-learn" in deps
|
|
852
|
+
assert "opencv-python" in deps
|
|
853
|
+
assert "sklearn" not in deps
|
|
854
|
+
assert "cv2" not in deps
|
|
855
|
+
|
|
856
|
+
|
|
844
857
|
def test_detect_r_dependencies_from_script(tmp_dir):
|
|
845
858
|
"""Test detection of R dependencies from a script."""
|
|
846
859
|
script_content = """
|
|
@@ -1025,7 +1038,7 @@ def test_detect_dependencies_from_python_notebook(tmp_dir):
|
|
|
1025
1038
|
deps = detect_dependencies_from_notebook("notebook.ipynb")
|
|
1026
1039
|
assert "numpy" in deps
|
|
1027
1040
|
assert "pandas" in deps
|
|
1028
|
-
assert "
|
|
1041
|
+
assert "scikit-learn" in deps
|
|
1029
1042
|
assert "matplotlib" in deps
|
|
1030
1043
|
assert "requests" in deps
|
|
1031
1044
|
|
|
@@ -1296,3 +1309,47 @@ def test_detect_project_artifacts_includes_results_and_presentations(tmp_dir):
|
|
|
1296
1309
|
out = calkit.detect.detect_project_artifacts(ck_info={})
|
|
1297
1310
|
assert "results/metrics.json" in out["results"]
|
|
1298
1311
|
assert "slides/deck.pdf" in out["presentations"]
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def test_detection_ignore(tmp_dir):
|
|
1315
|
+
from calkit.detect import (
|
|
1316
|
+
detect_artifact_kind,
|
|
1317
|
+
detect_figures,
|
|
1318
|
+
load_detection_ignore,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
# (e) No ignore file -> unchanged detection.
|
|
1322
|
+
assert load_detection_ignore() is None
|
|
1323
|
+
assert detect_figures(["figures/a.png"]) == ["figures/a.png"]
|
|
1324
|
+
os.makedirs(".calkit", exist_ok=True)
|
|
1325
|
+
with open(".calkit/ignore", "w") as f:
|
|
1326
|
+
f.write(
|
|
1327
|
+
"\n".join(
|
|
1328
|
+
[
|
|
1329
|
+
"# this is a comment",
|
|
1330
|
+
"",
|
|
1331
|
+
"paper/figs/report.pdf",
|
|
1332
|
+
"*.ipynb",
|
|
1333
|
+
"*.csv",
|
|
1334
|
+
"notebooks/scratch/",
|
|
1335
|
+
]
|
|
1336
|
+
)
|
|
1337
|
+
)
|
|
1338
|
+
ignore = load_detection_ignore()
|
|
1339
|
+
# (a) a path listed in .calkit/ignore is not detected as ANY artifact type
|
|
1340
|
+
path = "paper/figs/report.pdf"
|
|
1341
|
+
assert detect_artifact_kind(path) == "figure"
|
|
1342
|
+
assert detect_artifact_kind(path, ignore=ignore) is None
|
|
1343
|
+
assert path not in detect_figures([path], ignore=ignore)
|
|
1344
|
+
# (b) comments and blank lines are ignored (implied by parsing succeeding)
|
|
1345
|
+
assert ignore is not None
|
|
1346
|
+
assert not ignore.match_file("# this is a comment")
|
|
1347
|
+
# (c) a glob pattern excludes matching files
|
|
1348
|
+
globbed = "data/raw.csv"
|
|
1349
|
+
assert detect_artifact_kind(globbed) == "dataset"
|
|
1350
|
+
assert detect_artifact_kind(globbed, ignore=ignore) is None
|
|
1351
|
+
assert ignore.match_file("analysis.ipynb") is True
|
|
1352
|
+
# (d) a directory pattern excludes files under it
|
|
1353
|
+
in_dir = "notebooks/scratch/data/raw.parquet"
|
|
1354
|
+
assert detect_artifact_kind(in_dir) == "dataset"
|
|
1355
|
+
assert detect_artifact_kind(in_dir, ignore=ignore) is None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: calkit-python
|
|
3
|
-
Version: 0.41.
|
|
3
|
+
Version: 0.41.21
|
|
4
4
|
Summary: Reproducibility simplified.
|
|
5
5
|
Project-URL: Homepage, https://calkit.org
|
|
6
6
|
Project-URL: Issues, https://github.com/calkit/calkit/issues
|
|
@@ -26,7 +26,7 @@ Requires-Dist: arithmeval
|
|
|
26
26
|
Requires-Dist: bibtexparser
|
|
27
27
|
Requires-Dist: checksumdir
|
|
28
28
|
Requires-Dist: docx2pdf
|
|
29
|
-
Requires-Dist: dvc==3.67.1
|
|
29
|
+
Requires-Dist: dvc[all]==3.67.1
|
|
30
30
|
Requires-Dist: fastapi
|
|
31
31
|
Requires-Dist: fsspec>=2026.4.0
|
|
32
32
|
Requires-Dist: gitpython
|
|
@@ -78,15 +78,16 @@ Description-Content-Type: text/markdown
|
|
|
78
78
|
|
|
79
79
|
<!-- INCLUDE: docs/index.md -->
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
Typical research workflows are horizontally-siloed, i.e.,
|
|
82
|
+
various stages--data collection, analysis, writing--are performed in
|
|
83
|
+
disconnected systems,
|
|
84
|
+
turning research into a slow, error-prone, and tedious
|
|
85
|
+
[waterfall](https://en.wikipedia.org/wiki/Waterfall_model) process.
|
|
85
86
|
|
|
86
87
|
Calkit helps you integrate code, data, figures, results, publications,
|
|
87
88
|
and more into a cohesive, traceable, and portable _knowledge creation system_,
|
|
88
|
-
so every output can be traced back to its source
|
|
89
|
-
single command.
|
|
89
|
+
so every output can be traced back to its source (provenance)
|
|
90
|
+
and reproduced with a single command.
|
|
90
91
|
|
|
91
92
|
With industry standard tools combined into a unified and simplified experience
|
|
92
93
|
tailored for research,
|
|
@@ -8,11 +8,11 @@ calkit/config.py,sha256=tkrdAQU1m3__B9efpYgK6IhtLtS3WWFcVpY2zEQpBio,7573
|
|
|
8
8
|
calkit/core.py,sha256=ferYD6jmvbZ1h1MlNApmuHiMokrLPynWtuU62wudzsc,28534
|
|
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=IbkeuBUSk_6bjDK3G-HD7dDAAIp_OzYN3IRREasOaj8,83519
|
|
12
12
|
calkit/docker.py,sha256=EEcgbXjs-1ttixAZ-G-hmDlvnqSxYbIZmbzeAza37TI,15706
|
|
13
13
|
calkit/environments.py,sha256=PoAzpD3Esuupn3AGnR8J9ffvjHcxdBH1SQTXLxB1fY0,78060
|
|
14
14
|
calkit/fs.py,sha256=dKWUfi51F1ka7uikn_O__jxvPOUwY05ZmydxE1KaJUA,45459
|
|
15
|
-
calkit/git.py,sha256=
|
|
15
|
+
calkit/git.py,sha256=2o94iQkz8-kv6p-VgvKML-waWJOo79ts8eDrpcF0C8M,14602
|
|
16
16
|
calkit/github.py,sha256=8_yZ0ej8TKM4qk8iPeGq47R1I4MHdSoJ_Zpr1gRwNEM,1701
|
|
17
17
|
calkit/gui.py,sha256=2UCrMyosc5v4CLlDcHO7oDwW9eLn5_WbNaLaG5bjMTc,23
|
|
18
18
|
calkit/install.py,sha256=n989NLyZSTndj_rEaHceSip5aXP7zZIlG9QwbFs8kTc,7913
|
|
@@ -39,23 +39,23 @@ calkit/cli/describe.py,sha256=g6WWqa4nHSDda1Jt4D1g-VUm1k4bMskBfgKwDAhUokU,1817
|
|
|
39
39
|
calkit/cli/dev.py,sha256=cu4QsfDiA_KurGx4WJcgA2qgfghMoZutE8kl4IZbUcM,1070
|
|
40
40
|
calkit/cli/import_.py,sha256=5j2ANLq6v7HOku9kJUWylwvQnJfkb_OUKCNOdv1BQXU,18264
|
|
41
41
|
calkit/cli/latex.py,sha256=Zqw7EyodVohUqC1iR1i5QmPRMYg8quHwJdgS8I684ag,7739
|
|
42
|
-
calkit/cli/list.py,sha256=
|
|
43
|
-
calkit/cli/new.py,sha256=
|
|
42
|
+
calkit/cli/list.py,sha256=jzkZC7dgbmYz8aCBfEWibu_ZRm6i0xwXmIgbUR4NZgA,12040
|
|
43
|
+
calkit/cli/new.py,sha256=UDFUi3_MvQcI7q5VWFnRRrX7pW6KCoTcssmJtxkfXpw,137435
|
|
44
44
|
calkit/cli/notebooks.py,sha256=mMNHE8yT0BDN0iJE07GCBFAseyDF0jSRshIm1CQjgrY,24645
|
|
45
45
|
calkit/cli/office.py,sha256=jVSTvbl91TCzlglST5O2b8hdApZA_QHw_UiEQFJqxdc,1754
|
|
46
|
-
calkit/cli/overleaf.py,sha256=
|
|
46
|
+
calkit/cli/overleaf.py,sha256=xgcONFdyItlUHUfSD_iF-bhjddKO_-ObtHz_Khiz-xE,25384
|
|
47
47
|
calkit/cli/scheduler.py,sha256=OWJpW57G6Ebs5FTZ_nf2PpEG9QNd6WPe0mb3TmbVTyE,42402
|
|
48
48
|
calkit/cli/update.py,sha256=5Jjo-5uRTxd5UWHvOETPYFWtJJRr81qmKmsibthkLPk,43501
|
|
49
49
|
calkit/cli/main/__init__.py,sha256=qu1POZPyqs33ZKfasOxv_Wc-EzVcEgK3Dt_vwFL8Bi8,65
|
|
50
|
-
calkit/cli/main/core.py,sha256=
|
|
50
|
+
calkit/cli/main/core.py,sha256=1qP6pJB7j8Xsxt51naLhCh3A_PebeZrjVeVTuwC5XLs,125197
|
|
51
51
|
calkit/cli/main/xr.py,sha256=Bug5qJuiIq0tusmdu30t-ZdyaT_Sy5RwPSTiHvCtIqE,25600
|
|
52
52
|
calkit/dvc/__init__.py,sha256=-B6tilCb_jw7QFmJ2srILez24wDhNKUzIBhZUMt01dE,381
|
|
53
53
|
calkit/dvc/core.py,sha256=p42i_uCzgZD6ClGnLOn88zaiGTwrdSG09quL8Hb8Aas,30908
|
|
54
|
-
calkit/dvc/zip.py,sha256=
|
|
54
|
+
calkit/dvc/zip.py,sha256=nf4EzOyc5nI_OA3M6S7wwZpL7N4EQz9yCf8FbnPjlxM,21857
|
|
55
55
|
calkit/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
56
|
calkit/jupyterlab/routes.py,sha256=jnNd289CpNQFY6am5p3qaiL_Tcnkd6J_vDVdjIXlb1A,51465
|
|
57
57
|
calkit/models/__init__.py,sha256=vGYF4MZBGzDk1BPBzpp1bRb59RCJ2Bj2kzHuVVRacug,120
|
|
58
|
-
calkit/models/core.py,sha256=
|
|
58
|
+
calkit/models/core.py,sha256=zyfNnq5hPoWW7CFkT3LN4hEBOqJzMu-zCs3V6j2z0DE,13549
|
|
59
59
|
calkit/models/io.py,sha256=FgItZDxrSyph91YRMOEfJo9xWmy3drMin_632vy9WhA,1087
|
|
60
60
|
calkit/models/iteration.py,sha256=lodRMjj818ysec303PkuepOUr3dMPOa0cwKmTUaw9Ok,2947
|
|
61
61
|
calkit/models/pipeline.py,sha256=DhAEXahgaBBI4KqHn0mii2w2t5fE5mzH95jLfdCTY_U,49130
|
|
@@ -76,7 +76,7 @@ calkit/tests/test_cloud.py,sha256=yCSuMq0o-i-kN-uWdvbSSBfz6mgKpS_hC3cFA-4DKjs,13
|
|
|
76
76
|
calkit/tests/test_conda.py,sha256=t-Oy1mL3Vb5njj_t3LQ8HwMhsmLN64HAzSlebnN1A9s,14204
|
|
77
77
|
calkit/tests/test_core.py,sha256=CMhTx89ZpaHTK4nvll2S5M0NLJoBAuH_OBPhq9oEFfM,6272
|
|
78
78
|
calkit/tests/test_dependencies.py,sha256=HskXLzbvvRJz2TXnKPpPAo9lPhoZGYQFy4BmEF31nvk,11139
|
|
79
|
-
calkit/tests/test_detect.py,sha256=
|
|
79
|
+
calkit/tests/test_detect.py,sha256=TRrcjp5HF9FgfGZbP1KZGk-oVI08J2rSsAwJoyRxq0Y,45136
|
|
80
80
|
calkit/tests/test_docker.py,sha256=27d0K1GOWiCNv_5TkZSob6EaC0EtQ9DSBIDfkEAEzGk,2077
|
|
81
81
|
calkit/tests/test_environments.py,sha256=ms8avkZy70QPD4GSWAFa04O5rhd2oePaBeo0CBgnXAo,40300
|
|
82
82
|
calkit/tests/test_fs.py,sha256=merhHGLypHTI4qP5qp0L9QFzKtJ7NcOQ2WzH4_CUnF0,13635
|
|
@@ -99,47 +99,46 @@ calkit/tests/cli/test_config.py,sha256=Tl4LWfi81eQo6fUgJsoZRNPj_LfkKH-s42HRg5Mxh
|
|
|
99
99
|
calkit/tests/cli/test_delete.py,sha256=qIbgwKc8nBjmkynzyVuVAjrQB96XSmGRet_jfxs19_0,750
|
|
100
100
|
calkit/tests/cli/test_import.py,sha256=e2BToVPebf4NalFQuIgyTNJHwbc9VMc6AYO-FAhYbQ0,663
|
|
101
101
|
calkit/tests/cli/test_latex.py,sha256=3pG7o-56Rc9_kPbfkoCJ9DKojfNPTQY8yl5JB3SFh2Y,4608
|
|
102
|
-
calkit/tests/cli/test_list.py,sha256=
|
|
103
|
-
calkit/tests/cli/test_new.py,sha256=
|
|
102
|
+
calkit/tests/cli/test_list.py,sha256=bY74qAHlejgF04EIQgwR5RSdv0Oss3nUkLXcUiSoeFo,5491
|
|
103
|
+
calkit/tests/cli/test_new.py,sha256=G1CpwlhV7a4oiGYep0DgolaAHOrdxQshxE7fE_EeD8M,53283
|
|
104
104
|
calkit/tests/cli/test_notebooks.py,sha256=2t1KiGhEz9H9LcIdWy4jGM05REUxJpWiIg6fSpX12ME,10678
|
|
105
|
-
calkit/tests/cli/test_overleaf.py,sha256=
|
|
105
|
+
calkit/tests/cli/test_overleaf.py,sha256=tgGCuOpic0owtOPD_CtZDnffPkknC_73CSDpOT9S3io,18159
|
|
106
106
|
calkit/tests/cli/test_scheduler.py,sha256=mE0KLwcb9gozOnylTLsaL_xYGAptlD9hB8ZiKpPhmcc,17934
|
|
107
107
|
calkit/tests/cli/test_update.py,sha256=bwRF0kpqbCVxNvGH777LHFX3oUP2DyN3XXUBDCEKNoc,6356
|
|
108
108
|
calkit/tests/cli/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
109
|
-
calkit/tests/cli/main/test_core.py,sha256=
|
|
109
|
+
calkit/tests/cli/main/test_core.py,sha256=1OqdxnCreG-OqBMgOOYQDrBRjoE2eyECYEQW6nQcqxM,69420
|
|
110
110
|
calkit/tests/cli/main/test_lock.py,sha256=1iX8iSbGtr4O9OkaAi4d3gP3_QzNl6Rr6HLtbEpS6iU,9847
|
|
111
111
|
calkit/tests/cli/main/test_subprojects.py,sha256=ZR2ZQvZ4XZOi-iW8nlZEYu_2CUyUWoc0MABmnFrF9DQ,12730
|
|
112
112
|
calkit/tests/cli/main/test_xr.py,sha256=shk8LHS33bZpr_iAW5eA5sMD3nQDKm8fIwXlE6dA2uI,22527
|
|
113
113
|
calkit/tests/dvc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
114
114
|
calkit/tests/dvc/test_core.py,sha256=ywXBEyw1XpoSuu1EnMbFl-ebbdEJjpaBEPG157_3RBY,12914
|
|
115
|
-
calkit/tests/dvc/test_zip.py,sha256=
|
|
115
|
+
calkit/tests/dvc/test_zip.py,sha256=UjluBZYTnw5nisHELz42VRPDk88b0TvDx4QJNVaR-iA,12369
|
|
116
116
|
calkit/tests/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
117
117
|
calkit/tests/jupyterlab/conftest.py,sha256=Xsh5Zwg0fdqEFPkHDgG7oCHhHJj6WuZLWbmsl7ATABA,207
|
|
118
118
|
calkit/tests/jupyterlab/test_routes.py,sha256=TEEyoHiKnzzKOkQsJcMPKdqpKFQ25yBCvVSquqe9nsE,408
|
|
119
119
|
calkit/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
|
-
calkit/tests/models/test_core.py,sha256=
|
|
120
|
+
calkit/tests/models/test_core.py,sha256=PkB8WcTSCqOxoGESM395b4GXwTXSCzx6QxUK-bamNAM,1558
|
|
121
121
|
calkit/tests/models/test_iteration.py,sha256=vjuDKwyYQAV4w1qF9VJWQEHksq3KRd6gtT39_a-RhnI,644
|
|
122
122
|
calkit/tests/models/test_pipeline.py,sha256=9F0g6ij7vc8c2hIRIVTHpT0I-JHXC6i2NRB2O9JyrOo,16680
|
|
123
123
|
calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
|
|
124
124
|
calkit/agent_skills/conventions/SKILL.md,sha256=nAM2FjSMk6ED56Dr5zh2bL_dhfzDra-h2ZgzHU7ruS4,9524
|
|
125
125
|
calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
|
|
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.
|
|
141
|
-
calkit_python-0.41.
|
|
142
|
-
calkit_python-0.41.
|
|
143
|
-
calkit_python-0.41.
|
|
144
|
-
calkit_python-0.41.
|
|
145
|
-
calkit_python-0.41.19.dist-info/RECORD,,
|
|
126
|
+
calkit_python-0.41.21.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
|
|
127
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/package.json,sha256=39PQEWOzw0qtqD7Kep3OPbUR113vI7CG5TeD84v7clM,6224
|
|
128
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=NBG3t7gFYb6_2J-yPU29qS7Ck9pYgm8-wC5Q1LrzQeI,6082
|
|
129
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
|
|
130
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js,sha256=mixXcqFUZukj7_jQVxHTavsYl7cdZv83sEe4phJgkh0,59893
|
|
131
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js,sha256=LEEAOkUtQ9KzWEHn-QFv5yGi70B-sBOnrvztzHr0Shw,223
|
|
132
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js,sha256=pCoEaqUQj1T4-zAc9SUd__9ZGm7uL2wHQve2xwL89NI,8156
|
|
133
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js,sha256=4_nMNAiDSn_cw7UjIHEwone4fizrvqrqSgbwUWvjmoU,114571
|
|
134
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js,sha256=n_a0yu_gE7l6fauyoXXv9BZ7ZEYt2387mTfs7CbLcs4,51939
|
|
135
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt,sha256=eNJ8gc9n9IF8nW1d9sI9niuHstYzjNz5vqXx9UgWSPc,249
|
|
136
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
|
|
137
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.ac9035764d5b2adbb542.js,sha256=rJA1dk1bKtu1QkURrp5HEC2WCxzZppPV1SCYz-35GTc,8737
|
|
138
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=r89Jlk5v1drcwhCpv9FnC3Jig_JH4k24kZNIvxh6Htk,149
|
|
139
|
+
calkit_python-0.41.21.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
|
|
140
|
+
calkit_python-0.41.21.dist-info/METADATA,sha256=ajpJDiEj8nYKRBjH6gYzBithUWfetdaZTv3Myz7lXJY,12567
|
|
141
|
+
calkit_python-0.41.21.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
142
|
+
calkit_python-0.41.21.dist-info/entry_points.txt,sha256=2iQBBzjTAOdk66CwS-f3ecXXW5DjUqkG1KBRj8mHI1I,133
|
|
143
|
+
calkit_python-0.41.21.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
144
|
+
calkit_python-0.41.21.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
|