usdata 0.23.0__tar.gz → 0.25.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.23.0 → usdata-0.25.0}/PKG-INFO +2 -2
  2. {usdata-0.23.0 → usdata-0.25.0}/README.md +1 -1
  3. {usdata-0.23.0 → usdata-0.25.0}/pyproject.toml +1 -1
  4. {usdata-0.23.0 → usdata-0.25.0}/pyproject.toml.orig +1 -1
  5. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/data/registry.yaml +50 -8
  6. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/coops.py +66 -3
  7. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/goes.py +33 -19
  8. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/__init__.py +0 -0
  9. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/__main__.py +0 -0
  10. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_fetch.py +0 -0
  11. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_files.py +0 -0
  12. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_grib.py +0 -0
  13. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_hurdat2.py +0 -0
  14. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_netcdf.py +0 -0
  15. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_progress.py +0 -0
  16. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/_radar.py +0 -0
  17. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cache.py +0 -0
  18. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cache_ops.py +0 -0
  19. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cite.py +0 -0
  20. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/__init__.py +0 -0
  21. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/app.py +0 -0
  22. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/cache.py +0 -0
  23. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/cite.py +0 -0
  24. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/doctor.py +0 -0
  25. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/inspect.py +0 -0
  26. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/cli/progress.py +0 -0
  27. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/data/nexrad_sites.csv +0 -0
  28. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/data/places.csv +0 -0
  29. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/data/places.sources.json +0 -0
  30. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/doctor.py +0 -0
  31. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/inspect.py +0 -0
  32. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/manifest.py +0 -0
  33. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/mirror.py +0 -0
  34. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/models.py +0 -0
  35. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/protocols/__init__.py +0 -0
  36. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/protocols/erddap.py +0 -0
  37. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/protocols/http.py +0 -0
  38. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/protocols/listing.py +0 -0
  39. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/protocols/s3.py +0 -0
  40. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/provenance.py +0 -0
  41. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/__init__.py +0 -0
  42. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/base.py +0 -0
  43. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/fema/__init__.py +0 -0
  44. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/fema/declarations.py +0 -0
  45. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/http.py +0 -0
  46. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/__init__.py +0 -0
  47. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/coastwatch.py +0 -0
  48. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/gfs.py +0 -0
  49. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/ghcnd.py +0 -0
  50. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/glm.py +0 -0
  51. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/grib_index.py +0 -0
  52. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/gsom.py +0 -0
  53. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/gsoy.py +0 -0
  54. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/hrrr.py +0 -0
  55. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/hurdat2.py +0 -0
  56. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/ibtracs.py +0 -0
  57. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/lcd.py +0 -0
  58. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/mrms.py +0 -0
  59. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/nbm.py +0 -0
  60. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/nexrad.py +0 -0
  61. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/nexrad_level3.py +0 -0
  62. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/normals.py +0 -0
  63. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/nws_vtec.py +0 -0
  64. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/rap.py +0 -0
  65. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/sites.py +0 -0
  66. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/spc.py +0 -0
  67. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/noaa/storm_events.py +0 -0
  68. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/params.py +0 -0
  69. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/usgs/__init__.py +0 -0
  70. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/usgs/daily.py +0 -0
  71. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/providers/usgs/earthquakes.py +0 -0
  72. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/pull.py +0 -0
  73. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/py.typed +0 -0
  74. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/query.py +0 -0
  75. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/readers.py +0 -0
  76. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/registry.py +0 -0
  77. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/selection.py +0 -0
  78. {usdata-0.23.0 → usdata-0.25.0}/src/usdata/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usdata
3
- Version: 0.23.0
3
+ Version: 0.25.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
@@ -112,7 +112,7 @@ breaking changes.
112
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 |
113
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 |
114
114
 
115
- 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
116
116
  by agency and product family in the [catalog](docs/providers/README.md).
117
117
 
118
118
  ## How this compares
@@ -67,7 +67,7 @@ breaking changes.
67
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 |
68
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 |
69
69
 
70
- 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
71
71
  by agency and product family in the [catalog](docs/providers/README.md).
72
72
 
73
73
  ## How this compares
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usdata"
3
- version = "0.23.0"
3
+ version = "0.25.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.23.0"
3
+ version = "0.25.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"
@@ -209,30 +209,31 @@ datasets:
209
209
  system: noaa:goes-r
210
210
  domain: weather-satellites
211
211
  since: "0.8"
212
- title: GOES-R ABI CONUS Cloud and Moisture Imagery
212
+ title: GOES-R ABI Cloud and Moisture Imagery
213
213
  description: >-
214
- Single-channel CONUS Cloud and Moisture Imagery (ABI-L2-CMIPC) from
214
+ Single-channel CONUS (ABI-L2-CMIPC) and mesoscale (ABI-L2-CMIPM) imagery from
215
215
  GOES-16, 17, 18, and 19 in anonymous NOAA S3 buckets. Select an explicit
216
- satellite, channel, and scan-start interval; each asset is a complete
216
+ satellite, channel, scan-start interval, and M1 or M2 for mesoscale; each asset is a complete
217
217
  NetCDF scene with no geographic or variable subsetting.
218
- keywords: [satellite, imagery, goes, abi, clouds, infrared, reflectance, netcdf, conus]
218
+ keywords: [satellite, imagery, goes, abi, clouds, infrared, reflectance, netcdf, conus, mesoscale]
219
219
  protocol: s3
220
220
  homepage: https://registry.opendata.aws/noaa-goes/
221
221
  license: US Government Work (public domain)
222
222
  temporal_extent: { start: "2017-02-28T00:00:00Z" }
223
223
  capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false }
224
- summary: GOES CONUS imagery
224
+ summary: GOES CONUS and mesoscale imagery
225
225
  formats: [NetCDF4]
226
- selection: Whole single-channel CONUS scenes by inclusive UTC scan-start time
227
- inputs: Satellite, channel, and both timestamps
226
+ selection: Whole single-channel scenes by inclusive UTC scan-start time and explicit mesoscale sector
227
+ inputs: Satellite, channel, both timestamps; product and sector for mesoscale
228
228
  reader: netcdf
229
229
  guide: docs/providers/noaa-goes.md
230
230
  examples:
231
231
  - examples/goes-imagery/example.ipynb
232
+ - examples/goes-mesoscale/example.ipynb
232
233
  - examples/event-context/example.ipynb
233
234
  resolution:
234
235
  spatial: "0.5 km to 2 km at nadir, by ABI band"
235
- temporal: "One CONUS scan every 5 minutes on average"
236
+ temporal: "CONUS every 5 minutes; two mesoscale sectors every 60 seconds or one every 30 seconds"
236
237
  update_frequency: "New data is added as soon as it's available"
237
238
  citation: >-
238
239
  NOAA Geostationary Operational Environmental Satellites (GOES) 16, 17, 18 & 19 was
@@ -641,6 +642,47 @@ datasets:
641
642
  limits: { max_window: P366D }
642
643
  adapter: usdata.providers.noaa.coops:CoopsTidePredictions
643
644
 
645
+ - id: noaa:coops-currents
646
+ provider: noaa
647
+ status: available
648
+ system: noaa:coops
649
+ domain: ocean-physics
650
+ since: "0.24"
651
+ title: CO-OPS Observed Currents
652
+ description: >-
653
+ Native six-minute current speed and direction from the anonymous CO-OPS
654
+ Data API. Select one alphanumeric station, a positive bin, units, and a
655
+ UTC interval of at most 28 days. Raw CSV preserves gaps and blank values.
656
+ Bin depths can change between deployments; the CSV carries bin number,
657
+ not depth or a verification-quality flag. Predictions, all-bin queries,
658
+ station discovery, and resampling are not included.
659
+ keywords: [currents, current speed, current direction, coastal, ocean, coops, ports, stations, bin]
660
+ protocol: http
661
+ homepage: https://api.tidesandcurrents.noaa.gov/api/prod/
662
+ license: US Government Work (public domain)
663
+ capabilities: { spatial_subset: false, temporal_subset: true, variable_subset: false }
664
+ summary: Coastal current speed and direction
665
+ formats: [CSV]
666
+ selection: Native six-minute observations for one station and explicit bin; at most 28 days
667
+ inputs: Alphanumeric station, positive bin, and both minute-aligned timestamps
668
+ reader: pandas
669
+ guide: docs/providers/noaa-coops-currents.md
670
+ examples:
671
+ - examples/coastal-currents/example.ipynb
672
+ resolution:
673
+ spatial: One current-meter station and bin per request; bin depths depend on deployment
674
+ temporal: Six-minute observations
675
+ citation: >-
676
+ NOAA National Ocean Service, CO-OPS observed currents, accessed via usdata
677
+ terms: https://tidesandcurrents.noaa.gov/disclaimers.html
678
+ variables:
679
+ - { name: "Date Time", units: "UTC", description: "Observation time, requested with time_zone=gmt" }
680
+ - { name: "Speed", units: "cm/s", description: "Observed current speed in metric units; knots with english units" }
681
+ - { name: "Direction", units: "degrees", description: "Observed current direction" }
682
+ - { name: "Bin", description: "Requested bin number, not a depth measurement" }
683
+ limits: { max_window: P28D }
684
+ adapter: usdata.providers.noaa.coops:CoopsCurrents
685
+
644
686
  - id: noaa:gsom
645
687
  provider: noaa
646
688
  status: available
@@ -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,8 +1,9 @@
1
- """GOES ABI CONUS Cloud and Moisture Imagery from anonymous NOAA S3 buckets.
1
+ """GOES ABI Cloud and Moisture Imagery from anonymous NOAA S3 buckets.
2
2
 
3
3
  Require ``satellite`` (16, 17, 18, or 19), ``channel`` (1--16 or C01--C16), and
4
- both timestamps at most seven days apart. The optional ``product`` must be
5
- ``ABI-L2-CMIPC``. Select whole single-channel NetCDF files by inclusive
4
+ both timestamps at most seven days apart. ``product`` defaults to CONUS
5
+ ``ABI-L2-CMIPC``; ``ABI-L2-CMIPM`` requires ``sector=M1`` or ``M2``.
6
+ Select whole single-channel NetCDF files by inclusive
6
7
  scan-start time, never by scan overlap.
7
8
  Geographic and variable subsetting are not available for these archived files.
8
9
  ``list_scans`` is shared with the GLM adapter, which selects files under the
@@ -12,10 +13,10 @@ same bucket layout by the same inclusive start-time rule.
12
13
  from __future__ import annotations
13
14
 
14
15
  import re
15
- from collections.abc import Callable, Mapping
16
+ from collections.abc import Callable
16
17
  from datetime import UTC, datetime, timedelta
17
18
  from pathlib import Path
18
- from typing import Annotated
19
+ from typing import Annotated, Self
19
20
 
20
21
  import httpx
21
22
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@@ -24,13 +25,15 @@ from usdata.models import Asset, Protocol, Query, TimeRange
24
25
  from usdata.protocols import s3
25
26
  from usdata.providers.base import QueryError
26
27
  from usdata.providers.http import HttpProvider
27
- from usdata.providers.params import int_range
28
+ from usdata.providers.params import choice, int_range
28
29
 
29
30
  PRODUCT = "ABI-L2-CMIPC"
31
+ MESOSCALE_PRODUCT = "ABI-L2-CMIPM"
30
32
  PUBLIC_START = datetime(2017, 2, 28, tzinfo=UTC)
31
33
  MAX_WINDOW = timedelta(days=7)
32
34
  KEY_RE = re.compile(
33
- r"OR_ABI-L2-CMIPC-M[346]C(?P<channel>0[1-9]|1[0-6])_G(?P<satellite>1[6-9])"
35
+ r"OR_ABI-L2-CMIP(?P<sector>C|M[12])-M[346]C(?P<channel>0[1-9]|1[0-6])"
36
+ r"_G(?P<satellite>1[6-9])"
34
37
  r"_s(?P<start>\d{14})_e(?P<end>\d{14})_c\d{14}\.nc"
35
38
  )
36
39
 
@@ -54,7 +57,14 @@ class GoesAbiParams(BaseModel):
54
57
  channel: Annotated[int, int_range(1, 16)] = Field(
55
58
  description="Required ABI channel, 1 to 16 or C01 to C16."
56
59
  )
57
- product: str = Field(default=PRODUCT, description=f"ABI product; only {PRODUCT} is supported.")
60
+ product: Annotated[str, choice(PRODUCT, MESOSCALE_PRODUCT)] = Field(
61
+ default=PRODUCT,
62
+ description="ABI-L2-CMIPC (CONUS, default) or ABI-L2-CMIPM (mesoscale).",
63
+ )
64
+ sector: Annotated[str, choice("M1", "M2")] | None = Field(
65
+ default=None,
66
+ description="Required for ABI-L2-CMIPM: M1 or M2. Omit for CONUS.",
67
+ )
58
68
 
59
69
  @field_validator("channel", mode="before")
60
70
  @classmethod
@@ -63,13 +73,13 @@ class GoesAbiParams(BaseModel):
63
73
  text = value.strip() if isinstance(value, str) else value
64
74
  return text[1:] if isinstance(text, str) and text.startswith("C") else text
65
75
 
66
- @model_validator(mode="before")
67
- @classmethod
68
- def _one_supported_product(cls, data: object) -> object:
69
- """This adapter serves one ABI product, so name it rather than list the rest."""
70
- if isinstance(data, Mapping) and data.get("product", PRODUCT) != PRODUCT:
71
- raise ValueError(f"only product={PRODUCT} is supported")
72
- return data
76
+ @model_validator(mode="after")
77
+ def _sector_matches_product(self) -> Self:
78
+ if self.product == MESOSCALE_PRODUCT and self.sector is None:
79
+ raise ValueError("sector=M1 or M2 is required for product=ABI-L2-CMIPM")
80
+ if self.product == PRODUCT and self.sector is not None:
81
+ raise ValueError("sector is only supported with product=ABI-L2-CMIPM")
82
+ return self
73
83
 
74
84
 
75
85
  def list_scans(
@@ -120,7 +130,7 @@ def list_scans(
120
130
 
121
131
 
122
132
  class GoesAbi(HttpProvider):
123
- """Single-channel CONUS ABI imagery; params: satellite, channel, product."""
133
+ """Single-channel CONUS or mesoscale ABI imagery with explicit sector selection."""
124
134
 
125
135
  params_model = GoesAbiParams
126
136
 
@@ -139,16 +149,20 @@ class GoesAbi(HttpProvider):
139
149
  raise QueryError("GOES requests must span at most 7 days; split longer intervals")
140
150
  satellite, channel = params.satellite, params.channel
141
151
  if end < PUBLIC_START:
142
- raise QueryError("GOES CMIPC public observations begin on 2017-02-28")
152
+ raise QueryError("GOES ABI public observations begin on 2017-02-28")
143
153
  start = max(start, PUBLIC_START)
144
154
  return list_scans(
145
155
  self._http(),
146
156
  f"noaa-goes{satellite}",
147
- PRODUCT,
157
+ params.product,
148
158
  start,
149
159
  end,
150
160
  KEY_RE,
151
- lambda m: int(m["satellite"]) == satellite and int(m["channel"]) == channel,
161
+ lambda m: (
162
+ int(m["satellite"]) == satellite
163
+ and int(m["channel"]) == channel
164
+ and m["sector"] == (params.sector or "C")
165
+ ),
152
166
  self.dataset.id,
153
167
  )
154
168
 
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