usdata 0.31.0__tar.gz → 0.32.0__tar.gz
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.
- {usdata-0.31.0 → usdata-0.32.0}/PKG-INFO +1 -1
- {usdata-0.31.0 → usdata-0.32.0}/pyproject.toml +1 -1
- {usdata-0.31.0 → usdata-0.32.0}/pyproject.toml.orig +1 -1
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/__init__.py +2 -1
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_grib.py +23 -2
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/app.py +80 -1
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/doctor.py +3 -2
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/pull.py +136 -6
- {usdata-0.31.0 → usdata-0.32.0}/README.md +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/__main__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_aqs.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_census.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_fetch.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_files.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_hurdat2.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_netcdf.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_progress.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/_radar.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cache.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cache_ops.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cite.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/cache.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/cite.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/doctor.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/inspect.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/cli/progress.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/data/nexrad_sites.csv +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/data/places.csv +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/data/places.sources.json +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/data/registry.yaml +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/inspect.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/manifest.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/mirror.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/models.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/protocols/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/protocols/erddap.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/protocols/http.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/protocols/listing.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/protocols/s3.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/provenance.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/base.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/census/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/census/acs.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/credentials.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/epa/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/epa/aqs.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/fema/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/fema/declarations.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/http.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/coops.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/gfs.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/glm.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/goes.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/grib_index.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/gsom.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/gsoy.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/hrrr.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/lcd.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/mrms.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/nbm.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/nexrad.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/normals.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/rap.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/sites.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/spc.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/noaa/storm_events.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/params.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/usgs/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/usgs/daily.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/py.typed +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/query.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/readers.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/registry.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/selection.py +0 -0
- {usdata-0.31.0 → usdata-0.32.0}/src/usdata/testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usdata
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data
|
|
5
5
|
Keywords: noaa,usgs,open-data,scientific-data,provenance,reproducible-research,weather,climate,meteorology
|
|
6
6
|
Author: Jake Van Slyke
|
|
@@ -23,7 +23,7 @@ from usdata.models import (
|
|
|
23
23
|
TemporalSelection,
|
|
24
24
|
TimeRange,
|
|
25
25
|
)
|
|
26
|
-
from usdata.pull import pull, verify
|
|
26
|
+
from usdata.pull import compare_listing, pull, verify
|
|
27
27
|
from usdata.query import build_query
|
|
28
28
|
from usdata.registry import DatasetNotFound, Registry, SearchResult, default_registry
|
|
29
29
|
from usdata.selection import select_by_time
|
|
@@ -47,6 +47,7 @@ __all__ = [
|
|
|
47
47
|
"build_query",
|
|
48
48
|
"cite_dataset",
|
|
49
49
|
"cite_lockfile",
|
|
50
|
+
"compare_listing",
|
|
50
51
|
"datasets",
|
|
51
52
|
"default_registry",
|
|
52
53
|
"fetch",
|
|
@@ -19,9 +19,10 @@ import gzip
|
|
|
19
19
|
import re
|
|
20
20
|
import warnings
|
|
21
21
|
from collections.abc import Iterator, Mapping, Sequence
|
|
22
|
-
from contextlib import contextmanager
|
|
22
|
+
from contextlib import contextmanager, suppress
|
|
23
23
|
from datetime import UTC, datetime
|
|
24
24
|
from importlib import import_module
|
|
25
|
+
from importlib.util import find_spec
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
27
28
|
|
|
@@ -86,9 +87,29 @@ PROJECTION_KEYS = (
|
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
def import_eccodes() -> Any:
|
|
91
|
+
"""Import the ecCodes bindings, loading pyproj first when it is installed.
|
|
92
|
+
|
|
93
|
+
The eckitlib wheel that the ecCodes library wheel depends on bundles its own
|
|
94
|
+
copy of the PROJ library, and so does pyproj's wheel, which xradar and many
|
|
95
|
+
analysis stacks bring in. When eckitlib's copy is loaded first and pyproj is
|
|
96
|
+
imported later in the same process, both copies free the same memory as the
|
|
97
|
+
interpreter exits, which aborts it with "double free or corruption" and exit
|
|
98
|
+
status 134 after the work is done. Loading pyproj's copy first avoids it.
|
|
99
|
+
A pyproj that fails to import is left for whoever uses it to report.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ModuleNotFoundError: eccodes is not installed.
|
|
103
|
+
"""
|
|
104
|
+
if find_spec("pyproj") is not None:
|
|
105
|
+
with suppress(ImportError, OSError):
|
|
106
|
+
import_module("pyproj")
|
|
107
|
+
return import_module("eccodes")
|
|
108
|
+
|
|
109
|
+
|
|
89
110
|
def _modules() -> tuple[Any, Any, Any]:
|
|
90
111
|
try:
|
|
91
|
-
eccodes =
|
|
112
|
+
eccodes = import_eccodes()
|
|
92
113
|
xarray = import_module("xarray")
|
|
93
114
|
numpy = import_module("numpy")
|
|
94
115
|
except ModuleNotFoundError as error:
|
|
@@ -23,7 +23,15 @@ from usdata.manifest import lockfile_path
|
|
|
23
23
|
from usdata.models import READER_EXTRAS_TEXT, Dataset, Status, describe_duration
|
|
24
24
|
from usdata.providers import adapter_class, load_adapter
|
|
25
25
|
from usdata.providers.base import NotImplementedProvider
|
|
26
|
-
from usdata.pull import
|
|
26
|
+
from usdata.pull import (
|
|
27
|
+
EmptySource,
|
|
28
|
+
ListingComparison,
|
|
29
|
+
ManifestChanged,
|
|
30
|
+
Plan,
|
|
31
|
+
UnknownDatasets,
|
|
32
|
+
UpstreamChanged,
|
|
33
|
+
)
|
|
34
|
+
from usdata.pull import compare_listing as compare_manifest_listing
|
|
27
35
|
from usdata.pull import plan as plan_manifest
|
|
28
36
|
from usdata.pull import pull as pull_manifest
|
|
29
37
|
from usdata.pull import verify as verify_manifest
|
|
@@ -554,12 +562,32 @@ def pull(
|
|
|
554
562
|
def verify(
|
|
555
563
|
manifest: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
|
|
556
564
|
cache_dir: Annotated[Path | None, typer.Option(help="Override the cache directory.")] = None,
|
|
565
|
+
listing: Annotated[
|
|
566
|
+
bool,
|
|
567
|
+
typer.Option(
|
|
568
|
+
"--listing",
|
|
569
|
+
help="Instead of the cache, compare the lockfile with what each source lists today; "
|
|
570
|
+
"downloads nothing, but asks every source.",
|
|
571
|
+
),
|
|
572
|
+
] = False,
|
|
573
|
+
as_json: Annotated[
|
|
574
|
+
bool, typer.Option("--json", help="With --listing, emit the comparison as JSON on stdout.")
|
|
575
|
+
] = False,
|
|
557
576
|
) -> None:
|
|
558
577
|
"""Check cached files against a manifest's lockfile. Exit 1 on any drift."""
|
|
578
|
+
if as_json and not listing:
|
|
579
|
+
typer.secho("--json applies to --listing only", err=True, fg="red")
|
|
580
|
+
raise typer.Exit(code=2)
|
|
581
|
+
if listing and cache_dir is not None:
|
|
582
|
+
typer.secho("--listing reads no cache; drop --cache-dir", err=True, fg="red")
|
|
583
|
+
raise typer.Exit(code=2)
|
|
559
584
|
lock = lockfile_path(manifest)
|
|
560
585
|
if not lock.exists():
|
|
561
586
|
typer.secho(f"no lockfile at {lock}; run pull first", err=True, fg="red")
|
|
562
587
|
raise typer.Exit(code=2)
|
|
588
|
+
if listing:
|
|
589
|
+
_verify_listing(manifest, as_json=as_json)
|
|
590
|
+
return
|
|
563
591
|
try:
|
|
564
592
|
drift = verify_manifest(manifest, root=cache_dir)
|
|
565
593
|
except (ManifestChanged, ValueError, OSError) as e:
|
|
@@ -573,4 +601,55 @@ def verify(
|
|
|
573
601
|
typer.echo(f"all assets match {lock.name}", err=True)
|
|
574
602
|
|
|
575
603
|
|
|
604
|
+
def _verify_listing(manifest: Path, *, as_json: bool) -> None:
|
|
605
|
+
"""Compare the lockfile with today's listings; exit 1 when a re-resolve would differ."""
|
|
606
|
+
try:
|
|
607
|
+
comparison = compare_manifest_listing(manifest)
|
|
608
|
+
except EmptySource as e:
|
|
609
|
+
typer.secho(str(e), err=True, fg="yellow")
|
|
610
|
+
raise typer.Exit(code=1) from None
|
|
611
|
+
except (DatasetNotFound, UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
|
|
612
|
+
typer.secho(str(e), err=True, fg="red")
|
|
613
|
+
raise typer.Exit(code=2) from None
|
|
614
|
+
except NotImplementedProvider as e:
|
|
615
|
+
typer.secho(str(e), err=True, fg="yellow")
|
|
616
|
+
raise typer.Exit(code=3) from None
|
|
617
|
+
except httpx.HTTPError as e:
|
|
618
|
+
typer.secho(f"listing failed: {e}", err=True, fg="red")
|
|
619
|
+
raise typer.Exit(code=4) from None
|
|
620
|
+
_print_listing(comparison, as_json=as_json)
|
|
621
|
+
if not comparison.matches:
|
|
622
|
+
raise typer.Exit(code=1)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _print_listing(comparison: ListingComparison, *, as_json: bool) -> None:
|
|
626
|
+
"""One line per difference, a summary on stderr; or the whole comparison as JSON."""
|
|
627
|
+
name = comparison.lockfile.name
|
|
628
|
+
if as_json:
|
|
629
|
+
typer.echo(comparison.model_dump_json(indent=2))
|
|
630
|
+
else:
|
|
631
|
+
for source in comparison.sources:
|
|
632
|
+
for asset in source.added:
|
|
633
|
+
typer.echo(f"{source.source}\tadded\t{asset.id}\t{asset.href}")
|
|
634
|
+
for asset in source.removed:
|
|
635
|
+
typer.echo(f"{source.source}\tremoved\t{asset.id}\t{asset.href}")
|
|
636
|
+
for change in source.changed:
|
|
637
|
+
sizes = f"{_size(change.locked_size)} -> {_size(change.listed_size)} bytes"
|
|
638
|
+
typer.echo(f"{source.source}\tchanged\t{change.asset_id}\t{sizes}")
|
|
639
|
+
differing = [source.source for source in comparison.sources if not source.matches]
|
|
640
|
+
if differing:
|
|
641
|
+
typer.secho(
|
|
642
|
+
f"{len(differing)} source(s) list something other than {name} pins: "
|
|
643
|
+
f"{', '.join(differing)}. Pass pull --force to re-resolve.",
|
|
644
|
+
err=True,
|
|
645
|
+
fg="red",
|
|
646
|
+
)
|
|
647
|
+
else:
|
|
648
|
+
typer.echo(f"every source lists what {name} pins", err=True)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _size(value: int | None) -> str:
|
|
652
|
+
return "?" if value is None else str(value)
|
|
653
|
+
|
|
654
|
+
|
|
576
655
|
app.command()(doctor)
|
|
@@ -28,7 +28,7 @@ import httpx
|
|
|
28
28
|
from pydantic import BaseModel
|
|
29
29
|
|
|
30
30
|
from usdata import __version__
|
|
31
|
-
from usdata._grib import LIBRARY_HINT
|
|
31
|
+
from usdata._grib import LIBRARY_HINT, import_eccodes
|
|
32
32
|
from usdata.cache import ENV_VAR, cache_dir
|
|
33
33
|
from usdata.mirror import ENV_VAR as MIRROR_ENV_VAR
|
|
34
34
|
from usdata.models import READER_EXTRAS, Status
|
|
@@ -145,7 +145,8 @@ def _reader_check(extra: str) -> Check:
|
|
|
145
145
|
modules: list[Any] = []
|
|
146
146
|
for module in READER_MODULES[extra]:
|
|
147
147
|
try:
|
|
148
|
-
|
|
148
|
+
# ecCodes goes through the reader's import, which orders it after pyproj.
|
|
149
|
+
modules.append(import_eccodes() if module == "eccodes" else import_module(module))
|
|
149
150
|
except (ImportError, RuntimeError, OSError) as error:
|
|
150
151
|
# eccodes imports and then fails to load its shared library.
|
|
151
152
|
return Check(
|
|
@@ -157,6 +157,22 @@ class PullResult(BaseModel):
|
|
|
157
157
|
return items[0]
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def _source_keys(entries: Iterable[LockedAsset]) -> list[str]:
|
|
161
|
+
"""The manifest source key each lockfile entry belongs to, in entry order.
|
|
162
|
+
|
|
163
|
+
A lockfile written before sources had keys records none; such an entry falls
|
|
164
|
+
back to the one-based position of its dataset id among the datasets seen.
|
|
165
|
+
"""
|
|
166
|
+
positions: dict[str, str] = {}
|
|
167
|
+
keys: list[str] = []
|
|
168
|
+
for entry in entries:
|
|
169
|
+
dataset_id = entry.asset.dataset_id
|
|
170
|
+
if dataset_id not in positions:
|
|
171
|
+
positions[dataset_id] = str(len(positions) + 1)
|
|
172
|
+
keys.append(entry.source or positions[dataset_id])
|
|
173
|
+
return keys
|
|
174
|
+
|
|
175
|
+
|
|
160
176
|
def _by_source(pairs: Iterable[tuple[LockedAsset, FetchedAsset]]) -> dict[str, list[FetchedAsset]]:
|
|
161
177
|
"""Group fetched assets by the source key their lockfile entry records.
|
|
162
178
|
|
|
@@ -169,12 +185,9 @@ def _by_source(pairs: Iterable[tuple[LockedAsset, FetchedAsset]]) -> dict[str, l
|
|
|
169
185
|
them and a forced pull rewrites the lockfile.
|
|
170
186
|
"""
|
|
171
187
|
grouped: dict[str, list[FetchedAsset]] = {}
|
|
172
|
-
|
|
173
|
-
for
|
|
174
|
-
|
|
175
|
-
if dataset_id not in positions:
|
|
176
|
-
positions[dataset_id] = str(len(positions) + 1)
|
|
177
|
-
grouped.setdefault(entry.source or positions[dataset_id], []).append(item)
|
|
188
|
+
pairs = list(pairs)
|
|
189
|
+
for key, (_, item) in zip(_source_keys([entry for entry, _ in pairs]), pairs, strict=True):
|
|
190
|
+
grouped.setdefault(key, []).append(item)
|
|
178
191
|
by_id: dict[str, FetchedAsset] = {}
|
|
179
192
|
for items in grouped.values():
|
|
180
193
|
by_id.update({item.asset.id: item for item in items})
|
|
@@ -381,6 +394,123 @@ def plan(manifest_path: str | os.PathLike[str], *, registry: Registry | None = N
|
|
|
381
394
|
return Plan(manifest=manifest.name, sources=sources)
|
|
382
395
|
|
|
383
396
|
|
|
397
|
+
class AssetChange(BaseModel):
|
|
398
|
+
"""A pinned asset that today's listing still names, but describes differently."""
|
|
399
|
+
|
|
400
|
+
asset_id: str
|
|
401
|
+
locked_size: int | None = Field(description="Size the listing gave when the pin was written")
|
|
402
|
+
listed_size: int | None = Field(description="Size today's listing gives")
|
|
403
|
+
locked_href: str
|
|
404
|
+
listed_href: str
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class SourceComparison(BaseModel):
|
|
408
|
+
"""How one source's pinned assets compare with what its adapter lists today."""
|
|
409
|
+
|
|
410
|
+
source: str = Field(description="The source's manifest key: its name, or its position")
|
|
411
|
+
dataset_id: str
|
|
412
|
+
added: list[Asset] = Field(description="Listed today and not pinned")
|
|
413
|
+
removed: list[Asset] = Field(description="Pinned and no longer listed")
|
|
414
|
+
changed: list[AssetChange] = Field(
|
|
415
|
+
description="Pinned and listed, with a different href, or a different size where both "
|
|
416
|
+
"the pin and the listing give one"
|
|
417
|
+
)
|
|
418
|
+
unchanged: int = Field(description="How many pinned assets are listed exactly as pinned")
|
|
419
|
+
|
|
420
|
+
@computed_field(description="Whether a re-resolve of this source would pin the same assets")
|
|
421
|
+
@property
|
|
422
|
+
def matches(self) -> bool:
|
|
423
|
+
"""True when nothing was added, removed, or changed."""
|
|
424
|
+
return not (self.added or self.removed or self.changed)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ListingComparison(BaseModel):
|
|
428
|
+
"""A lockfile against today's listings: whether a re-resolve would pin the same assets.
|
|
429
|
+
|
|
430
|
+
Nothing is downloaded, so the bytes are not compared: restore checks those,
|
|
431
|
+
and ``verify`` checks the cache. What this sees is the set of files, an
|
|
432
|
+
asset added, removed, or re-described since the lockfile was written.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
manifest: str
|
|
436
|
+
lockfile: Path
|
|
437
|
+
sources: list[SourceComparison] = Field(description="One entry per manifest source, in order")
|
|
438
|
+
|
|
439
|
+
@computed_field(description="Whether a re-resolve of every source would pin the same assets")
|
|
440
|
+
@property
|
|
441
|
+
def matches(self) -> bool:
|
|
442
|
+
"""True when every source matches."""
|
|
443
|
+
return all(source.matches for source in self.sources)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _changed(pinned: Asset, listed: Asset) -> AssetChange | None:
|
|
447
|
+
"""How ``listed`` differs from the pin of the same id, or None when it does not."""
|
|
448
|
+
sizes_differ = None not in (pinned.size, listed.size) and pinned.size != listed.size
|
|
449
|
+
if not sizes_differ and pinned.href == listed.href:
|
|
450
|
+
return None
|
|
451
|
+
return AssetChange(
|
|
452
|
+
asset_id=pinned.id,
|
|
453
|
+
locked_size=pinned.size,
|
|
454
|
+
listed_size=listed.size,
|
|
455
|
+
locked_href=pinned.href,
|
|
456
|
+
listed_href=listed.href,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def compare_listing(
|
|
461
|
+
manifest_path: str | os.PathLike[str], *, registry: Registry | None = None
|
|
462
|
+
) -> ListingComparison:
|
|
463
|
+
"""Compare a manifest's lockfile with what its sources list today; download nothing.
|
|
464
|
+
|
|
465
|
+
Every source is listed as ``plan`` lists it, ``select`` rules included, and
|
|
466
|
+
its assets are matched to the lockfile's entries for that source by id. An
|
|
467
|
+
asset only in the listing is ``added``, one only in the lockfile ``removed``,
|
|
468
|
+
and one in both with a different href, or a different size where both give
|
|
469
|
+
one, ``changed``. A listing that reports no size cannot show a size change;
|
|
470
|
+
restoring the pin checks the bytes themselves.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
manifest_path: Path of the manifest YAML file, as a string or any ``os.PathLike``.
|
|
474
|
+
registry: Registry to resolve dataset ids against; the default one if omitted.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
A ``ListingComparison`` with one entry per manifest source.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
FileNotFoundError: The manifest has no lockfile yet.
|
|
481
|
+
ManifestChanged: The manifest changed since its lockfile was written.
|
|
482
|
+
"""
|
|
483
|
+
manifest_path = Path(manifest_path)
|
|
484
|
+
lock_path = lockfile_path(manifest_path)
|
|
485
|
+
if not lock_path.exists():
|
|
486
|
+
raise FileNotFoundError(f"no lockfile at {lock_path}; run pull first")
|
|
487
|
+
lock = Lockfile.load(lock_path)
|
|
488
|
+
_check_manifest(manifest_path, lock)
|
|
489
|
+
pinned: dict[str, dict[str, Asset]] = {}
|
|
490
|
+
for key, entry in zip(_source_keys(lock.assets), lock.assets, strict=True):
|
|
491
|
+
pinned.setdefault(key, {})[entry.asset.id] = entry.asset
|
|
492
|
+
sources: list[SourceComparison] = []
|
|
493
|
+
for listed in plan(manifest_path, registry=registry).sources:
|
|
494
|
+
locked = pinned.get(listed.source, {})
|
|
495
|
+
today = {asset.id: asset for asset in listed.assets}
|
|
496
|
+
changes = [
|
|
497
|
+
change
|
|
498
|
+
for asset_id, asset in locked.items()
|
|
499
|
+
if asset_id in today and (change := _changed(asset, today[asset_id])) is not None
|
|
500
|
+
]
|
|
501
|
+
sources.append(
|
|
502
|
+
SourceComparison(
|
|
503
|
+
source=listed.source,
|
|
504
|
+
dataset_id=listed.dataset_id,
|
|
505
|
+
added=[asset for asset in listed.assets if asset.id not in locked],
|
|
506
|
+
removed=[asset for asset_id, asset in locked.items() if asset_id not in today],
|
|
507
|
+
changed=changes,
|
|
508
|
+
unchanged=sum(1 for asset_id in locked if asset_id in today) - len(changes),
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
return ListingComparison(manifest=lock.manifest, lockfile=lock_path, sources=sources)
|
|
512
|
+
|
|
513
|
+
|
|
384
514
|
def _selected(lock: Lockfile, update: Iterable[str]) -> set[str]:
|
|
385
515
|
"""Ids of the locked assets that ``update`` selectors (asset or dataset ids) name."""
|
|
386
516
|
selectors = set(update)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|