usdata 0.14.0__tar.gz → 0.16.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.
Files changed (62) hide show
  1. usdata-0.16.0/PKG-INFO +122 -0
  2. usdata-0.16.0/README.md +88 -0
  3. {usdata-0.14.0 → usdata-0.16.0}/pyproject.toml +8 -1
  4. {usdata-0.14.0 → usdata-0.16.0}/pyproject.toml.orig +9 -1
  5. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/__init__.py +5 -0
  6. usdata-0.14.0/src/usdata/fetch.py → usdata-0.16.0/src/usdata/_fetch.py +41 -10
  7. usdata-0.16.0/src/usdata/_grib.py +308 -0
  8. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/_hurdat2.py +1 -1
  9. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/_netcdf.py +1 -1
  10. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/_radar.py +1 -1
  11. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/cache.py +3 -5
  12. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/cli/app.py +2 -2
  13. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/data/registry.yaml +284 -34
  14. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/models.py +0 -2
  15. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/protocols/listing.py +11 -6
  16. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/base.py +89 -6
  17. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/coastwatch.py +24 -11
  18. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/coops.py +69 -47
  19. usdata-0.16.0/src/usdata/providers/noaa/gfs.py +112 -0
  20. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/ghcnd.py +26 -32
  21. usdata-0.16.0/src/usdata/providers/noaa/glm.py +80 -0
  22. usdata-0.16.0/src/usdata/providers/noaa/goes.py +157 -0
  23. usdata-0.16.0/src/usdata/providers/noaa/hrrr.py +224 -0
  24. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/hurdat2.py +23 -12
  25. usdata-0.16.0/src/usdata/providers/noaa/mrms.py +154 -0
  26. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/nexrad.py +55 -39
  27. usdata-0.16.0/src/usdata/providers/noaa/nexrad_level3.py +154 -0
  28. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/normals.py +21 -24
  29. usdata-0.16.0/src/usdata/providers/noaa/spc.py +99 -0
  30. usdata-0.16.0/src/usdata/providers/params.py +126 -0
  31. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/usgs/daily.py +51 -38
  32. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/pull.py +33 -13
  33. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/readers.py +37 -4
  34. usdata-0.14.0/PKG-INFO +0 -93
  35. usdata-0.14.0/README.md +0 -64
  36. usdata-0.14.0/src/usdata/providers/noaa/goes.py +0 -116
  37. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/_files.py +0 -0
  38. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/_progress.py +0 -0
  39. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/cli/__init__.py +0 -0
  40. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/cli/progress.py +0 -0
  41. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/data/nexrad_sites.csv +0 -0
  42. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/data/places.csv +0 -0
  43. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/data/places.sources.json +0 -0
  44. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/manifest.py +0 -0
  45. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/protocols/__init__.py +0 -0
  46. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/protocols/erddap.py +0 -0
  47. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/protocols/http.py +0 -0
  48. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/protocols/s3.py +0 -0
  49. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/provenance.py +0 -0
  50. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/__init__.py +0 -0
  51. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/_http.py +0 -0
  52. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/__init__.py +0 -0
  53. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/gsom.py +0 -0
  54. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  55. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/lcd.py +0 -0
  56. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/sites.py +0 -0
  57. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  58. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/providers/usgs/__init__.py +0 -0
  59. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/py.typed +0 -0
  60. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/query.py +0 -0
  61. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/registry.py +0 -0
  62. {usdata-0.14.0 → usdata-0.16.0}/src/usdata/selection.py +0 -0
usdata-0.16.0/PKG-INFO ADDED
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: usdata
3
+ Version: 0.16.0
4
+ Summary: Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data
5
+ Keywords: noaa,usgs,nasa,open-data,scientific-data,provenance
6
+ Author: Jake Van Slyke
7
+ Author-email: Jake Van Slyke <jakervanslyke@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Dist: httpx>=0.28.1
14
+ Requires-Dist: pydantic>=2.7
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: typer>=0.18
17
+ Requires-Dist: eccodes>=2.48 ; extra == 'grib'
18
+ Requires-Dist: eccodeslib>=2.48 ; sys_platform == 'linux' and extra == 'grib'
19
+ Requires-Dist: xarray>=2025.1 ; extra == 'grib'
20
+ Requires-Dist: numpy>=2 ; extra == 'grib'
21
+ Requires-Dist: xarray>=2025.1 ; extra == 'netcdf'
22
+ Requires-Dist: h5netcdf[h5py]>=1.8.1 ; extra == 'netcdf'
23
+ Requires-Dist: pandas>=2.2 ; extra == 'pandas'
24
+ Requires-Dist: xradar>=0.12.0 ; extra == 'radar'
25
+ Requires-Python: >=3.11
26
+ Project-URL: Homepage, https://usdata.dev/
27
+ Project-URL: Documentation, https://docs.usdata.dev/
28
+ Project-URL: Repository, https://github.com/jakeryderv/usdata
29
+ Provides-Extra: grib
30
+ Provides-Extra: netcdf
31
+ Provides-Extra: pandas
32
+ Provides-Extra: radar
33
+ Description-Content-Type: text/markdown
34
+
35
+ # usdata
36
+
37
+ Discover U.S. scientific datasets, fetch their files, and keep a reproducible
38
+ record of where every input came from. One Python SDK and CLI across supported
39
+ NOAA and USGS datasets, with provenance sidecars and lockable manifests.
40
+
41
+ **Pre-alpha.** Features marked **Unreleased** in the documentation require a
42
+ source installation; the [changelog](CHANGELOG.md) lists what each published
43
+ version contains.
44
+
45
+ ## Documentation
46
+
47
+ | Site | For |
48
+ | --- | --- |
49
+ | [usdata.dev](https://usdata.dev/) | What the package is for: the dataset browser and worked examples with saved results |
50
+ | [docs.usdata.dev](https://docs.usdata.dev/) | Using the package: install, first dataset, guides, dataset notes, and reference |
51
+ | This repository | Changing the project: everything below, plus [contributing](CONTRIBUTING.md), [architecture](docs/architecture.md), [testing](docs/testing.md), [decisions](docs/adr/README.md), [roadmap](docs/roadmap.md), and [versioning](docs/versioning.md) |
52
+
53
+ ```sh
54
+ pip install "usdata[pandas]"
55
+ usdata search precipitation --location Oklahoma
56
+ usdata info noaa:ghcn-daily
57
+ ```
58
+
59
+ ## Development
60
+
61
+ Requires a source checkout, [uv](https://docs.astral.sh/uv/), and
62
+ [just](https://just.systems/):
63
+
64
+ ```sh
65
+ git clone https://github.com/jakeryderv/usdata && cd usdata
66
+ just setup # install toolchain and dependencies
67
+ just check # format, lint, typecheck, offline tests, generated docs, release notices
68
+ ```
69
+
70
+ `just setup` uses the tested Python 3.14.7 pin in `.python-version`. Older Linux
71
+ uv Python 3.14 builds can crash during NumPy array operations; see
72
+ [the upstream fix](https://github.com/astral-sh/python-build-standalone/issues/991).
73
+
74
+ ```sh
75
+ just hooks-install # optional: install fast staged-file checks
76
+ just test # all offline tests
77
+ just check-pandas # install the CSV extra and run the same checks
78
+ just check-radar # install the radar extra and run the same checks
79
+ just check-netcdf # install the NetCDF4 extra and run the same checks
80
+ just check-grib # install the GRIB2 extra and run the same checks (Linux, Windows <= 3.13)
81
+ just notebooks # launch the optional Jupyter examples environment
82
+ just run-notebooks # execute notebooks live in fresh kernels and temporary caches
83
+ just docs-serve # build and preview the documentation locally, with reload
84
+ just check-docs # validate generated content and build the site strictly
85
+ just build # build wheel and sdist
86
+ just smoke # exercise core and pandas wheel installations outside the checkout
87
+ just changes # preview upcoming release notes
88
+ just cleanup 123 # from main: remove PR #123's merged branch and clean worktree
89
+ just run search radar
90
+ ```
91
+
92
+ Commit hooks fix and format staged Python files using the locked Ruff version,
93
+ and check configuration, merge conflicts, and GitHub Actions workflows. Full
94
+ validation remains `just check`; `just check-hooks` runs the non-mutating config
95
+ and workflow checks alone. Hooks do not rewrite notebooks or raw data fixtures.
96
+ The first hook run downloads its isolated tools and requires network access.
97
+
98
+ Offline tests mechanically block network connections. Tests that hit live
99
+ services run with `just test-live`; see [testing levels and organization](docs/testing.md).
100
+ CI checks Python 3.11 and 3.14 on Linux with core-only, pandas, radar, NetCDF,
101
+ and GRIB2 dependency profiles. A further Python 3.11 profile replaces the locked
102
+ pandas with the oldest release the project declares, so the `>=` floor is
103
+ executed rather than assumed; the number comes from `pyproject.toml` through
104
+ `scripts/lowest_version.py`. Installed-wheel checks cover core, pandas, radar,
105
+ and NetCDF on Linux, macOS, and Windows. The full offline and live-service
106
+ suites run on Linux. `just setup` restores a core-only development environment;
107
+ the `check-*` commands install their respective extras.
108
+
109
+ Releases: `just release minor` prepares a release branch. Update release
110
+ notices, then `just release-pr` validates and opens a draft for review. Merging
111
+ the reviewed PR publishes to PyPI and creates the tag and GitHub release. See
112
+ [versioning](docs/versioning.md).
113
+
114
+ The main way the project grows is a new dataset: one registry entry and one
115
+ adapter, following [adding a dataset](docs/guides/adding-a-dataset.md). The two
116
+ websites deploy automatically from `main`; see
117
+ [website operations](docs/guides/website-operations.md) and
118
+ [maintaining documentation](docs/guides/documentation.md).
119
+
120
+ ## License
121
+
122
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,88 @@
1
+ # usdata
2
+
3
+ Discover U.S. scientific datasets, fetch their files, and keep a reproducible
4
+ record of where every input came from. One Python SDK and CLI across supported
5
+ NOAA and USGS datasets, with provenance sidecars and lockable manifests.
6
+
7
+ **Pre-alpha.** Features marked **Unreleased** in the documentation require a
8
+ source installation; the [changelog](CHANGELOG.md) lists what each published
9
+ version contains.
10
+
11
+ ## Documentation
12
+
13
+ | Site | For |
14
+ | --- | --- |
15
+ | [usdata.dev](https://usdata.dev/) | What the package is for: the dataset browser and worked examples with saved results |
16
+ | [docs.usdata.dev](https://docs.usdata.dev/) | Using the package: install, first dataset, guides, dataset notes, and reference |
17
+ | This repository | Changing the project: everything below, plus [contributing](CONTRIBUTING.md), [architecture](docs/architecture.md), [testing](docs/testing.md), [decisions](docs/adr/README.md), [roadmap](docs/roadmap.md), and [versioning](docs/versioning.md) |
18
+
19
+ ```sh
20
+ pip install "usdata[pandas]"
21
+ usdata search precipitation --location Oklahoma
22
+ usdata info noaa:ghcn-daily
23
+ ```
24
+
25
+ ## Development
26
+
27
+ Requires a source checkout, [uv](https://docs.astral.sh/uv/), and
28
+ [just](https://just.systems/):
29
+
30
+ ```sh
31
+ git clone https://github.com/jakeryderv/usdata && cd usdata
32
+ just setup # install toolchain and dependencies
33
+ just check # format, lint, typecheck, offline tests, generated docs, release notices
34
+ ```
35
+
36
+ `just setup` uses the tested Python 3.14.7 pin in `.python-version`. Older Linux
37
+ uv Python 3.14 builds can crash during NumPy array operations; see
38
+ [the upstream fix](https://github.com/astral-sh/python-build-standalone/issues/991).
39
+
40
+ ```sh
41
+ just hooks-install # optional: install fast staged-file checks
42
+ just test # all offline tests
43
+ just check-pandas # install the CSV extra and run the same checks
44
+ just check-radar # install the radar extra and run the same checks
45
+ just check-netcdf # install the NetCDF4 extra and run the same checks
46
+ just check-grib # install the GRIB2 extra and run the same checks (Linux, Windows <= 3.13)
47
+ just notebooks # launch the optional Jupyter examples environment
48
+ just run-notebooks # execute notebooks live in fresh kernels and temporary caches
49
+ just docs-serve # build and preview the documentation locally, with reload
50
+ just check-docs # validate generated content and build the site strictly
51
+ just build # build wheel and sdist
52
+ just smoke # exercise core and pandas wheel installations outside the checkout
53
+ just changes # preview upcoming release notes
54
+ just cleanup 123 # from main: remove PR #123's merged branch and clean worktree
55
+ just run search radar
56
+ ```
57
+
58
+ Commit hooks fix and format staged Python files using the locked Ruff version,
59
+ and check configuration, merge conflicts, and GitHub Actions workflows. Full
60
+ validation remains `just check`; `just check-hooks` runs the non-mutating config
61
+ and workflow checks alone. Hooks do not rewrite notebooks or raw data fixtures.
62
+ The first hook run downloads its isolated tools and requires network access.
63
+
64
+ Offline tests mechanically block network connections. Tests that hit live
65
+ services run with `just test-live`; see [testing levels and organization](docs/testing.md).
66
+ CI checks Python 3.11 and 3.14 on Linux with core-only, pandas, radar, NetCDF,
67
+ and GRIB2 dependency profiles. A further Python 3.11 profile replaces the locked
68
+ pandas with the oldest release the project declares, so the `>=` floor is
69
+ executed rather than assumed; the number comes from `pyproject.toml` through
70
+ `scripts/lowest_version.py`. Installed-wheel checks cover core, pandas, radar,
71
+ and NetCDF on Linux, macOS, and Windows. The full offline and live-service
72
+ suites run on Linux. `just setup` restores a core-only development environment;
73
+ the `check-*` commands install their respective extras.
74
+
75
+ Releases: `just release minor` prepares a release branch. Update release
76
+ notices, then `just release-pr` validates and opens a draft for review. Merging
77
+ the reviewed PR publishes to PyPI and creates the tag and GitHub release. See
78
+ [versioning](docs/versioning.md).
79
+
80
+ The main way the project grows is a new dataset: one registry entry and one
81
+ adapter, following [adding a dataset](docs/guides/adding-a-dataset.md). The two
82
+ websites deploy automatically from `main`; see
83
+ [website operations](docs/guides/website-operations.md) and
84
+ [maintaining documentation](docs/guides/documentation.md).
85
+
86
+ ## License
87
+
88
+ [Apache-2.0](LICENSE).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.14.0"
3
+ version = "0.16.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -37,6 +37,12 @@ netcdf = [
37
37
  "xarray>=2025.1",
38
38
  "h5netcdf[h5py]>=1.8.1",
39
39
  ]
40
+ grib = [
41
+ "eccodes>=2.48",
42
+ "eccodeslib>=2.48; sys_platform == 'linux'",
43
+ "xarray>=2025.1",
44
+ "numpy>=2",
45
+ ]
40
46
 
41
47
  [project.urls]
42
48
  Homepage = "https://usdata.dev/"
@@ -129,6 +135,7 @@ markers = [
129
135
  "pandas: optional CSV reader coverage",
130
136
  "radar: optional radar reader coverage",
131
137
  "netcdf: optional NetCDF reader coverage",
138
+ "grib: optional GRIB2 reader coverage",
132
139
  ]
133
140
  addopts = "-ra --strict-markers"
134
141
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.14.0"
3
+ version = "0.16.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -26,6 +26,13 @@ dependencies = [
26
26
  pandas = ["pandas>=2.2"]
27
27
  radar = ["xradar>=0.12.0"]
28
28
  netcdf = ["xarray>=2025.1", "h5netcdf[h5py]>=1.8.1"]
29
+ grib = [
30
+ "eccodes>=2.48",
31
+ # The eccodes sdist metadata omits the library wheel that its own wheel declares.
32
+ "eccodeslib>=2.48; sys_platform == 'linux'",
33
+ "xarray>=2025.1",
34
+ "numpy>=2",
35
+ ]
29
36
 
30
37
  [project.urls]
31
38
  Homepage = "https://usdata.dev/"
@@ -101,6 +108,7 @@ markers = [
101
108
  "pandas: optional CSV reader coverage",
102
109
  "radar: optional radar reader coverage",
103
110
  "netcdf: optional NetCDF reader coverage",
111
+ "grib: optional GRIB2 reader coverage",
104
112
  ]
105
113
  addopts = "-ra --strict-markers"
106
114
 
@@ -10,6 +10,7 @@ try:
10
10
  except PackageNotFoundError: # running from a source tree without an install
11
11
  __version__ = "0.0.0"
12
12
 
13
+ from usdata._fetch import ChecksumMismatch, FetchedAsset, fetch, fetch_asset
13
14
  from usdata.models import Asset, BBox, Dataset, Provenance, Query, TemporalSelection, TimeRange
14
15
  from usdata.pull import pull, verify
15
16
  from usdata.query import build_query
@@ -19,8 +20,10 @@ from usdata.selection import select_by_time
19
20
  __all__ = [
20
21
  "Asset",
21
22
  "BBox",
23
+ "ChecksumMismatch",
22
24
  "Dataset",
23
25
  "DatasetNotFound",
26
+ "FetchedAsset",
24
27
  "Provenance",
25
28
  "Query",
26
29
  "Registry",
@@ -30,6 +33,8 @@ __all__ = [
30
33
  "__version__",
31
34
  "build_query",
32
35
  "default_registry",
36
+ "fetch",
37
+ "fetch_asset",
33
38
  "get",
34
39
  "pull",
35
40
  "search",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -35,15 +36,17 @@ class FetchedAsset(BaseModel):
35
36
  usecols: list[str] | None = None,
36
37
  nrows: int | None = None,
37
38
  sweep: int | list[int] | None = None,
39
+ select: Mapping[str, Any] | None = None,
38
40
  ) -> Any:
39
- """Open local data with an optional ``pandas``, ``radar``, or ``netcdf`` reader.
41
+ """Open local data with an optional ``pandas``, ``radar``, ``netcdf``, or ``grib`` reader.
40
42
 
41
43
  ERDDAP units are kept in ``frame.attrs["units"]`` and source provenance
42
44
  in ``frame.attrs["usdata"]``. NEXRAD returns a xarray DataTree with provenance
43
- in ``radar.attrs["usdata"]``. NetCDF4 returns a loaded xarray Dataset with
44
- matching provenance in its attributes. See ``usdata.readers.open_asset`` for options.
45
- Use ``sweep=0`` or ``sweep=[0, 2]`` to load selected zero-based radar sweeps.
46
- Cached files and provenance sidecars are never changed.
45
+ in ``radar.attrs["usdata"]``. NetCDF4 and GRIB2 return a loaded xarray Dataset
46
+ with matching provenance in its attributes. See ``usdata.readers.open_asset``
47
+ for options. Use ``sweep=0`` or ``sweep=[0, 2]`` to load selected zero-based
48
+ radar sweeps, and ``select={"shortName": "cape", "typeOfLevel": "surface"}``
49
+ to choose GRIB2 messages. Cached files and provenance sidecars are never changed.
47
50
  """
48
51
  from usdata.readers import open_asset
49
52
 
@@ -55,9 +58,24 @@ class FetchedAsset(BaseModel):
55
58
  usecols=usecols,
56
59
  nrows=nrows,
57
60
  sweep=sweep,
61
+ select=select,
58
62
  )
59
63
 
60
64
 
65
+ def _sidecar_not_older(path: Path) -> bool:
66
+ """Whether ``path`` still carries the mtime it had when its sidecar was written.
67
+
68
+ The sidecar is always written after the data file lands, so a data file that
69
+ is newer than its sidecar was touched afterwards and the record no longer
70
+ implies its bytes. See ADR 0024.
71
+ """
72
+ try:
73
+ sidecar = provenance.sidecar_path(path).stat()
74
+ return path.stat().st_mtime_ns <= sidecar.st_mtime_ns
75
+ except OSError:
76
+ return False
77
+
78
+
61
79
  def _fetch_asset(
62
80
  dataset: Dataset,
63
81
  asset: Asset,
@@ -82,7 +100,8 @@ def _fetch_asset(
82
100
  and prov.source_url == asset.href
83
101
  and prov.size == path.stat().st_size
84
102
  and (asset.checksum is None or prov.checksum == asset.checksum)
85
- and sha256_file(path) == prov.checksum
103
+ # Trust an untouched cached file; hash whenever anything is unclear.
104
+ and (_sidecar_not_older(path) or sha256_file(path) == prov.checksum)
86
105
  ):
87
106
  _progress.emit(_progress.AssetProgress(asset.id, "cached", prov.size))
88
107
  return FetchedAsset(asset=asset, path=path, provenance=prov, from_cache=True)
@@ -100,16 +119,28 @@ def _fetch_asset(
100
119
  def fetch_asset(
101
120
  dataset: Dataset, asset: Asset, *, root: Path | None = None, force: bool = False
102
121
  ) -> FetchedAsset:
103
- """Fetch one asset, reusing the cache only when bytes and provenance agree."""
122
+ """Fetch one asset, reusing the cache only when its provenance still describes it."""
104
123
  with load_adapter(dataset) as adapter:
105
124
  return _fetch_asset(dataset, asset, adapter, root=root, force=force)
106
125
 
107
126
 
127
+ def _fetch_with(
128
+ adapter: Provider,
129
+ dataset: Dataset,
130
+ query: Query,
131
+ *,
132
+ root: Path | None = None,
133
+ force: bool = False,
134
+ ) -> list[FetchedAsset]:
135
+ """Run the loop on an adapter the caller opened, so one adapter can serve many queries."""
136
+ assets = adapter.list_assets(query)
137
+ _progress.batch([asset.size for asset in assets])
138
+ return [_fetch_asset(dataset, a, adapter, root=root, force=force) for a in assets]
139
+
140
+
108
141
  def fetch(
109
142
  dataset: Dataset, query: Query, *, root: Path | None = None, force: bool = False
110
143
  ) -> list[FetchedAsset]:
111
144
  """Resolve and fetch a query, sharing one adapter and closing its owned resources."""
112
145
  with load_adapter(dataset) as adapter:
113
- assets = adapter.list_assets(query)
114
- _progress.batch([asset.size for asset in assets])
115
- return [_fetch_asset(dataset, a, adapter, root=root, force=force) for a in assets]
146
+ return _fetch_with(adapter, dataset, query, root=root, force=force)