usdata 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {usdata-0.3.0 → usdata-0.5.0}/PKG-INFO +38 -12
  2. {usdata-0.3.0 → usdata-0.5.0}/README.md +37 -11
  3. {usdata-0.3.0 → usdata-0.5.0}/pyproject.toml +1 -1
  4. {usdata-0.3.0 → usdata-0.5.0}/pyproject.toml.orig +1 -1
  5. usdata-0.5.0/src/usdata/_files.py +29 -0
  6. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/cache.py +14 -2
  7. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/cli/app.py +26 -12
  8. usdata-0.5.0/src/usdata/data/places.csv +3292 -0
  9. usdata-0.5.0/src/usdata/data/places.sources.json +20 -0
  10. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/data/registry.yaml +23 -19
  11. usdata-0.5.0/src/usdata/fetch.py +79 -0
  12. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/manifest.py +31 -3
  13. usdata-0.5.0/src/usdata/protocols/erddap.py +115 -0
  14. usdata-0.5.0/src/usdata/protocols/http.py +96 -0
  15. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/protocols/s3.py +1 -1
  16. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/provenance.py +2 -1
  17. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/base.py +17 -0
  18. usdata-0.5.0/src/usdata/providers/noaa/coastwatch.py +148 -0
  19. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/noaa/ghcnd.py +19 -3
  20. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/noaa/nexrad.py +35 -5
  21. usdata-0.5.0/src/usdata/providers/usgs/__init__.py +1 -0
  22. usdata-0.5.0/src/usdata/providers/usgs/daily.py +145 -0
  23. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/pull.py +45 -22
  24. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/query.py +50 -16
  25. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/registry.py +2 -1
  26. usdata-0.3.0/src/usdata/data/places.yaml +0 -10
  27. usdata-0.3.0/src/usdata/fetch.py +0 -54
  28. usdata-0.3.0/src/usdata/protocols/http.py +0 -43
  29. usdata-0.3.0/src/usdata/providers/noaa/coastwatch.py +0 -20
  30. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/__init__.py +0 -0
  31. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/cli/__init__.py +0 -0
  32. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/data/nexrad_sites.csv +0 -0
  33. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/models.py +0 -0
  34. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/protocols/__init__.py +0 -0
  35. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/__init__.py +0 -0
  36. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/noaa/__init__.py +0 -0
  37. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/providers/noaa/sites.py +0 -0
  38. {usdata-0.3.0 → usdata-0.5.0}/src/usdata/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.3.0
3
+ Version: 0.5.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,nasa,open-data,scientific-data,provenance
6
6
  Author: Jake Van Slyke
@@ -24,24 +24,25 @@ Description-Content-Type: text/markdown
24
24
  Unified Python SDK and CLI for discovering, fetching, and tracking the
25
25
  provenance of U.S. public scientific data (NOAA, USGS, NASA, and more).
26
26
 
27
- > Status: pre-alpha. `noaa:ghcn-daily` and `noaa:nexrad-level2` fetch real
28
- > data with provenance; other registry entries are stubs.
27
+ > Status: pre-alpha. v0.5 supports GHCN-Daily, NEXRAD Level II, USGS daily
28
+ > values, and CoastWatch SST subsets with provenance, plus Census state/county
29
+ > lookup. Other datasets are planned.
29
30
  > See [docs/roadmap.md](docs/roadmap.md).
30
31
 
31
32
  ## Providers
32
33
 
33
34
  <!-- registry:start -->
34
- | Provider | Available | Stub | Planned | Next up (0.3) | Datasets |
35
+ | Provider | Available | Stub | Planned | Next up (0.6) | Datasets |
35
36
  |---|---:|---:|---:|---|---|
36
- | [NOAA](docs/providers/noaa.md) | 2 | 1 | 26 | `coastwatch-sst` | `ghcn-daily`, `nexrad-level2`, _coastwatch-sst_, +26 planned |
37
+ | [NOAA](docs/providers/noaa.md) | 3 | 0 | 26 | `gfs`, `hrrr`, `oisst`, `etopo` | `ghcn-daily`, `nexrad-level2`, `coastwatch-sst`, +26 planned |
38
+ | [USGS](docs/providers/usgs.md) | 1 | 0 | 2 | — | `water-daily`, +2 planned |
37
39
  | [Census Bureau](docs/providers/census.md) | 0 | 0 | 1 | — | +1 planned |
38
40
  | [EPA](docs/providers/epa.md) | 0 | 0 | 1 | — | +1 planned |
39
41
  | [FEMA](docs/providers/fema.md) | 0 | 0 | 1 | — | +1 planned |
40
- | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | — | +1 planned |
42
+ | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | `gpm-imerg` | +1 planned |
41
43
  | [USDA](docs/providers/usda.md) | 0 | 0 | 1 | — | +1 planned |
42
- | [USGS](docs/providers/usgs.md) | 0 | 0 | 3 | — | +3 planned |
43
44
 
44
- Available datasets are in `code`, stubs in _italics_; planned ones are counted. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
45
+ Available datasets are in `code`, stubs in _italics_; planned ones are counted. Available means implemented in this source checkout; consult the [releases](https://github.com/jakeryderv/usdata/releases) for published support. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
45
46
  <!-- registry:end -->
46
47
 
47
48
  ## Install
@@ -74,6 +75,7 @@ for item in fetch(ds, query):
74
75
 
75
76
  ```sh
76
77
  usdata search "tornado radar" --state OK
78
+ usdata search precipitation --location "Cleveland County, OK"
77
79
  usdata info noaa:ghcn-daily
78
80
  usdata fetch noaa:ghcn-daily --lat 35.39 --lon -97.60 --radius-km 15 \
79
81
  --start 2024-05-06 --end 2024-05-07 --vars PRCP,TMAX
@@ -81,6 +83,10 @@ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-01-01 --end 20
81
83
  usdata fetch noaa:nexrad-level2 --lat 35.47 --lon -97.52 \
82
84
  --start 2024-05-06T20:00 --end 2024-05-06T23:00 # nearest radar (KTLX)
83
85
  usdata fetch noaa:nexrad-level2 -p site=KTLX --start 2024-05-06T20:00 --end 2024-05-06T20:30 --dry-run
86
+ usdata fetch usgs:water-daily -p sites=07164500 --vars 00060 \
87
+ --start 2024-05-06 --end 2024-05-07
88
+ usdata fetch noaa:coastwatch-sst --bbox=-80.08,30.02,-80.02,30.08 \
89
+ --start 2024-05-06T12:00Z --end 2024-05-06T12:00Z # four grid cells
84
90
  usdata pull dataset.yaml # resolve, fetch, write dataset.lock.json
85
91
  usdata verify dataset.yaml # exit 1 if any cached input drifted
86
92
  ```
@@ -89,12 +95,27 @@ Fetched files land in `~/.cache/usdata/<provider>/<dataset>/` (override with
89
95
  `USDATA_CACHE_DIR` or `--cache-dir`), each with a `.provenance.json` sidecar
90
96
  recording source URL, retrieval time, checksum, size, and license.
91
97
 
98
+ Locations accept state names/postal codes, county/state names, and quoted FIPS
99
+ codes. These select bounding rectangles; see [place lookup](docs/reference/places.md)
100
+ for coverage, ambiguity, and antimeridian limits. CoastWatch CSV includes a
101
+ second header row containing units; see its [access notes](docs/providers/noaa.md#coastwatch-sst).
102
+
103
+ Manifest and source fields are validated strictly; unknown fields are errors.
104
+ Provider-specific options belong under `params`.
105
+
92
106
  A manifest declares every input a project needs. `pull` resolves each source,
93
107
  fetches it, and writes `dataset.lock.json` pinning every asset with its checksum
94
108
  and provenance. A second `pull` restores exactly what the lockfile pins without
95
109
  re-querying upstream, so the inputs stay reproducible even if the source
96
- changes. `verify` re-hashes the cached files against the lockfile. Editing the
97
- manifest after locking requires `pull --force` to re-resolve.
110
+ changes. `verify` checks the manifest checksum and re-hashes cached files
111
+ against the lockfile. Editing the manifest after locking requires `pull --force`
112
+ to re-resolve. A required source matching no assets fails the pull; set
113
+ `allow_empty: true` on a source only when an empty result is intentional.
114
+
115
+ Checksums detect upstream changes; they cannot recover historical bytes that
116
+ are no longer available. Preserve the cache for long-lived reproducibility.
117
+ See the [manifest reference](docs/reference/manifests.md) and the small
118
+ [NOAA/USGS example](examples/weather-and-streamflow/README.md).
98
119
 
99
120
  ```yaml
100
121
  name: tornado-environment
@@ -117,11 +138,16 @@ Requires [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/).
117
138
  git clone https://github.com/jakeryderv/usdata && cd usdata
118
139
  just setup # install toolchain and dependencies
119
140
  just test # unit tests
120
- just check # format, lint, typecheck, tests (what CI runs)
141
+ just check # format, lint, typecheck, offline tests, generated docs
142
+ just build # build wheel and sdist
143
+ just smoke # install and exercise the built wheel outside the checkout
121
144
  just run search radar
122
145
  ```
123
146
 
124
- Integration tests that hit live services run with `just test-integration`.
147
+ Unit tests mechanically block network connections. Integration tests that hit
148
+ live services run with `just test-integration`. CI checks Python 3.11 and 3.14 on
149
+ Linux and smoke-tests the installed wheel on Linux, macOS, and Windows. The
150
+ full unit and live-service suites currently run on Linux.
125
151
 
126
152
  Releases: `just release minor` opens a version-bump PR; merging it publishes
127
153
  to PyPI and creates the tag and GitHub release. See
@@ -3,24 +3,25 @@
3
3
  Unified Python SDK and CLI for discovering, fetching, and tracking the
4
4
  provenance of U.S. public scientific data (NOAA, USGS, NASA, and more).
5
5
 
6
- > Status: pre-alpha. `noaa:ghcn-daily` and `noaa:nexrad-level2` fetch real
7
- > data with provenance; other registry entries are stubs.
6
+ > Status: pre-alpha. v0.5 supports GHCN-Daily, NEXRAD Level II, USGS daily
7
+ > values, and CoastWatch SST subsets with provenance, plus Census state/county
8
+ > lookup. Other datasets are planned.
8
9
  > See [docs/roadmap.md](docs/roadmap.md).
9
10
 
10
11
  ## Providers
11
12
 
12
13
  <!-- registry:start -->
13
- | Provider | Available | Stub | Planned | Next up (0.3) | Datasets |
14
+ | Provider | Available | Stub | Planned | Next up (0.6) | Datasets |
14
15
  |---|---:|---:|---:|---|---|
15
- | [NOAA](docs/providers/noaa.md) | 2 | 1 | 26 | `coastwatch-sst` | `ghcn-daily`, `nexrad-level2`, _coastwatch-sst_, +26 planned |
16
+ | [NOAA](docs/providers/noaa.md) | 3 | 0 | 26 | `gfs`, `hrrr`, `oisst`, `etopo` | `ghcn-daily`, `nexrad-level2`, `coastwatch-sst`, +26 planned |
17
+ | [USGS](docs/providers/usgs.md) | 1 | 0 | 2 | — | `water-daily`, +2 planned |
16
18
  | [Census Bureau](docs/providers/census.md) | 0 | 0 | 1 | — | +1 planned |
17
19
  | [EPA](docs/providers/epa.md) | 0 | 0 | 1 | — | +1 planned |
18
20
  | [FEMA](docs/providers/fema.md) | 0 | 0 | 1 | — | +1 planned |
19
- | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | — | +1 planned |
21
+ | [NASA](docs/providers/nasa.md) | 0 | 0 | 1 | `gpm-imerg` | +1 planned |
20
22
  | [USDA](docs/providers/usda.md) | 0 | 0 | 1 | — | +1 planned |
21
- | [USGS](docs/providers/usgs.md) | 0 | 0 | 3 | — | +3 planned |
22
23
 
23
- Available datasets are in `code`, stubs in _italics_; planned ones are counted. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
24
+ Available datasets are in `code`, stubs in _italics_; planned ones are counted. Available means implemented in this source checkout; consult the [releases](https://github.com/jakeryderv/usdata/releases) for published support. Each provider page has access notes and full dataset details; [docs/roadmap.md](docs/roadmap.md) lists datasets by target version.
24
25
  <!-- registry:end -->
25
26
 
26
27
  ## Install
@@ -53,6 +54,7 @@ for item in fetch(ds, query):
53
54
 
54
55
  ```sh
55
56
  usdata search "tornado radar" --state OK
57
+ usdata search precipitation --location "Cleveland County, OK"
56
58
  usdata info noaa:ghcn-daily
57
59
  usdata fetch noaa:ghcn-daily --lat 35.39 --lon -97.60 --radius-km 15 \
58
60
  --start 2024-05-06 --end 2024-05-07 --vars PRCP,TMAX
@@ -60,6 +62,10 @@ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-01-01 --end 20
60
62
  usdata fetch noaa:nexrad-level2 --lat 35.47 --lon -97.52 \
61
63
  --start 2024-05-06T20:00 --end 2024-05-06T23:00 # nearest radar (KTLX)
62
64
  usdata fetch noaa:nexrad-level2 -p site=KTLX --start 2024-05-06T20:00 --end 2024-05-06T20:30 --dry-run
65
+ usdata fetch usgs:water-daily -p sites=07164500 --vars 00060 \
66
+ --start 2024-05-06 --end 2024-05-07
67
+ usdata fetch noaa:coastwatch-sst --bbox=-80.08,30.02,-80.02,30.08 \
68
+ --start 2024-05-06T12:00Z --end 2024-05-06T12:00Z # four grid cells
63
69
  usdata pull dataset.yaml # resolve, fetch, write dataset.lock.json
64
70
  usdata verify dataset.yaml # exit 1 if any cached input drifted
65
71
  ```
@@ -68,12 +74,27 @@ Fetched files land in `~/.cache/usdata/<provider>/<dataset>/` (override with
68
74
  `USDATA_CACHE_DIR` or `--cache-dir`), each with a `.provenance.json` sidecar
69
75
  recording source URL, retrieval time, checksum, size, and license.
70
76
 
77
+ Locations accept state names/postal codes, county/state names, and quoted FIPS
78
+ codes. These select bounding rectangles; see [place lookup](docs/reference/places.md)
79
+ for coverage, ambiguity, and antimeridian limits. CoastWatch CSV includes a
80
+ second header row containing units; see its [access notes](docs/providers/noaa.md#coastwatch-sst).
81
+
82
+ Manifest and source fields are validated strictly; unknown fields are errors.
83
+ Provider-specific options belong under `params`.
84
+
71
85
  A manifest declares every input a project needs. `pull` resolves each source,
72
86
  fetches it, and writes `dataset.lock.json` pinning every asset with its checksum
73
87
  and provenance. A second `pull` restores exactly what the lockfile pins without
74
88
  re-querying upstream, so the inputs stay reproducible even if the source
75
- changes. `verify` re-hashes the cached files against the lockfile. Editing the
76
- manifest after locking requires `pull --force` to re-resolve.
89
+ changes. `verify` checks the manifest checksum and re-hashes cached files
90
+ against the lockfile. Editing the manifest after locking requires `pull --force`
91
+ to re-resolve. A required source matching no assets fails the pull; set
92
+ `allow_empty: true` on a source only when an empty result is intentional.
93
+
94
+ Checksums detect upstream changes; they cannot recover historical bytes that
95
+ are no longer available. Preserve the cache for long-lived reproducibility.
96
+ See the [manifest reference](docs/reference/manifests.md) and the small
97
+ [NOAA/USGS example](examples/weather-and-streamflow/README.md).
77
98
 
78
99
  ```yaml
79
100
  name: tornado-environment
@@ -96,11 +117,16 @@ Requires [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/).
96
117
  git clone https://github.com/jakeryderv/usdata && cd usdata
97
118
  just setup # install toolchain and dependencies
98
119
  just test # unit tests
99
- just check # format, lint, typecheck, tests (what CI runs)
120
+ just check # format, lint, typecheck, offline tests, generated docs
121
+ just build # build wheel and sdist
122
+ just smoke # install and exercise the built wheel outside the checkout
100
123
  just run search radar
101
124
  ```
102
125
 
103
- Integration tests that hit live services run with `just test-integration`.
126
+ Unit tests mechanically block network connections. Integration tests that hit
127
+ live services run with `just test-integration`. CI checks Python 3.11 and 3.14 on
128
+ Linux and smoke-tests the installed wheel on Linux, macOS, and Windows. The
129
+ full unit and live-service suites currently run on Linux.
104
130
 
105
131
  Releases: `just release minor` opens a version-bump PR; merging it publishes
106
132
  to PyPI and creates the tag and GitHub release. See
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -0,0 +1,29 @@
1
+ """Atomic replacement helpers for downloads and JSON records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from tempfile import NamedTemporaryFile
9
+
10
+
11
+ @contextmanager
12
+ def staged_path(dest: Path) -> Iterator[Path]:
13
+ """Replace dest only after successful work in a unique sibling temporary file."""
14
+ dest.parent.mkdir(parents=True, exist_ok=True)
15
+ with NamedTemporaryFile(
16
+ prefix=f".{dest.name}.", suffix=".part", dir=dest.parent, delete=False
17
+ ) as f:
18
+ tmp = Path(f.name)
19
+ try:
20
+ yield tmp
21
+ tmp.replace(dest)
22
+ finally:
23
+ tmp.unlink(missing_ok=True)
24
+
25
+
26
+ def atomic_write_text(dest: Path, text: str) -> None:
27
+ """Write UTF-8 text without exposing an incomplete record to readers."""
28
+ with staged_path(dest) as tmp:
29
+ tmp.write_text(text, encoding="utf-8")
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import os
7
+ import re
7
8
  from pathlib import Path
8
9
 
9
10
  from usdata.models import Asset
@@ -22,9 +23,20 @@ def cache_dir() -> Path:
22
23
 
23
24
  def asset_path(asset: Asset, root: Path | None = None) -> Path:
24
25
  """Where an asset lives in the cache: <root>/<provider>/<dataset>/<asset id>."""
25
- provider, name = asset.dataset_id.split(":", 1)
26
+ provider, sep, name = asset.dataset_id.partition(":")
27
+ if not sep or any(
28
+ not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", part) for part in (provider, name)
29
+ ):
30
+ raise ValueError(f"unsafe dataset id: {asset.dataset_id!r}")
26
31
  safe_id = asset.id.replace("/", "_")
27
- return (root or cache_dir()) / provider / name / safe_id
32
+ if not safe_id or safe_id in {".", ".."} or "\\" in safe_id or "\x00" in safe_id:
33
+ raise ValueError(f"unsafe asset id: {asset.id!r}")
34
+ base = (root or cache_dir()).expanduser().resolve()
35
+ path = base / provider / name / safe_id
36
+ for candidate in (path, path.with_name(path.name + ".provenance.json")):
37
+ if not candidate.resolve().is_relative_to(base):
38
+ raise ValueError(f"cache path escapes root: {candidate}")
39
+ return path
28
40
 
29
41
 
30
42
  def sha256_file(path: Path, chunk_size: int = 1 << 20) -> str:
@@ -9,11 +9,12 @@ import httpx
9
9
  import typer
10
10
 
11
11
  from usdata import __version__, build_query, default_registry
12
- from usdata.fetch import ChecksumMismatch, fetch_asset
12
+ from usdata.fetch import ChecksumMismatch
13
+ from usdata.fetch import fetch as fetch_query
13
14
  from usdata.manifest import lockfile_path
14
15
  from usdata.providers import load_adapter
15
16
  from usdata.providers.base import NotImplementedProvider
16
- from usdata.pull import ManifestChanged, UnknownDatasets
17
+ from usdata.pull import EmptySource, ManifestChanged, UnknownDatasets
17
18
  from usdata.pull import pull as pull_manifest
18
19
  from usdata.pull import verify as verify_manifest
19
20
  from usdata.query import UnknownPlace
@@ -49,7 +50,10 @@ def search(
49
50
  provider: Annotated[
50
51
  str | None, typer.Option(help="Restrict to one provider, e.g. noaa.")
51
52
  ] = None,
52
- state: Annotated[str | None, typer.Option(help="State name or postal code.")] = None,
53
+ state: Annotated[
54
+ str | None,
55
+ typer.Option("--location", "--state", help="State, 'County, ST', or quoted FIPS code."),
56
+ ] = None,
53
57
  start: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
54
58
  end: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
55
59
  planned: Annotated[
@@ -59,8 +63,8 @@ def search(
59
63
  """Search the curated dataset registry."""
60
64
  try:
61
65
  query = build_query(text, provider=provider, location=state, start=start, end=end)
62
- except UnknownPlace as e:
63
- typer.secho(f"Unknown place: {e}", err=True, fg="red")
66
+ except ValueError as e:
67
+ typer.secho(str(e), err=True, fg="red")
64
68
  raise typer.Exit(code=2) from None
65
69
  results = default_registry().search(query, include_planned=planned)
66
70
  if not results:
@@ -103,7 +107,10 @@ def info(
103
107
  @app.command()
104
108
  def fetch(
105
109
  dataset_id: Annotated[str, typer.Argument(help="Dataset id, e.g. noaa:ghcn-daily")],
106
- state: Annotated[str | None, typer.Option(help="State name or postal code.")] = None,
110
+ state: Annotated[
111
+ str | None,
112
+ typer.Option("--location", "--state", help="State, 'County, ST', or quoted FIPS code."),
113
+ ] = None,
107
114
  bbox: Annotated[str | None, typer.Option(help="west,south,east,north in degrees.")] = None,
108
115
  lat: Annotated[float | None, typer.Option()] = None,
109
116
  lon: Annotated[float | None, typer.Option()] = None,
@@ -152,21 +159,21 @@ def fetch(
152
159
  variables=[v.strip() for v in variables.split(",")] if variables else None,
153
160
  **params,
154
161
  )
155
- adapter = load_adapter(ds)
156
- assets = adapter.list_assets(query)
157
162
  if dry_run:
163
+ with load_adapter(ds) as adapter:
164
+ assets = adapter.list_assets(query)
158
165
  for a in assets:
159
166
  typer.echo(f"{a.id}\t{a.href}")
160
167
  typer.echo(f"{len(assets)} asset(s) matched", err=True)
161
168
  return
162
- fetched = [fetch_asset(ds, a, root=cache_dir, force=force) for a in assets]
169
+ fetched = fetch_query(ds, query, root=cache_dir, force=force)
163
170
  except (DatasetNotFound, UnknownPlace, ValueError) as e:
164
171
  typer.secho(str(e), err=True, fg="red")
165
172
  raise typer.Exit(code=2) from None
166
173
  except NotImplementedProvider as e:
167
174
  typer.secho(str(e), err=True, fg="yellow")
168
175
  raise typer.Exit(code=3) from None
169
- except httpx.HTTPError as e:
176
+ except (httpx.HTTPError, ChecksumMismatch) as e:
170
177
  typer.secho(f"request failed: {e}", err=True, fg="red")
171
178
  raise typer.Exit(code=4) from None
172
179
  if not fetched:
@@ -189,7 +196,10 @@ def pull(
189
196
  """Fetch every source in a manifest and write (or restore from) its lockfile."""
190
197
  try:
191
198
  result = pull_manifest(manifest, root=cache_dir, force=force)
192
- except (UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
199
+ except EmptySource as e:
200
+ typer.secho(str(e), err=True, fg="yellow")
201
+ raise typer.Exit(code=1) from None
202
+ except (DatasetNotFound, UnknownDatasets, ManifestChanged, UnknownPlace, ValueError) as e:
193
203
  typer.secho(str(e), err=True, fg="red")
194
204
  raise typer.Exit(code=2) from None
195
205
  except NotImplementedProvider as e:
@@ -215,7 +225,11 @@ def verify(
215
225
  if not lock.exists():
216
226
  typer.secho(f"no lockfile at {lock}; run pull first", err=True, fg="red")
217
227
  raise typer.Exit(code=2)
218
- drift = verify_manifest(manifest, root=cache_dir)
228
+ try:
229
+ drift = verify_manifest(manifest, root=cache_dir)
230
+ except (ManifestChanged, ValueError, OSError) as e:
231
+ typer.secho(str(e), err=True, fg="red")
232
+ raise typer.Exit(code=2) from None
219
233
  for d in drift:
220
234
  typer.echo(f"{d.asset_id}\t{d.problem}\t{d.path}")
221
235
  if drift: