usdata 0.18.0__tar.gz → 0.19.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 (74) hide show
  1. usdata-0.19.0/PKG-INFO +124 -0
  2. usdata-0.19.0/README.md +79 -0
  3. {usdata-0.18.0 → usdata-0.19.0}/pyproject.toml +17 -3
  4. {usdata-0.18.0 → usdata-0.19.0}/pyproject.toml.orig +24 -3
  5. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_fetch.py +22 -2
  6. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/app.py +22 -4
  7. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/registry.yaml +41 -6
  8. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/doctor.py +3 -2
  9. usdata-0.19.0/src/usdata/mirror.py +60 -0
  10. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/models.py +4 -0
  11. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/coastwatch.py +1 -1
  12. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/coops.py +5 -1
  13. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/params.py +22 -0
  14. usdata-0.19.0/src/usdata/providers/usgs/earthquakes.py +147 -0
  15. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/pull.py +100 -12
  16. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/query.py +19 -5
  17. usdata-0.18.0/PKG-INFO +0 -123
  18. usdata-0.18.0/README.md +0 -89
  19. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/__init__.py +0 -0
  20. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/__main__.py +0 -0
  21. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_files.py +0 -0
  22. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_grib.py +0 -0
  23. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_hurdat2.py +0 -0
  24. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_netcdf.py +0 -0
  25. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_progress.py +0 -0
  26. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_radar.py +0 -0
  27. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cache.py +0 -0
  28. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cache_ops.py +0 -0
  29. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cite.py +0 -0
  30. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/__init__.py +0 -0
  31. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/cache.py +0 -0
  32. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/cite.py +0 -0
  33. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/doctor.py +0 -0
  34. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/inspect.py +0 -0
  35. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/progress.py +0 -0
  36. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/nexrad_sites.csv +0 -0
  37. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/places.csv +0 -0
  38. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/places.sources.json +0 -0
  39. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/inspect.py +0 -0
  40. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/manifest.py +0 -0
  41. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/__init__.py +0 -0
  42. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/erddap.py +0 -0
  43. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/http.py +0 -0
  44. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/listing.py +0 -0
  45. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/s3.py +0 -0
  46. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/provenance.py +0 -0
  47. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/__init__.py +0 -0
  48. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/base.py +0 -0
  49. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/http.py +0 -0
  50. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/__init__.py +0 -0
  51. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gfs.py +0 -0
  52. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
  53. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/glm.py +0 -0
  54. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/goes.py +0 -0
  55. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/grib_index.py +0 -0
  56. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gsom.py +0 -0
  57. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  58. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/hrrr.py +0 -0
  59. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
  60. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/lcd.py +0 -0
  61. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/mrms.py +0 -0
  62. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/nexrad.py +0 -0
  63. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
  64. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/normals.py +0 -0
  65. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/sites.py +0 -0
  66. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/spc.py +0 -0
  67. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  68. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/usgs/__init__.py +0 -0
  69. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/usgs/daily.py +0 -0
  70. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/py.typed +0 -0
  71. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/readers.py +0 -0
  72. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/registry.py +0 -0
  73. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/selection.py +0 -0
  74. {usdata-0.18.0 → usdata-0.19.0}/src/usdata/testing.py +0 -0
usdata-0.19.0/PKG-INFO ADDED
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: usdata
3
+ Version: 0.19.0
4
+ Summary: Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data
5
+ Keywords: noaa,usgs,open-data,scientific-data,provenance,reproducible-research,weather,climate,meteorology
6
+ Author: Jake Van Slyke
7
+ Author-email: Jake Van Slyke <jakervanslyke@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
20
+ Classifier: Topic :: Scientific/Engineering :: Hydrology
21
+ Requires-Dist: httpx>=0.28.1
22
+ Requires-Dist: pydantic>=2.7
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: typer>=0.18
25
+ Requires-Dist: eccodes>=2.48 ; extra == 'grib'
26
+ Requires-Dist: eccodeslib>=2.48 ; sys_platform == 'linux' and extra == 'grib'
27
+ Requires-Dist: xarray>=2025.1 ; extra == 'grib'
28
+ Requires-Dist: numpy>=2 ; extra == 'grib'
29
+ Requires-Dist: xarray>=2025.1 ; extra == 'netcdf'
30
+ Requires-Dist: h5netcdf[h5py]>=1.8.1 ; extra == 'netcdf'
31
+ Requires-Dist: pandas>=2.2 ; extra == 'pandas'
32
+ Requires-Dist: xradar>=0.12.0 ; extra == 'radar'
33
+ Requires-Python: >=3.11
34
+ Project-URL: Homepage, https://usdata.dev/
35
+ Project-URL: Documentation, https://docs.usdata.dev/
36
+ Project-URL: Repository, https://github.com/jakeryderv/usdata
37
+ Project-URL: Examples, https://usdata.dev/examples/
38
+ Project-URL: Changelog, https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md
39
+ Project-URL: Issues, https://github.com/jakeryderv/usdata/issues
40
+ Provides-Extra: grib
41
+ Provides-Extra: netcdf
42
+ Provides-Extra: pandas
43
+ Provides-Extra: radar
44
+ Description-Content-Type: text/markdown
45
+
46
+ # usdata
47
+
48
+ [![PyPI](https://img.shields.io/pypi/v/usdata)](https://pypi.org/project/usdata/)
49
+ [![Python](https://img.shields.io/pypi/pyversions/usdata)](https://pypi.org/project/usdata/)
50
+ [![CI](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml/badge.svg)](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml)
51
+ [![Live checks](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml/badge.svg)](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml)
52
+ [![License](https://img.shields.io/github/license/jakeryderv/usdata)](LICENSE)
53
+ [![Docs](https://img.shields.io/badge/docs-docs.usdata.dev-2f5bea)](https://docs.usdata.dev/)
54
+
55
+ [![usdata.dev: public science, reproducible inputs](https://usdata.dev/og.png)](https://usdata.dev/)
56
+
57
+ Reproducible acquisition of U.S. public scientific data. One Python SDK and
58
+ CLI discovers curated NOAA and USGS datasets, fetches their files, and keeps a
59
+ record of every input: a manifest names them, a lockfile pins them by checksum,
60
+ and the record of what was fetched is what a methods section cites. Analysis
61
+ stays in pandas and xarray; usdata only acquires. If you need every product
62
+ one agency publishes, that agency's own library is the better tool; usdata is
63
+ for pinning inputs across sources and proving later that they have not changed.
64
+
65
+ ```sh
66
+ pip install "usdata[pandas]"
67
+ usdata search precipitation --location Oklahoma
68
+ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-05-06 --end 2024-05-07
69
+ usdata pull dataset.yaml && usdata cite dataset.yaml
70
+ ```
71
+
72
+ The same four steps from Python:
73
+
74
+ ```python
75
+ from usdata import build_query, fetch, get, pull
76
+
77
+ query = build_query(start="2024-05-06", end="2024-05-07", stations="USW00013967")
78
+ items = fetch(get("noaa:ghcn-daily"), query)
79
+ frame = items[0].open() # pandas DataFrame, units and provenance in frame.attrs
80
+ result = pull("dataset.yaml") # every manifest source, pinned in dataset.lock.json
81
+ print(result.fetched[0].provenance.checksum) # what usdata cite reports
82
+ ```
83
+
84
+ **Alpha.** Releases are tested and lockfiles are stable, but the Python API
85
+ and CLI can change between minor versions; the [changelog](CHANGELOG.md) marks
86
+ breaking changes.
87
+
88
+ ## Where to look
89
+
90
+ | | |
91
+ | --- | --- |
92
+ | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/examples/) with saved results |
93
+ | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
94
+ | [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
95
+
96
+ Twenty datasets are available today and twenty-five more are planned, grouped
97
+ by agency and product family in the [catalog](docs/providers/README.md).
98
+
99
+ ## How this compares
100
+
101
+ usdata resolves a place, a time window, and a few parameters to the exact
102
+ files that satisfy them, then pins those files so the same inputs can be
103
+ restored and verified later. Neighbouring tools each do part of that.
104
+
105
+ | Tool | What it does | What usdata adds |
106
+ | --- | --- | --- |
107
+ | [pooch](https://www.fatiando.org/pooch/) | Downloads and verifies files from a registry of names, hashes, and a base URL you write | Discovery: the query resolves to the URLs, and the lockfile is written, not authored |
108
+ | [intake](https://intake.readthedocs.io/) | Describes datasets in catalogs and opens them through drivers | Checksums, lockfiles, and provenance; usdata stops at handing a file to pandas or xarray |
109
+ | [DVC](https://dvc.org/) | Versions data files and can track one URL by its ETag or hash | Knowing which URLs a scientific question needs, across agencies |
110
+ | [Herbie](https://herbie.readthedocs.io/) | Deep access to model output, including subsetting GRIB2 through index files | One interface across observations, radar, satellite, and models, with a lockfile; usdata's index-file fetch reproduces Herbie's for that reason |
111
+ | [dataretrieval](https://github.com/DOI-USGS/dataretrieval-python) | Full coverage of USGS water data | Cross-agency pinning; use dataretrieval for USGS breadth |
112
+
113
+ ## Contributing
114
+
115
+ Setup, commands, checks, and the release procedure are in
116
+ [CONTRIBUTING.md](CONTRIBUTING.md). The project record lives beside it:
117
+ [architecture](docs/architecture.md), [testing](docs/testing.md),
118
+ [decisions](docs/adr/README.md), [roadmap](docs/roadmap.md), and
119
+ [versioning](docs/versioning.md). The main way the project grows is a new
120
+ dataset, following [adding a dataset](docs/guides/adding-a-dataset.md).
121
+
122
+ ## License
123
+
124
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,79 @@
1
+ # usdata
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/usdata)](https://pypi.org/project/usdata/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/usdata)](https://pypi.org/project/usdata/)
5
+ [![CI](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml/badge.svg)](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml)
6
+ [![Live checks](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml/badge.svg)](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml)
7
+ [![License](https://img.shields.io/github/license/jakeryderv/usdata)](LICENSE)
8
+ [![Docs](https://img.shields.io/badge/docs-docs.usdata.dev-2f5bea)](https://docs.usdata.dev/)
9
+
10
+ [![usdata.dev: public science, reproducible inputs](https://usdata.dev/og.png)](https://usdata.dev/)
11
+
12
+ Reproducible acquisition of U.S. public scientific data. One Python SDK and
13
+ CLI discovers curated NOAA and USGS datasets, fetches their files, and keeps a
14
+ record of every input: a manifest names them, a lockfile pins them by checksum,
15
+ and the record of what was fetched is what a methods section cites. Analysis
16
+ stays in pandas and xarray; usdata only acquires. If you need every product
17
+ one agency publishes, that agency's own library is the better tool; usdata is
18
+ for pinning inputs across sources and proving later that they have not changed.
19
+
20
+ ```sh
21
+ pip install "usdata[pandas]"
22
+ usdata search precipitation --location Oklahoma
23
+ usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-05-06 --end 2024-05-07
24
+ usdata pull dataset.yaml && usdata cite dataset.yaml
25
+ ```
26
+
27
+ The same four steps from Python:
28
+
29
+ ```python
30
+ from usdata import build_query, fetch, get, pull
31
+
32
+ query = build_query(start="2024-05-06", end="2024-05-07", stations="USW00013967")
33
+ items = fetch(get("noaa:ghcn-daily"), query)
34
+ frame = items[0].open() # pandas DataFrame, units and provenance in frame.attrs
35
+ result = pull("dataset.yaml") # every manifest source, pinned in dataset.lock.json
36
+ print(result.fetched[0].provenance.checksum) # what usdata cite reports
37
+ ```
38
+
39
+ **Alpha.** Releases are tested and lockfiles are stable, but the Python API
40
+ and CLI can change between minor versions; the [changelog](CHANGELOG.md) marks
41
+ breaking changes.
42
+
43
+ ## Where to look
44
+
45
+ | | |
46
+ | --- | --- |
47
+ | [usdata.dev](https://usdata.dev/) | What it is for: the [dataset browser](https://usdata.dev/datasets/) and [worked examples](https://usdata.dev/examples/) with saved results |
48
+ | [docs.usdata.dev](https://docs.usdata.dev/) | How to use it: [install](https://docs.usdata.dev/install/), [getting started](https://docs.usdata.dev/getting-started/), guides, dataset notes, and reference |
49
+ | [Severe-weather case study](https://usdata.dev/examples/severe-weather-case-study/) | One tornado, six sources, one manifest and lockfile, ending in a citation |
50
+
51
+ Twenty datasets are available today and twenty-five more are planned, grouped
52
+ by agency and product family in the [catalog](docs/providers/README.md).
53
+
54
+ ## How this compares
55
+
56
+ usdata resolves a place, a time window, and a few parameters to the exact
57
+ files that satisfy them, then pins those files so the same inputs can be
58
+ restored and verified later. Neighbouring tools each do part of that.
59
+
60
+ | Tool | What it does | What usdata adds |
61
+ | --- | --- | --- |
62
+ | [pooch](https://www.fatiando.org/pooch/) | Downloads and verifies files from a registry of names, hashes, and a base URL you write | Discovery: the query resolves to the URLs, and the lockfile is written, not authored |
63
+ | [intake](https://intake.readthedocs.io/) | Describes datasets in catalogs and opens them through drivers | Checksums, lockfiles, and provenance; usdata stops at handing a file to pandas or xarray |
64
+ | [DVC](https://dvc.org/) | Versions data files and can track one URL by its ETag or hash | Knowing which URLs a scientific question needs, across agencies |
65
+ | [Herbie](https://herbie.readthedocs.io/) | Deep access to model output, including subsetting GRIB2 through index files | One interface across observations, radar, satellite, and models, with a lockfile; usdata's index-file fetch reproduces Herbie's for that reason |
66
+ | [dataretrieval](https://github.com/DOI-USGS/dataretrieval-python) | Full coverage of USGS water data | Cross-agency pinning; use dataretrieval for USGS breadth |
67
+
68
+ ## Contributing
69
+
70
+ Setup, commands, checks, and the release procedure are in
71
+ [CONTRIBUTING.md](CONTRIBUTING.md). The project record lives beside it:
72
+ [architecture](docs/architecture.md), [testing](docs/testing.md),
73
+ [decisions](docs/adr/README.md), [roadmap](docs/roadmap.md), and
74
+ [versioning](docs/versioning.md). The main way the project grows is a new
75
+ dataset, following [adding a dataset](docs/guides/adding-a-dataset.md).
76
+
77
+ ## License
78
+
79
+ [Apache-2.0](LICENSE).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.18.0"
3
+ version = "0.19.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"
@@ -8,16 +8,27 @@ requires-python = ">=3.11"
8
8
  keywords = [
9
9
  "noaa",
10
10
  "usgs",
11
- "nasa",
12
11
  "open-data",
13
12
  "scientific-data",
14
13
  "provenance",
14
+ "reproducible-research",
15
+ "weather",
16
+ "climate",
17
+ "meteorology",
15
18
  ]
16
19
  classifiers = [
17
- "Development Status :: 2 - Pre-Alpha",
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
18
22
  "Intended Audience :: Science/Research",
23
+ "Operating System :: OS Independent",
19
24
  "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
20
29
  "Topic :: Scientific/Engineering",
30
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
31
+ "Topic :: Scientific/Engineering :: Hydrology",
21
32
  ]
22
33
  dependencies = [
23
34
  "httpx>=0.28.1",
@@ -48,6 +59,9 @@ grib = [
48
59
  Homepage = "https://usdata.dev/"
49
60
  Documentation = "https://docs.usdata.dev/"
50
61
  Repository = "https://github.com/jakeryderv/usdata"
62
+ Examples = "https://usdata.dev/examples/"
63
+ Changelog = "https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md"
64
+ Issues = "https://github.com/jakeryderv/usdata/issues"
51
65
 
52
66
  [project.scripts]
53
67
  usdata = "usdata.cli:app"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.18.0"
3
+ version = "0.19.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"
@@ -8,12 +8,30 @@ authors = [
8
8
  { name = "Jake Van Slyke", email = "jakervanslyke@gmail.com" }
9
9
  ]
10
10
  requires-python = ">=3.11"
11
- keywords = ["noaa", "usgs", "nasa", "open-data", "scientific-data", "provenance"]
11
+ keywords = [
12
+ "noaa",
13
+ "usgs",
14
+ "open-data",
15
+ "scientific-data",
16
+ "provenance",
17
+ "reproducible-research",
18
+ "weather",
19
+ "climate",
20
+ "meteorology",
21
+ ]
12
22
  classifiers = [
13
- "Development Status :: 2 - Pre-Alpha",
23
+ "Development Status :: 3 - Alpha",
24
+ "Environment :: Console",
14
25
  "Intended Audience :: Science/Research",
26
+ "Operating System :: OS Independent",
15
27
  "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Programming Language :: Python :: 3.14",
16
32
  "Topic :: Scientific/Engineering",
33
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
34
+ "Topic :: Scientific/Engineering :: Hydrology",
17
35
  ]
18
36
  dependencies = [
19
37
  "httpx>=0.28.1",
@@ -38,6 +56,9 @@ grib = [
38
56
  Homepage = "https://usdata.dev/"
39
57
  Documentation = "https://docs.usdata.dev/"
40
58
  Repository = "https://github.com/jakeryderv/usdata"
59
+ Examples = "https://usdata.dev/examples/"
60
+ Changelog = "https://github.com/jakeryderv/usdata/blob/main/CHANGELOG.md"
61
+ Issues = "https://github.com/jakeryderv/usdata/issues"
41
62
 
42
63
  [project.scripts]
43
64
  usdata = "usdata.cli:app"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from collections.abc import Mapping
7
+ from datetime import UTC, datetime
7
8
  from pathlib import Path
8
9
  from typing import TYPE_CHECKING, Any
9
10
 
@@ -169,12 +170,31 @@ def _fetch_with(
169
170
  root: Path | None = None,
170
171
  force: bool = False,
171
172
  ) -> list[FetchedAsset]:
172
- """Run the loop on an adapter the caller opened, so one adapter can serve many queries."""
173
- assets = adapter.list_assets(query)
173
+ """Run the loop on an adapter the caller opened, so one adapter can serve many queries.
174
+
175
+ The listing is put in the order every result promises, by ``asset.time.start``
176
+ then id, so no adapter has to sort and no caller has to sort defensively.
177
+ """
178
+ assets = ordered(adapter.list_assets(query))
174
179
  _progress.batch([asset.size for asset in assets])
175
180
  return [_fetch_asset(dataset, a, adapter, root=root, force=force) for a in assets]
176
181
 
177
182
 
183
+ def ordered(assets: list[Asset]) -> list[Asset]:
184
+ """``assets`` by start time, then id; assets without a start time come last, by id."""
185
+ return sorted(
186
+ assets,
187
+ key=lambda asset: (
188
+ asset.time is None or asset.time.start is None,
189
+ asset.time.start if asset.time is not None and asset.time.start is not None else _NEVER,
190
+ asset.id,
191
+ ),
192
+ )
193
+
194
+
195
+ _NEVER = datetime.max.replace(tzinfo=UTC)
196
+
197
+
178
198
  def fetch(
179
199
  dataset: Dataset,
180
200
  query: Query,
@@ -184,7 +184,10 @@ def search(
184
184
  typer.Option("--location", "--state", help="State, 'County, ST', or quoted FIPS code."),
185
185
  ] = None,
186
186
  start: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
187
- end: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
187
+ end: Annotated[
188
+ str | None,
189
+ typer.Option(help="ISO date or datetime; a date alone runs to the end of that UTC day."),
190
+ ] = None,
188
191
  planned: Annotated[
189
192
  bool, typer.Option("--planned", help="Include planned datasets that have no adapter yet.")
190
193
  ] = False,
@@ -321,7 +324,10 @@ def fetch(
321
324
  lon: Annotated[float | None, typer.Option()] = None,
322
325
  radius_km: Annotated[float, typer.Option(help="Radius around --lat/--lon.")] = 50.0,
323
326
  start: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
324
- end: Annotated[str | None, typer.Option(help="ISO date or datetime.")] = None,
327
+ end: Annotated[
328
+ str | None,
329
+ typer.Option(help="ISO date or datetime; a date alone runs to the end of that UTC day."),
330
+ ] = None,
325
331
  variables: Annotated[
326
332
  str | None, typer.Option("--vars", help="Comma-separated variable names.")
327
333
  ] = None,
@@ -507,16 +513,28 @@ def pull(
507
513
  except (httpx.HTTPError, ChecksumMismatch) as e:
508
514
  typer.secho(f"fetch failed: {e}", err=True, fg="red")
509
515
  raise typer.Exit(code=4) from None
510
- updated = set(result.updated)
516
+ updated, mirrored = set(result.updated), set(result.mirrored)
511
517
  if not quiet:
512
518
  for f in result.fetched:
513
- tag = "updated" if f.asset.id in updated else "cached" if f.from_cache else "fetched"
519
+ if f.asset.id in updated:
520
+ tag = "updated"
521
+ elif f.asset.id in mirrored:
522
+ tag = "mirrored"
523
+ else:
524
+ tag = "cached" if f.from_cache else "fetched"
514
525
  typer.echo(f"{f.path}\t{tag}\t{f.provenance.size} bytes")
515
526
  if result.updated:
516
527
  mode = f"updated {len(result.updated)} pin(s) in"
517
528
  else:
518
529
  mode = "restored from" if result.from_lockfile else "wrote"
519
530
  typer.echo(f"{len(result.fetched)} asset(s); {mode} {result.lockfile_path}", err=True)
531
+ if result.mirrored:
532
+ typer.secho(
533
+ f"{len(result.mirrored)} asset(s) changed upstream and were restored from the "
534
+ "mirror; the lockfile still pins the source. Pass --update to accept new bytes.",
535
+ err=True,
536
+ fg="yellow",
537
+ )
520
538
 
521
539
 
522
540
  @app.command()
@@ -1227,19 +1227,54 @@ datasets:
1227
1227
 
1228
1228
  - id: usgs:earthquakes
1229
1229
  provider: usgs
1230
- status: planned
1230
+ status: available
1231
1231
  domain: natural-hazards
1232
- target: later
1232
+ since: "0.19"
1233
1233
  title: Earthquake Catalog (ComCat)
1234
1234
  description: >-
1235
- Global earthquake events with location, magnitude, and depth from the
1236
- ANSS Comprehensive Catalog via the FDSN event web service. Anonymous
1237
- REST with bbox, time, and magnitude filters.
1238
- keywords: [earthquakes, seismic, geology, hazards, comcat, fdsn]
1235
+ Global earthquake events with origin time, epicenter, depth, magnitude, and review
1236
+ status from the ANSS Comprehensive Catalog through the FDSN event web service.
1237
+ Anonymous REST filtered by bounding box, time window, magnitude, and depth, served as
1238
+ CSV with fixed columns; the service caps a response at 20,000 events, so longer
1239
+ selections arrive as pages ordered by time. Events are revised as networks review them.
1240
+ keywords: [earthquakes, seismic, geology, hazards, comcat, fdsn, anss]
1239
1241
  protocol: http
1240
1242
  homepage: https://earthquake.usgs.gov/fdsnws/event/1/
1241
1243
  license: US Government Work (public domain)
1244
+ spatial_extent: { west: -180.0, south: -90.0, east: 180.0, north: 90.0 }
1245
+ temporal_extent: { start: "1900-01-01T00:00:00Z" }
1242
1246
  capabilities: { spatial_subset: true, temporal_subset: true, variable_subset: false }
1247
+ summary: Earthquake events
1248
+ formats: [CSV]
1249
+ selection: Events inside an inclusive UTC window and optional box, magnitude, and depth bounds
1250
+ inputs: Both timestamps; optionally a location or bbox and magnitude or depth bounds
1251
+ reader: pandas
1252
+ guide: docs/providers/usgs-earthquakes.md
1253
+ examples:
1254
+ - examples/earthquakes/README.md
1255
+ resolution:
1256
+ spatial: "Point epicenters in decimal degrees, as located by the contributing network"
1257
+ temporal: "Origin times to the millisecond"
1258
+ update_frequency: >-
1259
+ Continuous; events post automatically from contributing networks and are superseded by
1260
+ reviewed versions
1261
+ citation: >-
1262
+ U.S. Geological Survey, Earthquake Hazards Program, 2017, Advanced National Seismic
1263
+ System (ANSS) Comprehensive Catalog of Earthquake Events and Products,
1264
+ doi:10.5066/F7MS3QZH
1265
+ terms: https://www.usgs.gov/information-policies-and-instructions/copyrights-and-credits
1266
+ variables:
1267
+ - { name: time, units: "ISO 8601 UTC", description: "Origin time of the event" }
1268
+ - { name: latitude, units: "degrees north", description: "Epicenter latitude" }
1269
+ - { name: longitude, units: "degrees east", description: "Epicenter longitude" }
1270
+ - { name: depth, units: km, description: "Hypocenter depth below sea level" }
1271
+ - { name: mag, units: "magnitude", description: "Magnitude on the scale magType names" }
1272
+ - { name: magType, description: "Magnitude scale, such as ml or mw" }
1273
+ - { name: place, description: "Nearby place and distance, as a phrase" }
1274
+ - { name: type, description: "Event type; earthquake unless otherwise" }
1275
+ - { name: status, description: "automatic or reviewed" }
1276
+ - { name: updated, units: "ISO 8601 UTC", description: "When the event was last revised" }
1277
+ adapter: usdata.providers.usgs.earthquakes:Earthquakes
1243
1278
 
1244
1279
  - id: usgs:3dep-elevation
1245
1280
  provider: usgs
@@ -29,6 +29,7 @@ from pydantic import BaseModel
29
29
  from usdata import __version__
30
30
  from usdata._grib import LIBRARY_HINT
31
31
  from usdata.cache import ENV_VAR, cache_dir
32
+ from usdata.mirror import ENV_VAR as MIRROR_ENV_VAR
32
33
  from usdata.models import READER_EXTRAS
33
34
  from usdata.protocols import http
34
35
  from usdata.registry import default_registry
@@ -39,7 +40,7 @@ READER_MODULES: dict[str, tuple[str, ...]] = {
39
40
  "netcdf": ("xarray", "h5netcdf"),
40
41
  "grib": ("eccodes", "xarray", "numpy"),
41
42
  }
42
- ENV_VARS = (ENV_VAR, "XDG_CACHE_HOME")
43
+ ENV_VARS = (ENV_VAR, "XDG_CACHE_HOME", MIRROR_ENV_VAR)
43
44
  PROBE_TIMEOUT = 5.0
44
45
  MAX_PROBES = 8
45
46
  GIB = 1024**3
@@ -190,7 +191,7 @@ def _cache_checks() -> Iterator[Check]:
190
191
 
191
192
 
192
193
  def _environment_checks() -> Iterator[Check]:
193
- """The environment variables that move the cache, reported only when set."""
194
+ """The environment variables that move the cache or name a mirror, reported only when set."""
194
195
  for name in ENV_VARS:
195
196
  value = os.environ.get(name)
196
197
  if value:
@@ -0,0 +1,60 @@
1
+ """A read-only mirror of pinned bytes, addressed by checksum.
2
+
3
+ An object lives at ``<mirror>/sha256/<hex>`` and nowhere else, so the lockfile
4
+ entry's checksum is the whole lookup and no schema changes. Restore consults
5
+ the mirror only after the pinned URL fails to reproduce its pin, and only when
6
+ ``USDATA_MIRROR_URL`` names one; the SDK never uploads. See ADR 0030.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+ import httpx
15
+
16
+ from usdata.cache import sha256_file
17
+ from usdata.protocols import http
18
+
19
+ ENV_VAR = "USDATA_MIRROR_URL"
20
+ PREFIX = "sha256/"
21
+
22
+
23
+ class MirrorMismatch(RuntimeError):
24
+ """The mirror served bytes whose checksum is not the one their key names."""
25
+
26
+
27
+ def mirror_url() -> str | None:
28
+ """The configured mirror base URL without a trailing slash, or ``None`` when unset."""
29
+ value = os.environ.get(ENV_VAR, "").strip().rstrip("/")
30
+ return value or None
31
+
32
+
33
+ def object_key(checksum: str) -> str:
34
+ """The mirror key for a pinned checksum: ``sha256/<hex>``."""
35
+ algorithm, sep, digest = checksum.partition(":")
36
+ if not sep or algorithm != "sha256" or len(digest) != 64 or not digest.isalnum():
37
+ raise ValueError(f"not a sha256 checksum: {checksum!r}")
38
+ return PREFIX + digest.lower()
39
+
40
+
41
+ def object_url(base: str, checksum: str) -> str:
42
+ """The public URL of one pinned object on a mirror."""
43
+ return f"{base.rstrip('/')}/{object_key(checksum)}"
44
+
45
+
46
+ def download(base: str, checksum: str, dest: Path, client: httpx.Client) -> str:
47
+ """Fetch the object a checksum names into ``dest`` and prove it is that object.
48
+
49
+ Returns:
50
+ The URL the bytes came from.
51
+
52
+ Raises:
53
+ httpx.HTTPStatusError: The mirror holds no such object, or refused it.
54
+ MirrorMismatch: The mirror answered with bytes of another checksum.
55
+ """
56
+ url = object_url(base, checksum)
57
+ http.download(url, dest, client)
58
+ if (found := sha256_file(dest)) != checksum:
59
+ raise MirrorMismatch(f"{url} holds {found}, not the {checksum} it is named for")
60
+ return url
@@ -534,6 +534,10 @@ class Provenance(BaseModel):
534
534
  object_etag: str | None = Field(
535
535
  default=None, description="ETag that object carried, re-sent as ``If-Match`` on a restore"
536
536
  )
537
+ mirror: str | None = Field(
538
+ default=None,
539
+ description="Mirror object that served these bytes after the source stopped (ADR 0030)",
540
+ )
537
541
 
538
542
  @property
539
543
  def is_partial(self) -> bool:
@@ -1,6 +1,6 @@
1
1
  """CoastWatch blended day/night SST as reproducible ERDDAP CSV subsets.
2
2
 
3
- Requires bbox and inclusive UTC start/end timestamps. Dates mean midnight UTC.
3
+ Requires bbox and inclusive UTC start/end timestamps; a bare end date runs to the end of its day.
4
4
  Variables default to analysed_sst; stride optionally subsamples both spatial
5
5
  axes by a positive integer. Empty intersections resolve to no assets. Requests
6
6
  exceeding one million grid rows must be narrowed or use a larger stride.
@@ -2,7 +2,8 @@
2
2
 
3
3
  Both datasets share one station model: one seven-digit ``station``, an explicit
4
4
  ``datum``, optional ``units`` (metric or english), and both timestamps at minute
5
- precision, requested in UTC. Observations span at most 28 days; predictions
5
+ precision, requested in UTC; a bare end date means 23:59 on that day.
6
+ Observations span at most 28 days; predictions
6
7
  span at most a year on any ``interval`` (six-minute by default, another minute
7
8
  step, hourly, or high/low), within NOAA's own limits.
8
9
  """
@@ -25,6 +26,7 @@ from usdata.protocols import http
25
26
  from usdata.providers.base import QueryError
26
27
  from usdata.providers.http import HttpProvider
27
28
  from usdata.providers.params import choice
29
+ from usdata.query import LAST_INSTANT
28
30
 
29
31
  DATA_URL = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"
30
32
  DATUMS = {"CRD", "IGLD", "LWD", "MHHW", "MHW", "MTL", "MSL", "MLW", "MLLW", "NAVD", "STND"}
@@ -156,6 +158,8 @@ class _CoopsStation(HttpProvider):
156
158
 
157
159
  def _window(self, query: Query, limit: timedelta) -> tuple[datetime, datetime]:
158
160
  start, end = self.utc_window(query)
161
+ if end.time() == LAST_INSTANT:
162
+ end = end.replace(second=0, microsecond=0) # a bare end date: its last minute
159
163
  if any(value.second or value.microsecond for value in (start, end)):
160
164
  raise QueryError("CO-OPS timestamps must have minute precision (zero seconds)")
161
165
  if end - start > limit:
@@ -8,6 +8,7 @@ would otherwise let through, notably booleans, floats, and non-ASCII digits.
8
8
  Use them as ``Annotated`` metadata on a field of a parameter model::
9
9
 
10
10
  cycle: Annotated[int, int_range(0, 23)]
11
+ min_magnitude: Annotated[float, number_range(-10, 10)]
11
12
  nearest: Annotated[int, positive_int()]
12
13
  forecast_hour: Annotated[list[int], int_list(0, 48)]
13
14
  file: Annotated[str, choice("sfc", "prs", "nat")]
@@ -18,6 +19,7 @@ Every message reads as the tail of a sentence about the field; the formatter in
18
19
 
19
20
  from __future__ import annotations
20
21
 
22
+ import math
21
23
  from typing import Annotated
22
24
 
23
25
  from pydantic import BeforeValidator
@@ -62,6 +64,25 @@ def _text(value: object) -> str:
62
64
  return value.strip()
63
65
 
64
66
 
67
+ def _number(value: object, low: float, high: float) -> float:
68
+ """One finite number inside ``[low, high]``, read from an int, float, or numeric string."""
69
+ message = f"must be a number from {low:g} to {high:g}"
70
+ if isinstance(value, bool) or not isinstance(value, (str, int, float)):
71
+ raise ValueError(message)
72
+ try:
73
+ number = float(str(value).strip())
74
+ except ValueError:
75
+ raise ValueError(message) from None
76
+ if not math.isfinite(number) or not low <= number <= high:
77
+ raise ValueError(message)
78
+ return number
79
+
80
+
81
+ def number_range(low: float, high: float) -> BeforeValidator:
82
+ """Validate a decimal field such as a magnitude or depth, accepting the CLI's strings."""
83
+ return BeforeValidator(lambda value: _number(value, low, high))
84
+
85
+
65
86
  def int_range(low: int, high: int) -> BeforeValidator:
66
87
  """Validate an integer field, accepting the digit strings the CLI passes."""
67
88
  return BeforeValidator(lambda value: _integer(value, low, high))
@@ -140,5 +161,6 @@ __all__ = [
140
161
  "choice",
141
162
  "int_list",
142
163
  "int_range",
164
+ "number_range",
143
165
  "positive_int",
144
166
  ]