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.
- usdata-0.19.0/PKG-INFO +124 -0
- usdata-0.19.0/README.md +79 -0
- {usdata-0.18.0 → usdata-0.19.0}/pyproject.toml +17 -3
- {usdata-0.18.0 → usdata-0.19.0}/pyproject.toml.orig +24 -3
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_fetch.py +22 -2
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/app.py +22 -4
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/registry.yaml +41 -6
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/doctor.py +3 -2
- usdata-0.19.0/src/usdata/mirror.py +60 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/models.py +4 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/coastwatch.py +1 -1
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/coops.py +5 -1
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/params.py +22 -0
- usdata-0.19.0/src/usdata/providers/usgs/earthquakes.py +147 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/pull.py +100 -12
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/query.py +19 -5
- usdata-0.18.0/PKG-INFO +0 -123
- usdata-0.18.0/README.md +0 -89
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/__main__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_files.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_grib.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_hurdat2.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_netcdf.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_progress.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/_radar.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cache.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cache_ops.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cite.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/cache.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/cite.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/doctor.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/inspect.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/cli/progress.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/nexrad_sites.csv +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/places.csv +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/data/places.sources.json +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/inspect.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/manifest.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/erddap.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/http.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/listing.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/protocols/s3.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/provenance.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/base.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/http.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gfs.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/glm.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/goes.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/grib_index.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gsom.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/gsoy.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/hrrr.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/lcd.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/mrms.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/nexrad.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/normals.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/sites.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/spc.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/noaa/storm_events.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/usgs/__init__.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/providers/usgs/daily.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/py.typed +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/readers.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/registry.py +0 -0
- {usdata-0.18.0 → usdata-0.19.0}/src/usdata/selection.py +0 -0
- {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
|
+
[](https://pypi.org/project/usdata/)
|
|
49
|
+
[](https://pypi.org/project/usdata/)
|
|
50
|
+
[](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml)
|
|
51
|
+
[](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
[](https://docs.usdata.dev/)
|
|
54
|
+
|
|
55
|
+
[](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).
|
usdata-0.19.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# usdata
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/usdata/)
|
|
4
|
+
[](https://pypi.org/project/usdata/)
|
|
5
|
+
[](https://github.com/jakeryderv/usdata/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/jakeryderv/usdata/actions/workflows/integration.yml)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://docs.usdata.dev/)
|
|
9
|
+
|
|
10
|
+
[](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.
|
|
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 ::
|
|
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.
|
|
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 = [
|
|
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 ::
|
|
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
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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:
|
|
1230
|
+
status: available
|
|
1231
1231
|
domain: natural-hazards
|
|
1232
|
-
|
|
1232
|
+
since: "0.19"
|
|
1233
1233
|
title: Earthquake Catalog (ComCat)
|
|
1234
1234
|
description: >-
|
|
1235
|
-
Global earthquake events with
|
|
1236
|
-
ANSS Comprehensive Catalog
|
|
1237
|
-
REST
|
|
1238
|
-
|
|
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
|
|
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
|
|
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
|
]
|