usdata 0.26.0__tar.gz → 0.27.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 (82) hide show
  1. {usdata-0.26.0 → usdata-0.27.0}/PKG-INFO +11 -10
  2. {usdata-0.26.0 → usdata-0.27.0}/README.md +9 -8
  3. {usdata-0.26.0 → usdata-0.27.0}/pyproject.toml +2 -2
  4. {usdata-0.26.0 → usdata-0.27.0}/pyproject.toml.orig +2 -2
  5. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_fetch.py +70 -26
  6. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_grib.py +19 -6
  7. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_radar.py +3 -2
  8. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/data/registry.yaml +58 -50
  9. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/gfs.py +1 -1
  10. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/hrrr.py +1 -1
  11. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/nbm.py +1 -1
  12. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/rap.py +1 -1
  13. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/pull.py +40 -20
  14. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/readers.py +150 -125
  15. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/__init__.py +0 -0
  16. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/__main__.py +0 -0
  17. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_aqs.py +0 -0
  18. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_files.py +0 -0
  19. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_hurdat2.py +0 -0
  20. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_netcdf.py +0 -0
  21. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/_progress.py +0 -0
  22. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cache.py +0 -0
  23. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cache_ops.py +0 -0
  24. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cite.py +0 -0
  25. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/__init__.py +0 -0
  26. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/app.py +0 -0
  27. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/cache.py +0 -0
  28. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/cite.py +0 -0
  29. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/doctor.py +0 -0
  30. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/inspect.py +0 -0
  31. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/cli/progress.py +0 -0
  32. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/data/nexrad_sites.csv +0 -0
  33. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/data/places.csv +0 -0
  34. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/data/places.sources.json +0 -0
  35. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/doctor.py +0 -0
  36. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/inspect.py +0 -0
  37. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/manifest.py +0 -0
  38. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/mirror.py +0 -0
  39. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/models.py +0 -0
  40. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/protocols/__init__.py +0 -0
  41. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/protocols/erddap.py +0 -0
  42. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/protocols/http.py +0 -0
  43. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/protocols/listing.py +0 -0
  44. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/protocols/s3.py +0 -0
  45. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/provenance.py +0 -0
  46. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/__init__.py +0 -0
  47. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/base.py +0 -0
  48. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/credentials.py +0 -0
  49. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/epa/__init__.py +0 -0
  50. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/epa/aqs.py +0 -0
  51. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/fema/__init__.py +0 -0
  52. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/fema/declarations.py +0 -0
  53. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/http.py +0 -0
  54. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/__init__.py +0 -0
  55. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
  56. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/coops.py +0 -0
  57. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
  58. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/glm.py +0 -0
  59. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/goes.py +0 -0
  60. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/grib_index.py +0 -0
  61. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/gsom.py +0 -0
  62. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  63. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
  64. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
  65. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/lcd.py +0 -0
  66. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/mrms.py +0 -0
  67. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/nexrad.py +0 -0
  68. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
  69. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/normals.py +0 -0
  70. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
  71. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/sites.py +0 -0
  72. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/spc.py +0 -0
  73. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  74. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/params.py +0 -0
  75. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/usgs/__init__.py +0 -0
  76. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/usgs/daily.py +0 -0
  77. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
  78. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/py.typed +0 -0
  79. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/query.py +0 -0
  80. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/registry.py +0 -0
  81. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/selection.py +0 -0
  82. {usdata-0.26.0 → usdata-0.27.0}/src/usdata/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.26.0
3
+ Version: 0.27.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
@@ -34,7 +34,7 @@ Requires-Python: >=3.11
34
34
  Project-URL: Homepage, https://usdata.dev/
35
35
  Project-URL: Documentation, https://docs.usdata.dev/
36
36
  Project-URL: Repository, https://github.com/jakeryderv/usdata
37
- Project-URL: Examples, https://usdata.dev/examples/
37
+ Project-URL: Examples, https://usdata.dev/studies/
38
38
  Project-URL: Changelog, https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md
39
39
  Project-URL: Issues, https://github.com/jakeryderv/usdata/issues
40
40
  Provides-Extra: grib
@@ -55,12 +55,13 @@ Description-Content-Type: text/markdown
55
55
  [![usdata.dev: public science, reproducible inputs](https://usdata.dev/og.png)](https://usdata.dev/)
56
56
 
57
57
  Reproducible acquisition of U.S. public scientific data. One Python SDK and
58
- CLI discovers curated NOAA and USGS datasets, fetches their files, and keeps a
59
- record of every input: a manifest names them, a lockfile pins them by checksum,
60
- and the record of what was fetched is what a methods section cites. Analysis
61
- stays in pandas and xarray; usdata only acquires. If you need every product
62
- one agency publishes, that agency's own library is the better tool; usdata is
63
- for pinning inputs across sources and proving later that they have not changed.
58
+ CLI discovers curated datasets from agencies such as NOAA, USGS, and EPA,
59
+ fetches their files, and keeps a record of every input: a manifest names them,
60
+ a lockfile pins them by checksum, and the record of what was fetched is what a
61
+ methods section cites. Analysis stays in pandas and xarray; usdata only
62
+ acquires. If you need every product one agency publishes, that agency's own
63
+ library is the better tool; usdata is for pinning inputs across sources and
64
+ proving later that they have not changed.
64
65
 
65
66
  ```sh
66
67
  pip install "usdata[pandas]"
@@ -108,9 +109,9 @@ breaking changes.
108
109
 
109
110
  | | |
110
111
  | --- | --- |
111
- | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/examples/) with saved results |
112
+ | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/studies/) with saved results |
112
113
  | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
113
- | [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
114
+ | [Severe-weather case study](https://usdata.dev/studies/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
114
115
 
115
116
  Twenty-seven datasets are available today and nineteen more are planned, grouped
116
117
  by agency and product family in the [catalog](docs/providers/README.md).
@@ -10,12 +10,13 @@
10
10
  [![usdata.dev: public science, reproducible inputs](https://usdata.dev/og.png)](https://usdata.dev/)
11
11
 
12
12
  Reproducible acquisition of U.S. public scientific data. One Python SDK and
13
- CLI discovers curated NOAA and USGS datasets, fetches their files, and keeps a
14
- record of every input: a manifest names them, a lockfile pins them by checksum,
15
- and the record of what was fetched is what a methods section cites. Analysis
16
- stays in pandas and xarray; usdata only acquires. If you need every product
17
- one agency publishes, that agency's own library is the better tool; usdata is
18
- for pinning inputs across sources and proving later that they have not changed.
13
+ CLI discovers curated datasets from agencies such as NOAA, USGS, and EPA,
14
+ fetches their files, and keeps a record of every input: a manifest names them,
15
+ a lockfile pins them by checksum, and the record of what was fetched is what a
16
+ methods section cites. Analysis stays in pandas and xarray; usdata only
17
+ acquires. If you need every product one agency publishes, that agency's own
18
+ library is the better tool; usdata is for pinning inputs across sources and
19
+ proving later that they have not changed.
19
20
 
20
21
  ```sh
21
22
  pip install "usdata[pandas]"
@@ -63,9 +64,9 @@ breaking changes.
63
64
 
64
65
  | | |
65
66
  | --- | --- |
66
- | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/examples/) with saved results |
67
+ | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/studies/) with saved results |
67
68
  | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
68
- | [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
69
+ | [Severe-weather case study](https://usdata.dev/studies/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
69
70
 
70
71
  Twenty-seven datasets are available today and nineteen more are planned, grouped
71
72
  by agency and product family in the [catalog](docs/providers/README.md).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.26.0"
3
+ version = "0.27.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"
@@ -59,7 +59,7 @@ grib = [
59
59
  Homepage = "https://usdata.dev/"
60
60
  Documentation = "https://docs.usdata.dev/"
61
61
  Repository = "https://github.com/jakeryderv/usdata"
62
- Examples = "https://usdata.dev/examples/"
62
+ Examples = "https://usdata.dev/studies/"
63
63
  Changelog = "https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md"
64
64
  Issues = "https://github.com/jakeryderv/usdata/issues"
65
65
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.26.0"
3
+ version = "0.27.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"
@@ -56,7 +56,7 @@ grib = [
56
56
  Homepage = "https://usdata.dev/"
57
57
  Documentation = "https://docs.usdata.dev/"
58
58
  Repository = "https://github.com/jakeryderv/usdata"
59
- Examples = "https://usdata.dev/examples/"
59
+ Examples = "https://usdata.dev/studies/"
60
60
  Changelog = "https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md"
61
61
  Issues = "https://github.com/jakeryderv/usdata/issues"
62
62
 
@@ -17,6 +17,10 @@ from usdata.models import Asset, Dataset, Provenance, Query
17
17
  from usdata.providers import Provider, load_adapter
18
18
 
19
19
  if TYPE_CHECKING:
20
+ # Each is an optional extra: without it installed the result is simply untyped.
21
+ import pandas as pd # pyright: ignore[reportMissingImports]
22
+ import xarray as xr # pyright: ignore[reportMissingImports]
23
+
20
24
  from usdata.inspect import Summary
21
25
 
22
26
 
@@ -32,46 +36,74 @@ class FetchedAsset(BaseModel):
32
36
  provenance: Provenance
33
37
  from_cache: bool
34
38
 
35
- def open(
39
+ def open(self) -> Any:
40
+ """Open local data with the reader its format implies, with that reader's defaults.
41
+
42
+ CSV and ERDDAP CSV, HURDAT2 and AQS daily JSON return a pandas DataFrame;
43
+ NetCDF4 and GRIB2 a loaded xarray Dataset; NEXRAD Level II an xarray
44
+ DataTree. Provenance is kept in the result's ``attrs["usdata"]``. A file
45
+ that needs options, or whose metadata leaves its format ambiguous, is
46
+ opened with the method for its format instead: ``open_csv``,
47
+ ``open_nexrad``, ``open_grib2``, or ``open_netcdf``. Cached files and
48
+ provenance sidecars are never changed. See ``usdata.readers.open_asset``.
49
+ """
50
+ from usdata.readers import open_asset
51
+
52
+ return open_asset(self)
53
+
54
+ def open_csv(
36
55
  self,
37
56
  *,
38
- reader: str | None = None,
39
57
  dtype: dict[str, str] | None = None,
40
58
  parse_dates: list[str] | None = None,
41
59
  usecols: list[str] | None = None,
42
60
  nrows: int | None = None,
43
- sweep: int | list[int] | None = None,
44
- select: Mapping[str, Any] | None = None,
45
- strict: bool = False,
46
- ) -> Any:
47
- """Open local data with an optional ``pandas``, ``radar``, ``netcdf``, or ``grib`` reader.
48
-
49
- ERDDAP units are kept in ``frame.attrs["units"]`` and source provenance
50
- in ``frame.attrs["usdata"]``. NEXRAD returns a xarray DataTree with provenance
51
- in ``radar.attrs["usdata"]``. NetCDF4 and GRIB2 return a loaded xarray Dataset
52
- with matching provenance in its attributes, and with units and long names
53
- the file leaves unstated filled from the registry entry's variables. See
54
- ``usdata.readers.open_asset`` for options. Use ``sweep=0`` or ``sweep=[0, 2]``
55
- to load selected zero-based radar sweeps, and
56
- ``select={"shortName": "cape", "typeOfLevel": "surface"}``
57
- to choose GRIB2 messages; ``strict=True`` raises instead of warning when a
58
- GRIB2 select value matches none of the selected messages. Cached files and
59
- provenance sidecars are never changed.
61
+ units_row: bool | None = None,
62
+ ) -> pd.DataFrame:
63
+ """Open a CSV as a pandas DataFrame; see ``usdata.readers.open_csv``.
64
+
65
+ ``units_row`` says whether a units row follows the header, as in ERDDAP
66
+ CSV, and is inferred when ``None``; the units go to ``frame.attrs["units"]``.
60
67
  """
61
- from usdata.readers import open_asset
68
+ from usdata.readers import open_csv
62
69
 
63
- return open_asset(
70
+ return open_csv(
64
71
  self,
65
- reader=reader,
66
72
  dtype=dtype,
67
73
  parse_dates=parse_dates,
68
74
  usecols=usecols,
69
75
  nrows=nrows,
70
- sweep=sweep,
71
- select=select,
72
- strict=strict,
76
+ units_row=units_row,
73
77
  )
74
78
 
79
+ def open_nexrad(self, *, sweep: int | list[int] | None = None) -> xr.DataTree:
80
+ """Open a NEXRAD Level II volume as an xarray DataTree; see ``usdata.readers.open_nexrad``.
81
+
82
+ Use ``sweep=0`` or ``sweep=[0, 2]`` to load selected zero-based sweeps.
83
+ """
84
+ from usdata.readers import open_nexrad
85
+
86
+ return open_nexrad(self, sweep=sweep)
87
+
88
+ def open_grib2(
89
+ self, *, select: Mapping[str, Any] | None = None, strict: bool = False
90
+ ) -> xr.Dataset:
91
+ """Open GRIB2 messages as one xarray Dataset; see ``usdata.readers.open_grib2``.
92
+
93
+ ``select={"shortName": "cape", "typeOfLevel": "surface"}`` chooses
94
+ messages; ``strict=True`` raises instead of warning when a select value
95
+ matches none of the selected messages.
96
+ """
97
+ from usdata.readers import open_grib2
98
+
99
+ return open_grib2(self, select=select, strict=strict)
100
+
101
+ def open_netcdf(self) -> xr.Dataset:
102
+ """Open a NetCDF4 file as an xarray Dataset; see ``usdata.readers.open_netcdf``."""
103
+ from usdata.readers import open_netcdf
104
+
105
+ return open_netcdf(self)
106
+
75
107
  def inspect(self) -> Summary:
76
108
  """Summarize this file: provenance, format, and what that format holds.
77
109
 
@@ -108,12 +140,18 @@ def _fetch_asset(
108
140
  root: Path | None = None,
109
141
  force: bool = False,
110
142
  pinned: Provenance | None = None,
143
+ staging: Path | None = None,
111
144
  ) -> FetchedAsset:
112
145
  """Fetch one asset through the cache, passing any pinned record to the adapter.
113
146
 
114
147
  ``pinned`` is the provenance a lockfile holds for this asset. The adapter sees
115
148
  it through ``prepare_fetch``, so an asset that pins byte ranges is reproduced
116
149
  from the record rather than by resolving its query again.
150
+
151
+ ``staging`` is a root a download is written under instead of the cache. The
152
+ cache at ``root`` is still checked, and a hit is returned from there; a
153
+ miss comes back with its ``path`` under ``staging``, for the caller to move
154
+ home once it can pin it (ADR 0031).
117
155
  """
118
156
  if asset.dataset_id != dataset.id:
119
157
  raise ValueError(f"asset dataset {asset.dataset_id!r} does not match {dataset.id!r}")
@@ -138,6 +176,8 @@ def _fetch_asset(
138
176
  ):
139
177
  _progress.emit(_progress.AssetProgress(asset.id, "cached", prov.size))
140
178
  return FetchedAsset(asset=asset, path=path, provenance=prov, from_cache=True)
179
+ if staging is not None:
180
+ path = asset_path(asset, staging)
141
181
  with staged_path(path) as tmp:
142
182
  partial = adapter.prepare_fetch(asset, pinned)
143
183
  if partial is None:
@@ -174,15 +214,19 @@ def _fetch_with(
174
214
  *,
175
215
  root: Path | None = None,
176
216
  force: bool = False,
217
+ staging: Path | None = None,
177
218
  ) -> list[FetchedAsset]:
178
219
  """Run the loop on an adapter the caller opened, so one adapter can serve many queries.
179
220
 
180
221
  The listing is put in the order every result promises, by ``asset.time.start``
181
222
  then id, so no adapter has to sort and no caller has to sort defensively.
223
+ ``staging`` is passed to each fetch; see ``_fetch_asset``.
182
224
  """
183
225
  assets = ordered(adapter.list_assets(query))
184
226
  _progress.batch([asset.size for asset in assets])
185
- return [_fetch_asset(dataset, a, adapter, root=root, force=force) for a in assets]
227
+ return [
228
+ _fetch_asset(dataset, a, adapter, root=root, force=force, staging=staging) for a in assets
229
+ ]
186
230
 
187
231
 
188
232
  def ordered(assets: list[Asset]) -> list[Asset]:
@@ -9,7 +9,8 @@ repeat, so the same select always yields the same names. Values arrive from
9
9
  ecCodes as float64, are masked to NaN where the message's bitmap marks them
10
10
  missing, and are stored as float32; the float64 array is released before the
11
11
  Dataset is returned. Rows are ordered north to south and columns west to east
12
- regardless of the message's scanning mode. See ADR 0022.
12
+ regardless of the message's scanning mode, including grids whose adjacent rows
13
+ scan in opposite directions. See ADR 0022.
13
14
  """
14
15
 
15
16
  from __future__ import annotations
@@ -279,6 +280,8 @@ class _Grid:
279
280
  rows, cols = shape
280
281
  self.flip_rows = bool(_get(eccodes, h, "jScansPositively", int))
281
282
  self.flip_cols = bool(_get(eccodes, h, "iScansNegatively", int))
283
+ # Adjacent rows scan in opposite directions (NBM's CONUS grid does this).
284
+ self.alternating = bool(_get(eccodes, h, "alternativeRowScanning", int))
282
285
  if _get(eccodes, h, "jPointsAreConsecutive", int):
283
286
  raise ValueError("grids with consecutive j points are not supported")
284
287
  self.regular = self.grid_type == "regular_ll"
@@ -305,15 +308,25 @@ class _Grid:
305
308
  value = _get(eccodes, h, key)
306
309
  if value is not None:
307
310
  self.attrs[key] = value
308
- self.key = (self.grid_type, self.shape, self.flip_rows, self.flip_cols)
311
+ self.key = (self.grid_type, self.shape, self.flip_rows, self.flip_cols, self.alternating)
309
312
 
310
313
  def _orient(self, numpy: Any, axis: Any, *, rows: bool) -> Any:
311
314
  return axis[::-1].copy() if (self.flip_rows if rows else self.flip_cols) else axis
312
315
 
313
- def reshape(self, numpy: Any, flat: Any) -> Any:
316
+ def reshape(self, numpy: Any, flat: Any, *, stored: bool = False) -> Any:
317
+ """Order a flat array north to south and west to east.
318
+
319
+ ``stored`` marks data values, which ecCodes returns in the order the
320
+ message stores them: with alternative row scanning every second row
321
+ runs the other way and is reversed here. The latitudes and longitudes
322
+ ecCodes computes do not alternate, so coordinates skip that step.
323
+ """
314
324
  if flat.size != self.shape[0] * self.shape[1]:
315
325
  raise ValueError(f"message has {flat.size} values for a {self.shape} grid")
316
326
  grid = flat.reshape(self.shape)
327
+ if stored and self.alternating:
328
+ grid = grid.copy()
329
+ grid[1::2] = grid[1::2, ::-1]
317
330
  if self.flip_rows:
318
331
  grid = grid[::-1, :]
319
332
  if self.flip_cols:
@@ -423,7 +436,7 @@ def open_grib2(
423
436
  missing = _get(eccodes, h, "missingValue", float)
424
437
  if missing is not None:
425
438
  values[values == missing] = numpy.nan
426
- data = grid.reshape(numpy, values).astype(numpy.float32)
439
+ data = grid.reshape(numpy, values, stored=True).astype(numpy.float32)
427
440
  del values
428
441
  attrs = {
429
442
  key: value for key in VARIABLE_KEYS if (value := _get(eccodes, h, key)) is not None
@@ -454,8 +467,8 @@ def open_grib2(
454
467
  selected.append(_Field(short=short, data=data, attrs=attrs, message=message))
455
468
  if count > 1 and needs_select:
456
469
  raise ValueError(
457
- f"{count} messages; pass select={{...}} with ecCodes keys to choose, "
458
- f"for example select={{'shortName': ..., 'typeOfLevel': ...}}. "
470
+ f"{count} messages; pass select={{...}} to open_grib2 with ecCodes keys to "
471
+ f"choose, for example select={{'shortName': ..., 'typeOfLevel': ...}}. "
459
472
  f"Available (shortName, typeOfLevel, level): {_available(fetched.path)}"
460
473
  )
461
474
  if grid is None or not selected:
@@ -67,8 +67,9 @@ def _check_sweeps(content: bytes, sweep: int | list[int] | None) -> None:
67
67
  ):
68
68
  raise RadarDecodeError(
69
69
  f"cannot safely decode sweep {index}: NEXRAD moment and coordinate records "
70
- "do not agree; select an unaffected sweep explicitly with open(sweep=...) "
71
- "or use another decoder. No sweeps were silently dropped."
70
+ "do not agree; select an unaffected sweep explicitly with "
71
+ "open_nexrad(sweep=...) or use another decoder. "
72
+ "No sweeps were silently dropped."
72
73
  )
73
74
 
74
75