usdata 0.19.0__tar.gz → 0.20.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.19.0 → usdata-0.20.0}/PKG-INFO +2 -2
- {usdata-0.19.0 → usdata-0.20.0}/README.md +1 -1
- {usdata-0.19.0 → usdata-0.20.0}/pyproject.toml +1 -1
- {usdata-0.19.0 → usdata-0.20.0}/pyproject.toml.orig +1 -1
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_fetch.py +6 -1
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_grib.py +32 -11
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/inspect.py +2 -1
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/data/registry.yaml +141 -35
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/inspect.py +69 -20
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/protocols/http.py +4 -2
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/protocols/listing.py +36 -10
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/base.py +29 -6
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/grib_index.py +75 -26
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/hrrr.py +31 -10
- usdata-0.20.0/src/usdata/providers/noaa/ibtracs.py +200 -0
- usdata-0.20.0/src/usdata/providers/noaa/nbm.py +106 -0
- usdata-0.20.0/src/usdata/providers/noaa/rap.py +110 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/pull.py +155 -83
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/readers.py +100 -15
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/testing.py +6 -1
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/__main__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_files.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_hurdat2.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_netcdf.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_progress.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/_radar.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cache.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cache_ops.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cite.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/app.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/cache.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/cite.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/doctor.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/cli/progress.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/data/nexrad_sites.csv +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/data/places.csv +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/data/places.sources.json +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/doctor.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/manifest.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/mirror.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/models.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/protocols/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/protocols/erddap.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/protocols/s3.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/provenance.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/http.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/coops.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/gfs.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/glm.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/goes.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/gsom.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/gsoy.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/lcd.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/mrms.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/nexrad.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/normals.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/sites.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/spc.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/noaa/storm_events.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/params.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/usgs/__init__.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/usgs/daily.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/py.typed +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/query.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/registry.py +0 -0
- {usdata-0.19.0 → usdata-0.20.0}/src/usdata/selection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usdata
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.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
|
|
@@ -93,7 +93,7 @@ breaking changes.
|
|
|
93
93
|
| [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 |
|
|
94
94
|
| [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
|
|
95
95
|
|
|
96
|
-
Twenty datasets are available today and twenty-
|
|
96
|
+
Twenty-three datasets are available today and twenty-one more are planned, grouped
|
|
97
97
|
by agency and product family in the [catalog](docs/providers/README.md).
|
|
98
98
|
|
|
99
99
|
## How this compares
|
|
@@ -48,7 +48,7 @@ breaking changes.
|
|
|
48
48
|
| [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 |
|
|
49
49
|
| [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
|
|
50
50
|
|
|
51
|
-
Twenty datasets are available today and twenty-
|
|
51
|
+
Twenty-three datasets are available today and twenty-one more are planned, grouped
|
|
52
52
|
by agency and product family in the [catalog](docs/providers/README.md).
|
|
53
53
|
|
|
54
54
|
## How this compares
|
|
@@ -130,6 +130,8 @@ def _fetch_asset(
|
|
|
130
130
|
and prov.provider == dataset.provider
|
|
131
131
|
and prov.source_url == asset.href
|
|
132
132
|
and prov.size == path.stat().st_size
|
|
133
|
+
# A listing that now reports another size describes a rebuilt file.
|
|
134
|
+
and (asset.size is None or prov.size == asset.size)
|
|
133
135
|
and (asset.checksum is None or prov.checksum == asset.checksum)
|
|
134
136
|
# Trust an untouched cached file; hash whenever anything is unclear.
|
|
135
137
|
and (_sidecar_not_older(path) or sha256_file(path) == prov.checksum)
|
|
@@ -138,7 +140,10 @@ def _fetch_asset(
|
|
|
138
140
|
return FetchedAsset(asset=asset, path=path, provenance=prov, from_cache=True)
|
|
139
141
|
with staged_path(path) as tmp:
|
|
140
142
|
partial = adapter.prepare_fetch(asset, pinned)
|
|
141
|
-
|
|
143
|
+
if partial is None:
|
|
144
|
+
adapter.fetch(asset, tmp)
|
|
145
|
+
else:
|
|
146
|
+
adapter.fetch_partial(asset, tmp, partial)
|
|
142
147
|
prov = provenance.record(dataset, asset, tmp, partial)
|
|
143
148
|
if asset.checksum and prov.checksum != asset.checksum:
|
|
144
149
|
raise ChecksumMismatch(f"{asset.id}: expected {asset.checksum}, got {prov.checksum}")
|
|
@@ -81,6 +81,9 @@ def _modules() -> tuple[Any, Any, Any]:
|
|
|
81
81
|
except (RuntimeError, OSError) as error:
|
|
82
82
|
# eccodes imports, then findlibs fails to locate the shared library.
|
|
83
83
|
raise MissingReaderDependency(f"{LIBRARY_HINT} ({error})") from error
|
|
84
|
+
# A GRIB2 message may hold several fields (RAP packs wind components that way);
|
|
85
|
+
# without this ecCodes yields only the first and silently drops the rest.
|
|
86
|
+
eccodes.codes_grib_multi_support_on()
|
|
84
87
|
return eccodes, xarray, numpy
|
|
85
88
|
|
|
86
89
|
|
|
@@ -92,9 +95,15 @@ def _handle(eccodes: Any, handle: int) -> Iterator[int]:
|
|
|
92
95
|
eccodes.codes_release(handle)
|
|
93
96
|
|
|
94
97
|
|
|
95
|
-
def _messages_from_bytes(eccodes: Any, data: bytes) -> Iterator[int]:
|
|
96
|
-
"""
|
|
98
|
+
def _messages_from_bytes(eccodes: Any, data: bytes) -> Iterator[tuple[int, int]]:
|
|
99
|
+
"""Message index and handle for each field in an in-memory GRIB byte string.
|
|
100
|
+
|
|
101
|
+
Messages are walked by their section 0 lengths; an in-memory message yields
|
|
102
|
+
its first field only, which is every field of the single-field products
|
|
103
|
+
that arrive compressed.
|
|
104
|
+
"""
|
|
97
105
|
offset = 0
|
|
106
|
+
index = 0
|
|
98
107
|
while offset < len(data):
|
|
99
108
|
if data[offset : offset + 4] != b"GRIB":
|
|
100
109
|
raise ValueError(f"not a GRIB message at byte {offset}")
|
|
@@ -104,12 +113,18 @@ def _messages_from_bytes(eccodes: Any, data: bytes) -> Iterator[int]:
|
|
|
104
113
|
if length < 16 or offset + length > len(data):
|
|
105
114
|
raise ValueError(f"truncated GRIB message at byte {offset}")
|
|
106
115
|
with _handle(eccodes, eccodes.codes_new_from_message(data[offset : offset + length])) as h:
|
|
107
|
-
yield h
|
|
116
|
+
yield index, h
|
|
108
117
|
offset += length
|
|
118
|
+
index += 1
|
|
109
119
|
|
|
110
120
|
|
|
111
|
-
def _messages(eccodes: Any, path: Path) -> Iterator[int]:
|
|
112
|
-
"""
|
|
121
|
+
def _messages(eccodes: Any, path: Path) -> Iterator[tuple[int, int]]:
|
|
122
|
+
"""Message index and handle for each field, decompressing gzip in memory.
|
|
123
|
+
|
|
124
|
+
The index counts GRIB2 messages from zero, so the fields of one message
|
|
125
|
+
that holds several share it; that is the number a partial fetch's record
|
|
126
|
+
pairs with, and the ``file_index`` the reader and ``inspect`` report.
|
|
127
|
+
"""
|
|
113
128
|
with path.open("rb") as probe:
|
|
114
129
|
compressed = probe.read(2) == b"\x1f\x8b"
|
|
115
130
|
if compressed:
|
|
@@ -117,12 +132,18 @@ def _messages(eccodes: Any, path: Path) -> Iterator[int]:
|
|
|
117
132
|
yield from _messages_from_bytes(eccodes, data)
|
|
118
133
|
return
|
|
119
134
|
# ecCodes reads through the descriptor, so it gets a file object it alone positions.
|
|
135
|
+
index = -1
|
|
136
|
+
last_offset: int | None = None
|
|
120
137
|
with path.open("rb") as raw:
|
|
121
138
|
while (handle := eccodes.codes_grib_new_from_file(raw)) is not None:
|
|
122
139
|
with _handle(eccodes, handle) as h:
|
|
123
140
|
if eccodes.codes_get(h, "edition") != 2:
|
|
124
141
|
raise ValueError("only GRIB edition 2 is supported")
|
|
125
|
-
|
|
142
|
+
offset = _get(eccodes, h, "offset", int)
|
|
143
|
+
if offset is None or offset != last_offset:
|
|
144
|
+
index += 1
|
|
145
|
+
last_offset = offset
|
|
146
|
+
yield index, h
|
|
126
147
|
|
|
127
148
|
|
|
128
149
|
def _get(eccodes: Any, h: int, key: str, ktype: type | None = None) -> Any:
|
|
@@ -203,7 +224,7 @@ def inventory(path: Path) -> list[GribMessage]:
|
|
|
203
224
|
"""Every message in a local GRIB2 file, in file order, without decoding its values."""
|
|
204
225
|
eccodes = _modules()[0]
|
|
205
226
|
messages = []
|
|
206
|
-
for index, h in
|
|
227
|
+
for index, h in _messages(eccodes, path):
|
|
207
228
|
keys = {key: _get(eccodes, h, key, str) for key in INVENTORY_KEYS}
|
|
208
229
|
messages.append(
|
|
209
230
|
GribMessage(
|
|
@@ -373,7 +394,7 @@ def open_grib2(
|
|
|
373
394
|
needs_select = select is None and not fetched.provenance.is_partial
|
|
374
395
|
object_messages = fetched.provenance.object_messages
|
|
375
396
|
selectors = fetched.provenance.selectors if object_messages else []
|
|
376
|
-
for h in _messages(eccodes, fetched.path):
|
|
397
|
+
for index, h in _messages(eccodes, fetched.path):
|
|
377
398
|
count += 1
|
|
378
399
|
if count > 1 and needs_select:
|
|
379
400
|
continue
|
|
@@ -415,15 +436,15 @@ def open_grib2(
|
|
|
415
436
|
if stamp:
|
|
416
437
|
attrs[label] = stamp
|
|
417
438
|
message: dict[str, Any] = {
|
|
418
|
-
"file_index":
|
|
419
|
-
"object_index": object_messages[
|
|
439
|
+
"file_index": index,
|
|
440
|
+
"object_index": object_messages[index] if object_messages else None,
|
|
420
441
|
"shortName": _get(eccodes, h, "shortName", str),
|
|
421
442
|
"typeOfLevel": attrs.get("typeOfLevel"),
|
|
422
443
|
"level": attrs.get("level"),
|
|
423
444
|
"step": attrs.get("step"),
|
|
424
445
|
}
|
|
425
446
|
if selectors:
|
|
426
|
-
message["selector"] = selectors[
|
|
447
|
+
message["selector"] = selectors[index]
|
|
427
448
|
short = message["shortName"]
|
|
428
449
|
if short in (None, "", "unknown", "~"):
|
|
429
450
|
short = _product_name(fetched.asset.id) or (
|
|
@@ -94,7 +94,8 @@ def _echo_detail(summary: Summary, width: int) -> None:
|
|
|
94
94
|
else:
|
|
95
95
|
typer.echo(" columns:")
|
|
96
96
|
for name in frame.columns:
|
|
97
|
-
|
|
97
|
+
unit = frame.units.get(name)
|
|
98
|
+
typer.echo(f" {name} ({unit})" if unit else f" {name}")
|
|
98
99
|
if (scene := summary.netcdf) is not None:
|
|
99
100
|
_echo_table(
|
|
100
101
|
"variables",
|
|
@@ -473,21 +473,77 @@ datasets:
|
|
|
473
473
|
|
|
474
474
|
- id: noaa:ibtracs
|
|
475
475
|
provider: noaa
|
|
476
|
-
status:
|
|
476
|
+
status: available
|
|
477
477
|
domain: tropical-cyclones
|
|
478
|
-
|
|
478
|
+
since: "0.20"
|
|
479
479
|
title: IBTrACS Global Tropical Cyclone Tracks
|
|
480
480
|
description: >-
|
|
481
|
-
International Best Track Archive for Climate Stewardship:
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
International Best Track Archive for Climate Stewardship: every agency's
|
|
482
|
+
tropical cyclone best tracks merged into one global record since 1842,
|
|
483
|
+
published by NCEI as whole files under an anonymous HTTPS directory. A
|
|
484
|
+
subset parameter names one of the eleven files NCEI builds (the complete
|
|
485
|
+
record, active storms, the last three years, everything since 1980, or
|
|
486
|
+
one of seven basins), a format parameter picks the CSV list or the
|
|
487
|
+
NetCDF file, and the newest product version wins unless one is pinned.
|
|
488
|
+
There is no query interface, so dates and geographic filters are
|
|
489
|
+
rejected; the files are rebuilt in place under unchanging names, so a
|
|
490
|
+
lockfile's checksum is what pins the bytes. The CSV opens with the units-row reader
|
|
491
|
+
and the NetCDF with xarray; filter the table locally.
|
|
484
492
|
keywords: [tropical cyclone, hurricane, typhoon, best track, ibtracs, global]
|
|
485
493
|
protocol: http
|
|
486
494
|
homepage: https://www.ncei.noaa.gov/products/international-best-track-archive
|
|
487
495
|
license: US Government Work (public domain)
|
|
488
|
-
temporal_extent: { start: "1842-
|
|
496
|
+
temporal_extent: { start: "1842-10-25T03:00:00Z" }
|
|
489
497
|
capabilities: { spatial_subset: false, temporal_subset: false, variable_subset: false }
|
|
490
|
-
|
|
498
|
+
summary: Global tropical cyclone best tracks
|
|
499
|
+
formats: [CSV with a units row, NetCDF4]
|
|
500
|
+
selection: One whole subset file per query, from the newest or a pinned product version
|
|
501
|
+
inputs: Required subset; optional format and version; no dates or geographic filters
|
|
502
|
+
reader: pandas
|
|
503
|
+
guide: docs/providers/noaa-ibtracs.md
|
|
504
|
+
examples:
|
|
505
|
+
- examples/ibtracs/README.md
|
|
506
|
+
resolution:
|
|
507
|
+
spatial: "Positions to a tenth of a degree"
|
|
508
|
+
temporal: "Three-hourly points, interpolated between the agencies' six-hourly best tracks"
|
|
509
|
+
update_frequency: >-
|
|
510
|
+
Files are rebuilt in place as agencies deliver best tracks, stamped with the build
|
|
511
|
+
date; the v04r01 change log records twenty-three additions and corrections between
|
|
512
|
+
2024-06-21 and 2026-07-31
|
|
513
|
+
citation: >-
|
|
514
|
+
Gahtan, J., K. R. Knapp, C. J. Schreck, H. J. Diamond, J. P. Kossin, and M. C. Kruk, 2024:
|
|
515
|
+
International Best Track Archive for Climate Stewardship (IBTrACS) Project, Version 4r01.
|
|
516
|
+
NOAA National Centers for Environmental Information, doi:10.25921/82ty-9e16. Knapp, K. R.,
|
|
517
|
+
M. C. Kruk, D. H. Levinson, H. J. Diamond, and C. J. Neumann, 2010: The International Best
|
|
518
|
+
Track Archive for Climate Stewardship (IBTrACS). Bull. Amer. Meteor. Soc., 91, 363-376
|
|
519
|
+
terms: https://www.ncei.noaa.gov/products/international-best-track-archive
|
|
520
|
+
variables:
|
|
521
|
+
- { name: "sid", description: "IBTrACS serial id, such as 2004086S29318; SID in the CSV" }
|
|
522
|
+
- { name: "season", units: "year", description: "Season the storm is assigned to" }
|
|
523
|
+
- { name: "number", description: "Storm number within the season" }
|
|
524
|
+
- { name: "basin", description: "Basin at this point: NA, EP, WP, NI, SI, SP, or SA" }
|
|
525
|
+
- { name: "subbasin", description: "Sub-basin at this point, or MM when none applies" }
|
|
526
|
+
- { name: "name", description: "Storm name, or UNNAMED" }
|
|
527
|
+
- { name: "time", units: "UTC", description: "Track-point time; ISO_TIME in the CSV" }
|
|
528
|
+
- { name: "nature", description: "Nature of the cyclone: TS, ET, SS, DS, NR, or MX" }
|
|
529
|
+
- { name: "lat", units: "degrees_north", description: "Track-point latitude" }
|
|
530
|
+
- { name: "lon", units: "degrees_east", description: "Track-point longitude in [-180, 180]" }
|
|
531
|
+
- { name: "wmo_wind", units: "knots", description: "Maximum sustained wind from the responsible WMO agency, in that agency's averaging period" }
|
|
532
|
+
- { name: "wmo_pres", units: "millibars", description: "Minimum central pressure from the responsible WMO agency" }
|
|
533
|
+
- { name: "wmo_agency", description: "The WMO agency responsible for the storm at this point" }
|
|
534
|
+
- { name: "track_type", description: "main, PROVISIONAL, or a spur track merged, split, or otherwise set aside" }
|
|
535
|
+
- { name: "dist2land", units: "km", description: "Distance to land at this point" }
|
|
536
|
+
- { name: "landfall", units: "km", description: "Minimum distance to land before the next point; 0 marks a landfall" }
|
|
537
|
+
- { name: "iflag", description: "One interpolation flag per agency: original, interpolated, or filled" }
|
|
538
|
+
- { name: "usa_wind", units: "knots", description: "Maximum sustained 1-minute wind from the U.S. agency" }
|
|
539
|
+
- { name: "usa_pres", units: "millibars", description: "Minimum central pressure from the U.S. agency" }
|
|
540
|
+
- { name: "usa_status", description: "U.S. agency status, such as TD, TS, HU, or EX" }
|
|
541
|
+
- { name: "usa_sshs", description: "Saffir-Simpson category from the U.S. wind; -5 to 5, with the negatives for non-hurricane statuses" }
|
|
542
|
+
- { name: "usa_rmw", units: "nautical miles", description: "Radius of maximum wind from the U.S. agency" }
|
|
543
|
+
- { name: "usa_r34", units: "nautical miles", description: "34 kt wind extent by quadrant; USA_R34_NE through USA_R34_NW in the CSV" }
|
|
544
|
+
- { name: "storm_speed", units: "knots", description: "Storm translation speed" }
|
|
545
|
+
- { name: "storm_dir", units: "degrees", description: "Storm translation direction" }
|
|
546
|
+
adapter: usdata.providers.noaa.ibtracs:Ibtracs
|
|
491
547
|
- id: noaa:coops-water-levels
|
|
492
548
|
provider: noaa
|
|
493
549
|
status: available
|
|
@@ -946,19 +1002,50 @@ datasets:
|
|
|
946
1002
|
|
|
947
1003
|
- id: noaa:nbm
|
|
948
1004
|
provider: noaa
|
|
949
|
-
status:
|
|
1005
|
+
status: available
|
|
1006
|
+
system: noaa:ncep-models
|
|
950
1007
|
domain: weather-models
|
|
951
|
-
|
|
1008
|
+
since: "0.20"
|
|
952
1009
|
title: National Blend of Models
|
|
953
1010
|
description: >-
|
|
954
|
-
|
|
955
|
-
noaa-nbm-grib2-pds S3 bucket,
|
|
956
|
-
|
|
957
|
-
|
|
1011
|
+
NOAA's statistically blended, bias-corrected forecast guidance in GRIB2 from the anonymous
|
|
1012
|
+
noaa-nbm-grib2-pds S3 bucket, one whole core file per hourly run, forecast hour, and
|
|
1013
|
+
region. Select a run by initialization window and cycle hour, the forecast hours from 1 to
|
|
1014
|
+
264, and the CONUS, Alaska, Hawaii, Puerto Rico, or Guam grid. The CONUS file is about
|
|
1015
|
+
170 MB per hour; the server subsets nothing, so fields are chosen either by naming GRIB2
|
|
1016
|
+
messages, which fetches their byte ranges through the object's index sidecar, or after
|
|
1017
|
+
download with the GRIB2 reader. Each file holds hundreds of fields; the catalog lists the
|
|
1018
|
+
handful the guide and example select.
|
|
1019
|
+
keywords: [forecast, blend, nbm, grib2, guidance, temperature, precipitation, nwp]
|
|
958
1020
|
protocol: s3
|
|
959
1021
|
homepage: https://registry.opendata.aws/noaa-nbm/
|
|
960
1022
|
license: US Government Work (public domain)
|
|
961
|
-
|
|
1023
|
+
temporal_extent: { start: "2020-09-29T12:00:00Z" }
|
|
1024
|
+
capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false, partial_fetch: true }
|
|
1025
|
+
summary: NBM forecast guidance
|
|
1026
|
+
formats: [GRIB2]
|
|
1027
|
+
selection: Regional core files, whole or by named GRIB2 message, chosen by run initialization window, cycle hour, forecast hours, and region
|
|
1028
|
+
inputs: Both timestamps, cycle, and forecast_hour
|
|
1029
|
+
reader: grib
|
|
1030
|
+
guide: docs/providers/noaa-nbm.md
|
|
1031
|
+
examples:
|
|
1032
|
+
- examples/nbm-forecast/README.md
|
|
1033
|
+
resolution:
|
|
1034
|
+
spatial: "2.5 km Lambert conformal CONUS grid, 2345 x 1597 points; separate Alaska, Hawaii, Puerto Rico, and Guam grids"
|
|
1035
|
+
temporal: >-
|
|
1036
|
+
Hourly runs; forecast hours hourly to 36, then every 3 hours to about 190, then every
|
|
1037
|
+
6 hours to 264, on a schedule that varies by cycle
|
|
1038
|
+
update_frequency: "Hourly"
|
|
1039
|
+
citation: >-
|
|
1040
|
+
NOAA National Blend of Models (NBM) was accessed on [date] from
|
|
1041
|
+
https://registry.opendata.aws/noaa-nbm
|
|
1042
|
+
terms: https://www.noaa.gov/information-technology/open-data-dissemination
|
|
1043
|
+
variables:
|
|
1044
|
+
- { name: "2t", units: "K", description: "2 m temperature" }
|
|
1045
|
+
- { name: "2d", units: "K", description: "2 m dewpoint temperature" }
|
|
1046
|
+
- { name: "tp", units: "kg/m2", description: "Total precipitation accumulated over the step" }
|
|
1047
|
+
limits: { max_window: P1D }
|
|
1048
|
+
adapter: usdata.providers.noaa.nbm:Nbm
|
|
962
1049
|
|
|
963
1050
|
- id: noaa:oisst
|
|
964
1051
|
provider: noaa
|
|
@@ -1378,33 +1465,52 @@ datasets:
|
|
|
1378
1465
|
# Documentation composition keyed by stable dataset ID; validated by just docs.
|
|
1379
1466
|
- id: noaa:rap
|
|
1380
1467
|
provider: noaa
|
|
1381
|
-
status:
|
|
1468
|
+
status: available
|
|
1469
|
+
system: noaa:ncep-models
|
|
1382
1470
|
domain: weather-models
|
|
1383
|
-
|
|
1471
|
+
since: "0.20"
|
|
1384
1472
|
title: RAP Model Output
|
|
1385
1473
|
description: >-
|
|
1386
|
-
Rapid Refresh 13 km hourly analyses and forecasts in GRIB2 from the
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
runs.
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
- grib2
|
|
1397
|
-
- weather
|
|
1398
|
-
- nwp
|
|
1474
|
+
Rapid Refresh 13 km hourly analyses and forecasts in GRIB2 from the anonymous noaa-rap-pds
|
|
1475
|
+
S3 bucket, one whole file per run, forecast hour, and grid family. Select a run by
|
|
1476
|
+
initialization window and cycle hour, the forecast hours, and the file family: the 13 km
|
|
1477
|
+
CONUS pressure-level grid (about 18 MB per file), its secondary fields, or the native-grid
|
|
1478
|
+
pressure and hybrid-level files. Runs reach 21 hours, or 51 from the 03, 09, 15, and 21
|
|
1479
|
+
UTC runs. The server subsets nothing, so fields are chosen either by naming GRIB2 messages,
|
|
1480
|
+
which fetches their byte ranges through the object's index sidecar, or after download with
|
|
1481
|
+
the GRIB2 reader. Each file holds hundreds of fields; the catalog lists the handful the
|
|
1482
|
+
guide and example select.
|
|
1483
|
+
keywords: [forecast, model, rap, grib2, weather, nwp, cape, helicity, convective]
|
|
1399
1484
|
protocol: s3
|
|
1400
1485
|
homepage: https://registry.opendata.aws/noaa-rap/
|
|
1401
1486
|
license: US Government Work (public domain)
|
|
1402
|
-
temporal_extent:
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1487
|
+
temporal_extent: { start: "2021-02-22T00:00:00Z" }
|
|
1488
|
+
capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false, partial_fetch: true }
|
|
1489
|
+
summary: RAP model output
|
|
1490
|
+
formats: [GRIB2]
|
|
1491
|
+
selection: Files, whole or by named GRIB2 message, chosen by run initialization window, cycle hour, forecast hours, and file family
|
|
1492
|
+
inputs: Both timestamps, cycle, and forecast_hour
|
|
1493
|
+
reader: grib
|
|
1494
|
+
guide: docs/providers/noaa-rap.md
|
|
1495
|
+
examples:
|
|
1496
|
+
- examples/rap-environment/README.md
|
|
1497
|
+
resolution:
|
|
1498
|
+
spatial: "13 km Lambert conformal CONUS grid, 451 x 337 points, for the awp130 family"
|
|
1499
|
+
temporal: >-
|
|
1500
|
+
Hourly runs; forecast hours to 21, or to 51 from the 03, 09, 15, and 21 UTC runs
|
|
1501
|
+
update_frequency: "Hourly"
|
|
1502
|
+
citation: >-
|
|
1503
|
+
NOAA Rapid Refresh (RAP) was accessed on [date] from https://registry.opendata.aws/noaa-rap
|
|
1504
|
+
terms: https://www.noaa.gov/information-technology/open-data-dissemination
|
|
1505
|
+
variables:
|
|
1506
|
+
- { name: "cape", units: "J/kg", description: "Convective available potential energy, surface or a ground layer" }
|
|
1507
|
+
- { name: "cin", units: "J/kg", description: "Convective inhibition, surface or a ground layer" }
|
|
1508
|
+
- { name: "hlcy", units: "m2/s2", description: "Storm-relative helicity over the 0-1 km and 0-3 km layers" }
|
|
1509
|
+
- { name: "refc", units: "dBZ", description: "Composite reflectivity" }
|
|
1510
|
+
- { name: "2t", units: "K", description: "2 m temperature" }
|
|
1511
|
+
- { name: "2d", units: "K", description: "2 m dewpoint temperature" }
|
|
1512
|
+
limits: { max_window: P1D }
|
|
1513
|
+
adapter: usdata.providers.noaa.rap:Rap
|
|
1408
1514
|
|
|
1409
1515
|
- id: noaa:igra
|
|
1410
1516
|
provider: noaa
|
|
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING
|
|
|
21
21
|
from pydantic import BaseModel, Field
|
|
22
22
|
|
|
23
23
|
from usdata import provenance, readers
|
|
24
|
-
from usdata.models import Provenance
|
|
24
|
+
from usdata.models import Protocol, Provenance
|
|
25
25
|
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from usdata._fetch import FetchedAsset
|
|
@@ -47,7 +47,16 @@ class CsvSummary(BaseModel):
|
|
|
47
47
|
"""A delimited text file's header and how many data rows the scan counted."""
|
|
48
48
|
|
|
49
49
|
columns: list[str] = Field(description="Header fields, in file order")
|
|
50
|
-
|
|
50
|
+
units: dict[str, str] = Field(
|
|
51
|
+
default_factory=dict,
|
|
52
|
+
description=(
|
|
53
|
+
"Units per column, from the units row an ERDDAP or IBTrACS CSV lays under its "
|
|
54
|
+
"header; empty for a CSV with no such row"
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
row_count: int = Field(
|
|
58
|
+
ge=0, description="Data rows counted, excluding the header and any units row"
|
|
59
|
+
)
|
|
51
60
|
row_limit: int = Field(gt=0, description="Rows the scan reads before it stops")
|
|
52
61
|
truncated: bool = Field(description="The scan stopped early, so row_count is a lower bound")
|
|
53
62
|
|
|
@@ -77,7 +86,10 @@ class GribMessage(BaseModel):
|
|
|
77
86
|
``selector``, the index selector the message was fetched for.
|
|
78
87
|
"""
|
|
79
88
|
|
|
80
|
-
file_index: int = Field(
|
|
89
|
+
file_index: int = Field(
|
|
90
|
+
ge=0,
|
|
91
|
+
description="Zero-based message position in the local file; fields of one message share it",
|
|
92
|
+
)
|
|
81
93
|
object_index: int | None = Field(
|
|
82
94
|
default=None,
|
|
83
95
|
ge=1,
|
|
@@ -186,6 +198,7 @@ def inspect_asset(fetched: FetchedAsset) -> Summary:
|
|
|
186
198
|
fetched.asset.id,
|
|
187
199
|
fetched.provenance,
|
|
188
200
|
_detect(fetched.asset.id, fetched.asset.media_type),
|
|
201
|
+
fetched.asset.protocol,
|
|
189
202
|
)
|
|
190
203
|
|
|
191
204
|
|
|
@@ -198,23 +211,39 @@ def inspect_path(path: str | os.PathLike[str]) -> Summary:
|
|
|
198
211
|
|
|
199
212
|
Returns:
|
|
200
213
|
The summary ``inspect_asset`` builds, with the format taken from the file
|
|
201
|
-
name because a sidecar records no media type
|
|
214
|
+
name because a sidecar records no media type, and the protocol from the
|
|
215
|
+
registry entry of the dataset the sidecar names, for the same reason.
|
|
202
216
|
|
|
203
217
|
Raises:
|
|
204
218
|
OSError: The file or its provenance sidecar is missing or unreadable.
|
|
205
219
|
ValueError: The sidecar is not a provenance record.
|
|
206
220
|
"""
|
|
207
221
|
local = Path(path)
|
|
208
|
-
|
|
222
|
+
record = provenance.read(local)
|
|
223
|
+
return _summarize(
|
|
224
|
+
local, local.name, record, _detect(local.name, None), _registered_protocol(record)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _registered_protocol(record: Provenance) -> Protocol | None:
|
|
229
|
+
"""The protocol the registry gives the dataset a sidecar names, or None when it has none."""
|
|
230
|
+
from usdata.registry import DatasetNotFound, default_registry
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
return default_registry().get(record.dataset_id).protocol
|
|
234
|
+
except DatasetNotFound:
|
|
235
|
+
return None
|
|
209
236
|
|
|
210
237
|
|
|
211
|
-
def _summarize(
|
|
238
|
+
def _summarize(
|
|
239
|
+
path: Path, asset_id: str, record: Provenance, fmt: AssetFormat, protocol: Protocol | None
|
|
240
|
+
) -> Summary:
|
|
212
241
|
"""One summary, with the detail its format allows and a note where it allows none."""
|
|
213
242
|
detail: CsvSummary | NetcdfSummary | Grib2Summary | None = None
|
|
214
243
|
note: str | None = None
|
|
215
244
|
if fmt is not AssetFormat.BYTES:
|
|
216
245
|
try:
|
|
217
|
-
detail = _detail(path, fmt, record)
|
|
246
|
+
detail = _detail(path, fmt, record, protocol)
|
|
218
247
|
except readers.MissingReaderDependency as error:
|
|
219
248
|
note = str(error)
|
|
220
249
|
# A cached file that no longer decodes is reported, not raised: a summary
|
|
@@ -259,11 +288,11 @@ def _detect(name: str, media_type: str | None) -> AssetFormat:
|
|
|
259
288
|
|
|
260
289
|
|
|
261
290
|
def _detail(
|
|
262
|
-
path: Path, fmt: AssetFormat, record: Provenance
|
|
291
|
+
path: Path, fmt: AssetFormat, record: Provenance, protocol: Protocol | None
|
|
263
292
|
) -> CsvSummary | NetcdfSummary | Grib2Summary:
|
|
264
293
|
"""The detail for one recognized format, reading only what that format needs."""
|
|
265
294
|
if fmt is AssetFormat.CSV:
|
|
266
|
-
return _csv_summary(path)
|
|
295
|
+
return _csv_summary(path, units_row=readers.has_units_row(record.dataset_id, protocol))
|
|
267
296
|
if fmt is AssetFormat.NETCDF:
|
|
268
297
|
from usdata._netcdf import variables
|
|
269
298
|
|
|
@@ -275,23 +304,35 @@ def _paired(messages: list[GribMessage], record: Provenance) -> list[GribMessage
|
|
|
275
304
|
"""Each message as the fetch that took it described it, where provenance says.
|
|
276
305
|
|
|
277
306
|
The file itself carries neither number nor selector: a partial fetch recorded
|
|
278
|
-
one message number and one selector per range,
|
|
279
|
-
|
|
307
|
+
one message number and one selector per range, and ``file_index`` counts the
|
|
308
|
+
file's messages the same way, so the fields of a message that holds several
|
|
309
|
+
all pair with its number. A whole file, or a record that does not pair,
|
|
310
|
+
leaves both unset.
|
|
280
311
|
"""
|
|
281
312
|
numbers = record.object_messages
|
|
282
|
-
if len(numbers)
|
|
313
|
+
if not numbers or any(message.file_index >= len(numbers) for message in messages):
|
|
283
314
|
return messages
|
|
284
|
-
selectors = (
|
|
285
|
-
record.selectors if len(record.selectors) == len(messages) else [None] * len(messages)
|
|
286
|
-
)
|
|
315
|
+
selectors = record.selectors if len(record.selectors) == len(numbers) else []
|
|
287
316
|
return [
|
|
288
|
-
message.model_copy(
|
|
289
|
-
|
|
317
|
+
message.model_copy(
|
|
318
|
+
update={
|
|
319
|
+
"object_index": numbers[message.file_index],
|
|
320
|
+
"selector": selectors[message.file_index] if selectors else None,
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
for message in messages
|
|
290
324
|
]
|
|
291
325
|
|
|
292
326
|
|
|
293
|
-
def _csv_summary(path: Path) -> CsvSummary:
|
|
294
|
-
"""Header and data-row count through the standard library, stopping at ``ROW_LIMIT`` rows.
|
|
327
|
+
def _csv_summary(path: Path, *, units_row: bool = False) -> CsvSummary:
|
|
328
|
+
"""Header and data-row count through the standard library, stopping at ``ROW_LIMIT`` rows.
|
|
329
|
+
|
|
330
|
+
With ``units_row`` the row under the header is read as units, as ``open``
|
|
331
|
+
reads it, rather than counted as data.
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
ValueError: A units row was expected and does not match the header.
|
|
335
|
+
"""
|
|
295
336
|
with path.open("rb") as raw:
|
|
296
337
|
compressed = raw.read(2) == b"\x1f\x8b"
|
|
297
338
|
raw.seek(0)
|
|
@@ -299,13 +340,21 @@ def _csv_summary(path: Path) -> CsvSummary:
|
|
|
299
340
|
with io.TextIOWrapper(binary, encoding="utf-8-sig", newline="") as stream:
|
|
300
341
|
records = csv.reader(stream)
|
|
301
342
|
columns = next(records, [])
|
|
343
|
+
units: dict[str, str] = {}
|
|
344
|
+
if units_row and columns:
|
|
345
|
+
values = next(records, [])
|
|
346
|
+
if len(values) != len(columns):
|
|
347
|
+
raise ValueError("CSV must have a units row matching the header")
|
|
348
|
+
units = dict(zip(columns, (value.strip() for value in values), strict=True))
|
|
302
349
|
rows = 0
|
|
303
350
|
for _ in records:
|
|
304
351
|
rows += 1
|
|
305
352
|
if rows == ROW_LIMIT:
|
|
306
353
|
break
|
|
307
354
|
truncated = next(records, None) is not None
|
|
308
|
-
return CsvSummary(
|
|
355
|
+
return CsvSummary(
|
|
356
|
+
columns=columns, units=units, row_count=rows, row_limit=ROW_LIMIT, truncated=truncated
|
|
357
|
+
)
|
|
309
358
|
|
|
310
359
|
|
|
311
360
|
__all__ = [
|
|
@@ -174,7 +174,8 @@ def download_ranges(
|
|
|
174
174
|
url: The object's URL; ``s3://`` callers map it with ``s3.object_url`` first.
|
|
175
175
|
dest: Path the concatenated bytes are moved to once every run has arrived.
|
|
176
176
|
ranges: Ascending, non-overlapping inclusive byte ranges to fetch.
|
|
177
|
-
etag: The object's ETag, sent as ``If-Match`` so a
|
|
177
|
+
etag: The object's ETag, bare or quoted, sent as a quoted ``If-Match`` so a
|
|
178
|
+
republished object fails.
|
|
178
179
|
total: The object's size, checked against every ``Content-Range``.
|
|
179
180
|
http: Client to use; one is created and closed here when it is omitted.
|
|
180
181
|
|
|
@@ -191,7 +192,8 @@ def download_ranges(
|
|
|
191
192
|
own = http is None
|
|
192
193
|
active = http or client()
|
|
193
194
|
attempt = 0
|
|
194
|
-
|
|
195
|
+
# An entity-tag is a quoted string (RFC 9110); records keep the bare value.
|
|
196
|
+
headers = {"If-Match": '"' + etag.strip('"') + '"'}
|
|
195
197
|
|
|
196
198
|
def request() -> Path:
|
|
197
199
|
nonlocal attempt
|