calkit-python 0.41.20__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.
Files changed (33) hide show
  1. calkit/cli/list.py +35 -1
  2. calkit/cli/main/core.py +13 -0
  3. calkit/cli/new.py +4 -8
  4. calkit/cli/overleaf.py +12 -2
  5. calkit/detect.py +102 -25
  6. calkit/git.py +29 -0
  7. calkit/models/core.py +11 -1
  8. calkit/tests/cli/main/test_core.py +67 -0
  9. calkit/tests/cli/test_list.py +41 -1
  10. calkit/tests/cli/test_new.py +14 -19
  11. calkit/tests/cli/test_overleaf.py +47 -0
  12. calkit/tests/models/test_core.py +22 -1
  13. calkit/tests/test_detect.py +59 -2
  14. {calkit_python-0.41.20.dist-info → calkit_python-0.41.21.dist-info}/METADATA +8 -7
  15. {calkit_python-0.41.20.dist-info → calkit_python-0.41.21.dist-info}/RECORD +32 -33
  16. {calkit_python-0.41.20.dist-info → calkit_python-0.41.21.dist-info}/WHEEL +1 -1
  17. calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/install.json +0 -5
  18. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
  19. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
  20. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
  21. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
  22. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
  23. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
  24. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
  25. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
  26. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
  27. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
  28. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
  29. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.ac9035764d5b2adbb542.js +0 -0
  30. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
  31. {calkit_python-0.41.20.data → calkit_python-0.41.21.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
  32. {calkit_python-0.41.20.dist-info → calkit_python-0.41.21.dist-info}/entry_points.txt +0 -0
  33. {calkit_python-0.41.20.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
- typer.echo(f"{n}. {question}")
223
+ _echo_question(n, question)
190
224
 
191
225
 
192
226
  @list_app.command(name="publications|pubs")
calkit/cli/main/core.py CHANGED
@@ -786,6 +786,8 @@ def add(
786
786
  elif to == "git":
787
787
  subprocess.call(["git", "add"] + paths)
788
788
  elif to == "dvc":
789
+ for path in paths:
790
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
789
791
  calkit.dvc.run_dvc_command(["add"] + paths)
790
792
  elif to == "dvc-zip":
791
793
  for path in paths:
@@ -899,6 +901,7 @@ def add(
899
901
  f"Adding {path} to DVC since it's already tracked "
900
902
  "with DVC"
901
903
  )
904
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
902
905
  calkit.dvc.run_dvc_command(["add", path])
903
906
  elif posix_path in pipeline_output_storage:
904
907
  # Respect storage explicitly set in the pipeline definition
@@ -943,6 +946,7 @@ def add(
943
946
  typer.echo(f"Would add {path} to DVC (per extension)")
944
947
  else:
945
948
  typer.echo(f"Adding {path} to DVC per its extension")
949
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
946
950
  calkit.dvc.run_dvc_command(["add", path])
947
951
  elif calkit.dvc.zip.is_zip_candidate(path):
948
952
  if dry_run:
@@ -963,6 +967,7 @@ def add(
963
967
  typer.echo(
964
968
  f"Adding {path} to DVC since it's greater than 1 MB"
965
969
  )
970
+ calkit.git.ensure_dvc_pointer_is_not_ignored(repo, path)
966
971
  calkit.dvc.run_dvc_command(["add", path])
967
972
  else:
968
973
  if dry_run:
@@ -3218,6 +3223,14 @@ def call_dvc(
3218
3223
  and DVC is not installed.
3219
3224
  """
3220
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
+ )
3221
3234
  sys.exit(result)
3222
3235
 
3223
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
- stem, ext = os.path.splitext(os.path.basename(path))
3270
- stored_filename = f"{project_name}-{stem}-{name}{ext}"
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 = [os.path.dirname(p) if os.path.isfile(p) else p for p in 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 = [os.path.dirname(p) if os.path.isfile(p) else p for p in 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(top_level)
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(top_level)
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(rel_path: str) -> bool:
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(rel_path: str) -> bool:
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(rel_path: str) -> bool:
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(rel_path: str) -> bool:
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 is_figure_path(rel_path):
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(rel_path: str) -> bool:
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 is_figure_path(rel_path) or is_presentation_path(rel_path):
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(rel_path: str) -> str | None:
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(candidates, reserved_paths=reserved)
2331
+ figures = detect_figures(
2332
+ candidates, reserved_paths=reserved, ignore=ignore
2333
+ )
2264
2334
  datasets = detect_datasets(
2265
- candidates, reserved_paths=reserved, figure_paths=figures
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/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: list[FigureEvidence | ResultsEvidence] | None = None
368
+ evidence: (
369
+ list[FigureEvidence | ResultsEvidence | PublicationEvidence] | None
370
+ ) = None
361
371
 
362
372
 
363
373
  class Dependency(BaseModel):
@@ -1836,3 +1836,70 @@ def test_run_downstream_does_not_submit_unrelated_sweep(tmp_dir):
1836
1836
  assert os.path.exists("other.txt")
1837
1837
  assert not os.path.exists("out-1.txt")
1838
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
+ )
@@ -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
- assert json.loads(out) == ["Does it work?"]
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):
@@ -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 to Mo for review.",
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 to Mo for review."
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"] == ".calkit/releases/v1/test-project-raw-v1.zip"
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}"
@@ -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."
@@ -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 "sklearn" in deps
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 "sklearn" in deps
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.20
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
@@ -78,15 +78,16 @@ Description-Content-Type: text/markdown
78
78
 
79
79
  <!-- INCLUDE: docs/index.md -->
80
80
 
81
- It's six months since you submitted your paper,
82
- do you know exactly how your figures and results were generated?
83
-
84
- You will if they're part of a Calkit project.
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 and reproduced with a
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=3uD0Jjhx_vuDVuAkwI90_TBuMhB9nS-tSujmBabEct4,81122
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=rHAWdI13vA35m7q-MhlHj2KHMa42s4ecisTvKbdXKMw,13321
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,15 +39,15 @@ 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=W-ftvc6sYwt5iT7NIYr5_4jJWjDPbB-8pAropPnvaFc,10704
43
- calkit/cli/new.py,sha256=yFrwl4qB8iUyV2wQ1fR-lLtVH9WuVVpeQUwHXAQapTc,137621
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=bS-SkYsSqqhC9YsP-NCzpYJCGS3820w18w_s8JOFihg,25216
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=lPLVIEUHOYhvHVt2kncB4Y5Jt3D7KDFBY44F5bTFtXY,124496
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
@@ -55,7 +55,7 @@ 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=0qVnOy5C8n2Bbekj7Fy_WvbVWc6LPoj-Eh6VMiVhVTg,13323
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=XlETyNwNaJKPgAPh9glMcjmuDz-LB2a-Is7EG-hFDAk,43092
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,14 +99,14 @@ 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=ejMg6q8vtjCbKxvS9zZ32rv_oeKH94JbvpEiVjnw5QA,3859
103
- calkit/tests/cli/test_new.py,sha256=3d_RoSLfYQVvMA-IF2sh0qXHs-RT3VCrgSF4vnrh_IY,53263
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=Y-ud4kZHh3nhyoZlXrtMfx61z-GPNytUpfDHMad3tuA,16474
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=kDB0e-13jkx5-1wYAFgzIwzt2A10r_1eExYFf8v6sd4,66919
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
@@ -117,29 +117,28 @@ calkit/tests/jupyterlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
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=EY-8CRR-Nh6PIjqRfAvyz_An3fkf6Hzk7ed4da02afg,872
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.20.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
127
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/package.json,sha256=39PQEWOzw0qtqD7Kep3OPbUR113vI7CG5TeD84v7clM,6224
128
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=NBG3t7gFYb6_2J-yPU29qS7Ck9pYgm8-wC5Q1LrzQeI,6082
129
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
130
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js,sha256=mixXcqFUZukj7_jQVxHTavsYl7cdZv83sEe4phJgkh0,59893
131
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js,sha256=LEEAOkUtQ9KzWEHn-QFv5yGi70B-sBOnrvztzHr0Shw,223
132
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js,sha256=pCoEaqUQj1T4-zAc9SUd__9ZGm7uL2wHQve2xwL89NI,8156
133
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js,sha256=4_nMNAiDSn_cw7UjIHEwone4fizrvqrqSgbwUWvjmoU,114571
134
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js,sha256=n_a0yu_gE7l6fauyoXXv9BZ7ZEYt2387mTfs7CbLcs4,51939
135
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt,sha256=eNJ8gc9n9IF8nW1d9sI9niuHstYzjNz5vqXx9UgWSPc,249
136
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
137
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.ac9035764d5b2adbb542.js,sha256=rJA1dk1bKtu1QkURrp5HEC2WCxzZppPV1SCYz-35GTc,8737
138
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=r89Jlk5v1drcwhCpv9FnC3Jig_JH4k24kZNIvxh6Htk,149
139
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
140
- calkit_python-0.41.20.data/data/share/jupyter/labextensions/calkit/install.json,sha256=DK9d8G-q-rMVlcT3rAeGIyo3REWKw6FySBZceLU9yaw,187
141
- calkit_python-0.41.20.dist-info/METADATA,sha256=kNDMzEEG3WA9MQu9EOFf8G2oaCTjymyEfA3KFAIQtDA,12442
142
- calkit_python-0.41.20.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
143
- calkit_python-0.41.20.dist-info/entry_points.txt,sha256=2iQBBzjTAOdk66CwS-f3ecXXW5DjUqkG1KBRj8mHI1I,133
144
- calkit_python-0.41.20.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
145
- calkit_python-0.41.20.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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,5 +0,0 @@
1
- {
2
- "packageManager": "python",
3
- "packageName": "calkit-python",
4
- "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package calkit-python"
5
- }