usdata 0.31.0__tar.gz → 0.33.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.33.0}/PKG-INFO +2 -2
- {usdata-0.31.0 → usdata-0.33.0}/README.md +1 -1
- {usdata-0.31.0 → usdata-0.33.0}/pyproject.toml +1 -1
- {usdata-0.31.0 → usdata-0.33.0}/pyproject.toml.orig +1 -1
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/__init__.py +5 -1
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_grib.py +29 -3
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/app.py +169 -23
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/data/registry.yaml +127 -7
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/doctor.py +3 -2
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/inspect.py +18 -15
- usdata-0.33.0/src/usdata/messages.py +60 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/models.py +67 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/provenance.py +1 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/base.py +31 -1
- usdata-0.33.0/src/usdata/providers/noaa/enso.py +76 -0
- usdata-0.33.0/src/usdata/providers/noaa/ersst.py +95 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/grib_index.py +24 -37
- usdata-0.33.0/src/usdata/providers/noaa/gsom_files.py +83 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/hrrr.py +18 -1
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/pull.py +136 -6
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/testing.py +8 -4
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/__main__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_aqs.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_census.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_fetch.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_files.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_hurdat2.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_netcdf.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_progress.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/_radar.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cache.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cache_ops.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cite.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/cache.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/cite.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/doctor.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/inspect.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/cli/progress.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/data/nexrad_sites.csv +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/data/places.csv +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/data/places.sources.json +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/manifest.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/mirror.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/protocols/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/protocols/erddap.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/protocols/http.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/protocols/listing.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/protocols/s3.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/census/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/census/acs.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/credentials.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/epa/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/epa/aqs.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/fema/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/fema/declarations.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/http.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/coops.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/gfs.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/glm.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/goes.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/gsom.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/gsoy.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/lcd.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/mrms.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/nbm.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/nexrad.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/normals.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/rap.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/sites.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/spc.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/noaa/storm_events.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/params.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/usgs/__init__.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/usgs/daily.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/py.typed +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/query.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/readers.py +0 -0
- {usdata-0.31.0 → usdata-0.33.0}/src/usdata/registry.py +0 -0
- {usdata-0.31.0 → usdata-0.33.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.33.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
|
|
@@ -113,7 +113,7 @@ breaking changes.
|
|
|
113
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 |
|
|
114
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 |
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Thirty-one datasets are available today and seventeen more are planned, grouped
|
|
117
117
|
by agency and product family in the [catalog](docs/providers/README.md).
|
|
118
118
|
|
|
119
119
|
## How this compares
|
|
@@ -68,7 +68,7 @@ breaking changes.
|
|
|
68
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 |
|
|
69
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 |
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Thirty-one datasets are available today and seventeen more are planned, grouped
|
|
72
72
|
by agency and product family in the [catalog](docs/providers/README.md).
|
|
73
73
|
|
|
74
74
|
## How this compares
|
|
@@ -13,6 +13,7 @@ except PackageNotFoundError: # running from a source tree without an install
|
|
|
13
13
|
from usdata._fetch import ChecksumMismatch, FetchedAsset, fetch, fetch_asset
|
|
14
14
|
from usdata.cite import Citation, cite_dataset, cite_lockfile
|
|
15
15
|
from usdata.inspect import inspect_asset, inspect_path
|
|
16
|
+
from usdata.messages import MessageListing, list_messages
|
|
16
17
|
from usdata.models import (
|
|
17
18
|
Asset,
|
|
18
19
|
BBox,
|
|
@@ -23,7 +24,7 @@ from usdata.models import (
|
|
|
23
24
|
TemporalSelection,
|
|
24
25
|
TimeRange,
|
|
25
26
|
)
|
|
26
|
-
from usdata.pull import pull, verify
|
|
27
|
+
from usdata.pull import compare_listing, pull, verify
|
|
27
28
|
from usdata.query import build_query
|
|
28
29
|
from usdata.registry import DatasetNotFound, Registry, SearchResult, default_registry
|
|
29
30
|
from usdata.selection import select_by_time
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"Dataset",
|
|
37
38
|
"DatasetNotFound",
|
|
38
39
|
"FetchedAsset",
|
|
40
|
+
"MessageListing",
|
|
39
41
|
"Place",
|
|
40
42
|
"Provenance",
|
|
41
43
|
"Query",
|
|
@@ -47,6 +49,7 @@ __all__ = [
|
|
|
47
49
|
"build_query",
|
|
48
50
|
"cite_dataset",
|
|
49
51
|
"cite_lockfile",
|
|
52
|
+
"compare_listing",
|
|
50
53
|
"datasets",
|
|
51
54
|
"default_registry",
|
|
52
55
|
"fetch",
|
|
@@ -54,6 +57,7 @@ __all__ = [
|
|
|
54
57
|
"get",
|
|
55
58
|
"inspect_asset",
|
|
56
59
|
"inspect_path",
|
|
60
|
+
"list_messages",
|
|
57
61
|
"pull",
|
|
58
62
|
"search",
|
|
59
63
|
"select_by_time",
|
|
@@ -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,31 @@ 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. This is
|
|
100
|
+
a workaround for https://github.com/ecmwf/eckit/issues/354; once an eckitlib
|
|
101
|
+
wheel no longer clashes with pyproj's PROJ, it can go.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ModuleNotFoundError: eccodes is not installed.
|
|
105
|
+
"""
|
|
106
|
+
if find_spec("pyproj") is not None:
|
|
107
|
+
with suppress(ImportError, OSError):
|
|
108
|
+
import_module("pyproj")
|
|
109
|
+
return import_module("eccodes")
|
|
110
|
+
|
|
111
|
+
|
|
89
112
|
def _modules() -> tuple[Any, Any, Any]:
|
|
90
113
|
try:
|
|
91
|
-
eccodes =
|
|
114
|
+
eccodes = import_eccodes()
|
|
92
115
|
xarray = import_module("xarray")
|
|
93
116
|
numpy = import_module("numpy")
|
|
94
117
|
except ModuleNotFoundError as error:
|
|
@@ -460,8 +483,11 @@ def open_grib2(
|
|
|
460
483
|
needs_select = select is None and not fetched.provenance.is_partial
|
|
461
484
|
object_messages = fetched.provenance.object_messages
|
|
462
485
|
selectors = fetched.provenance.selectors if object_messages else []
|
|
486
|
+
fields_seen: dict[int, int] = {}
|
|
463
487
|
for index, h in _messages(eccodes, fetched.path):
|
|
464
488
|
count += 1
|
|
489
|
+
# Fields of one message share its index; the nth of them is the index's field n+1.
|
|
490
|
+
field = fields_seen[index] = fields_seen.get(index, -1) + 1
|
|
465
491
|
if count > 1 and needs_select:
|
|
466
492
|
continue
|
|
467
493
|
hits = {key: _matched(eccodes, h, key, wanted) for key, wanted in options.items()}
|
|
@@ -515,7 +541,7 @@ def open_grib2(
|
|
|
515
541
|
"step": attrs.get("step"),
|
|
516
542
|
}
|
|
517
543
|
if selectors:
|
|
518
|
-
message["selector"] =
|
|
544
|
+
message["selector"] = fetched.provenance.field_selector(index, field)
|
|
519
545
|
short = base_name(
|
|
520
546
|
message["shortName"], [attrs.get(key) for key in PARAMETER_KEYS], fetched.asset.id
|
|
521
547
|
)
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from collections.abc import Iterable
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Annotated
|
|
8
|
+
from typing import Annotated, Any
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
import typer
|
|
@@ -20,10 +20,19 @@ from usdata.cli.doctor import doctor
|
|
|
20
20
|
from usdata.cli.inspect import inspect
|
|
21
21
|
from usdata.cli.progress import progress
|
|
22
22
|
from usdata.manifest import lockfile_path
|
|
23
|
+
from usdata.messages import list_messages
|
|
23
24
|
from usdata.models import READER_EXTRAS_TEXT, Dataset, Status, describe_duration
|
|
24
25
|
from usdata.providers import adapter_class, load_adapter
|
|
25
26
|
from usdata.providers.base import NotImplementedProvider
|
|
26
|
-
from usdata.pull import
|
|
27
|
+
from usdata.pull import (
|
|
28
|
+
EmptySource,
|
|
29
|
+
ListingComparison,
|
|
30
|
+
ManifestChanged,
|
|
31
|
+
Plan,
|
|
32
|
+
UnknownDatasets,
|
|
33
|
+
UpstreamChanged,
|
|
34
|
+
)
|
|
35
|
+
from usdata.pull import compare_listing as compare_manifest_listing
|
|
27
36
|
from usdata.pull import plan as plan_manifest
|
|
28
37
|
from usdata.pull import pull as pull_manifest
|
|
29
38
|
from usdata.pull import verify as verify_manifest
|
|
@@ -59,6 +68,33 @@ _QUERY_FLAGS = {
|
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
|
|
71
|
+
def _parse_params(
|
|
72
|
+
param: list[str] | None, command: str, flags: dict[str, str | None]
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
"""``--param key=value`` pairs as a dict of strings; exit 2 on a malformed or reserved key.
|
|
75
|
+
|
|
76
|
+
``flags`` maps each reserved query option to the flag ``command`` takes it
|
|
77
|
+
through, or to ``None`` when ``command`` does not take it.
|
|
78
|
+
"""
|
|
79
|
+
params: dict[str, Any] = {}
|
|
80
|
+
for item in param or []:
|
|
81
|
+
key, sep, value = item.partition("=")
|
|
82
|
+
if not sep:
|
|
83
|
+
typer.secho(f"--param expects key=value, got {item!r}", err=True, fg="red")
|
|
84
|
+
raise typer.Exit(code=2)
|
|
85
|
+
if key in flags:
|
|
86
|
+
flag = flags[key]
|
|
87
|
+
hint = (
|
|
88
|
+
f"use {flag} instead of --param"
|
|
89
|
+
if flag is not None
|
|
90
|
+
else f"{command} does not support this option"
|
|
91
|
+
)
|
|
92
|
+
typer.secho(f"{key} is a reserved query option; {hint}", err=True, fg="red")
|
|
93
|
+
raise typer.Exit(code=2)
|
|
94
|
+
params[key] = value
|
|
95
|
+
return params
|
|
96
|
+
|
|
97
|
+
|
|
62
98
|
def _size_phrase(known_bytes: int, unknown: int, *, sources: Iterable[str] = ()) -> str:
|
|
63
99
|
"""How many bytes a dry run would move, said honestly when some size is missing.
|
|
64
100
|
|
|
@@ -349,26 +385,7 @@ def fetch(
|
|
|
349
385
|
] = False,
|
|
350
386
|
) -> None:
|
|
351
387
|
"""Resolve a query against one dataset and download the matching assets."""
|
|
352
|
-
params
|
|
353
|
-
for item in param or []:
|
|
354
|
-
key, sep, value = item.partition("=")
|
|
355
|
-
if not sep:
|
|
356
|
-
typer.secho(f"--param expects key=value, got {item!r}", err=True, fg="red")
|
|
357
|
-
raise typer.Exit(code=2)
|
|
358
|
-
if key in _QUERY_FLAGS:
|
|
359
|
-
flag = _QUERY_FLAGS[key]
|
|
360
|
-
hint = (
|
|
361
|
-
f"use {flag} instead of --param"
|
|
362
|
-
if flag is not None
|
|
363
|
-
else "fetch does not support this option"
|
|
364
|
-
)
|
|
365
|
-
typer.secho(
|
|
366
|
-
f"{key} is a reserved query option; {hint}",
|
|
367
|
-
err=True,
|
|
368
|
-
fg="red",
|
|
369
|
-
)
|
|
370
|
-
raise typer.Exit(code=2)
|
|
371
|
-
params[key] = value
|
|
388
|
+
params = _parse_params(param, "fetch", _QUERY_FLAGS)
|
|
372
389
|
box = None
|
|
373
390
|
if bbox:
|
|
374
391
|
try:
|
|
@@ -429,6 +446,64 @@ def fetch(
|
|
|
429
446
|
typer.echo(f"{f.path}\t{tag}\t{f.provenance.size} bytes")
|
|
430
447
|
|
|
431
448
|
|
|
449
|
+
_MESSAGES_FLAGS: dict[str, str | None] = {
|
|
450
|
+
key: flag if key in ("start", "end") else None for key, flag in _QUERY_FLAGS.items()
|
|
451
|
+
}
|
|
452
|
+
"""The query options ``messages`` takes: a model run is selected by its window alone."""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@app.command()
|
|
456
|
+
def messages(
|
|
457
|
+
dataset_id: Annotated[str, typer.Argument(help="Dataset id, e.g. noaa:hrrr")],
|
|
458
|
+
start: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
|
|
459
|
+
end: Annotated[
|
|
460
|
+
str | None,
|
|
461
|
+
typer.Option(help="ISO date or datetime; a date alone runs to the end of that UTC day."),
|
|
462
|
+
] = None,
|
|
463
|
+
param: Annotated[
|
|
464
|
+
list[str] | None,
|
|
465
|
+
typer.Option("--param", "-p", help="Provider-specific key=value, repeatable."),
|
|
466
|
+
] = None,
|
|
467
|
+
as_json: Annotated[
|
|
468
|
+
bool,
|
|
469
|
+
typer.Option("--json", help="Emit a JSON array of listings and nothing else on stdout."),
|
|
470
|
+
] = False,
|
|
471
|
+
) -> None:
|
|
472
|
+
"""List the GRIB2 messages each file of a query holds, spelled as the messages param takes them.
|
|
473
|
+
|
|
474
|
+
One tab-separated line per field: asset id, message number, bytes, and the
|
|
475
|
+
selector that names exactly that field. Only the index sidecars are read.
|
|
476
|
+
"""
|
|
477
|
+
params = _parse_params(param, "messages", _MESSAGES_FLAGS)
|
|
478
|
+
try:
|
|
479
|
+
ds = default_registry().get(dataset_id)
|
|
480
|
+
listings = list_messages(ds, build_query(start=start, end=end, **params))
|
|
481
|
+
except (DatasetNotFound, ValueError) as e:
|
|
482
|
+
typer.secho(str(e), err=True, fg="red")
|
|
483
|
+
raise typer.Exit(code=2) from None
|
|
484
|
+
except NotImplementedProvider as e:
|
|
485
|
+
typer.secho(str(e), err=True, fg="yellow")
|
|
486
|
+
raise typer.Exit(code=3) from None
|
|
487
|
+
except httpx.HTTPError as e:
|
|
488
|
+
typer.secho(f"request failed: {e}", err=True, fg="red")
|
|
489
|
+
raise typer.Exit(code=4) from None
|
|
490
|
+
if as_json:
|
|
491
|
+
typer.echo(json.dumps([listing.model_dump(mode="json") for listing in listings], indent=2))
|
|
492
|
+
else:
|
|
493
|
+
for listing in listings:
|
|
494
|
+
for entry in listing.messages:
|
|
495
|
+
number = entry.number if entry.field is None else f"{entry.number}.{entry.field}"
|
|
496
|
+
typer.echo(f"{listing.asset.id}\t{number}\t{entry.length}\t{entry.selector}")
|
|
497
|
+
count = sum(len(listing.messages) for listing in listings)
|
|
498
|
+
typer.echo(
|
|
499
|
+
f"{count} message(s) in {len(listings)} file(s); a selector may stop after its "
|
|
500
|
+
"level text, as CAPE:surface, to match every step",
|
|
501
|
+
err=True,
|
|
502
|
+
)
|
|
503
|
+
if not listings:
|
|
504
|
+
raise typer.Exit(code=1)
|
|
505
|
+
|
|
506
|
+
|
|
432
507
|
def _print_plan(plan: Plan, *, as_json: bool, quiet: bool) -> None:
|
|
433
508
|
"""Report what a pull would fetch: assets, a subtotal per source, a total on stderr."""
|
|
434
509
|
if as_json:
|
|
@@ -554,12 +629,35 @@ def pull(
|
|
|
554
629
|
def verify(
|
|
555
630
|
manifest: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
|
|
556
631
|
cache_dir: Annotated[Path | None, typer.Option(help="Override the cache directory.")] = None,
|
|
632
|
+
listing: Annotated[
|
|
633
|
+
bool,
|
|
634
|
+
typer.Option(
|
|
635
|
+
"--listing",
|
|
636
|
+
help="Instead of the cache, compare the lockfile with what each source lists today; "
|
|
637
|
+
"downloads nothing, but asks every source.",
|
|
638
|
+
),
|
|
639
|
+
] = False,
|
|
640
|
+
as_json: Annotated[
|
|
641
|
+
bool, typer.Option("--json", help="With --listing, emit the comparison as JSON on stdout.")
|
|
642
|
+
] = False,
|
|
557
643
|
) -> None:
|
|
558
|
-
"""Check cached files
|
|
644
|
+
"""Check cached files, or with --listing today's source listings, against the lockfile.
|
|
645
|
+
|
|
646
|
+
Exit 1 on any drift.
|
|
647
|
+
"""
|
|
648
|
+
if as_json and not listing:
|
|
649
|
+
typer.secho("--json applies to --listing only", err=True, fg="red")
|
|
650
|
+
raise typer.Exit(code=2)
|
|
651
|
+
if listing and cache_dir is not None:
|
|
652
|
+
typer.secho("--listing reads no cache; drop --cache-dir", err=True, fg="red")
|
|
653
|
+
raise typer.Exit(code=2)
|
|
559
654
|
lock = lockfile_path(manifest)
|
|
560
655
|
if not lock.exists():
|
|
561
656
|
typer.secho(f"no lockfile at {lock}; run pull first", err=True, fg="red")
|
|
562
657
|
raise typer.Exit(code=2)
|
|
658
|
+
if listing:
|
|
659
|
+
_verify_listing(manifest, as_json=as_json)
|
|
660
|
+
return
|
|
563
661
|
try:
|
|
564
662
|
drift = verify_manifest(manifest, root=cache_dir)
|
|
565
663
|
except (ManifestChanged, ValueError, OSError) as e:
|
|
@@ -573,4 +671,52 @@ def verify(
|
|
|
573
671
|
typer.echo(f"all assets match {lock.name}", err=True)
|
|
574
672
|
|
|
575
673
|
|
|
674
|
+
def _verify_listing(manifest: Path, *, as_json: bool) -> None:
|
|
675
|
+
"""Compare the lockfile with today's listings; exit 1 when a re-resolve would differ."""
|
|
676
|
+
try:
|
|
677
|
+
comparison = compare_manifest_listing(manifest)
|
|
678
|
+
except (DatasetNotFound, UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
|
|
679
|
+
typer.secho(str(e), err=True, fg="red")
|
|
680
|
+
raise typer.Exit(code=2) from None
|
|
681
|
+
except NotImplementedProvider as e:
|
|
682
|
+
typer.secho(str(e), err=True, fg="yellow")
|
|
683
|
+
raise typer.Exit(code=3) from None
|
|
684
|
+
except httpx.HTTPError as e:
|
|
685
|
+
typer.secho(f"listing failed: {e}", err=True, fg="red")
|
|
686
|
+
raise typer.Exit(code=4) from None
|
|
687
|
+
_print_listing(comparison, as_json=as_json)
|
|
688
|
+
if not comparison.matches:
|
|
689
|
+
raise typer.Exit(code=1)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _print_listing(comparison: ListingComparison, *, as_json: bool) -> None:
|
|
693
|
+
"""One line per difference, a summary on stderr; or the whole comparison as JSON."""
|
|
694
|
+
name = comparison.lockfile.name
|
|
695
|
+
if as_json:
|
|
696
|
+
typer.echo(comparison.model_dump_json(indent=2))
|
|
697
|
+
else:
|
|
698
|
+
for source in comparison.sources:
|
|
699
|
+
for asset in source.added:
|
|
700
|
+
typer.echo(f"{source.source}\tadded\t{asset.id}\t{asset.href}")
|
|
701
|
+
for asset in source.removed:
|
|
702
|
+
typer.echo(f"{source.source}\tremoved\t{asset.id}\t{asset.href}")
|
|
703
|
+
for change in source.changed:
|
|
704
|
+
sizes = f"{_size(change.locked_size)} -> {_size(change.listed_size)} bytes"
|
|
705
|
+
typer.echo(f"{source.source}\tchanged\t{change.asset_id}\t{sizes}")
|
|
706
|
+
differing = [source.source for source in comparison.sources if not source.matches]
|
|
707
|
+
if differing:
|
|
708
|
+
typer.secho(
|
|
709
|
+
f"{len(differing)} source(s) list something other than {name} pins: "
|
|
710
|
+
f"{', '.join(differing)}. Pass pull --force to re-resolve.",
|
|
711
|
+
err=True,
|
|
712
|
+
fg="red",
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
typer.echo(f"every source lists what {name} pins", err=True)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _size(value: int | None) -> str:
|
|
719
|
+
return "?" if value is None else str(value)
|
|
720
|
+
|
|
721
|
+
|
|
576
722
|
app.command()(doctor)
|
|
@@ -732,6 +732,52 @@ datasets:
|
|
|
732
732
|
- { name: "TAVG", units: "degrees Celsius", description: "Monthly mean temperature (metric units)" }
|
|
733
733
|
adapter: usdata.providers.noaa.gsom:GlobalSummaryMonthly
|
|
734
734
|
|
|
735
|
+
- id: noaa:gsom-station-files
|
|
736
|
+
provider: noaa
|
|
737
|
+
status: available
|
|
738
|
+
domain: surface-weather
|
|
739
|
+
since: "0.33"
|
|
740
|
+
title: Global Summary of the Month Station Files
|
|
741
|
+
description: >-
|
|
742
|
+
The same Global Summary of the Month as noaa:gsom, as NCEI's static per-station files:
|
|
743
|
+
one CSV per station holding its whole monthly record, every element with its attributes,
|
|
744
|
+
in metric units. Each station is one plain HTTPS GET that does not go through the Access
|
|
745
|
+
Data Service, which suits pulling many stations' long records. There is no selection by
|
|
746
|
+
time, place, or element; name stations and select months and columns after opening.
|
|
747
|
+
Variables are NCEI element codes, the catalog lists the common ones, and the delivered
|
|
748
|
+
set is open and depends on the station.
|
|
749
|
+
keywords: [climate, monthly, stations, snowfall, temperature, precipitation, gsom, ncei]
|
|
750
|
+
protocol: http
|
|
751
|
+
homepage: https://www.ncei.noaa.gov/data/global-summary-of-the-month/
|
|
752
|
+
license: US Government Work (public domain)
|
|
753
|
+
spatial_extent: { west: -180.0, south: -90.0, east: 180.0, north: 90.0 }
|
|
754
|
+
temporal_extent: { start: "1763-01-01T00:00:00Z" }
|
|
755
|
+
capabilities: { spatial_subset: false, temporal_subset: false, variable_subset: false }
|
|
756
|
+
summary: Whole monthly station records, one file per station
|
|
757
|
+
formats: [CSV]
|
|
758
|
+
selection: One whole file per named station, every month and element it holds
|
|
759
|
+
inputs: Station IDs; no dates, geographic filters, or variables
|
|
760
|
+
reader: pandas
|
|
761
|
+
guide: docs/providers/noaa-gsom-station-files.md
|
|
762
|
+
examples:
|
|
763
|
+
- examples/datasets/noaa-gsom-station-files/noaa-gsom-station-files.ipynb
|
|
764
|
+
- examples/studies/el-nino-snowfall/el-nino-snowfall.ipynb
|
|
765
|
+
resolution:
|
|
766
|
+
spatial: "Land surface stations, derived from GHCN-Daily"
|
|
767
|
+
temporal: "Monthly"
|
|
768
|
+
update_frequency: "Weekly"
|
|
769
|
+
citation: >-
|
|
770
|
+
Lawrimore, Jay H.; Ray, Ron; Applequist, Scott; Korzeniewski, Bryant; Menne, Matthew J.
|
|
771
|
+
(2016): Global Summary of the Month (GSOM), Version 1. NOAA National Centers for
|
|
772
|
+
Environmental Information. https://doi.org/10.7289/V5QV3JJ5
|
|
773
|
+
terms: https://www.ncei.noaa.gov/metadata/geoportal/rest/metadata/item/gov.noaa.ncdc:C00946/html
|
|
774
|
+
variables:
|
|
775
|
+
- { name: "PRCP", units: "mm", description: "Monthly precipitation total" }
|
|
776
|
+
- { name: "SNOW", units: "mm", description: "Monthly snowfall total" }
|
|
777
|
+
- { name: "SNOW_ATTRIBUTES", description: "Days missing, measurement, quality, and source flags for SNOW" }
|
|
778
|
+
- { name: "TAVG", units: "degrees Celsius", description: "Monthly mean temperature" }
|
|
779
|
+
adapter: usdata.providers.noaa.gsom_files:GsomStationFiles
|
|
780
|
+
|
|
735
781
|
- id: noaa:gsoy
|
|
736
782
|
provider: noaa
|
|
737
783
|
status: available
|
|
@@ -1125,19 +1171,93 @@ datasets:
|
|
|
1125
1171
|
|
|
1126
1172
|
- id: noaa:ersst
|
|
1127
1173
|
provider: noaa
|
|
1128
|
-
status:
|
|
1174
|
+
status: available
|
|
1129
1175
|
domain: ocean-physics
|
|
1130
|
-
|
|
1131
|
-
title: ERSST Monthly Sea Surface Temperature
|
|
1176
|
+
since: "0.33"
|
|
1177
|
+
title: ERSST v6 Monthly Sea Surface Temperature
|
|
1132
1178
|
description: >-
|
|
1133
|
-
Extended Reconstructed
|
|
1134
|
-
|
|
1135
|
-
|
|
1179
|
+
Extended Reconstructed Sea Surface Temperature version 6: NCEI's monthly global
|
|
1180
|
+
analysis on a 2 degree grid since January 1850, built from ship, buoy, and Argo
|
|
1181
|
+
observations. One NetCDF4 file per month in an anonymous NCEI HTTPS directory, holding
|
|
1182
|
+
sea surface temperature and its anomaly from the 1991-2020 ERSSTv6 climatology. Every
|
|
1183
|
+
month a window touches is selected whole; there is no server-side spatial or variable
|
|
1184
|
+
subsetting. Each monthly update writes the month just ended and rewrites the month
|
|
1185
|
+
before it, so the newest file is preliminary.
|
|
1186
|
+
keywords: [sst, sea surface temperature, ocean, climate, ersst, monthly, netcdf, enso, el nino]
|
|
1136
1187
|
protocol: http
|
|
1137
1188
|
homepage: https://www.ncei.noaa.gov/products/extended-reconstructed-sst
|
|
1138
1189
|
license: US Government Work (public domain)
|
|
1139
|
-
|
|
1190
|
+
spatial_extent: { west: -180.0, south: -89.0, east: 180.0, north: 89.0 }
|
|
1191
|
+
temporal_extent: { start: "1850-01-01T00:00:00Z" }
|
|
1140
1192
|
capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false }
|
|
1193
|
+
summary: Monthly global 2 degree sea surface temperature since 1850
|
|
1194
|
+
formats: [NetCDF4]
|
|
1195
|
+
selection: Whole global monthly files, one for every month the window touches
|
|
1196
|
+
inputs: Both dates (selects the months they span)
|
|
1197
|
+
reader: netcdf
|
|
1198
|
+
guide: docs/providers/noaa-ersst.md
|
|
1199
|
+
examples:
|
|
1200
|
+
- examples/datasets/noaa-ersst/noaa-ersst.ipynb
|
|
1201
|
+
resolution:
|
|
1202
|
+
spatial: "2.0 degree grid"
|
|
1203
|
+
temporal: "Monthly means"
|
|
1204
|
+
update_frequency: >-
|
|
1205
|
+
Monthly, around the third: each update writes the month just ended and rewrites the
|
|
1206
|
+
month before it
|
|
1207
|
+
citation: >-
|
|
1208
|
+
Huang, B., X. Yin, T. Boyer, C. Liu, M. Menne, Y. D. Rao, T. Smith, R. Vose, and
|
|
1209
|
+
H.-M. Zhang, 2025: Extended Reconstructed Sea Surface Temperature Version 6 (ERSSTv6):
|
|
1210
|
+
Part I. An Artificial Neural Network Approach. Journal of Climate, 38, 1105-1121,
|
|
1211
|
+
doi:10.1175/JCLI-D-23-0707.1
|
|
1212
|
+
terms: https://www.ncei.noaa.gov/products/extended-reconstructed-sst
|
|
1213
|
+
variables:
|
|
1214
|
+
- { name: "sst", units: "degree_C", description: "Extended reconstructed sea surface temperature" }
|
|
1215
|
+
- { name: "ssta", units: "degree_C", description: "Anomaly from the 1991-2020 ERSSTv6 climatology" }
|
|
1216
|
+
adapter: usdata.providers.noaa.ersst:Ersst
|
|
1217
|
+
|
|
1218
|
+
- id: noaa:enso-indices
|
|
1219
|
+
provider: noaa
|
|
1220
|
+
status: available
|
|
1221
|
+
domain: climate
|
|
1222
|
+
since: "0.33"
|
|
1223
|
+
title: CPC ENSO Indices (RONI and ONI)
|
|
1224
|
+
description: >-
|
|
1225
|
+
The Climate Prediction Center's seasonal El Niño-Southern Oscillation indices: the
|
|
1226
|
+
Relative Oceanic Niño Index (RONI), which NOAA has used for official ENSO monitoring
|
|
1227
|
+
since 1 February 2026, and the traditional Oceanic Niño Index (ONI). Both are
|
|
1228
|
+
three-month running means of Niño 3.4 sea surface temperature anomalies from ERSST v6;
|
|
1229
|
+
RONI first subtracts the tropical mean anomaly. Each index is one whole plain-text table
|
|
1230
|
+
from DJF 1950, rewritten in place each month, with no server-side selection.
|
|
1231
|
+
keywords: [enso, el nino, la nina, oni, roni, nino 3.4, climate index, cpc, sea surface temperature]
|
|
1232
|
+
protocol: http
|
|
1233
|
+
homepage: https://www.cpc.ncep.noaa.gov/products/analysis_monitoring/enso/roni/
|
|
1234
|
+
license: US Government Work (public domain)
|
|
1235
|
+
spatial_extent: { west: -170.0, south: -5.0, east: -120.0, north: 5.0 }
|
|
1236
|
+
temporal_extent: { start: "1949-12-01T00:00:00Z" }
|
|
1237
|
+
capabilities: { spatial_subset: false, temporal_subset: false, variable_subset: false }
|
|
1238
|
+
summary: Seasonal El Niño indices, the official RONI and the traditional ONI
|
|
1239
|
+
formats: [whitespace-delimited text (no reader)]
|
|
1240
|
+
selection: One whole index table, every season from DJF 1950; select seasons locally
|
|
1241
|
+
inputs: Optional index (roni or oni); no dates or geographic filters
|
|
1242
|
+
reader: null
|
|
1243
|
+
guide: docs/providers/noaa-enso-indices.md
|
|
1244
|
+
examples:
|
|
1245
|
+
- examples/datasets/noaa-enso-indices/noaa-enso-indices.ipynb
|
|
1246
|
+
- examples/studies/el-nino-snowfall/el-nino-snowfall.ipynb
|
|
1247
|
+
resolution:
|
|
1248
|
+
spatial: "One value for the Niño 3.4 region, 5N-5S and 170W-120W"
|
|
1249
|
+
temporal: "Overlapping three-month seasons, one per month"
|
|
1250
|
+
update_frequency: "Monthly, by the 5th"
|
|
1251
|
+
citation: >-
|
|
1252
|
+
NOAA Climate Prediction Center, Relative Oceanic Niño Index and Oceanic Niño Index,
|
|
1253
|
+
accessed via usdata
|
|
1254
|
+
terms: https://www.weather.gov/disclaimer
|
|
1255
|
+
variables:
|
|
1256
|
+
- { name: "SEAS", description: "Three-month season, such as DJF" }
|
|
1257
|
+
- { name: "YR", description: "Year of the season's middle month" }
|
|
1258
|
+
- { name: "ANOM", units: "degree_C", description: "Index value: the season's anomaly" }
|
|
1259
|
+
- { name: "TOTAL", units: "degree_C", description: "Niño 3.4 mean temperature; ONI table only" }
|
|
1260
|
+
adapter: usdata.providers.noaa.enso:EnsoIndices
|
|
1141
1261
|
|
|
1142
1262
|
- id: noaa:etopo
|
|
1143
1263
|
provider: noaa
|
|
@@ -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(
|
|
@@ -137,7 +137,8 @@ class Grib2Summary(BaseModel):
|
|
|
137
137
|
KeyError: No message here was fetched for that selector; the message
|
|
138
138
|
lists the selectors that were.
|
|
139
139
|
ValueError: That selector fetched a message holding several fields,
|
|
140
|
-
as RAP publishes wind components, and the record
|
|
140
|
+
as RAP publishes wind components, and the record was written
|
|
141
|
+
before each field's selector was recorded, so it cannot say
|
|
141
142
|
which field it named; the message lists their variables.
|
|
142
143
|
"""
|
|
143
144
|
from usdata._grib import variable_names
|
|
@@ -158,7 +159,7 @@ class Grib2Summary(BaseModel):
|
|
|
158
159
|
if message.selector == selector
|
|
159
160
|
]
|
|
160
161
|
if len(matched) > 1:
|
|
161
|
-
#
|
|
162
|
+
# An older record keeps one selector per range, and one range can hold several fields.
|
|
162
163
|
raise ValueError(
|
|
163
164
|
f"selector {selector!r} fetched one message holding {len(matched)} fields "
|
|
164
165
|
f"({', '.join(matched)}); pick the variable by name"
|
|
@@ -333,24 +334,26 @@ def _paired(messages: list[GribMessage], record: Provenance) -> list[GribMessage
|
|
|
333
334
|
"""Each message as the fetch that took it described it, where provenance says.
|
|
334
335
|
|
|
335
336
|
The file itself carries neither number nor selector: a partial fetch recorded
|
|
336
|
-
one message number and one selector per
|
|
337
|
-
file's messages the same way, so the fields of a message that
|
|
338
|
-
all pair with its number
|
|
339
|
-
leaves both unset.
|
|
337
|
+
one message number per range and one selector per field, and ``file_index``
|
|
338
|
+
counts the file's messages the same way, so the fields of a message that
|
|
339
|
+
holds several all pair with its number and each with its own selector. A
|
|
340
|
+
whole file, or a record that does not pair, leaves both unset.
|
|
340
341
|
"""
|
|
341
342
|
numbers = record.object_messages
|
|
342
343
|
if not numbers or any(message.file_index >= len(numbers) for message in messages):
|
|
343
344
|
return messages
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
345
|
+
paired = len(record.selectors) == len(numbers)
|
|
346
|
+
fields_seen: dict[int, int] = {}
|
|
347
|
+
result = []
|
|
348
|
+
for message in messages:
|
|
349
|
+
field = fields_seen[message.file_index] = fields_seen.get(message.file_index, -1) + 1
|
|
350
|
+
selector = record.field_selector(message.file_index, field) if paired else None
|
|
351
|
+
result.append(
|
|
352
|
+
message.model_copy(
|
|
353
|
+
update={"object_index": numbers[message.file_index], "selector": selector}
|
|
354
|
+
)
|
|
351
355
|
)
|
|
352
|
-
|
|
353
|
-
]
|
|
356
|
+
return result
|
|
354
357
|
|
|
355
358
|
|
|
356
359
|
def _csv_summary(path: Path, *, units_row: bool = False) -> CsvSummary:
|