calkit-python 0.41.7__py3-none-any.whl → 0.41.8__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/main/core.py +10 -1
- calkit/cli/new.py +47 -16
- calkit/core.py +8 -0
- calkit/invenio.py +20 -0
- calkit/pipeline.py +113 -2
- calkit/tests/cli/main/test_subprojects.py +53 -0
- calkit/tests/cli/test_new.py +316 -0
- calkit/tests/test_core.py +19 -0
- calkit/tests/test_invenio.py +20 -0
- calkit/tests/test_pipeline.py +111 -0
- {calkit_python-0.41.7.dist-info → calkit_python-0.41.8.dist-info}/METADATA +1 -1
- {calkit_python-0.41.7.dist-info → calkit_python-0.41.8.dist-info}/RECORD +30 -30
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/install.json +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.41.7.data → calkit_python-0.41.8.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.41.7.dist-info → calkit_python-0.41.8.dist-info}/WHEEL +0 -0
- {calkit_python-0.41.7.dist-info → calkit_python-0.41.8.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.41.7.dist-info → calkit_python-0.41.8.dist-info}/licenses/LICENSE +0 -0
calkit/cli/main/core.py
CHANGED
|
@@ -2418,7 +2418,16 @@ def run_in_env(
|
|
|
2418
2418
|
)
|
|
2419
2419
|
# TODO: Prefix should only be in the env file or calkit.yaml, not both?
|
|
2420
2420
|
prefix = env.get("prefix")
|
|
2421
|
-
|
|
2421
|
+
# Conda is often not on the PATH (especially on Windows), so search
|
|
2422
|
+
# typical install locations rather than relying on the bare name,
|
|
2423
|
+
# which would fail with a confusing FileNotFoundError traceback.
|
|
2424
|
+
conda_exe = calkit.conda.find_conda_exe()
|
|
2425
|
+
if conda_exe is None:
|
|
2426
|
+
raise_error(
|
|
2427
|
+
"Cannot find Conda executable; "
|
|
2428
|
+
"ensure Conda is installed and on the PATH"
|
|
2429
|
+
)
|
|
2430
|
+
conda_cmd = [conda_exe, "run"]
|
|
2422
2431
|
if prefix is not None:
|
|
2423
2432
|
conda_cmd += ["--prefix", os.path.abspath(prefix)]
|
|
2424
2433
|
else:
|
calkit/cli/new.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import pathlib
|
|
8
8
|
import shutil
|
|
9
9
|
import subprocess
|
|
10
|
+
import time
|
|
10
11
|
from enum import Enum
|
|
11
12
|
|
|
12
13
|
import typer
|
|
@@ -3325,22 +3326,23 @@ def new_release(
|
|
|
3325
3326
|
f"/records/{record_id}/draft/files/{filename}/commit",
|
|
3326
3327
|
service=to, # type: ignore
|
|
3327
3328
|
)
|
|
3328
|
-
#
|
|
3329
|
-
if
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3329
|
+
# Reserve a DOI on the draft before publishing. The publish action's
|
|
3330
|
+
# response only echoes back the DOI under "pids" if one was reserved
|
|
3331
|
+
# on the draft first (otherwise "pids" comes back empty), so always
|
|
3332
|
+
# reserve here to get a stable identifier.
|
|
3333
|
+
typer.echo(f"Reserving DOI for {to} draft record ID {record_id}")
|
|
3334
|
+
doi_resp = calkit.invenio.post(
|
|
3335
|
+
f"/records/{record_id}/draft/pids/doi",
|
|
3336
|
+
service=to, # type: ignore
|
|
3337
|
+
)
|
|
3338
|
+
if verbose:
|
|
3339
|
+
typer.echo(f"DOI reservation response:\n{doi_resp}")
|
|
3340
|
+
doi = calkit.invenio.extract_doi(doi_resp)
|
|
3341
|
+
if doi is None:
|
|
3342
|
+
raise_error(
|
|
3343
|
+
f"Failed to reserve DOI for {to} draft record {record_id}"
|
|
3335
3344
|
)
|
|
3336
|
-
|
|
3337
|
-
typer.echo(f"DOI response for draft:\n{doi_resp}")
|
|
3338
|
-
try:
|
|
3339
|
-
doi = doi_resp["pids"]["doi"]["identifier"]
|
|
3340
|
-
except KeyError:
|
|
3341
|
-
doi = doi_resp["doi"]
|
|
3342
|
-
except Exception as e:
|
|
3343
|
-
raise_error(f"Failed to reserve DOI for draft: {e}")
|
|
3345
|
+
if draft_only:
|
|
3344
3346
|
url = f"https://doi.org/{doi}"
|
|
3345
3347
|
typer.echo(f"Created {to} draft with reserved DOI: {doi}")
|
|
3346
3348
|
else:
|
|
@@ -3350,8 +3352,37 @@ def new_release(
|
|
|
3350
3352
|
f"/records/{record_id}/draft/actions/publish",
|
|
3351
3353
|
service=to, # type: ignore
|
|
3352
3354
|
)
|
|
3355
|
+
if verbose:
|
|
3356
|
+
typer.echo(f"Publish response:\n{invenio_dep}")
|
|
3353
3357
|
record_id = invenio_dep["id"]
|
|
3354
|
-
|
|
3358
|
+
# Prefer the DOI from the publish response, but fall back to the
|
|
3359
|
+
# reserved DOI (and a fetch of the published record) since the
|
|
3360
|
+
# publish action returns 202 and may not echo "pids" immediately.
|
|
3361
|
+
# Polling failures are tolerated: we already have the reserved DOI,
|
|
3362
|
+
# so a transient error while the record settles should not abort
|
|
3363
|
+
# the release.
|
|
3364
|
+
published_doi = calkit.invenio.extract_doi(invenio_dep)
|
|
3365
|
+
for _ in range(10):
|
|
3366
|
+
if published_doi is not None:
|
|
3367
|
+
break
|
|
3368
|
+
time.sleep(1)
|
|
3369
|
+
try:
|
|
3370
|
+
record = calkit.invenio.get(
|
|
3371
|
+
f"/records/{record_id}",
|
|
3372
|
+
service=to, # type: ignore
|
|
3373
|
+
)
|
|
3374
|
+
except Exception as e:
|
|
3375
|
+
if verbose:
|
|
3376
|
+
typer.echo(f"Polling for published DOI failed: {e}")
|
|
3377
|
+
continue
|
|
3378
|
+
published_doi = calkit.invenio.extract_doi(record)
|
|
3379
|
+
if published_doi is not None:
|
|
3380
|
+
doi = published_doi
|
|
3381
|
+
else:
|
|
3382
|
+
typer.echo(
|
|
3383
|
+
"Could not confirm minted DOI from published record; "
|
|
3384
|
+
f"falling back to reserved DOI {doi}"
|
|
3385
|
+
)
|
|
3355
3386
|
url = f"https://doi.org/{doi}"
|
|
3356
3387
|
typer.echo(f"Published to {to} with DOI: {doi}")
|
|
3357
3388
|
else:
|
calkit/core.py
CHANGED
|
@@ -341,6 +341,14 @@ def check_dep_exists(
|
|
|
341
341
|
return True
|
|
342
342
|
if system_info is not None and system_info.get(f"{name}_version"):
|
|
343
343
|
return True
|
|
344
|
+
# Conda and mamba are frequently installed but not on the PATH (most
|
|
345
|
+
# commonly on Windows), so search their typical install locations
|
|
346
|
+
# rather than relying on the bare name being directly executable.
|
|
347
|
+
if name in ("conda", "mamba"):
|
|
348
|
+
from calkit.conda import find_conda_exe, find_mamba_exe
|
|
349
|
+
|
|
350
|
+
exe = find_conda_exe() if name == "conda" else find_mamba_exe()
|
|
351
|
+
return exe is not None
|
|
344
352
|
cmd = [name]
|
|
345
353
|
# Executables with non-conventional CLIs
|
|
346
354
|
if name == "matlab":
|
calkit/invenio.py
CHANGED
|
@@ -107,6 +107,26 @@ put = partial(_request, "put")
|
|
|
107
107
|
delete = partial(_request, "delete")
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def extract_doi(record: dict) -> str | None:
|
|
111
|
+
"""Extract the DOI identifier from an InvenioRDM record or response.
|
|
112
|
+
|
|
113
|
+
Depending on the endpoint and whether the DOI has been minted yet, the
|
|
114
|
+
identifier can live in different places, so check all known locations and
|
|
115
|
+
return ``None`` if it isn't present yet.
|
|
116
|
+
"""
|
|
117
|
+
pids = record.get("pids")
|
|
118
|
+
if isinstance(pids, dict):
|
|
119
|
+
doi = pids.get("doi")
|
|
120
|
+
if isinstance(doi, dict) and doi.get("identifier"):
|
|
121
|
+
return doi["identifier"]
|
|
122
|
+
if record.get("doi"):
|
|
123
|
+
return record["doi"]
|
|
124
|
+
metadata = record.get("metadata")
|
|
125
|
+
if isinstance(metadata, dict) and metadata.get("doi"):
|
|
126
|
+
return metadata["doi"]
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
110
130
|
def get_download_urls(
|
|
111
131
|
record_id: int | str,
|
|
112
132
|
service: ServiceName = DEFAULT_SERVICE,
|
calkit/pipeline.py
CHANGED
|
@@ -96,10 +96,14 @@ class PipelineStatus(BaseModel):
|
|
|
96
96
|
def always_run_stage_names(self) -> list[str]:
|
|
97
97
|
# Only list stages whose sole change indicator is always_run; stages
|
|
98
98
|
# that also have real changes are reported under stale_stage_names.
|
|
99
|
+
# Subproject stages are excluded: their always-changed status is a
|
|
100
|
+
# delegation detail, not a user-meaningful always-run stage of the
|
|
101
|
+
# parent project.
|
|
99
102
|
return [
|
|
100
103
|
name
|
|
101
104
|
for name, stage in self.stale_stages.items()
|
|
102
105
|
if stage.always_run
|
|
106
|
+
and not stage.is_subproject
|
|
103
107
|
and not stage.modified_command
|
|
104
108
|
and not stage.modified_inputs
|
|
105
109
|
and not stage.modified_outputs
|
|
@@ -145,6 +149,12 @@ class StaleStage(BaseModel):
|
|
|
145
149
|
modified_outputs: list[str] = Field(default_factory=list)
|
|
146
150
|
modified_command: bool = False
|
|
147
151
|
always_run: bool = False
|
|
152
|
+
# True for stages that originate from a subproject (either an individual
|
|
153
|
+
# "{sp}:{stage}" stage or a kept "{sp} (subproject)" wrapper). Subproject
|
|
154
|
+
# wrapper stages are marked always-changed purely as a delegation
|
|
155
|
+
# mechanism, so they should not be advertised as always-run stages of the
|
|
156
|
+
# parent project.
|
|
157
|
+
is_subproject: bool = False
|
|
148
158
|
|
|
149
159
|
@staticmethod
|
|
150
160
|
def _as_path_list(paths: object) -> list[str]:
|
|
@@ -227,6 +237,7 @@ class StaleStage(BaseModel):
|
|
|
227
237
|
status_data: list | dict | str,
|
|
228
238
|
configured_outputs: list[str] | None = None,
|
|
229
239
|
path_prefix: str | None = None,
|
|
240
|
+
is_subproject: bool = False,
|
|
230
241
|
) -> "StaleStage":
|
|
231
242
|
modified_inputs = []
|
|
232
243
|
output_paths = []
|
|
@@ -312,6 +323,7 @@ class StaleStage(BaseModel):
|
|
|
312
323
|
modified_outputs=modified_outputs,
|
|
313
324
|
modified_command=modified_command,
|
|
314
325
|
always_run=always_run,
|
|
326
|
+
is_subproject=is_subproject,
|
|
315
327
|
)
|
|
316
328
|
|
|
317
329
|
|
|
@@ -546,6 +558,74 @@ def collapse_dvc_stages(
|
|
|
546
558
|
return stage
|
|
547
559
|
|
|
548
560
|
|
|
561
|
+
def _status_target_matches(
|
|
562
|
+
target: str,
|
|
563
|
+
display_name: str,
|
|
564
|
+
bare_name: str,
|
|
565
|
+
subproject: str | None,
|
|
566
|
+
stale_stage: StaleStage,
|
|
567
|
+
) -> bool:
|
|
568
|
+
"""Return True if a status ``target`` selects this stage.
|
|
569
|
+
|
|
570
|
+
Because the full DVC status is now computed and filtered locally (rather
|
|
571
|
+
than asking DVC to filter by target), this is where each ``target`` passed
|
|
572
|
+
to :func:`get_status` is resolved against a single stage. A ``target`` may
|
|
573
|
+
be a stage name in any of calkit's forms (``stage``, ``dvc.yaml:stage``,
|
|
574
|
+
``subproject:stage``, or a whole ``subproject``) or a repo path, which is
|
|
575
|
+
matched against the stage's stale/modified inputs and outputs.
|
|
576
|
+
|
|
577
|
+
Iterated/matrix stages use a ``name@param`` key in DVC status, so the base
|
|
578
|
+
name (the part before ``@``) is matched too — targeting the base name
|
|
579
|
+
selects every iteration, consistent with how ``calkit run`` expands matrix
|
|
580
|
+
targets.
|
|
581
|
+
"""
|
|
582
|
+
base_name = bare_name.split("@")[0]
|
|
583
|
+
# Stage-name matching is done against the raw target so it covers all of
|
|
584
|
+
# calkit's stage-target forms as well as DVC's native ``dvc.yaml:stage``
|
|
585
|
+
# and ``<subproject>/dvc.yaml:stage`` forms (the latter is what
|
|
586
|
+
# translate_run_targets emits for inline subprojects).
|
|
587
|
+
stage_aliases = {display_name, bare_name, base_name}
|
|
588
|
+
if subproject is not None:
|
|
589
|
+
subproject_name = Path(subproject).name
|
|
590
|
+
if target in {subproject, subproject_name}:
|
|
591
|
+
return display_name.startswith(
|
|
592
|
+
f"{subproject}:"
|
|
593
|
+
) or display_name == (f"{subproject} (subproject)")
|
|
594
|
+
stage_aliases |= {
|
|
595
|
+
f"{subproject}:{bare_name}",
|
|
596
|
+
f"{subproject_name}:{bare_name}",
|
|
597
|
+
f"{subproject}:{base_name}",
|
|
598
|
+
f"{subproject_name}:{base_name}",
|
|
599
|
+
f"{subproject}/dvc.yaml:{bare_name}",
|
|
600
|
+
f"{subproject}/dvc.yaml:{base_name}",
|
|
601
|
+
}
|
|
602
|
+
else:
|
|
603
|
+
stage_aliases |= {
|
|
604
|
+
f"dvc.yaml:{bare_name}",
|
|
605
|
+
f"dvc.yaml:{base_name}",
|
|
606
|
+
}
|
|
607
|
+
if target in stage_aliases:
|
|
608
|
+
return True
|
|
609
|
+
# Otherwise treat the target as a repo path and match it against the
|
|
610
|
+
# stage's stale/modified inputs and outputs. A target is path-like if it
|
|
611
|
+
# exists on disk or contains a separator; this keeps root-level paths
|
|
612
|
+
# without separators (e.g. ``data`` or ``out.csv``) selectable, which a
|
|
613
|
+
# separator-only heuristic would miss. A target that looks like a stage
|
|
614
|
+
# selector (contains ``:``) but matched no stage above is not a path.
|
|
615
|
+
if ":" in target and not os.path.exists(target):
|
|
616
|
+
return False
|
|
617
|
+
is_pathlike = os.path.exists(target) or "/" in target or "\\" in target
|
|
618
|
+
if not is_pathlike:
|
|
619
|
+
return False
|
|
620
|
+
norm_target = Path(target).as_posix().rstrip("/")
|
|
621
|
+
candidate_paths = (
|
|
622
|
+
stale_stage.stale_outputs
|
|
623
|
+
+ stale_stage.modified_inputs
|
|
624
|
+
+ stale_stage.modified_outputs
|
|
625
|
+
)
|
|
626
|
+
return any(_paths_overlap(norm_target, p) for p in candidate_paths)
|
|
627
|
+
|
|
628
|
+
|
|
549
629
|
def get_status(
|
|
550
630
|
ck_info: dict | None = None,
|
|
551
631
|
targets: list[str] | None = None,
|
|
@@ -628,7 +708,11 @@ def get_status(
|
|
|
628
708
|
dvc_repo = calkit.dvc.get_dvc_repo()
|
|
629
709
|
# calkit.dvc.core installs a filter on dvc.repo.status that drops
|
|
630
710
|
# the noisy frozen-stage warning, so the call here stays quiet.
|
|
631
|
-
|
|
711
|
+
# Ask DVC for the complete stale set, then filter locally.
|
|
712
|
+
# DVC's own target filtering can miss stale propagation from
|
|
713
|
+
# isolated subprojects, which makes targeted status disagree with
|
|
714
|
+
# the full-project status view.
|
|
715
|
+
raw_status = dvc_repo.status()
|
|
632
716
|
raw_status = calkit.dvc.status_as_posix(raw_status)
|
|
633
717
|
except Exception as e:
|
|
634
718
|
result["errors"].append(
|
|
@@ -794,20 +878,47 @@ def get_status(
|
|
|
794
878
|
for output in stage_cfg.get("outputs", [])
|
|
795
879
|
]
|
|
796
880
|
if subproject:
|
|
881
|
+
# Use posix separators so these match DVC's reported paths
|
|
882
|
+
# (which are always forward-slash, repo-root-relative). Using
|
|
883
|
+
# the OS-native separator here would, on Windows, produce a
|
|
884
|
+
# backslash variant that escapes path dedup and shows up as a
|
|
885
|
+
# duplicate stale output alongside DVC's posix path.
|
|
797
886
|
configured_outputs = [
|
|
798
|
-
|
|
887
|
+
(Path(subproject) / p).as_posix() for p in raw_outputs
|
|
799
888
|
]
|
|
800
889
|
else:
|
|
801
890
|
configured_outputs = raw_outputs
|
|
802
891
|
# For isolated subprojects, DVC reports paths relative to the
|
|
803
892
|
# subproject dir; prefix them so all paths are parent-relative.
|
|
893
|
+
# A stage is subproject-originating if it carries a subproject
|
|
894
|
+
# path ("{sp}:{stage}") or is a kept wrapper for a subproject
|
|
895
|
+
# ("{sp} (subproject)", whose bare name is a known subproject).
|
|
896
|
+
is_subproject = (
|
|
897
|
+
subproject is not None or bare_name in sp_by_stage_name
|
|
898
|
+
)
|
|
804
899
|
ordered_stale_stages[display_name] = StaleStage.from_status_data(
|
|
805
900
|
status_data=status_data,
|
|
806
901
|
configured_outputs=configured_outputs,
|
|
807
902
|
path_prefix=subproject
|
|
808
903
|
if subproject in isolated_sp_paths
|
|
809
904
|
else None,
|
|
905
|
+
is_subproject=is_subproject,
|
|
810
906
|
)
|
|
907
|
+
if targets:
|
|
908
|
+
ordered_stale_stages = {
|
|
909
|
+
display_name: stale_stage
|
|
910
|
+
for display_name, stale_stage in ordered_stale_stages.items()
|
|
911
|
+
if any(
|
|
912
|
+
_status_target_matches(
|
|
913
|
+
target,
|
|
914
|
+
display_name,
|
|
915
|
+
raw_stale_stages[display_name][0],
|
|
916
|
+
raw_stale_stages[display_name][1],
|
|
917
|
+
stale_stage,
|
|
918
|
+
)
|
|
919
|
+
for target in targets
|
|
920
|
+
)
|
|
921
|
+
}
|
|
811
922
|
result["stale_stages"] = ordered_stale_stages
|
|
812
923
|
return PipelineStatus(
|
|
813
924
|
has_pipeline=result["has_pipeline"],
|
|
@@ -296,3 +296,56 @@ def test_isolated_subproject_external_dep(tmp_dir):
|
|
|
296
296
|
check_environments=False, compile_to_dvc=False
|
|
297
297
|
)
|
|
298
298
|
assert not status.is_stale
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_targeted_status_uses_full_subproject_status_for_stage_targets(
|
|
302
|
+
tmp_dir, monkeypatch
|
|
303
|
+
):
|
|
304
|
+
os.makedirs("solver/.dvc", exist_ok=True)
|
|
305
|
+
root_ck = {
|
|
306
|
+
"subprojects": [{"path": "solver"}],
|
|
307
|
+
"pipeline": {
|
|
308
|
+
"stages": {
|
|
309
|
+
"post-process": {
|
|
310
|
+
"outputs": [{"path": "final.txt", "storage": "git"}]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
solver_ck = {
|
|
316
|
+
"pipeline": {
|
|
317
|
+
"stages": {
|
|
318
|
+
"package-paper": {
|
|
319
|
+
"outputs": [{"path": "paper.txt", "storage": "git"}]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class FakeRepo:
|
|
326
|
+
def status(self, targets=None):
|
|
327
|
+
if targets is None:
|
|
328
|
+
return {
|
|
329
|
+
"solver/dvc.yaml:package-paper": ["always changed"],
|
|
330
|
+
"post-process": [
|
|
331
|
+
{"changed deps": {"solver/docs/figs": "modified"}}
|
|
332
|
+
],
|
|
333
|
+
}
|
|
334
|
+
return {}
|
|
335
|
+
|
|
336
|
+
monkeypatch.setattr(
|
|
337
|
+
calkit,
|
|
338
|
+
"load_calkit_info",
|
|
339
|
+
lambda wdir=None: solver_ck if wdir == "solver" else root_ck,
|
|
340
|
+
)
|
|
341
|
+
monkeypatch.setattr(calkit.dvc, "get_dvc_repo", lambda *args: FakeRepo())
|
|
342
|
+
monkeypatch.setattr(calkit.dvc, "status_as_posix", lambda status: status)
|
|
343
|
+
status = calkit.pipeline.get_status(
|
|
344
|
+
ck_info=root_ck,
|
|
345
|
+
targets=["post-process"],
|
|
346
|
+
check_environments=False,
|
|
347
|
+
clean_notebooks=False,
|
|
348
|
+
compile_to_dvc=False,
|
|
349
|
+
)
|
|
350
|
+
assert status.is_stale
|
|
351
|
+
assert status.stale_stage_names == ["post-process"]
|
calkit/tests/cli/test_new.py
CHANGED
|
@@ -853,6 +853,322 @@ def test_new_release(tmp_dir, monkeypatch, httpserver):
|
|
|
853
853
|
# )
|
|
854
854
|
|
|
855
855
|
|
|
856
|
+
def test_new_release_publish_empty_pids(tmp_dir, monkeypatch, httpserver):
|
|
857
|
+
"""Regression test for issue #927.
|
|
858
|
+
|
|
859
|
+
Zenodo's publish action returns 202 and its body may come back with an
|
|
860
|
+
empty ``pids`` payload, so the DOI cannot be read from the publish
|
|
861
|
+
response. We must not assume ``pids.doi.identifier`` is present: instead we
|
|
862
|
+
reserve the DOI on the draft beforehand and recover the minted DOI by
|
|
863
|
+
polling ``GET /records/{id}``. This mocks exactly that shape so a
|
|
864
|
+
regression back to ``resp["pids"]["doi"]["identifier"]`` would fail here.
|
|
865
|
+
"""
|
|
866
|
+
record_id = "test-record-empty-pids"
|
|
867
|
+
reserved_doi = "10.5072/zenodo.reserved999"
|
|
868
|
+
monkeypatch.setenv(
|
|
869
|
+
"CALKIT_INVENIO_BASE_URL_ZENODO",
|
|
870
|
+
httpserver.url_for("").rstrip("/"),
|
|
871
|
+
)
|
|
872
|
+
monkeypatch.setenv("ZENODO_TOKEN", "test-token")
|
|
873
|
+
# Create draft
|
|
874
|
+
httpserver.expect_request(
|
|
875
|
+
re.compile(r"^/records$"), method="POST"
|
|
876
|
+
).respond_with_json({"id": record_id, "pids": {}})
|
|
877
|
+
# File upload slot + content + commit
|
|
878
|
+
httpserver.expect_request(
|
|
879
|
+
re.compile(rf"^/records/{record_id}/draft/files$"), method="POST"
|
|
880
|
+
).respond_with_json({"entries": []})
|
|
881
|
+
httpserver.expect_request(
|
|
882
|
+
re.compile(rf"^/records/{record_id}/draft/files/.+/content$"),
|
|
883
|
+
method="PUT",
|
|
884
|
+
).respond_with_data("", status=200)
|
|
885
|
+
httpserver.expect_request(
|
|
886
|
+
re.compile(rf"^/records/{record_id}/draft/files/.+/commit$"),
|
|
887
|
+
method="POST",
|
|
888
|
+
).respond_with_json({"key": "file", "status": "completed"})
|
|
889
|
+
# Reserve a DOI on the draft – this is where the identifier actually comes
|
|
890
|
+
# from in the #927 scenario
|
|
891
|
+
httpserver.expect_request(
|
|
892
|
+
re.compile(rf"^/records/{record_id}/draft/pids/doi$"), method="POST"
|
|
893
|
+
).respond_with_json({"pids": {"doi": {"identifier": reserved_doi}}})
|
|
894
|
+
# Publish returns an EMPTY pids payload (the bug scenario)
|
|
895
|
+
httpserver.expect_request(
|
|
896
|
+
re.compile(rf"^/records/{record_id}/draft/actions/publish$"),
|
|
897
|
+
method="POST",
|
|
898
|
+
).respond_with_json({"id": record_id, "pids": {}})
|
|
899
|
+
# The published record echoes the minted DOI, recovered by the poll
|
|
900
|
+
httpserver.expect_request(
|
|
901
|
+
re.compile(rf"^/records/{record_id}$"), method="GET"
|
|
902
|
+
).respond_with_json(
|
|
903
|
+
{"id": record_id, "pids": {"doi": {"identifier": reserved_doi}}}
|
|
904
|
+
)
|
|
905
|
+
subprocess.check_call(
|
|
906
|
+
[
|
|
907
|
+
"calkit",
|
|
908
|
+
"new",
|
|
909
|
+
"project",
|
|
910
|
+
".",
|
|
911
|
+
"--title",
|
|
912
|
+
"Test project",
|
|
913
|
+
"--name",
|
|
914
|
+
"test-project",
|
|
915
|
+
]
|
|
916
|
+
)
|
|
917
|
+
subprocess.check_call(
|
|
918
|
+
[
|
|
919
|
+
"git",
|
|
920
|
+
"remote",
|
|
921
|
+
"add",
|
|
922
|
+
"origin",
|
|
923
|
+
"https://github.com/calkit/test-project.git",
|
|
924
|
+
]
|
|
925
|
+
)
|
|
926
|
+
calkit.releases.set_cff_authors(
|
|
927
|
+
[{"first_name": "Alice", "last_name": "Smith", "affiliation": "SomeU"}]
|
|
928
|
+
)
|
|
929
|
+
subprocess.check_call(
|
|
930
|
+
["calkit", "update", "license", "--copyright-holder", "Some Person"]
|
|
931
|
+
)
|
|
932
|
+
# One-shot publish (no --draft) so the new.py publish/poll path runs
|
|
933
|
+
subprocess.check_call(
|
|
934
|
+
[
|
|
935
|
+
"calkit",
|
|
936
|
+
"new",
|
|
937
|
+
"release",
|
|
938
|
+
"--name",
|
|
939
|
+
"v0.1.0",
|
|
940
|
+
"--description",
|
|
941
|
+
"First release.",
|
|
942
|
+
"--no-github",
|
|
943
|
+
"--no-push",
|
|
944
|
+
"--verbose",
|
|
945
|
+
]
|
|
946
|
+
)
|
|
947
|
+
release = calkit.load_calkit_info()["releases"]["v0.1.0"]
|
|
948
|
+
# Despite the empty pids in the publish response, the DOI is recovered
|
|
949
|
+
assert release["doi"] == reserved_doi
|
|
950
|
+
assert release["url"] == f"https://doi.org/{reserved_doi}"
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def _setup_zenodo_sandbox_project(monkeypatch):
|
|
954
|
+
"""Shared setup for the live Zenodo sandbox tests.
|
|
955
|
+
|
|
956
|
+
Skips unless the test is explicitly opted into via
|
|
957
|
+
``CALKIT_TEST_ZENODO_SANDBOX=1`` (so it never runs in CI -- GitHub Actions
|
|
958
|
+
doesn't set that variable) and a sandbox token is available. Resolves the
|
|
959
|
+
token (allowing it to live in a local ``.env``), points the client at the
|
|
960
|
+
sandbox, and creates a project with authors and a license ready to be
|
|
961
|
+
released. Must be called from within a ``tmp_dir`` test.
|
|
962
|
+
"""
|
|
963
|
+
if os.getenv("CALKIT_TEST_ZENODO_SANDBOX") != "1":
|
|
964
|
+
pytest.skip(
|
|
965
|
+
"Live Zenodo sandbox test; set CALKIT_TEST_ZENODO_SANDBOX=1 "
|
|
966
|
+
"(and provide a sandbox ZENODO_TOKEN) to run it"
|
|
967
|
+
)
|
|
968
|
+
# Allow the token to live in a local .env file (the usual way calkit
|
|
969
|
+
# resolves credentials). The calkit subprocesses run in tmp_dir, outside
|
|
970
|
+
# the repo, so their own load_dotenv() won't find it -- we load it here and
|
|
971
|
+
# export it below so they inherit it.
|
|
972
|
+
import dotenv
|
|
973
|
+
|
|
974
|
+
dotenv.load_dotenv()
|
|
975
|
+
token = (
|
|
976
|
+
os.getenv("ZENODO_TOKEN")
|
|
977
|
+
or os.getenv("CALKIT_TEST_ZENODO_TOKEN")
|
|
978
|
+
or os.getenv("CALKIT_ZENODO_TOKEN")
|
|
979
|
+
)
|
|
980
|
+
if not token:
|
|
981
|
+
pytest.skip(
|
|
982
|
+
"No sandbox Zenodo token found; set ZENODO_TOKEN (or "
|
|
983
|
+
"CALKIT_TEST_ZENODO_TOKEN) to a sandbox.zenodo.org token"
|
|
984
|
+
)
|
|
985
|
+
# Export the token as ZENODO_TOKEN so every calkit subprocess picks it up,
|
|
986
|
+
# and make sure nothing forces us onto the production base URL.
|
|
987
|
+
monkeypatch.setenv("ZENODO_TOKEN", token)
|
|
988
|
+
monkeypatch.delenv("CALKIT_USE_PROD_FOR_TESTS", raising=False)
|
|
989
|
+
monkeypatch.delenv("CALKIT_INVENIO_BASE_URL_ZENODO", raising=False)
|
|
990
|
+
# Sanity check: in the test env we should be pointed at the sandbox
|
|
991
|
+
assert (
|
|
992
|
+
calkit.invenio.get_base_url("zenodo")
|
|
993
|
+
== "https://sandbox.zenodo.org/api"
|
|
994
|
+
)
|
|
995
|
+
subprocess.check_call(
|
|
996
|
+
[
|
|
997
|
+
"calkit",
|
|
998
|
+
"new",
|
|
999
|
+
"project",
|
|
1000
|
+
".",
|
|
1001
|
+
"--title",
|
|
1002
|
+
"Test project",
|
|
1003
|
+
"--name",
|
|
1004
|
+
"test-project",
|
|
1005
|
+
]
|
|
1006
|
+
)
|
|
1007
|
+
subprocess.check_call(
|
|
1008
|
+
[
|
|
1009
|
+
"git",
|
|
1010
|
+
"remote",
|
|
1011
|
+
"add",
|
|
1012
|
+
"origin",
|
|
1013
|
+
"https://github.com/calkit/test-project.git",
|
|
1014
|
+
]
|
|
1015
|
+
)
|
|
1016
|
+
calkit.releases.set_cff_authors(
|
|
1017
|
+
[
|
|
1018
|
+
{
|
|
1019
|
+
"first_name": "Alice",
|
|
1020
|
+
"last_name": "Smith",
|
|
1021
|
+
"affiliation": "SomeU",
|
|
1022
|
+
"orcid": "0000-0001-2345-6789",
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
"first_name": "Bob",
|
|
1026
|
+
"last_name": "Jones",
|
|
1027
|
+
"affiliation": None,
|
|
1028
|
+
"orcid": None,
|
|
1029
|
+
},
|
|
1030
|
+
]
|
|
1031
|
+
)
|
|
1032
|
+
subprocess.check_call(
|
|
1033
|
+
["calkit", "update", "license", "--copyright-holder", "Some Person"]
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def test_new_release_zenodo_sandbox(tmp_dir, monkeypatch):
|
|
1038
|
+
"""Live integration test against the real Zenodo sandbox API.
|
|
1039
|
+
|
|
1040
|
+
Unlike ``test_new_release`` (which mocks the Invenio API), this exercises
|
|
1041
|
+
the full release flow against ``sandbox.zenodo.org`` so we can catch
|
|
1042
|
+
regressions the mock can't -- e.g. metadata our client builds that the
|
|
1043
|
+
real API rejects. It is deliberately gated behind
|
|
1044
|
+
``CALKIT_TEST_ZENODO_SANDBOX=1`` so it never runs in CI (GitHub Actions
|
|
1045
|
+
does not set that variable); it's only for occasional manual local
|
|
1046
|
+
verification.
|
|
1047
|
+
|
|
1048
|
+
Requirements to run locally::
|
|
1049
|
+
|
|
1050
|
+
CALKIT_TEST_ZENODO_SANDBOX=1
|
|
1051
|
+
ZENODO_TOKEN=<a sandbox.zenodo.org personal access token with the
|
|
1052
|
+
deposit:write and deposit:actions scopes>
|
|
1053
|
+
|
|
1054
|
+
The test only ever creates (and then deletes) a *draft* record -- it never
|
|
1055
|
+
publishes, because a published sandbox record cannot be removed via the
|
|
1056
|
+
API and would litter the account.
|
|
1057
|
+
"""
|
|
1058
|
+
_setup_zenodo_sandbox_project(monkeypatch)
|
|
1059
|
+
record_id = None
|
|
1060
|
+
try:
|
|
1061
|
+
subprocess.check_call(
|
|
1062
|
+
[
|
|
1063
|
+
"calkit",
|
|
1064
|
+
"new",
|
|
1065
|
+
"release",
|
|
1066
|
+
"--name",
|
|
1067
|
+
"v0.1.0",
|
|
1068
|
+
"--description",
|
|
1069
|
+
"First release.",
|
|
1070
|
+
"--draft",
|
|
1071
|
+
"--no-github",
|
|
1072
|
+
"--verbose",
|
|
1073
|
+
]
|
|
1074
|
+
)
|
|
1075
|
+
ck_info = calkit.load_calkit_info()
|
|
1076
|
+
assert "v0.1.0" in ck_info["releases"]
|
|
1077
|
+
release = ck_info["releases"]["v0.1.0"]
|
|
1078
|
+
record_id = release["record_id"]
|
|
1079
|
+
assert record_id is not None
|
|
1080
|
+
# A DOI should have been reserved for the draft
|
|
1081
|
+
assert release["doi"] is not None
|
|
1082
|
+
# The draft really exists on the sandbox with the metadata we sent;
|
|
1083
|
+
# fetching it confirms our client and the real API agree on shape
|
|
1084
|
+
draft = calkit.invenio.get(f"/records/{record_id}/draft")
|
|
1085
|
+
metadata = draft["metadata"]
|
|
1086
|
+
assert metadata["title"] == "Test project"
|
|
1087
|
+
# NOTE: our client POSTs the InvenioRDM metadata schema (creators with
|
|
1088
|
+
# person_or_org/given_name/family_name), but the Zenodo draft GET
|
|
1089
|
+
# endpoint echoes back the *legacy* Zenodo serialization, where each
|
|
1090
|
+
# creator is a "Last, First" name string. Asserting on the real shape
|
|
1091
|
+
# here is the whole point of this test -- the mock can't reveal it.
|
|
1092
|
+
creators = metadata["creators"]
|
|
1093
|
+
assert creators[0]["name"] == "Smith, Alice"
|
|
1094
|
+
assert creators[0]["affiliation"] == "SomeU"
|
|
1095
|
+
assert creators[1]["name"] == "Jones, Bob"
|
|
1096
|
+
# License and the GitHub related-identifier round-trip correctly
|
|
1097
|
+
assert metadata["license"]["id"] == "cc-by-4.0"
|
|
1098
|
+
related = metadata["related_identifiers"]
|
|
1099
|
+
assert (
|
|
1100
|
+
related[0]["identifier"]
|
|
1101
|
+
== "https://github.com/calkit/test-project"
|
|
1102
|
+
)
|
|
1103
|
+
# Reuploading the draft should also succeed against the real API
|
|
1104
|
+
subprocess.check_call(
|
|
1105
|
+
["calkit", "update", "release", "--name", "v0.1.0", "--reupload"]
|
|
1106
|
+
)
|
|
1107
|
+
finally:
|
|
1108
|
+
# Always clean up the draft so we don't accumulate records on the
|
|
1109
|
+
# sandbox, even if an assertion above failed.
|
|
1110
|
+
if record_id is not None:
|
|
1111
|
+
try:
|
|
1112
|
+
calkit.invenio.delete(
|
|
1113
|
+
f"/records/{record_id}/draft", as_json=False
|
|
1114
|
+
)
|
|
1115
|
+
except Exception as e:
|
|
1116
|
+
print(f"Warning: failed to clean up sandbox draft: {e}")
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def test_new_release_zenodo_sandbox_publish(tmp_dir, monkeypatch):
|
|
1120
|
+
"""Live test of the full *publish* path against the Zenodo sandbox.
|
|
1121
|
+
|
|
1122
|
+
This is the companion to ``test_new_release_zenodo_sandbox``: instead of a
|
|
1123
|
+
draft, it runs a one-shot ``new release`` (no ``--draft``), which is the
|
|
1124
|
+
only path that exercises the DOI-minting logic this branch hardens --
|
|
1125
|
+
reserving a DOI on the draft, publishing, and then (because the publish
|
|
1126
|
+
action returns 202 and may not echo the DOI immediately) retrying a fetch
|
|
1127
|
+
of the published record and pulling the DOI out with
|
|
1128
|
+
``invenio.extract_doi``.
|
|
1129
|
+
|
|
1130
|
+
Same opt-in gate as the draft test (``CALKIT_TEST_ZENODO_SANDBOX=1``), so
|
|
1131
|
+
it never runs in CI. WARNING: unlike the draft test, this PUBLISHES a real
|
|
1132
|
+
sandbox record, which cannot be deleted via the API, so each run leaves one
|
|
1133
|
+
behind on the sandbox account. It is intentionally separate so the draft
|
|
1134
|
+
test can stay self-cleaning.
|
|
1135
|
+
"""
|
|
1136
|
+
_setup_zenodo_sandbox_project(monkeypatch)
|
|
1137
|
+
# No --draft: reserve + publish + mint DOI in one shot. --no-push skips the
|
|
1138
|
+
# git push and GitHub release entirely.
|
|
1139
|
+
subprocess.check_call(
|
|
1140
|
+
[
|
|
1141
|
+
"calkit",
|
|
1142
|
+
"new",
|
|
1143
|
+
"release",
|
|
1144
|
+
"--name",
|
|
1145
|
+
"v0.1.0",
|
|
1146
|
+
"--description",
|
|
1147
|
+
"First release.",
|
|
1148
|
+
"--no-github",
|
|
1149
|
+
"--no-push",
|
|
1150
|
+
"--verbose",
|
|
1151
|
+
]
|
|
1152
|
+
)
|
|
1153
|
+
ck_info = calkit.load_calkit_info()
|
|
1154
|
+
assert "v0.1.0" in ck_info["releases"]
|
|
1155
|
+
release = ck_info["releases"]["v0.1.0"]
|
|
1156
|
+
record_id = release["record_id"]
|
|
1157
|
+
assert record_id is not None
|
|
1158
|
+
# The minted DOI should have been resolved and saved (this is exactly what
|
|
1159
|
+
# the publish-path fix is about)
|
|
1160
|
+
doi = release["doi"]
|
|
1161
|
+
assert doi is not None
|
|
1162
|
+
assert doi.startswith("10.")
|
|
1163
|
+
print(f"Published sandbox record {record_id} with DOI {doi}")
|
|
1164
|
+
# The published record really exists and exposes the same DOI via the API,
|
|
1165
|
+
# confirming extract_doi agrees with the real published record
|
|
1166
|
+
record = calkit.invenio.get(f"/records/{record_id}")
|
|
1167
|
+
assert calkit.invenio.extract_doi(record) == doi
|
|
1168
|
+
# A Git tag for the release should have been created locally
|
|
1169
|
+
assert "v0.1.0" in [tag.name for tag in git.Repo().tags]
|
|
1170
|
+
|
|
1171
|
+
|
|
856
1172
|
def test_new_release_is_runnable(tmp_dir, monkeypatch):
|
|
857
1173
|
# Provide a dummy Zenodo token so `new_release` can pass its early
|
|
858
1174
|
# token-validation step even in dry-run mode.
|
calkit/tests/test_core.py
CHANGED
|
@@ -108,6 +108,25 @@ def test_check_system_deps(tmp_dir):
|
|
|
108
108
|
calkit.check_system_deps()
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def test_check_dep_exists_conda_off_path(monkeypatch):
|
|
112
|
+
# Conda is frequently installed but not on the PATH (especially on
|
|
113
|
+
# Windows). check_dep_exists should locate it via find_conda_exe rather
|
|
114
|
+
# than relying on ``conda --version`` being directly runnable.
|
|
115
|
+
import calkit.conda
|
|
116
|
+
|
|
117
|
+
# Simulate conda being absent from the PATH but present in a typical
|
|
118
|
+
# install location that find_conda_exe discovers.
|
|
119
|
+
monkeypatch.setattr(
|
|
120
|
+
calkit.conda, "find_conda_exe", lambda: "/opt/miniconda3/bin/conda"
|
|
121
|
+
)
|
|
122
|
+
monkeypatch.setattr(calkit.conda, "find_mamba_exe", lambda: None)
|
|
123
|
+
assert calkit.check_dep_exists("conda", "app")
|
|
124
|
+
assert not calkit.check_dep_exists("mamba", "app")
|
|
125
|
+
# When neither is findable, it should report missing.
|
|
126
|
+
monkeypatch.setattr(calkit.conda, "find_conda_exe", lambda: None)
|
|
127
|
+
assert not calkit.check_dep_exists("conda", "app")
|
|
128
|
+
|
|
129
|
+
|
|
111
130
|
def test_get_md5(tmp_dir):
|
|
112
131
|
with open("file1.txt", "w") as f:
|
|
113
132
|
f.write("Hello world")
|
calkit/tests/test_invenio.py
CHANGED
|
@@ -13,3 +13,23 @@ def test_get_base_url():
|
|
|
13
13
|
def test_get_token():
|
|
14
14
|
token = calkit.invenio.get_token("zenodo")
|
|
15
15
|
assert isinstance(token, str)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_extract_doi():
|
|
19
|
+
extract_doi = calkit.invenio.extract_doi
|
|
20
|
+
# DOI under pids (e.g. publish response or reserved draft)
|
|
21
|
+
assert (
|
|
22
|
+
extract_doi({"pids": {"doi": {"identifier": "10.5281/zenodo.1"}}})
|
|
23
|
+
== "10.5281/zenodo.1"
|
|
24
|
+
)
|
|
25
|
+
# DOI at the top level
|
|
26
|
+
assert extract_doi({"doi": "10.5281/zenodo.2"}) == "10.5281/zenodo.2"
|
|
27
|
+
# DOI under metadata
|
|
28
|
+
assert (
|
|
29
|
+
extract_doi({"metadata": {"doi": "10.5281/zenodo.3"}})
|
|
30
|
+
== "10.5281/zenodo.3"
|
|
31
|
+
)
|
|
32
|
+
# Empty pids (publish response without a reserved DOI) returns None
|
|
33
|
+
assert extract_doi({"pids": {}}) is None
|
|
34
|
+
assert extract_doi({"pids": {"doi": {}}}) is None
|
|
35
|
+
assert extract_doi({}) is None
|
calkit/tests/test_pipeline.py
CHANGED
|
@@ -10,7 +10,9 @@ import calkit
|
|
|
10
10
|
import calkit.pipeline
|
|
11
11
|
from calkit.environments import get_env_lock_fpath
|
|
12
12
|
from calkit.pipeline import (
|
|
13
|
+
StaleStage,
|
|
13
14
|
_expand_dep_excluding_subprojects,
|
|
15
|
+
_status_target_matches,
|
|
14
16
|
collapse_dvc_stages,
|
|
15
17
|
stages_are_similar,
|
|
16
18
|
)
|
|
@@ -1595,6 +1597,115 @@ def test_stale_stage_path_prefix():
|
|
|
1595
1597
|
assert stale_cross.modified_inputs == ["shared.txt"]
|
|
1596
1598
|
|
|
1597
1599
|
|
|
1600
|
+
def test_subproject_configured_outputs_posix_no_duplicate():
|
|
1601
|
+
# A non-isolated subproject stage's configured outputs are prefixed with
|
|
1602
|
+
# the subproject path. DVC reports those same outputs as repo-root-relative
|
|
1603
|
+
# posix paths. Building the configured path with the OS-native separator
|
|
1604
|
+
# produced, on Windows, a backslash variant ("sub\out.csv") that escaped
|
|
1605
|
+
# path dedup and showed up as a duplicate stale output alongside DVC's
|
|
1606
|
+
# posix path ("sub/out.csv"). The configured path must therefore be posix.
|
|
1607
|
+
from pathlib import PureWindowsPath
|
|
1608
|
+
|
|
1609
|
+
# The exact construction get_status performs; as_posix() normalizes the
|
|
1610
|
+
# separator regardless of platform (PureWindowsPath stands in for Windows).
|
|
1611
|
+
configured_output = (
|
|
1612
|
+
PureWindowsPath("example-basic") / "data/raw/data.csv"
|
|
1613
|
+
).as_posix()
|
|
1614
|
+
assert configured_output == "example-basic/data/raw/data.csv"
|
|
1615
|
+
# DVC flagged the stage via a changed command and reports the output as a
|
|
1616
|
+
# modified out using the same posix, repo-root-relative path.
|
|
1617
|
+
stale_stage = calkit.pipeline.StaleStage.from_status_data(
|
|
1618
|
+
status_data=[
|
|
1619
|
+
{"changed outs": {"example-basic/data/raw/data.csv": "modified"}},
|
|
1620
|
+
"changed command",
|
|
1621
|
+
],
|
|
1622
|
+
configured_outputs=[configured_output],
|
|
1623
|
+
)
|
|
1624
|
+
# The configured output and the DVC-reported output are the same file, so
|
|
1625
|
+
# it must appear exactly once -- no separator-variant duplicate.
|
|
1626
|
+
assert stale_stage.stale_outputs == ["example-basic/data/raw/data.csv"]
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
def test_always_run_excludes_subproject_stages():
|
|
1630
|
+
# A subproject wrapper stage is marked always-changed purely as a
|
|
1631
|
+
# delegation mechanism, so it must not be advertised as an always-run
|
|
1632
|
+
# stage of the parent project. A genuine root always-run stage still is.
|
|
1633
|
+
status = calkit.pipeline.PipelineStatus(
|
|
1634
|
+
has_pipeline=True,
|
|
1635
|
+
stale_stages={
|
|
1636
|
+
"ticker": StaleStage(always_run=True),
|
|
1637
|
+
"example-basic (subproject)": StaleStage(
|
|
1638
|
+
always_run=True, is_subproject=True
|
|
1639
|
+
),
|
|
1640
|
+
},
|
|
1641
|
+
)
|
|
1642
|
+
assert status.always_run_stage_names == ["ticker"]
|
|
1643
|
+
# The subproject wrapper isn't stale either (pure always-run delegation).
|
|
1644
|
+
assert "example-basic (subproject)" not in status.stale_stage_names
|
|
1645
|
+
assert not status.is_stale
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
def test_status_target_matches():
|
|
1649
|
+
ss = StaleStage(
|
|
1650
|
+
stale_outputs=["data/out.csv"],
|
|
1651
|
+
modified_inputs=["data/in.csv"],
|
|
1652
|
+
)
|
|
1653
|
+
# Root stage selected by its name, the dvc.yaml: form, but not by an
|
|
1654
|
+
# unrelated name.
|
|
1655
|
+
assert _status_target_matches("build", "build", "build", None, ss)
|
|
1656
|
+
assert _status_target_matches("dvc.yaml:build", "build", "build", None, ss)
|
|
1657
|
+
assert not _status_target_matches("other", "build", "build", None, ss)
|
|
1658
|
+
# A path target (one containing a separator, so it isn't mistaken for a
|
|
1659
|
+
# bare stage name) matches a stage with overlapping stale/modified paths.
|
|
1660
|
+
assert _status_target_matches("data/out.csv", "build", "build", None, ss)
|
|
1661
|
+
assert _status_target_matches("data/in.csv", "build", "build", None, ss)
|
|
1662
|
+
assert not _status_target_matches(
|
|
1663
|
+
"other/path.csv", "build", "build", None, ss
|
|
1664
|
+
)
|
|
1665
|
+
# Iterated/matrix stages use a name@param key — targeting the base name
|
|
1666
|
+
# (or its dvc.yaml: form) must select each iteration, matching how DVC
|
|
1667
|
+
# filtered matrix targets before status was filtered locally.
|
|
1668
|
+
assert _status_target_matches("sim", "sim@a", "sim@a", None, ss)
|
|
1669
|
+
assert _status_target_matches("dvc.yaml:sim", "sim@a", "sim@a", None, ss)
|
|
1670
|
+
assert _status_target_matches("sim@a", "sim@a", "sim@a", None, ss)
|
|
1671
|
+
# Subproject stages selected by sp:stage, the whole subproject, and the
|
|
1672
|
+
# matrix base name in either form.
|
|
1673
|
+
assert _status_target_matches("sp:run", "sp:run", "run", "sp", ss)
|
|
1674
|
+
assert _status_target_matches("sp", "sp:run", "run", "sp", ss)
|
|
1675
|
+
assert _status_target_matches("sp:sim", "sp:sim@a", "sim@a", "sp", ss)
|
|
1676
|
+
assert not _status_target_matches("sp:other", "sp:run", "run", "sp", ss)
|
|
1677
|
+
# DVC's native inline-subproject stage form (what translate_run_targets
|
|
1678
|
+
# emits for inline subprojects) must also select the stage.
|
|
1679
|
+
assert _status_target_matches("sp/dvc.yaml:run", "sp:run", "run", "sp", ss)
|
|
1680
|
+
assert _status_target_matches(
|
|
1681
|
+
"sp/dvc.yaml:sim", "sp:sim@a", "sim@a", "sp", ss
|
|
1682
|
+
)
|
|
1683
|
+
assert not _status_target_matches(
|
|
1684
|
+
"sp/dvc.yaml:other", "sp:run", "run", "sp", ss
|
|
1685
|
+
)
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def test_status_target_matches_root_level_path(tmp_path, monkeypatch):
|
|
1689
|
+
# A root-level path target without any separator (e.g. ``out.csv`` or a
|
|
1690
|
+
# directory ``data``) must still be selectable when it exists on disk; a
|
|
1691
|
+
# separator-only heuristic would wrongly treat it as a bare stage name.
|
|
1692
|
+
monkeypatch.chdir(tmp_path)
|
|
1693
|
+
(tmp_path / "out.csv").write_text("x")
|
|
1694
|
+
(tmp_path / "data").mkdir()
|
|
1695
|
+
ss = StaleStage(
|
|
1696
|
+
stale_outputs=["out.csv"],
|
|
1697
|
+
modified_inputs=["data/in.csv"],
|
|
1698
|
+
)
|
|
1699
|
+
assert _status_target_matches("out.csv", "build", "build", None, ss)
|
|
1700
|
+
assert _status_target_matches("./out.csv", "build", "build", None, ss)
|
|
1701
|
+
# A directory target overlaps a modified input nested under it.
|
|
1702
|
+
assert _status_target_matches("data", "build", "build", None, ss)
|
|
1703
|
+
assert _status_target_matches("data/", "build", "build", None, ss)
|
|
1704
|
+
# A separator-less target that is neither a stage name nor an existing
|
|
1705
|
+
# path is not selected.
|
|
1706
|
+
assert not _status_target_matches("missing", "build", "build", None, ss)
|
|
1707
|
+
|
|
1708
|
+
|
|
1598
1709
|
def test_collapse_dvc_stages():
|
|
1599
1710
|
def out_paths(stage):
|
|
1600
1711
|
return {
|
|
@@ -5,7 +5,7 @@ calkit/check.py,sha256=VVJErHeCzX-bcdRtu0dgMpLLMU7M38SZI_wqyMgdzwg,9080
|
|
|
5
5
|
calkit/cloud.py,sha256=65380HbUVEINxXue9YZnbOX5PsnszdgR9ZCli0DR2Wg,15772
|
|
6
6
|
calkit/conda.py,sha256=Jb1Fz3jwkk4SZ0NgoWXd4lf50gEC8d8U87ElrYjdiwk,30752
|
|
7
7
|
calkit/config.py,sha256=tkrdAQU1m3__B9efpYgK6IhtLtS3WWFcVpY2zEQpBio,7573
|
|
8
|
-
calkit/core.py,sha256=
|
|
8
|
+
calkit/core.py,sha256=NbuNHcSRzmvovSIuCKB05aX_XZj3Hl38Szk-VQXCuM4,27873
|
|
9
9
|
calkit/datasets.py,sha256=9lOlOreey2aJGMw6MiV9MDzSHp-fYkCf1k1AZbWTdQE,2235
|
|
10
10
|
calkit/dependencies.py,sha256=y5N_4ctwp9rFZVg82Si9_2x8Yh64Ex-WqP0iiN5NNBE,13276
|
|
11
11
|
calkit/detect.py,sha256=w8H8r2p0R38645TUWFz9xtmeinKic_T8E6eTuldN6OM,62418
|
|
@@ -16,7 +16,7 @@ calkit/git.py,sha256=rHAWdI13vA35m7q-MhlHj2KHMa42s4ecisTvKbdXKMw,13321
|
|
|
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
|
|
19
|
-
calkit/invenio.py,sha256=
|
|
19
|
+
calkit/invenio.py,sha256=aJH04-X9eFzxnrnbrPs71XBeFYBgKYYPiVM2KtyCXEE,4294
|
|
20
20
|
calkit/julia.py,sha256=fYj4KuwEPDianrDf6k5DHvfMYqs09v4ts7MOQTWa3OI,3329
|
|
21
21
|
calkit/jupyter.py,sha256=lE8kKmauHqspY7aPOiNpLGQyw9jSn4taGZScm6aR87I,1157
|
|
22
22
|
calkit/licenses.py,sha256=3ZNBFxwRrfSjx3poG3G07z7GRCKAkdWiZ1AFe1yr8Gg,6853
|
|
@@ -26,7 +26,7 @@ calkit/notebooks.py,sha256=obKRJaVf-rrQslcu76yx8lP18QKWGJOD_Irw7yHw9ek,8913
|
|
|
26
26
|
calkit/office.py,sha256=mZqCxkIsmWthBAeJZ5SiGe21fn5zD2zXsy1E6nQIpWE,1291
|
|
27
27
|
calkit/ops.py,sha256=lLlZAFeplU8uLeMavr99d__Iof-YYwKijus74WfLipk,854
|
|
28
28
|
calkit/overleaf.py,sha256=5SI8HwglLVtVlzj7d15Bft_3ZC6AHn3IrAbqMKOAPX0,30170
|
|
29
|
-
calkit/pipeline.py,sha256=
|
|
29
|
+
calkit/pipeline.py,sha256=PYOk_RHQAQL_SiSh-nV69OFxQ6Da7zbC4mBd2dw5rCo,63212
|
|
30
30
|
calkit/releases.py,sha256=YK-PLD5fQ7Zapw0lrYP0tY_QRPTfKf9-8rkwHnK-1Nk,13742
|
|
31
31
|
calkit/server.py,sha256=GQm1bI5Q1NLFNwQL-iP6j4iWKUpKpP3cLEyfj5IMwe4,22009
|
|
32
32
|
calkit/cli/__init__.py,sha256=pP_1wFuewdUUvEWTtaEcdKblPaSYuTvthQb2Ef08UMo,1921
|
|
@@ -39,14 +39,14 @@ calkit/cli/dev.py,sha256=cu4QsfDiA_KurGx4WJcgA2qgfghMoZutE8kl4IZbUcM,1070
|
|
|
39
39
|
calkit/cli/import_.py,sha256=5j2ANLq6v7HOku9kJUWylwvQnJfkb_OUKCNOdv1BQXU,18264
|
|
40
40
|
calkit/cli/latex.py,sha256=dhcbVNcR3YjVSD5mo-OBtZN1FhH4mv4mlGnuP19EbjU,6220
|
|
41
41
|
calkit/cli/list.py,sha256=iWP3gfXnl13ZciXVpbZQlcu4hAQkBnfvp_D--4T08-0,6084
|
|
42
|
-
calkit/cli/new.py,sha256=
|
|
42
|
+
calkit/cli/new.py,sha256=i3_kaWAW78vhSEdbdoGyMj0sqkaZcvFPQPBiQqKJMbQ,125465
|
|
43
43
|
calkit/cli/notebooks.py,sha256=dEjJ1qeh6SG99chhgUaxe_Cpv9hGERR98fBE6dscZmo,21381
|
|
44
44
|
calkit/cli/office.py,sha256=jVSTvbl91TCzlglST5O2b8hdApZA_QHw_UiEQFJqxdc,1754
|
|
45
45
|
calkit/cli/overleaf.py,sha256=bS-SkYsSqqhC9YsP-NCzpYJCGS3820w18w_s8JOFihg,25216
|
|
46
46
|
calkit/cli/scheduler.py,sha256=cxWpXJ71O8Wx4afEKkxoUPTK0RTJLLmvls2UlYVTS9E,28971
|
|
47
47
|
calkit/cli/update.py,sha256=GX8vcO46JDCDRrDK4YmZJ2ODLgGz6_s5pTjao7S_trI,25543
|
|
48
48
|
calkit/cli/main/__init__.py,sha256=qu1POZPyqs33ZKfasOxv_Wc-EzVcEgK3Dt_vwFL8Bi8,65
|
|
49
|
-
calkit/cli/main/core.py,sha256=
|
|
49
|
+
calkit/cli/main/core.py,sha256=_QgV8c-9LDd3xZyFQQhZ6JRITVKadQ2U4tzAZnuenGc,121134
|
|
50
50
|
calkit/cli/main/xr.py,sha256=Bug5qJuiIq0tusmdu30t-ZdyaT_Sy5RwPSTiHvCtIqE,25600
|
|
51
51
|
calkit/dvc/__init__.py,sha256=-B6tilCb_jw7QFmJ2srILez24wDhNKUzIBhZUMt01dE,381
|
|
52
52
|
calkit/dvc/core.py,sha256=54s-LwttJ6GWWwkwhAkBcwJBXpVajEoYPcJGxLD5zlc,23780
|
|
@@ -73,7 +73,7 @@ calkit/tests/test_calc.py,sha256=h-wOUIRqfyi06LTzhY_rS-a1xChZTkmwdEfBybMUM7E,203
|
|
|
73
73
|
calkit/tests/test_check.py,sha256=ngRZq1_dSRQoa0oSjcx_QpR94omG9Bpas_h0wqjwrK0,1475
|
|
74
74
|
calkit/tests/test_cloud.py,sha256=583oy2zH0fLexBBB_cmrxCgu7qsouBe7tKY-kkbzhXE,10227
|
|
75
75
|
calkit/tests/test_conda.py,sha256=t-Oy1mL3Vb5njj_t3LQ8HwMhsmLN64HAzSlebnN1A9s,14204
|
|
76
|
-
calkit/tests/test_core.py,sha256=
|
|
76
|
+
calkit/tests/test_core.py,sha256=qZ090GbIu-gqu-0qJqwPj47t7k1Q_N_5mv0sT2VAeZc,5496
|
|
77
77
|
calkit/tests/test_dependencies.py,sha256=HskXLzbvvRJz2TXnKPpPAo9lPhoZGYQFy4BmEF31nvk,11139
|
|
78
78
|
calkit/tests/test_detect.py,sha256=1QLei1SjLdxy3IWwFZi3PIz8WHpKmG50Hjygc_HxYxg,32907
|
|
79
79
|
calkit/tests/test_docker.py,sha256=27d0K1GOWiCNv_5TkZSob6EaC0EtQ9DSBIDfkEAEzGk,2077
|
|
@@ -81,14 +81,14 @@ calkit/tests/test_environments.py,sha256=KlIQ7tZhhPSFlJ3cM4nLCpCxShB18MCHUBW0y2K
|
|
|
81
81
|
calkit/tests/test_fs.py,sha256=8XFlC6bsw_aGodhmDq87OnI60L2licz459yUlDcAieY,9448
|
|
82
82
|
calkit/tests/test_git.py,sha256=bxPVTn-0DD9mhlarBjEnm6V8XSJHfvm2etl9Z5Pq-qg,15076
|
|
83
83
|
calkit/tests/test_install.py,sha256=Yh7-OFQ2L4S0uRkYPfQpqe4V2y1A9DwZddMEi8pQbMU,9228
|
|
84
|
-
calkit/tests/test_invenio.py,sha256=
|
|
84
|
+
calkit/tests/test_invenio.py,sha256=aDI8cFOUUQEkfgHSK_Nv1cMNMsnmNr2W2weuZhrxshI,998
|
|
85
85
|
calkit/tests/test_julia.py,sha256=fw8pTkXJEUYow3B8q-_oWw859MH4UDy9AuDk6nNaDQA,3188
|
|
86
86
|
calkit/tests/test_jupyter.py,sha256=YTL6zI740UM2KUjskSipzvSKxsyQ8rVPFQIX5cb2tMQ,114
|
|
87
87
|
calkit/tests/test_licenses.py,sha256=AxoydeUG1Z6rDVeJJ64YXZ8xCJeyQyJdq6GSkDjlwJE,4001
|
|
88
88
|
calkit/tests/test_magics.py,sha256=1O_hYPLMBVQJj8rLCFV3sU2DejlLLnk6Tv4_Tr-FZHQ,2101
|
|
89
89
|
calkit/tests/test_matlab.py,sha256=7yFKOYDCtxYIkRyFOwTIxlvzx03Pac0FKGIUT4Zx7zA,7589
|
|
90
90
|
calkit/tests/test_notebooks.py,sha256=6xUpHjCbCv08oJkArj-NkTUMlz9zzm8pf1qam3s27M0,2692
|
|
91
|
-
calkit/tests/test_pipeline.py,sha256=
|
|
91
|
+
calkit/tests/test_pipeline.py,sha256=crqI5YJcSb0_-imFubxv-ZWnRJ1j8NYgsuNjI-z9iUg,83661
|
|
92
92
|
calkit/tests/test_releases.py,sha256=_sh7s3hjfAAw5vJ7Zdjp5CRfE8uv4j63jZu_I-N_CPc,10338
|
|
93
93
|
calkit/tests/test_templates.py,sha256=RN4RIgmuFlmFYQFgfJUYS-KaLmie8fnWzmfg5Ay1egk,430
|
|
94
94
|
calkit/tests/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -98,14 +98,14 @@ calkit/tests/cli/test_config.py,sha256=Tl4LWfi81eQo6fUgJsoZRNPj_LfkKH-s42HRg5Mxh
|
|
|
98
98
|
calkit/tests/cli/test_import.py,sha256=e2BToVPebf4NalFQuIgyTNJHwbc9VMc6AYO-FAhYbQ0,663
|
|
99
99
|
calkit/tests/cli/test_latex.py,sha256=SqDbenoFtnsgkfqFjR2Jhw6TJsYQBtLqIGjM3PJmddc,3604
|
|
100
100
|
calkit/tests/cli/test_list.py,sha256=vwPGDANLzFR9neWmeT4idS8vaYS2w7UoZZ0Q3ItF9kw,1801
|
|
101
|
-
calkit/tests/cli/test_new.py,sha256=
|
|
101
|
+
calkit/tests/cli/test_new.py,sha256=aFIoqLSPF43rOU9zend6NDhjP3CylerF0y23fwq2dwk,44448
|
|
102
102
|
calkit/tests/cli/test_notebooks.py,sha256=pJX6KV45tNBd-ZpqyoayDzNop9-maRIN_fC2j3EcPf0,7899
|
|
103
103
|
calkit/tests/cli/test_overleaf.py,sha256=Y-ud4kZHh3nhyoZlXrtMfx61z-GPNytUpfDHMad3tuA,16474
|
|
104
104
|
calkit/tests/cli/test_scheduler.py,sha256=orCXPBbn9Mx4moemWNSfsil5cGCOcNgRgw6Yy5zDwjk,6061
|
|
105
105
|
calkit/tests/cli/test_update.py,sha256=68Dzrm4eO4lZZ0zcpdQCo-1mgwrxE6n9lQao0UJx9ag,3212
|
|
106
106
|
calkit/tests/cli/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
107
107
|
calkit/tests/cli/main/test_core.py,sha256=Zv0Vyt6WZbJP9Z2PdP4ZTwtQ6WckoeTGfNSO9lE8dZA,59106
|
|
108
|
-
calkit/tests/cli/main/test_subprojects.py,sha256=
|
|
108
|
+
calkit/tests/cli/main/test_subprojects.py,sha256=ZR2ZQvZ4XZOi-iW8nlZEYu_2CUyUWoc0MABmnFrF9DQ,12730
|
|
109
109
|
calkit/tests/cli/main/test_xr.py,sha256=shk8LHS33bZpr_iAW5eA5sMD3nQDKm8fIwXlE6dA2uI,22527
|
|
110
110
|
calkit/tests/dvc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
111
111
|
calkit/tests/dvc/test_core.py,sha256=YkN0VsB8wB68K5N_L3V-Wq9EyGxOaZil4_FISf6iuB0,10623
|
|
@@ -118,23 +118,23 @@ calkit/tests/models/test_pipeline.py,sha256=AJsMAX4928U3_Omt0_8K96U5LU_KoNErgpHe
|
|
|
118
118
|
calkit/agent_skills/add-pipeline-stage/SKILL.md,sha256=kpYYb-rJsS4B9p1Ub3gL21irCG5ZZ4DvWZpdJeEwPZk,3772
|
|
119
119
|
calkit/agent_skills/conventions/SKILL.md,sha256=nAM2FjSMk6ED56Dr5zh2bL_dhfzDra-h2ZgzHU7ruS4,9524
|
|
120
120
|
calkit/agent_skills/create-pipeline/SKILL.md,sha256=2FGw1iDQVRk5FUq79GP3lPdgvgof2Wi1O9O-abGGtgs,5422
|
|
121
|
-
calkit_python-0.41.
|
|
122
|
-
calkit_python-0.41.
|
|
123
|
-
calkit_python-0.41.
|
|
124
|
-
calkit_python-0.41.
|
|
125
|
-
calkit_python-0.41.
|
|
126
|
-
calkit_python-0.41.
|
|
127
|
-
calkit_python-0.41.
|
|
128
|
-
calkit_python-0.41.
|
|
129
|
-
calkit_python-0.41.
|
|
130
|
-
calkit_python-0.41.
|
|
131
|
-
calkit_python-0.41.
|
|
132
|
-
calkit_python-0.41.
|
|
133
|
-
calkit_python-0.41.
|
|
134
|
-
calkit_python-0.41.
|
|
135
|
-
calkit_python-0.41.
|
|
136
|
-
calkit_python-0.41.
|
|
137
|
-
calkit_python-0.41.
|
|
138
|
-
calkit_python-0.41.
|
|
139
|
-
calkit_python-0.41.
|
|
140
|
-
calkit_python-0.41.
|
|
121
|
+
calkit_python-0.41.8.data/data/etc/jupyter/jupyter_server_config.d/calkit.json,sha256=CWrZP--JDGz8fsvbAlKr_duTL1vDriGT48SL1YCtkY0,81
|
|
122
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/package.json,sha256=INxx8QhvSl7KP6yL7A7-m2-FIsnB__mYEWG_Eq5RP-c,6651
|
|
123
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig,sha256=6w0b5y8LztEv2tErajJ5d39yBryld02crX0CG3zZFNs,6509
|
|
124
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json,sha256=YSpIrwpwB-lQBk9Mwv-npedWTOOZreO6nlWZhqlWyGI,840
|
|
125
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/502.9a2c5772a15466e923ef.js,sha256=mixXcqFUZukj7_jQVxHTavsYl7cdZv83sEe4phJgkh0,59893
|
|
126
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/695.2c41003a452d43d2b358.js,sha256=LEEAOkUtQ9KzWEHn-QFv5yGi70B-sBOnrvztzHr0Shw,223
|
|
127
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/867.a42a046aa5108f54f8fb.js,sha256=pCoEaqUQj1T4-zAc9SUd__9ZGm7uL2wHQve2xwL89NI,8156
|
|
128
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/909.e3f9cc3408834a7fdcc3.js,sha256=4_nMNAiDSn_cw7UjIHEwone4fizrvqrqSgbwUWvjmoU,114571
|
|
129
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js,sha256=n_a0yu_gE7l6fauyoXXv9BZ7ZEYt2387mTfs7CbLcs4,51939
|
|
130
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/946.050af2abf7845cfbdbd2.js.LICENSE.txt,sha256=eNJ8gc9n9IF8nW1d9sI9niuHstYzjNz5vqXx9UgWSPc,249
|
|
131
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/b2f1c3efe70cb539d121.png,sha256=svHD7-cMtTnRITFwugwsVaB9nZ-h8A61ose8z32HiRE,24850
|
|
132
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/remoteEntry.65469af996e7a96aa983.js,sha256=ZUaa-ZbnqWqpgxIpcQcRX7auvhOBkPH2YHNID5udGQs,8737
|
|
133
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/style.js,sha256=r89Jlk5v1drcwhCpv9FnC3Jig_JH4k24kZNIvxh6Htk,149
|
|
134
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json,sha256=2BGdItLJwO3fAopLl4eJvp26TEwuscYC0-yFaF-LaLU,13683
|
|
135
|
+
calkit_python-0.41.8.data/data/share/jupyter/labextensions/calkit/install.json,sha256=DK9d8G-q-rMVlcT3rAeGIyo3REWKw6FySBZceLU9yaw,187
|
|
136
|
+
calkit_python-0.41.8.dist-info/METADATA,sha256=rQxTqA_zGG2rCn-JWp5zyi1w5RrBRIk0FVfH3zfaWUU,12070
|
|
137
|
+
calkit_python-0.41.8.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
138
|
+
calkit_python-0.41.8.dist-info/entry_points.txt,sha256=2iQBBzjTAOdk66CwS-f3ecXXW5DjUqkG1KBRj8mHI1I,133
|
|
139
|
+
calkit_python-0.41.8.dist-info/licenses/LICENSE,sha256=9ZamCaSUTZk9rcrnf-sWFKLOHr3ws-S_dgKMegW4nw8,1056
|
|
140
|
+
calkit_python-0.41.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|