usdata 0.22.0__tar.gz → 0.24.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 (78) hide show
  1. {usdata-0.22.0 → usdata-0.24.0}/PKG-INFO +22 -3
  2. {usdata-0.22.0 → usdata-0.24.0}/README.md +21 -2
  3. {usdata-0.22.0 → usdata-0.24.0}/pyproject.toml +1 -1
  4. {usdata-0.22.0 → usdata-0.24.0}/pyproject.toml.orig +1 -1
  5. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/data/registry.yaml +52 -7
  6. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/coops.py +66 -3
  7. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/normals.py +7 -4
  8. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/__init__.py +0 -0
  9. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/__main__.py +0 -0
  10. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_fetch.py +0 -0
  11. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_files.py +0 -0
  12. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_grib.py +0 -0
  13. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_hurdat2.py +0 -0
  14. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_netcdf.py +0 -0
  15. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_progress.py +0 -0
  16. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/_radar.py +0 -0
  17. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cache.py +0 -0
  18. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cache_ops.py +0 -0
  19. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cite.py +0 -0
  20. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/__init__.py +0 -0
  21. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/app.py +0 -0
  22. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/cache.py +0 -0
  23. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/cite.py +0 -0
  24. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/doctor.py +0 -0
  25. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/inspect.py +0 -0
  26. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/cli/progress.py +0 -0
  27. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/data/nexrad_sites.csv +0 -0
  28. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/data/places.csv +0 -0
  29. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/data/places.sources.json +0 -0
  30. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/doctor.py +0 -0
  31. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/inspect.py +0 -0
  32. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/manifest.py +0 -0
  33. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/mirror.py +0 -0
  34. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/models.py +0 -0
  35. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/protocols/__init__.py +0 -0
  36. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/protocols/erddap.py +0 -0
  37. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/protocols/http.py +0 -0
  38. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/protocols/listing.py +0 -0
  39. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/protocols/s3.py +0 -0
  40. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/provenance.py +0 -0
  41. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/__init__.py +0 -0
  42. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/base.py +0 -0
  43. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/fema/__init__.py +0 -0
  44. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/fema/declarations.py +0 -0
  45. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/http.py +0 -0
  46. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/__init__.py +0 -0
  47. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
  48. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/gfs.py +0 -0
  49. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
  50. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/glm.py +0 -0
  51. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/goes.py +0 -0
  52. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/grib_index.py +0 -0
  53. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/gsom.py +0 -0
  54. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  55. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/hrrr.py +0 -0
  56. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
  57. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
  58. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/lcd.py +0 -0
  59. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/mrms.py +0 -0
  60. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/nbm.py +0 -0
  61. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/nexrad.py +0 -0
  62. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
  63. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
  64. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/rap.py +0 -0
  65. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/sites.py +0 -0
  66. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/spc.py +0 -0
  67. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  68. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/params.py +0 -0
  69. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/usgs/__init__.py +0 -0
  70. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/usgs/daily.py +0 -0
  71. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
  72. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/pull.py +0 -0
  73. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/py.typed +0 -0
  74. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/query.py +0 -0
  75. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/readers.py +0 -0
  76. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/registry.py +0 -0
  77. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/selection.py +0 -0
  78. {usdata-0.22.0 → usdata-0.24.0}/src/usdata/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.22.0
3
+ Version: 0.24.0
4
4
  Summary: Unified Python SDK and CLI for discovering, fetching, and tracking provenance of U.S. public scientific data
5
5
  Keywords: noaa,usgs,open-data,scientific-data,provenance,reproducible-research,weather,climate,meteorology
6
6
  Author: Jake Van Slyke
@@ -66,10 +66,29 @@ for pinning inputs across sources and proving later that they have not changed.
66
66
  pip install "usdata[pandas]"
67
67
  usdata search precipitation --location Oklahoma
68
68
  usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-05-06 --end 2024-05-07
69
+ ```
70
+
71
+ Save the same station query as `dataset.yaml` in your working directory:
72
+
73
+ ```yaml
74
+ name: first-station
75
+ sources:
76
+ - dataset: noaa:ghcn-daily
77
+ start: 2024-05-06
78
+ end: 2024-05-07
79
+ params:
80
+ stations: USW00013967
81
+ ```
82
+
83
+ Then fetch and pin its inputs, and print their citation:
84
+
85
+ ```sh
69
86
  usdata pull dataset.yaml && usdata cite dataset.yaml
70
87
  ```
71
88
 
72
- The same four steps from Python:
89
+ The first pull writes `dataset.lock.json`; later pulls restore its pinned assets.
90
+
91
+ From Python, using the same `dataset.yaml`:
73
92
 
74
93
  ```python
75
94
  from usdata import build_query, fetch, get, pull
@@ -93,7 +112,7 @@ breaking changes.
93
112
  | [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
113
  | [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
114
 
96
- Twenty-five datasets are available today and twenty more are planned, grouped
115
+ Twenty-six datasets are available today and twenty more are planned, grouped
97
116
  by agency and product family in the [catalog](docs/providers/README.md).
98
117
 
99
118
  ## How this compares
@@ -21,10 +21,29 @@ for pinning inputs across sources and proving later that they have not changed.
21
21
  pip install "usdata[pandas]"
22
22
  usdata search precipitation --location Oklahoma
23
23
  usdata fetch noaa:ghcn-daily -p stations=USW00013967 --start 2024-05-06 --end 2024-05-07
24
+ ```
25
+
26
+ Save the same station query as `dataset.yaml` in your working directory:
27
+
28
+ ```yaml
29
+ name: first-station
30
+ sources:
31
+ - dataset: noaa:ghcn-daily
32
+ start: 2024-05-06
33
+ end: 2024-05-07
34
+ params:
35
+ stations: USW00013967
36
+ ```
37
+
38
+ Then fetch and pin its inputs, and print their citation:
39
+
40
+ ```sh
24
41
  usdata pull dataset.yaml && usdata cite dataset.yaml
25
42
  ```
26
43
 
27
- The same four steps from Python:
44
+ The first pull writes `dataset.lock.json`; later pulls restore its pinned assets.
45
+
46
+ From Python, using the same `dataset.yaml`:
28
47
 
29
48
  ```python
30
49
  from usdata import build_query, fetch, get, pull
@@ -48,7 +67,7 @@ breaking changes.
48
67
  | [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
68
  | [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
69
 
51
- Twenty-five datasets are available today and twenty more are planned, grouped
70
+ Twenty-six datasets are available today and twenty more are planned, grouped
52
71
  by agency and product family in the [catalog](docs/providers/README.md).
53
72
 
54
73
  ## How this compares
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.22.0"
3
+ version = "0.24.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.22.0"
3
+ version = "0.24.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"
@@ -641,6 +641,47 @@ datasets:
641
641
  limits: { max_window: P366D }
642
642
  adapter: usdata.providers.noaa.coops:CoopsTidePredictions
643
643
 
644
+ - id: noaa:coops-currents
645
+ provider: noaa
646
+ status: available
647
+ system: noaa:coops
648
+ domain: ocean-physics
649
+ since: "0.24"
650
+ title: CO-OPS Observed Currents
651
+ description: >-
652
+ Native six-minute current speed and direction from the anonymous CO-OPS
653
+ Data API. Select one alphanumeric station, a positive bin, units, and a
654
+ UTC interval of at most 28 days. Raw CSV preserves gaps and blank values.
655
+ Bin depths can change between deployments; the CSV carries bin number,
656
+ not depth or a verification-quality flag. Predictions, all-bin queries,
657
+ station discovery, and resampling are not included.
658
+ keywords: [currents, current speed, current direction, coastal, ocean, coops, ports, stations, bin]
659
+ protocol: http
660
+ homepage: https://api.tidesandcurrents.noaa.gov/api/prod/
661
+ license: US Government Work (public domain)
662
+ capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false }
663
+ summary: Coastal current speed and direction
664
+ formats: [CSV]
665
+ selection: Native six-minute observations for one station and explicit bin; at most 28 days
666
+ inputs: Alphanumeric station, positive bin, and both minute-aligned timestamps
667
+ reader: pandas
668
+ guide: docs/providers/noaa-coops-currents.md
669
+ examples:
670
+ - examples/coastal-currents/example.ipynb
671
+ resolution:
672
+ spatial: One current-meter station and bin per request; bin depths depend on deployment
673
+ temporal: Six-minute observations
674
+ citation: >-
675
+ NOAA National Ocean Service, CO-OPS observed currents, accessed via usdata
676
+ terms: https://tidesandcurrents.noaa.gov/disclaimers.html
677
+ variables:
678
+ - { name: "Date Time", units: "UTC", description: "Observation time, requested with time_zone=gmt" }
679
+ - { name: "Speed", units: "cm/s", description: "Observed current speed in metric units; knots with english units" }
680
+ - { name: "Direction", units: "degrees", description: "Observed current direction" }
681
+ - { name: "Bin", description: "Requested bin number, not a depth measurement" }
682
+ limits: { max_window: P28D }
683
+ adapter: usdata.providers.noaa.coops:CoopsCurrents
684
+
644
685
  - id: noaa:gsom
645
686
  provider: noaa
646
687
  status: available
@@ -731,14 +772,15 @@ datasets:
731
772
  since: "0.11"
732
773
  title: U.S. Climate Normals 1991-2020
733
774
  description: >-
734
- Daily, monthly, and annual/seasonal 30-year station normals via the NCEI Access Data
735
- Service datasets normals-daily-1991-2020, normals-monthly-1991-2020, and
736
- normals-annualseasonal-1991-2020, chosen with a period parameter. Dates are optional;
737
- daily and monthly normals accept a month-day window inside a placeholder year. Explicit
775
+ Hourly, daily, monthly, and annual/seasonal 30-year station normals via the NCEI Access Data
776
+ Service datasets normals-hourly-1991-2020, normals-daily-1991-2020, normals-monthly-1991-2020,
777
+ and normals-annualseasonal-1991-2020, chosen with a period parameter. Dates are optional;
778
+ hourly, daily, and monthly normals accept a month-day window inside a placeholder year.
779
+ Hourly labels use local standard time, with no February 29 values. Explicit
738
780
  stations or station discovery through the companion search service. Variables are NCEI
739
781
  data-type codes and the catalog lists the probed ones; the delivered set is the open NCEI
740
782
  normals list.
741
- keywords: [climate, normals, averages, stations, temperature, precipitation, ncei]
783
+ keywords: [climate, normals, averages, stations, temperature, precipitation, hourly, ncei]
742
784
  protocol: http
743
785
  homepage: https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals
744
786
  license: US Government Work (public domain)
@@ -747,17 +789,18 @@ datasets:
747
789
  capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: true }
748
790
  summary: 30-year station climate normals
749
791
  formats: [CSV]
750
- selection: Monthly, daily, or annual/seasonal normals per station; optional month-day window for daily and monthly
792
+ selection: Hourly, daily, monthly, or annual/seasonal normals per station; optional month-day window except annual/seasonal; hourly returns whole days
751
793
  inputs: Station IDs or a geographic query; dates optional
752
794
  reader: pandas
753
795
  guide: docs/providers/noaa-normals.md
754
796
  examples:
755
797
  - examples/climate-normals/README.md
756
798
  - examples/climate-anomalies/example.ipynb
799
+ - examples/hourly-anomalies/example.ipynb
757
800
  resolution:
758
801
  spatial: "U.S. land surface stations with 1991-2020 normals coverage"
759
802
  temporal: >-
760
- Daily, monthly, or annual and seasonal normals, chosen with the period parameter
803
+ Hourly, daily, monthly, or annual and seasonal normals, chosen with the period parameter
761
804
  update_frequency: "As needed; the normals are republished about once a decade"
762
805
  citation: >-
763
806
  Palecki, Michael; Durre, Imke; Applequist, Scott; Arguez, Anthony; Lawrimore, Jay (2021).
@@ -765,6 +808,7 @@ datasets:
765
808
  Information; cite the record for the period used
766
809
  terms: https://www.ncei.noaa.gov/products/land-based-station/us-climate-normals
767
810
  variables:
811
+ - { name: "HLY-TEMP-NORMAL", units: "degrees Celsius", description: "Hourly normal mean temperature (metric units); local standard time" }
768
812
  - { name: "MLY-TMAX-NORMAL", units: "degrees Celsius", description: "Monthly normal maximum temperature" }
769
813
  - { name: "MLY-TMIN-NORMAL", units: "degrees Celsius", description: "Monthly normal minimum temperature" }
770
814
  - { name: "MLY-PRCP-NORMAL", units: "mm", description: "Monthly normal precipitation total" }
@@ -1144,6 +1188,7 @@ datasets:
1144
1188
  guide: docs/providers/noaa-lcd.md
1145
1189
  examples:
1146
1190
  - examples/hourly-observations/README.md
1191
+ - examples/hourly-anomalies/example.ipynb
1147
1192
  resolution:
1148
1193
  spatial: "Airport and first-order stations, addressed by eleven-digit station id"
1149
1194
  temporal: "Hourly, special, and synoptic reports, plus daily and monthly summaries"
@@ -1,17 +1,21 @@
1
- """CO-OPS observed water levels and tide predictions as raw CSV.
1
+ """CO-OPS observed water levels, currents, and tide predictions as raw CSV.
2
2
 
3
- Both datasets share one station model: one seven-digit ``station``, an explicit
3
+ Water levels and tide predictions share one station model: one seven-digit ``station``, an explicit
4
4
  ``datum``, optional ``units`` (metric or english), and both timestamps at minute
5
5
  precision, requested in UTC; a bare end date means 23:59 on that day.
6
6
  Observations span at most 28 days; predictions
7
7
  span at most a year on any ``interval`` (six-minute by default, another minute
8
8
  step, hourly, or high/low), within NOAA's own limits.
9
+ Currents instead name an alphanumeric station and explicit positive bin, with
10
+ no datum or interval selector. Their native six-minute observations share the
11
+ 28-day window and use cm/s (metric) or knots (english).
9
12
  """
10
13
 
11
14
  from __future__ import annotations
12
15
 
13
16
  import csv
14
17
  import hashlib
18
+ import math
15
19
  from collections.abc import Iterator
16
20
  from datetime import UTC, datetime, timedelta
17
21
  from pathlib import Path
@@ -25,7 +29,7 @@ from usdata.models import Asset, Protocol, Query, TimeRange
25
29
  from usdata.protocols import http
26
30
  from usdata.providers.base import QueryError
27
31
  from usdata.providers.http import HttpProvider
28
- from usdata.providers.params import choice
32
+ from usdata.providers.params import choice, positive_int
29
33
  from usdata.query import LAST_INSTANT
30
34
 
31
35
  DATA_URL = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"
@@ -44,6 +48,7 @@ OBSERVATION_COLUMNS = {
44
48
  "Quality",
45
49
  }
46
50
  PREDICTION_COLUMNS = {"Date Time", "Prediction"}
51
+ CURRENTS_COLUMNS = {"Date Time", "Speed", "Direction", "Bin"}
47
52
 
48
53
 
49
54
  def _rows(path: Path, asset: Asset, required: set[str]) -> Iterator[dict[str, str]]:
@@ -150,6 +155,29 @@ class CoopsPredictionParams(CoopsParams):
150
155
  return token
151
156
 
152
157
 
158
+ class CoopsCurrentsParams(BaseModel):
159
+ """One current-meter station and bin; a bin is not a permanent depth."""
160
+
161
+ model_config = ConfigDict(extra="forbid")
162
+
163
+ station: str = Field(
164
+ description="Required alphanumeric CO-OPS currents station id, for example 'cb0102'."
165
+ )
166
+ bin: Annotated[int, positive_int()] = Field(
167
+ description="Required positive bin number; 0 (all bins) is not supported."
168
+ )
169
+ units: Annotated[str, choice("metric", "english")] = Field(
170
+ default="metric", description="metric (default, cm/s) or english (knots)."
171
+ )
172
+
173
+ @field_validator("station", mode="before")
174
+ @classmethod
175
+ def _one_station_id(cls, value: object) -> object:
176
+ if not isinstance(value, str) or not value.isascii() or not value.isalnum():
177
+ raise ValueError("must be an alphanumeric string, for example 'cb0102'")
178
+ return value
179
+
180
+
153
181
  class _CoopsStation(HttpProvider):
154
182
  """Shared window, request, and validation rules for CO-OPS station products."""
155
183
 
@@ -255,3 +283,38 @@ class CoopsTidePredictions(_CoopsStation):
255
283
  def _validate(self, path: Path, asset: Asset) -> None:
256
284
  interval = httpx.URL(asset.href).params.get("interval", "6")
257
285
  _validate_predictions(path, asset, interval)
286
+
287
+
288
+ class CoopsCurrents(_CoopsStation):
289
+ """One station's observed current speed and direction at an explicit bin."""
290
+
291
+ product = "currents"
292
+ label = "current"
293
+ params_model = CoopsCurrentsParams
294
+
295
+ def list_assets(self, query: Query) -> list[Asset]:
296
+ """Describe one native-interval CSV request; check availability on fetch."""
297
+ params = self.parse_params(query, CoopsCurrentsParams)
298
+ self.reject(query, "bbox", "text", "variables", hint="CO-OPS requires an explicit station")
299
+ start, end = self._window(query, MAX_INTERVAL)
300
+ request = {"bin": str(params.bin), "units": params.units}
301
+ return [self._asset(params.station, start, end, request)]
302
+
303
+ def _validate(self, path: Path, asset: Asset) -> None:
304
+ expected_bin = int(httpx.URL(asset.href).params["bin"])
305
+ count = 0
306
+ for row in _rows(path, asset, CURRENTS_COLUMNS):
307
+ token = row["Bin"]
308
+ if not token.isascii() or not token.isdigit() or int(token) != expected_bin:
309
+ raise ValueError("record does not match the requested bin")
310
+ # Preserve blank measurements and missing timestamps, not invented values.
311
+ for field in ("Speed", "Direction"):
312
+ if row[field]:
313
+ value = float(row[field])
314
+ if not math.isfinite(value) or value < 0:
315
+ raise ValueError(f"invalid current {field.lower()}")
316
+ if field == "Direction" and value > 360:
317
+ raise ValueError("current direction must be between 0 and 360 degrees")
318
+ count += 1
319
+ if not count:
320
+ raise ValueError("no current observations returned")
@@ -1,14 +1,16 @@
1
1
  """U.S. Climate Normals 1991-2020 through the NCEI Access Data Service.
2
2
 
3
- Params: ``period`` (``monthly`` by default, ``daily``, or ``annualseasonal``),
3
+ Params: ``period`` (``monthly`` by default, ``daily``, ``annualseasonal``, or ``hourly``),
4
4
  ``stations`` (list or comma string), and ``units`` (metric or standard).
5
5
 
6
- Normals are 30-year averages, not observations, so dates are optional. For daily
7
- and monthly normals an optional ``start``/``end`` pair selects a calendar window
6
+ Normals are 30-year averages, not observations, so dates are optional. For hourly,
7
+ daily, and monthly normals an optional ``start``/``end`` pair selects a calendar window
8
8
  by month and day; the year is ignored and sent as the placeholder 2020, a leap
9
9
  year so February 29 is valid. Windows cannot cross the new year. Without dates
10
10
  the whole year is requested. Annual/seasonal normals accept no dates. Assets
11
11
  carry the 1991-2020 normals period as their time bounds.
12
+ Hourly normals return all hours of each selected day, labeled in local standard
13
+ time as ``MM-DDTHH:MM:SS``. They have no February 29 values.
12
14
  """
13
15
 
14
16
  from __future__ import annotations
@@ -29,6 +31,7 @@ PERIODS = {
29
31
  "monthly": "normals-monthly-1991-2020",
30
32
  "daily": "normals-daily-1991-2020",
31
33
  "annualseasonal": "normals-annualseasonal-1991-2020",
34
+ "hourly": "normals-hourly-1991-2020",
32
35
  }
33
36
  PLACEHOLDER_YEAR = 2020
34
37
  NORMALS_PERIOD = TimeRange(
@@ -41,7 +44,7 @@ class ClimateNormalsParams(GhcnDailyParams):
41
44
  """A normals query also names which averaging period it wants."""
42
45
 
43
46
  period: Annotated[str, choice(*PERIODS)] = Field(
44
- default="monthly", description="monthly (default), daily, or annualseasonal."
47
+ default="monthly", description="monthly (default), daily, annualseasonal, or hourly."
45
48
  )
46
49
 
47
50
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes