usdata 0.32.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.32.0 → usdata-0.33.0}/PKG-INFO +2 -2
- {usdata-0.32.0 → usdata-0.33.0}/README.md +1 -1
- {usdata-0.32.0 → usdata-0.33.0}/pyproject.toml +1 -1
- {usdata-0.32.0 → usdata-0.33.0}/pyproject.toml.orig +1 -1
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/__init__.py +3 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_grib.py +7 -2
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/app.py +92 -25
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/data/registry.yaml +127 -7
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/inspect.py +18 -15
- usdata-0.33.0/src/usdata/messages.py +60 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/models.py +67 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/provenance.py +1 -0
- {usdata-0.32.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.32.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.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/hrrr.py +18 -1
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/testing.py +8 -4
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/__main__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_aqs.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_census.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_fetch.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_files.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_hurdat2.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_netcdf.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_progress.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/_radar.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cache.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cache_ops.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cite.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/cache.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/cite.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/doctor.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/inspect.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/cli/progress.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/data/nexrad_sites.csv +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/data/places.csv +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/data/places.sources.json +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/doctor.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/manifest.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/mirror.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/protocols/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/protocols/erddap.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/protocols/http.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/protocols/listing.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/protocols/s3.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/census/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/census/acs.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/credentials.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/epa/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/epa/aqs.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/fema/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/fema/declarations.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/http.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/coops.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/gfs.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/glm.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/goes.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/gsom.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/gsoy.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/lcd.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/mrms.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/nbm.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/nexrad.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/normals.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/rap.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/sites.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/spc.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/noaa/storm_events.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/params.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/usgs/__init__.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/usgs/daily.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/pull.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/py.typed +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/query.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/readers.py +0 -0
- {usdata-0.32.0 → usdata-0.33.0}/src/usdata/registry.py +0 -0
- {usdata-0.32.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,
|
|
@@ -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",
|
|
@@ -55,6 +57,7 @@ __all__ = [
|
|
|
55
57
|
"get",
|
|
56
58
|
"inspect_asset",
|
|
57
59
|
"inspect_path",
|
|
60
|
+
"list_messages",
|
|
58
61
|
"pull",
|
|
59
62
|
"search",
|
|
60
63
|
"select_by_time",
|
|
@@ -96,7 +96,9 @@ def import_eccodes() -> Any:
|
|
|
96
96
|
imported later in the same process, both copies free the same memory as the
|
|
97
97
|
interpreter exits, which aborts it with "double free or corruption" and exit
|
|
98
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.
|
|
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.
|
|
100
102
|
|
|
101
103
|
Raises:
|
|
102
104
|
ModuleNotFoundError: eccodes is not installed.
|
|
@@ -481,8 +483,11 @@ def open_grib2(
|
|
|
481
483
|
needs_select = select is None and not fetched.provenance.is_partial
|
|
482
484
|
object_messages = fetched.provenance.object_messages
|
|
483
485
|
selectors = fetched.provenance.selectors if object_messages else []
|
|
486
|
+
fields_seen: dict[int, int] = {}
|
|
484
487
|
for index, h in _messages(eccodes, fetched.path):
|
|
485
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
|
|
486
491
|
if count > 1 and needs_select:
|
|
487
492
|
continue
|
|
488
493
|
hits = {key: _matched(eccodes, h, key, wanted) for key, wanted in options.items()}
|
|
@@ -536,7 +541,7 @@ def open_grib2(
|
|
|
536
541
|
"step": attrs.get("step"),
|
|
537
542
|
}
|
|
538
543
|
if selectors:
|
|
539
|
-
message["selector"] =
|
|
544
|
+
message["selector"] = fetched.provenance.field_selector(index, field)
|
|
540
545
|
short = base_name(
|
|
541
546
|
message["shortName"], [attrs.get(key) for key in PARAMETER_KEYS], fetched.asset.id
|
|
542
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,6 +20,7 @@ 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
|
|
@@ -67,6 +68,33 @@ _QUERY_FLAGS = {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
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
|
+
|
|
70
98
|
def _size_phrase(known_bytes: int, unknown: int, *, sources: Iterable[str] = ()) -> str:
|
|
71
99
|
"""How many bytes a dry run would move, said honestly when some size is missing.
|
|
72
100
|
|
|
@@ -357,26 +385,7 @@ def fetch(
|
|
|
357
385
|
] = False,
|
|
358
386
|
) -> None:
|
|
359
387
|
"""Resolve a query against one dataset and download the matching assets."""
|
|
360
|
-
params
|
|
361
|
-
for item in param or []:
|
|
362
|
-
key, sep, value = item.partition("=")
|
|
363
|
-
if not sep:
|
|
364
|
-
typer.secho(f"--param expects key=value, got {item!r}", err=True, fg="red")
|
|
365
|
-
raise typer.Exit(code=2)
|
|
366
|
-
if key in _QUERY_FLAGS:
|
|
367
|
-
flag = _QUERY_FLAGS[key]
|
|
368
|
-
hint = (
|
|
369
|
-
f"use {flag} instead of --param"
|
|
370
|
-
if flag is not None
|
|
371
|
-
else "fetch does not support this option"
|
|
372
|
-
)
|
|
373
|
-
typer.secho(
|
|
374
|
-
f"{key} is a reserved query option; {hint}",
|
|
375
|
-
err=True,
|
|
376
|
-
fg="red",
|
|
377
|
-
)
|
|
378
|
-
raise typer.Exit(code=2)
|
|
379
|
-
params[key] = value
|
|
388
|
+
params = _parse_params(param, "fetch", _QUERY_FLAGS)
|
|
380
389
|
box = None
|
|
381
390
|
if bbox:
|
|
382
391
|
try:
|
|
@@ -437,6 +446,64 @@ def fetch(
|
|
|
437
446
|
typer.echo(f"{f.path}\t{tag}\t{f.provenance.size} bytes")
|
|
438
447
|
|
|
439
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
|
+
|
|
440
507
|
def _print_plan(plan: Plan, *, as_json: bool, quiet: bool) -> None:
|
|
441
508
|
"""Report what a pull would fetch: assets, a subtotal per source, a total on stderr."""
|
|
442
509
|
if as_json:
|
|
@@ -574,7 +641,10 @@ def verify(
|
|
|
574
641
|
bool, typer.Option("--json", help="With --listing, emit the comparison as JSON on stdout.")
|
|
575
642
|
] = False,
|
|
576
643
|
) -> None:
|
|
577
|
-
"""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
|
+
"""
|
|
578
648
|
if as_json and not listing:
|
|
579
649
|
typer.secho("--json applies to --listing only", err=True, fg="red")
|
|
580
650
|
raise typer.Exit(code=2)
|
|
@@ -605,9 +675,6 @@ def _verify_listing(manifest: Path, *, as_json: bool) -> None:
|
|
|
605
675
|
"""Compare the lockfile with today's listings; exit 1 when a re-resolve would differ."""
|
|
606
676
|
try:
|
|
607
677
|
comparison = compare_manifest_listing(manifest)
|
|
608
|
-
except EmptySource as e:
|
|
609
|
-
typer.secho(str(e), err=True, fg="yellow")
|
|
610
|
-
raise typer.Exit(code=1) from None
|
|
611
678
|
except (DatasetNotFound, UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
|
|
612
679
|
typer.secho(str(e), err=True, fg="red")
|
|
613
680
|
raise typer.Exit(code=2) from None
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""The GRIB2 messages a query's files hold, read from their index sidecars.
|
|
2
|
+
|
|
3
|
+
A dataset declaring ``partial_fetch`` takes a ``messages`` parameter spelled in
|
|
4
|
+
its objects' index vocabulary, and this is where that vocabulary is listed.
|
|
5
|
+
Listing resolves the query as a fetch would and reads each object's index,
|
|
6
|
+
which is a few kilobytes; nothing else is downloaded, and nothing is cached or
|
|
7
|
+
recorded. See ADR 0046.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from usdata.models import Asset, Dataset, IndexEntry, Query
|
|
15
|
+
from usdata.providers import QueryError, load_adapter
|
|
16
|
+
|
|
17
|
+
PARTIAL_PARAM = "messages"
|
|
18
|
+
"""The parameter a ``partial_fetch`` dataset selects messages through."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageListing(BaseModel):
|
|
22
|
+
"""One file a query resolved to, and every field its index describes."""
|
|
23
|
+
|
|
24
|
+
asset: Asset = Field(description="The whole-file asset, as a fetch without messages lists it")
|
|
25
|
+
messages: list[IndexEntry] = Field(description="Every field in the object's index, in order")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_messages(dataset: Dataset, query: Query) -> list[MessageListing]:
|
|
29
|
+
"""List each file a query resolves to with the fields its index describes.
|
|
30
|
+
|
|
31
|
+
Each entry's ``selector`` is a ``messages`` value that names exactly that
|
|
32
|
+
field. A selector may stop after its level text, as ``CAPE:surface``, to
|
|
33
|
+
name that field at whatever step each file holds.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
dataset: A dataset declaring ``partial_fetch``.
|
|
37
|
+
query: The query a fetch would take, without ``messages``.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
One listing per file, in the order a fetch would list them.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
QueryError: The dataset selects no messages, the query names
|
|
44
|
+
``messages``, or a file publishes no readable index.
|
|
45
|
+
"""
|
|
46
|
+
if not dataset.capabilities.partial_fetch:
|
|
47
|
+
raise QueryError(
|
|
48
|
+
f"{dataset.id} fetches whole files, so it has no messages to list; "
|
|
49
|
+
"only datasets that declare partial_fetch do"
|
|
50
|
+
)
|
|
51
|
+
if PARTIAL_PARAM in query.params:
|
|
52
|
+
raise QueryError(
|
|
53
|
+
f"drop {PARTIAL_PARAM}: the listing shows every message each file holds, "
|
|
54
|
+
"whatever a selection would keep"
|
|
55
|
+
)
|
|
56
|
+
with load_adapter(dataset) as adapter:
|
|
57
|
+
return [
|
|
58
|
+
MessageListing(asset=asset, messages=adapter.list_messages(asset))
|
|
59
|
+
for asset in adapter.list_assets(query)
|
|
60
|
+
]
|
|
@@ -560,6 +560,42 @@ class ByteRange(BaseModel):
|
|
|
560
560
|
return f"bytes={self.start}-{self.end}"
|
|
561
561
|
|
|
562
562
|
|
|
563
|
+
class IndexEntry(BaseModel):
|
|
564
|
+
"""One field of one GRIB2 object, as the object's wgrib2 ``.idx`` sidecar describes it.
|
|
565
|
+
|
|
566
|
+
``number`` is the GRIB2 message the field belongs to and ``field`` its
|
|
567
|
+
position inside a message that holds several (``12.2``), or ``None`` for a
|
|
568
|
+
message holding one. Fields of one message share an offset and a length.
|
|
569
|
+
``selector`` is the ``messages`` text that names exactly this field.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
number: int = Field(ge=1, description="One-based GRIB2 message number in the object")
|
|
573
|
+
offset: int = Field(ge=0, description="Byte offset of the message in the object")
|
|
574
|
+
length: int = Field(gt=0, description="Bytes the message occupies")
|
|
575
|
+
short_name: str = Field(description="wgrib2 short name, such as CAPE")
|
|
576
|
+
level: str = Field(description="Level text, such as '2 m above ground'")
|
|
577
|
+
step: str = Field(description="Step text, such as 'anl' or '1 hour fcst'")
|
|
578
|
+
field: int | None = Field(
|
|
579
|
+
default=None, ge=1, description="Position inside a message holding several fields"
|
|
580
|
+
)
|
|
581
|
+
extra: str = Field(
|
|
582
|
+
default="", description="Further text some products add, such as 'ens std dev'"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def byte_range(self) -> ByteRange:
|
|
587
|
+
"""The inclusive byte interval this field's message occupies in the object."""
|
|
588
|
+
return ByteRange(start=self.offset, end=self.offset + self.length - 1)
|
|
589
|
+
|
|
590
|
+
@property
|
|
591
|
+
def selector(self) -> str:
|
|
592
|
+
"""The ``messages`` selector naming exactly this field, short name through the last text."""
|
|
593
|
+
parts = [self.short_name, self.level, self.step]
|
|
594
|
+
if self.extra:
|
|
595
|
+
parts.append(self.extra)
|
|
596
|
+
return ":".join(parts)
|
|
597
|
+
|
|
598
|
+
|
|
563
599
|
PARTIAL_FRAGMENT = "messages"
|
|
564
600
|
"""Href fragment key naming the GRIB2 messages a partial asset selects."""
|
|
565
601
|
|
|
@@ -583,6 +619,13 @@ class PartialFetch(BaseModel):
|
|
|
583
619
|
default_factory=list,
|
|
584
620
|
description="Index selector naming each of those messages, in the same order",
|
|
585
621
|
)
|
|
622
|
+
field_selectors: list[list[str]] = Field(
|
|
623
|
+
default_factory=list,
|
|
624
|
+
description=(
|
|
625
|
+
"Index selector naming each field of each of those messages, in the same order; "
|
|
626
|
+
"a message holding several fields, as RAP packs wind components, has one per field"
|
|
627
|
+
),
|
|
628
|
+
)
|
|
586
629
|
|
|
587
630
|
@model_validator(mode="after")
|
|
588
631
|
def _aligned(self) -> PartialFetch:
|
|
@@ -590,6 +633,10 @@ class PartialFetch(BaseModel):
|
|
|
590
633
|
raise ValueError("a partial fetch needs one byte range per selected message")
|
|
591
634
|
if self.selectors and len(self.selectors) != len(self.messages):
|
|
592
635
|
raise ValueError("a partial fetch needs one selector per selected message")
|
|
636
|
+
if self.field_selectors and (
|
|
637
|
+
len(self.field_selectors) != len(self.messages) or not all(self.field_selectors)
|
|
638
|
+
):
|
|
639
|
+
raise ValueError("a partial fetch needs one or more field selectors per message")
|
|
593
640
|
if sorted(set(self.messages)) != self.messages:
|
|
594
641
|
raise ValueError("selected messages must be ascending and distinct")
|
|
595
642
|
return self
|
|
@@ -655,6 +702,13 @@ class Provenance(BaseModel):
|
|
|
655
702
|
default_factory=list,
|
|
656
703
|
description="Index selector each of those ranges was fetched for, in the same order",
|
|
657
704
|
)
|
|
705
|
+
field_selectors: list[list[str]] = Field(
|
|
706
|
+
default_factory=list,
|
|
707
|
+
description=(
|
|
708
|
+
"Index selector naming each field each of those ranges holds, in the same order; "
|
|
709
|
+
"empty in a record written before fields were recorded"
|
|
710
|
+
),
|
|
711
|
+
)
|
|
658
712
|
object_size: int | None = Field(
|
|
659
713
|
default=None, ge=0, description="Size of the whole object those ranges came from"
|
|
660
714
|
)
|
|
@@ -680,6 +734,19 @@ class Provenance(BaseModel):
|
|
|
680
734
|
"""Whether this record describes selected byte ranges rather than a whole object."""
|
|
681
735
|
return any(entry.startswith(PARTIAL_TRANSFORMATION) for entry in self.transformations)
|
|
682
736
|
|
|
737
|
+
def field_selector(self, message: int, field: int) -> str | None:
|
|
738
|
+
"""The selector one field on disk was fetched for, or ``None`` where none was recorded.
|
|
739
|
+
|
|
740
|
+
``message`` counts the local file's messages from zero and ``field`` the
|
|
741
|
+
fields inside that message from zero, as a reader meets them. A record
|
|
742
|
+
that names no fields, as one written before they were recorded does,
|
|
743
|
+
gives every field of a message that message's one selector.
|
|
744
|
+
"""
|
|
745
|
+
fields = self.field_selectors[message] if message < len(self.field_selectors) else []
|
|
746
|
+
if field < len(fields):
|
|
747
|
+
return fields[field]
|
|
748
|
+
return self.selectors[message] if message < len(self.selectors) else None
|
|
749
|
+
|
|
683
750
|
@property
|
|
684
751
|
def object_messages(self) -> list[int]:
|
|
685
752
|
"""The source object's own message numbers, in the order the local file holds them.
|
|
@@ -50,6 +50,7 @@ def record(
|
|
|
50
50
|
index_checksum=None if partial is None else partial.index_checksum,
|
|
51
51
|
ranges=[] if partial is None else list(partial.ranges),
|
|
52
52
|
selectors=[] if partial is None else list(partial.selectors),
|
|
53
|
+
field_selectors=[] if partial is None else [list(f) for f in partial.field_selectors],
|
|
53
54
|
object_size=None if partial is None else partial.object_size,
|
|
54
55
|
object_etag=None if partial is None else partial.object_etag,
|
|
55
56
|
credentials=[] if dataset.credentials is None else list(dataset.credentials.variables),
|