calkit-python 0.45.0__py3-none-any.whl → 0.45.1__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/__init__.py +6 -2
- calkit/cli/check.py +1 -1
- calkit/cli/import_.py +18 -5
- calkit/cli/list.py +13 -0
- calkit/cli/main/core.py +25 -15
- calkit/cli/new.py +98 -2
- calkit/cli/update.py +117 -11
- calkit/core.py +16 -2
- calkit/dvc/core.py +29 -0
- calkit/models/core.py +435 -10
- calkit/models/pipeline.py +54 -6
- calkit/notebooks.py +23 -12
- calkit/procedures.py +53 -0
- calkit/provenance.py +28 -0
- calkit/reproducibility.py +476 -0
- calkit/templates/core.py +27 -3
- calkit/templates/latex/ieee-conference/paper.tex +61 -0
- calkit/templates/latex/report/paper.tex +64 -0
- calkit/tests/cli/main/test_core.py +46 -0
- calkit/tests/cli/test_list.py +5 -0
- calkit/tests/cli/test_new.py +134 -0
- calkit/tests/cli/test_update.py +125 -0
- calkit/tests/dvc/test_core.py +39 -0
- calkit/tests/test_models.py +458 -0
- calkit/tests/test_procedures.py +64 -0
- calkit/tests/test_reproducibility.py +201 -0
- {calkit_python-0.45.0.dist-info → calkit_python-0.45.1.dist-info}/METADATA +1 -1
- {calkit_python-0.45.0.dist-info → calkit_python-0.45.1.dist-info}/RECORD +45 -39
- calkit/check.py +0 -248
- calkit/tests/test_check.py +0 -54
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/909.6d8285ce7c45878ac508.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.d7a43c7948f690d37d19.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.45.0.data → calkit_python-0.45.1.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.45.0.dist-info → calkit_python-0.45.1.dist-info}/WHEEL +0 -0
- {calkit_python-0.45.0.dist-info → calkit_python-0.45.1.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.45.0.dist-info → calkit_python-0.45.1.dist-info}/licenses/LICENSE +0 -0
calkit/__init__.py
CHANGED
|
@@ -10,7 +10,6 @@ from .core import * # noqa: F403, I001
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from calkit import ( # noqa: F401
|
|
12
12
|
calc,
|
|
13
|
-
check,
|
|
14
13
|
conda,
|
|
15
14
|
config,
|
|
16
15
|
datasets,
|
|
@@ -37,7 +36,10 @@ if TYPE_CHECKING:
|
|
|
37
36
|
ops,
|
|
38
37
|
overleaf,
|
|
39
38
|
pipeline,
|
|
39
|
+
procedures,
|
|
40
|
+
provenance,
|
|
40
41
|
releases,
|
|
42
|
+
reproducibility,
|
|
41
43
|
resources,
|
|
42
44
|
schema,
|
|
43
45
|
server,
|
|
@@ -59,7 +61,9 @@ _SUBMODULES = {
|
|
|
59
61
|
"templates",
|
|
60
62
|
"conda",
|
|
61
63
|
"calc",
|
|
62
|
-
"
|
|
64
|
+
"procedures",
|
|
65
|
+
"provenance",
|
|
66
|
+
"reproducibility",
|
|
63
67
|
"github",
|
|
64
68
|
"invenio",
|
|
65
69
|
"releases",
|
calkit/cli/check.py
CHANGED
|
@@ -308,7 +308,7 @@ def check_repro(
|
|
|
308
308
|
] = ".",
|
|
309
309
|
) -> None:
|
|
310
310
|
"""Check the reproducibility of a project."""
|
|
311
|
-
from calkit.
|
|
311
|
+
from calkit.reproducibility import check_reproducibility
|
|
312
312
|
|
|
313
313
|
res = check_reproducibility(wdir=wdir, log_func=typer.echo)
|
|
314
314
|
calkit.echo(res.to_pretty())
|
calkit/cli/import_.py
CHANGED
|
@@ -208,7 +208,9 @@ def import_dataset(
|
|
|
208
208
|
filter_paths=filter_paths,
|
|
209
209
|
),
|
|
210
210
|
)
|
|
211
|
-
|
|
211
|
+
# Nulls left out, so the entry reads as what was recorded rather than as
|
|
212
|
+
# a form with most of its fields blank
|
|
213
|
+
datasets.append(new_ds.model_dump(exclude_none=True))
|
|
212
214
|
ck_info["datasets"] = datasets
|
|
213
215
|
with open("calkit.yaml", "w") as f:
|
|
214
216
|
calkit.ryaml.dump(ck_info, f)
|
|
@@ -465,13 +467,24 @@ def import_from_zenodo(
|
|
|
465
467
|
if kind is not None:
|
|
466
468
|
ck_info = calkit.load_calkit_info()
|
|
467
469
|
items = ck_info.get(kind + "s", [])
|
|
470
|
+
# A record without a DOI (not every one has been minted one) is still
|
|
471
|
+
# somewhere in particular, so fall back to its URL rather than
|
|
472
|
+
# writing ``doi: null``, which nothing would then accept
|
|
473
|
+
doi = calkit.invenio.extract_doi(record)
|
|
474
|
+
if doi is not None:
|
|
475
|
+
imported_from: dict = {"doi": doi}
|
|
476
|
+
else:
|
|
477
|
+
imported_from = {
|
|
478
|
+
"url": record.get("links", {}).get("self_html")
|
|
479
|
+
or f"https://zenodo.org/records/{record_id}"
|
|
480
|
+
}
|
|
468
481
|
item_record = {
|
|
469
482
|
"path": dest_dir,
|
|
470
|
-
"imported_from":
|
|
471
|
-
"doi": record.get("doi"),
|
|
472
|
-
},
|
|
473
|
-
"title": record.get("metadata", {}).get("title"),
|
|
483
|
+
"imported_from": imported_from,
|
|
474
484
|
}
|
|
485
|
+
title = record.get("metadata", {}).get("title")
|
|
486
|
+
if title:
|
|
487
|
+
item_record["title"] = title
|
|
475
488
|
items.append(item_record)
|
|
476
489
|
ck_info[kind + "s"] = items
|
|
477
490
|
with open("calkit.yaml", "w") as f:
|
calkit/cli/list.py
CHANGED
|
@@ -45,6 +45,7 @@ def _list_objects(
|
|
|
45
45
|
"figures",
|
|
46
46
|
"references",
|
|
47
47
|
"publications",
|
|
48
|
+
"misc",
|
|
48
49
|
],
|
|
49
50
|
json_output: bool = False,
|
|
50
51
|
):
|
|
@@ -241,6 +242,18 @@ def list_publications(
|
|
|
241
242
|
_list_objects("publications", json_output)
|
|
242
243
|
|
|
243
244
|
|
|
245
|
+
@list_app.command(name="misc")
|
|
246
|
+
def list_misc(
|
|
247
|
+
json_output: Annotated[
|
|
248
|
+
bool, typer.Option("--json", help="Output result as JSON.")
|
|
249
|
+
] = False,
|
|
250
|
+
):
|
|
251
|
+
"""List misc artifacts in the project, i.e., attributed paths that
|
|
252
|
+
aren't one of the typed kinds.
|
|
253
|
+
"""
|
|
254
|
+
_list_objects("misc", json_output)
|
|
255
|
+
|
|
256
|
+
|
|
244
257
|
@list_app.command(name="references|refs")
|
|
245
258
|
def list_references(
|
|
246
259
|
json_output: Annotated[
|
calkit/cli/main/core.py
CHANGED
|
@@ -944,9 +944,11 @@ def add(
|
|
|
944
944
|
if dvc_repo is not None:
|
|
945
945
|
dvc_status = dvc_repo.data_status()
|
|
946
946
|
uncommitted = dvc_status["uncommitted"]
|
|
947
|
+
dvc_not_in_cache = set(dvc_status.get("not_in_cache", []))
|
|
947
948
|
dvc_uncommitted_all = uncommitted.get(
|
|
948
949
|
"modified", []
|
|
949
950
|
) + uncommitted.get("deleted", [])
|
|
951
|
+
missing_from_cache = []
|
|
950
952
|
for dvc_uncommitted in dvc_uncommitted_all:
|
|
951
953
|
if os.path.exists(dvc_uncommitted):
|
|
952
954
|
if dry_run:
|
|
@@ -955,11 +957,11 @@ def add(
|
|
|
955
957
|
)
|
|
956
958
|
else:
|
|
957
959
|
typer.echo(f"Adding {dvc_uncommitted} to DVC")
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
960
|
+
calkit.dvc.commit_path(dvc_repo, dvc_uncommitted)
|
|
961
|
+
elif dvc_uncommitted in dvc_not_in_cache:
|
|
962
|
+
# The data isn't on this machine at all, so this isn't
|
|
963
|
+
# a real deletion and there's nothing to commit
|
|
964
|
+
missing_from_cache.append(dvc_uncommitted)
|
|
963
965
|
else:
|
|
964
966
|
if dry_run:
|
|
965
967
|
typer.echo(
|
|
@@ -969,11 +971,20 @@ def add(
|
|
|
969
971
|
typer.echo(
|
|
970
972
|
f"Committing deleted {dvc_uncommitted} to DVC"
|
|
971
973
|
)
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
974
|
+
calkit.dvc.commit_path(dvc_repo, dvc_uncommitted)
|
|
975
|
+
if missing_from_cache:
|
|
976
|
+
n_shown = 5
|
|
977
|
+
paths_txt = ", ".join(missing_from_cache[:n_shown])
|
|
978
|
+
if len(missing_from_cache) > n_shown:
|
|
979
|
+
paths_txt += (
|
|
980
|
+
f", and {len(missing_from_cache) - n_shown} more"
|
|
981
|
+
)
|
|
982
|
+
warn(
|
|
983
|
+
f"Not committing {len(missing_from_cache)} missing "
|
|
984
|
+
f"DVC-tracked path(s) since their data is not in the "
|
|
985
|
+
f"cache ({paths_txt}); "
|
|
986
|
+
"run 'calkit pull' to fetch their data"
|
|
987
|
+
)
|
|
977
988
|
if not disable_auto_ignore:
|
|
978
989
|
for untracked_file in untracked_git_files:
|
|
979
990
|
if (
|
|
@@ -3506,15 +3517,14 @@ def run_procedure(
|
|
|
3506
3517
|
return bool(value)
|
|
3507
3518
|
return value
|
|
3508
3519
|
|
|
3509
|
-
from calkit.models import Procedure
|
|
3510
|
-
|
|
3511
3520
|
ck_info = calkit.load_calkit_info()
|
|
3512
3521
|
calkit.set_env_vars(ck_info=ck_info)
|
|
3513
|
-
|
|
3514
|
-
if name not in procs:
|
|
3522
|
+
if name not in ck_info.get("procedures", {}):
|
|
3515
3523
|
raise_error(f"'{name}' is not defined as a procedure")
|
|
3524
|
+
# Read from its own file if that's where it's kept, so the rest of this
|
|
3525
|
+
# sees the same thing either way
|
|
3516
3526
|
try:
|
|
3517
|
-
proc =
|
|
3527
|
+
proc = calkit.procedures.load(name, ck_info=ck_info)
|
|
3518
3528
|
except Exception as e:
|
|
3519
3529
|
raise_error(f"Procedure '{name}' is invalid: {e}")
|
|
3520
3530
|
git_repo = calkit.git.get_repo()
|
calkit/cli/new.py
CHANGED
|
@@ -32,6 +32,39 @@ def _check_path_dir(path: str):
|
|
|
32
32
|
os.makedirs(dirname, exist_ok=True)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _person_from_options(
|
|
36
|
+
option: str, email: str | None, orcid: str | None, with_ai: list[str]
|
|
37
|
+
) -> dict | None:
|
|
38
|
+
"""Build a ``created_by`` entry from CLI options.
|
|
39
|
+
|
|
40
|
+
Returns ``None`` when none were given. Validated through the model so
|
|
41
|
+
a mistyped ORCID or a ``--with-ai`` with nobody to answer for it is
|
|
42
|
+
refused here rather than by the next ``calkit.yaml`` validation.
|
|
43
|
+
"""
|
|
44
|
+
from pydantic import ValidationError
|
|
45
|
+
|
|
46
|
+
from calkit.models.core import _Person
|
|
47
|
+
|
|
48
|
+
if email is None and orcid is None and not with_ai:
|
|
49
|
+
return None
|
|
50
|
+
# ``with_ai`` is a list, one entry per --with-ai given. A single tool is
|
|
51
|
+
# written as a scalar, which is what the docs show and what reads best
|
|
52
|
+
# in calkit.yaml; several stay a list
|
|
53
|
+
with_ai_value: str | list[str] | None = None
|
|
54
|
+
if len(with_ai) == 1:
|
|
55
|
+
with_ai_value = with_ai[0]
|
|
56
|
+
elif with_ai:
|
|
57
|
+
with_ai_value = with_ai
|
|
58
|
+
try:
|
|
59
|
+
person = _Person(email=email, orcid=orcid, with_ai=with_ai_value)
|
|
60
|
+
except ValidationError as e:
|
|
61
|
+
raise_error(
|
|
62
|
+
f"Invalid --{option}: "
|
|
63
|
+
+ "; ".join(str(err["msg"]) for err in e.errors())
|
|
64
|
+
)
|
|
65
|
+
return person.model_dump(exclude_none=True)
|
|
66
|
+
|
|
67
|
+
|
|
35
68
|
def _split_template_subdir(
|
|
36
69
|
template: str, git_url: str
|
|
37
70
|
) -> tuple[str, str | None]:
|
|
@@ -600,6 +633,32 @@ def new_figure(
|
|
|
600
633
|
help="Stage name from which to add outputs as dependencies.",
|
|
601
634
|
),
|
|
602
635
|
] = None,
|
|
636
|
+
created_by_email: Annotated[
|
|
637
|
+
str | None,
|
|
638
|
+
typer.Option(
|
|
639
|
+
"--created-by-email",
|
|
640
|
+
help=(
|
|
641
|
+
"Email of whoever made this figure, for one drawn by hand "
|
|
642
|
+
"rather than produced by a stage."
|
|
643
|
+
),
|
|
644
|
+
),
|
|
645
|
+
] = None,
|
|
646
|
+
created_by_orcid: Annotated[
|
|
647
|
+
str | None,
|
|
648
|
+
typer.Option(
|
|
649
|
+
"--created-by-orcid", help="ORCID of whoever made this figure."
|
|
650
|
+
),
|
|
651
|
+
] = None,
|
|
652
|
+
created_with_ai: Annotated[
|
|
653
|
+
list[str],
|
|
654
|
+
typer.Option(
|
|
655
|
+
"--created-with-ai",
|
|
656
|
+
help=(
|
|
657
|
+
"Generative AI tool they used, e.g. 'Claude Opus 5'. "
|
|
658
|
+
"Repeat for several."
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
] = [],
|
|
603
662
|
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
604
663
|
overwrite: Annotated[
|
|
605
664
|
bool,
|
|
@@ -626,11 +685,16 @@ def new_figure(
|
|
|
626
685
|
raise_error("Command must be provided")
|
|
627
686
|
if (deps or outs or outs_from_stage) and not stage_name:
|
|
628
687
|
raise_error("Stage name must be provided")
|
|
629
|
-
|
|
688
|
+
created_by = _person_from_options(
|
|
689
|
+
"created-by", created_by_email, created_by_orcid, created_with_ai
|
|
690
|
+
)
|
|
691
|
+
obj: dict = dict(path=path, title=title)
|
|
630
692
|
if description is not None:
|
|
631
693
|
obj["description"] = description
|
|
632
694
|
if stage_name is not None:
|
|
633
695
|
obj["stage"] = stage_name
|
|
696
|
+
if created_by is not None:
|
|
697
|
+
obj["created_by"] = created_by
|
|
634
698
|
if cmd:
|
|
635
699
|
if outs_from_stage:
|
|
636
700
|
pipeline = calkit.dvc.read_pipeline()
|
|
@@ -1197,6 +1261,33 @@ def new_dataset(
|
|
|
1197
1261
|
help="Stage name from which to add outputs as dependencies.",
|
|
1198
1262
|
),
|
|
1199
1263
|
] = None,
|
|
1264
|
+
created_by_email: Annotated[
|
|
1265
|
+
str | None,
|
|
1266
|
+
typer.Option(
|
|
1267
|
+
"--created-by-email",
|
|
1268
|
+
help=(
|
|
1269
|
+
"Email of whoever collected this data for the project, "
|
|
1270
|
+
"which marks it as primary rather than imported or computed."
|
|
1271
|
+
),
|
|
1272
|
+
),
|
|
1273
|
+
] = None,
|
|
1274
|
+
created_by_orcid: Annotated[
|
|
1275
|
+
str | None,
|
|
1276
|
+
typer.Option(
|
|
1277
|
+
"--created-by-orcid",
|
|
1278
|
+
help="ORCID of whoever collected this data.",
|
|
1279
|
+
),
|
|
1280
|
+
] = None,
|
|
1281
|
+
created_with_ai: Annotated[
|
|
1282
|
+
list[str],
|
|
1283
|
+
typer.Option(
|
|
1284
|
+
"--created-with-ai",
|
|
1285
|
+
help=(
|
|
1286
|
+
"Generative AI tool they used, e.g. 'Claude Opus 5'. "
|
|
1287
|
+
"Repeat for several."
|
|
1288
|
+
),
|
|
1289
|
+
),
|
|
1290
|
+
] = [],
|
|
1200
1291
|
no_commit: Annotated[bool, typer.Option("--no-commit")] = False,
|
|
1201
1292
|
overwrite: Annotated[
|
|
1202
1293
|
bool,
|
|
@@ -1223,11 +1314,16 @@ def new_dataset(
|
|
|
1223
1314
|
raise_error("Command must be provided")
|
|
1224
1315
|
if (deps or outs or outs_from_stage) and not stage_name:
|
|
1225
1316
|
raise_error("Stage name must be provided")
|
|
1226
|
-
|
|
1317
|
+
created_by = _person_from_options(
|
|
1318
|
+
"created-by", created_by_email, created_by_orcid, created_with_ai
|
|
1319
|
+
)
|
|
1320
|
+
obj: dict = dict(path=path, title=title)
|
|
1227
1321
|
if description is not None:
|
|
1228
1322
|
obj["description"] = description
|
|
1229
1323
|
if stage_name is not None:
|
|
1230
1324
|
obj["stage"] = stage_name
|
|
1325
|
+
if created_by is not None:
|
|
1326
|
+
obj["created_by"] = created_by
|
|
1231
1327
|
if cmd:
|
|
1232
1328
|
if outs_from_stage:
|
|
1233
1329
|
pipeline = calkit.dvc.read_pipeline()
|
calkit/cli/update.py
CHANGED
|
@@ -1258,6 +1258,45 @@ def update_dataset(
|
|
|
1258
1258
|
help="URL the dataset was imported from.",
|
|
1259
1259
|
),
|
|
1260
1260
|
] = None,
|
|
1261
|
+
imported_from_doi: Annotated[
|
|
1262
|
+
str | None,
|
|
1263
|
+
typer.Option(
|
|
1264
|
+
"--imported-from-doi",
|
|
1265
|
+
help="DOI the dataset was imported from, e.g. 10.5281/zenodo.1.",
|
|
1266
|
+
),
|
|
1267
|
+
] = None,
|
|
1268
|
+
imported_from_git_url: Annotated[
|
|
1269
|
+
str | None,
|
|
1270
|
+
typer.Option(
|
|
1271
|
+
"--imported-from-git-url",
|
|
1272
|
+
help="Clone URL of the Git repo the dataset was imported from.",
|
|
1273
|
+
),
|
|
1274
|
+
] = None,
|
|
1275
|
+
imported_from_git_rev: Annotated[
|
|
1276
|
+
str | None,
|
|
1277
|
+
typer.Option(
|
|
1278
|
+
"--imported-from-git-rev",
|
|
1279
|
+
help=(
|
|
1280
|
+
"Commit hash it was taken from. A branch or tag isn't "
|
|
1281
|
+
"accepted, since it would move."
|
|
1282
|
+
),
|
|
1283
|
+
),
|
|
1284
|
+
] = None,
|
|
1285
|
+
imported_from_git_path: Annotated[
|
|
1286
|
+
str | None,
|
|
1287
|
+
typer.Option(
|
|
1288
|
+
"--imported-from-git-path",
|
|
1289
|
+
help="Path within that repo, if it isn't the whole thing.",
|
|
1290
|
+
),
|
|
1291
|
+
] = None,
|
|
1292
|
+
imported_from_date: Annotated[
|
|
1293
|
+
datetime | None,
|
|
1294
|
+
typer.Option(
|
|
1295
|
+
"--imported-from-date",
|
|
1296
|
+
formats=["%Y-%m-%d"],
|
|
1297
|
+
help="Date it was downloaded, as YYYY-MM-DD.",
|
|
1298
|
+
),
|
|
1299
|
+
] = None,
|
|
1261
1300
|
stage: Annotated[
|
|
1262
1301
|
str | None,
|
|
1263
1302
|
typer.Option(
|
|
@@ -1267,23 +1306,90 @@ def update_dataset(
|
|
|
1267
1306
|
] = None,
|
|
1268
1307
|
) -> None:
|
|
1269
1308
|
"""Update a dataset entry in calkit.yaml."""
|
|
1270
|
-
|
|
1309
|
+
from pydantic import ValidationError
|
|
1310
|
+
|
|
1311
|
+
from calkit.models.core import (
|
|
1312
|
+
Dataset,
|
|
1313
|
+
_GitSource,
|
|
1314
|
+
_ImportedFromDoi,
|
|
1315
|
+
_ImportedFromGit,
|
|
1316
|
+
_ImportedFromUrl,
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# One source, since the entry records where the data came from rather
|
|
1320
|
+
# than every place it could be found
|
|
1321
|
+
source_options = {
|
|
1322
|
+
"--imported-from-url": imported_from_url,
|
|
1323
|
+
"--imported-from-doi": imported_from_doi,
|
|
1324
|
+
"--imported-from-git-url": imported_from_git_url,
|
|
1325
|
+
}
|
|
1326
|
+
sources_given = [k for k, v in source_options.items() if v is not None]
|
|
1327
|
+
if len(sources_given) > 1:
|
|
1328
|
+
raise_error("Specify only one of " + ", ".join(source_options) + ".")
|
|
1329
|
+
if not sources_given and (
|
|
1330
|
+
imported_from_git_rev is not None
|
|
1331
|
+
or imported_from_git_path is not None
|
|
1332
|
+
or imported_from_date is not None
|
|
1333
|
+
):
|
|
1334
|
+
raise_error(
|
|
1335
|
+
"--imported-from-git-rev, --imported-from-git-path, and "
|
|
1336
|
+
"--imported-from-date go with one of "
|
|
1337
|
+
+ ", ".join(source_options)
|
|
1338
|
+
+ "."
|
|
1339
|
+
)
|
|
1340
|
+
if not sources_given and stage is None:
|
|
1271
1341
|
raise_error("No updates specified.")
|
|
1342
|
+
imported_from: dict | None = None
|
|
1343
|
+
if sources_given:
|
|
1344
|
+
date = imported_from_date.date() if imported_from_date else None
|
|
1345
|
+
try:
|
|
1346
|
+
source: _ImportedFromUrl | _ImportedFromDoi | _ImportedFromGit
|
|
1347
|
+
if imported_from_url is not None:
|
|
1348
|
+
source = _ImportedFromUrl(url=imported_from_url, date=date)
|
|
1349
|
+
elif imported_from_doi is not None:
|
|
1350
|
+
source = _ImportedFromDoi(doi=imported_from_doi, date=date)
|
|
1351
|
+
else:
|
|
1352
|
+
if imported_from_git_rev is None:
|
|
1353
|
+
raise_error(
|
|
1354
|
+
"--imported-from-git-rev is required with "
|
|
1355
|
+
"--imported-from-git-url."
|
|
1356
|
+
)
|
|
1357
|
+
source = _ImportedFromGit(
|
|
1358
|
+
git=_GitSource(
|
|
1359
|
+
repo_url=calkit.normalize_git_url(
|
|
1360
|
+
imported_from_git_url or ""
|
|
1361
|
+
),
|
|
1362
|
+
rev=imported_from_git_rev,
|
|
1363
|
+
path=imported_from_git_path,
|
|
1364
|
+
),
|
|
1365
|
+
date=date,
|
|
1366
|
+
)
|
|
1367
|
+
except ValidationError as e:
|
|
1368
|
+
raise_error(
|
|
1369
|
+
"Invalid import source: "
|
|
1370
|
+
+ "; ".join(str(err["msg"]) for err in e.errors())
|
|
1371
|
+
)
|
|
1372
|
+
imported_from = source.model_dump(exclude_none=True)
|
|
1272
1373
|
ck_info = calkit.load_calkit_info()
|
|
1273
1374
|
datasets = ck_info.get("datasets", [])
|
|
1274
1375
|
for ds in datasets:
|
|
1275
1376
|
if ds.get("path") == path:
|
|
1276
|
-
if imported_from_url is not None:
|
|
1277
|
-
ds["imported_from"] = {"url": imported_from_url}
|
|
1278
|
-
if stage is not None:
|
|
1279
|
-
ds["stage"] = stage
|
|
1280
1377
|
break
|
|
1281
1378
|
else:
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
entry["imported_from"] = {"url": imported_from_url}
|
|
1285
|
-
if stage is not None:
|
|
1286
|
-
entry["stage"] = stage
|
|
1287
|
-
datasets.append(entry)
|
|
1379
|
+
ds = {"path": path}
|
|
1380
|
+
datasets.append(ds)
|
|
1288
1381
|
ck_info["datasets"] = datasets
|
|
1382
|
+
if imported_from is not None:
|
|
1383
|
+
ds["imported_from"] = imported_from
|
|
1384
|
+
if stage is not None:
|
|
1385
|
+
ds["stage"] = stage
|
|
1386
|
+
# Checked as a whole, so an import added to a dataset someone collected
|
|
1387
|
+
# is refused here rather than left for the next validation to find
|
|
1388
|
+
try:
|
|
1389
|
+
Dataset.model_validate(ds)
|
|
1390
|
+
except ValidationError as e:
|
|
1391
|
+
raise_error(
|
|
1392
|
+
"Invalid dataset: "
|
|
1393
|
+
+ "; ".join(str(err["msg"]) for err in e.errors())
|
|
1394
|
+
)
|
|
1289
1395
|
calkit.save_calkit_info(ck_info)
|
calkit/core.py
CHANGED
|
@@ -1123,6 +1123,21 @@ def detect_project_name(
|
|
|
1123
1123
|
return name
|
|
1124
1124
|
|
|
1125
1125
|
|
|
1126
|
+
def normalize_git_url(url: str) -> str:
|
|
1127
|
+
"""An SSH GitHub URL as its HTTPS form; anything else unchanged.
|
|
1128
|
+
|
|
1129
|
+
``git@github.com:owner/repo.git`` and ``https://github.com/owner/repo``
|
|
1130
|
+
name the same repo, and only the latter is a link, so that's the one
|
|
1131
|
+
worth recording.
|
|
1132
|
+
"""
|
|
1133
|
+
url = url.strip()
|
|
1134
|
+
if url.startswith("git@github.com:"):
|
|
1135
|
+
url = "https://github.com/" + url[len("git@github.com:") :]
|
|
1136
|
+
if url.startswith("https://github.com/") and url.endswith(".git"):
|
|
1137
|
+
url = url[: -len(".git")]
|
|
1138
|
+
return url
|
|
1139
|
+
|
|
1140
|
+
|
|
1126
1141
|
def detect_project_github_url(wdir: str | None = None) -> str | None:
|
|
1127
1142
|
"""Detect the GitHub URL for the current project."""
|
|
1128
1143
|
try:
|
|
@@ -1134,8 +1149,7 @@ def detect_project_github_url(wdir: str | None = None) -> str | None:
|
|
|
1134
1149
|
warnings.warn("Git remote is not a GitHub URL")
|
|
1135
1150
|
return None
|
|
1136
1151
|
url = url.removesuffix(".git")
|
|
1137
|
-
|
|
1138
|
-
url = url.replace("git@github.com:", "https://github.com/")
|
|
1152
|
+
url = normalize_git_url(url)
|
|
1139
1153
|
return url
|
|
1140
1154
|
|
|
1141
1155
|
|
calkit/dvc/core.py
CHANGED
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
|
+
from itertools import groupby
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Literal
|
|
13
14
|
|
|
@@ -354,6 +355,34 @@ def get_dvc_repo(wdir: str | None = None) -> dvc.repo.Repo:
|
|
|
354
355
|
return dvc.repo.Repo(wdir)
|
|
355
356
|
|
|
356
357
|
|
|
358
|
+
def commit_path(
|
|
359
|
+
dvc_repo: dvc.repo.Repo, path: str, allow_missing: bool = True
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Commit a path to DVC, skipping the run cache.
|
|
362
|
+
|
|
363
|
+
This is like ``dvc_repo.commit(path, force=True)``, except stages are not
|
|
364
|
+
saved to the run cache, since doing so requires hashing all of their
|
|
365
|
+
dependencies, which will fail if any are missing from the workspace, e.g.,
|
|
366
|
+
if their data has never been pulled.
|
|
367
|
+
"""
|
|
368
|
+
with dvc.repo.lock_repo(dvc_repo):
|
|
369
|
+
groups = groupby(
|
|
370
|
+
dvc_repo.stage.collect_granular(path),
|
|
371
|
+
key=lambda info: info.stage.dvcfile,
|
|
372
|
+
)
|
|
373
|
+
for dvcfile, stages_info_group in groups:
|
|
374
|
+
to_dump = []
|
|
375
|
+
for stage_info in stages_info_group:
|
|
376
|
+
stage = stage_info.stage
|
|
377
|
+
stage.save(allow_missing=allow_missing, run_cache=False)
|
|
378
|
+
stage.commit(
|
|
379
|
+
filter_info=stage_info.filter_info,
|
|
380
|
+
allow_missing=allow_missing,
|
|
381
|
+
)
|
|
382
|
+
to_dump.append(stage)
|
|
383
|
+
dvcfile.dump_stages(to_dump, update_pipeline=False)
|
|
384
|
+
|
|
385
|
+
|
|
357
386
|
def ensure_dvc_lock_not_ignored(wdir: str | None = None) -> bool:
|
|
358
387
|
"""Ensure ``dvc.lock`` is not Git-ignored.
|
|
359
388
|
|