pyopenmeteo 0.1.0__py3-none-any.whl

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 (41) hide show
  1. pyopenmeteo/__init__.py +78 -0
  2. pyopenmeteo/api/__init__.py +26 -0
  3. pyopenmeteo/api/air_quality.py +142 -0
  4. pyopenmeteo/api/climate.py +125 -0
  5. pyopenmeteo/api/ensemble.py +161 -0
  6. pyopenmeteo/api/flood.py +123 -0
  7. pyopenmeteo/api/forecast.py +220 -0
  8. pyopenmeteo/api/geo.py +213 -0
  9. pyopenmeteo/api/historical.py +153 -0
  10. pyopenmeteo/api/marine.py +174 -0
  11. pyopenmeteo/api/satellite.py +158 -0
  12. pyopenmeteo/api/seasonal.py +165 -0
  13. pyopenmeteo/core/__init__.py +27 -0
  14. pyopenmeteo/core/base_api.py +105 -0
  15. pyopenmeteo/core/exceptions.py +47 -0
  16. pyopenmeteo/core/http_client.py +76 -0
  17. pyopenmeteo/core/request_builder.py +33 -0
  18. pyopenmeteo/core/response.py +250 -0
  19. pyopenmeteo/formatters/__init__.py +8 -0
  20. pyopenmeteo/formatters/csv_fmt.py +39 -0
  21. pyopenmeteo/formatters/numpy_fmt.py +36 -0
  22. pyopenmeteo/formatters/pandas_fmt.py +60 -0
  23. pyopenmeteo/formatters/xarray_fmt.py +111 -0
  24. pyopenmeteo/params/__init__.py +17 -0
  25. pyopenmeteo/params/airquality_vars.py +55 -0
  26. pyopenmeteo/params/archive_vars.py +147 -0
  27. pyopenmeteo/params/climate_vars.py +31 -0
  28. pyopenmeteo/params/ensemble_vars.py +222 -0
  29. pyopenmeteo/params/flood_vars.py +19 -0
  30. pyopenmeteo/params/forecast_vars.py +236 -0
  31. pyopenmeteo/params/marine_vars.py +80 -0
  32. pyopenmeteo/params/pressure_levels.py +98 -0
  33. pyopenmeteo/params/satellite_vars.py +36 -0
  34. pyopenmeteo/params/seasonal_vars.py +256 -0
  35. pyopenmeteo/params/solar.py +57 -0
  36. pyopenmeteo/params/units.py +34 -0
  37. pyopenmeteo-0.1.0.dist-info/METADATA +548 -0
  38. pyopenmeteo-0.1.0.dist-info/RECORD +41 -0
  39. pyopenmeteo-0.1.0.dist-info/WHEEL +5 -0
  40. pyopenmeteo-0.1.0.dist-info/licenses/LICENSE.md +7 -0
  41. pyopenmeteo-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,78 @@
1
+ """pyopenmeteo - a Python wrapper for the Open-Meteo weather API family."""
2
+
3
+ from pyopenmeteo.api.forecast import ForecastAPI
4
+ from pyopenmeteo.api.historical import HistoricalAPI, HistoricalForecastAPI
5
+ from pyopenmeteo.api.geo import GeocodingAPI, ElevationAPI, Location
6
+ from pyopenmeteo.api.marine import MarineAPI
7
+ from pyopenmeteo.api.climate import ClimateAPI
8
+ from pyopenmeteo.api.air_quality import AirQualityAPI
9
+ from pyopenmeteo.api.ensemble import EnsembleAPI
10
+ from pyopenmeteo.api.flood import FloodAPI
11
+ from pyopenmeteo.api.satellite import SatelliteRadiationAPI
12
+ from pyopenmeteo.api.seasonal import SeasonalAPI
13
+ from pyopenmeteo.core.exceptions import (
14
+ MeteoPyError,
15
+ APIError,
16
+ RateLimitError,
17
+ ServerError,
18
+ GeocodingError,
19
+ LocationNotFoundError,
20
+ ValidationError,
21
+ ConnectionError,
22
+ )
23
+ from pyopenmeteo.core.response import WeatherResponse
24
+ from pyopenmeteo.params.units import (
25
+ TemperatureUnit,
26
+ WindSpeedUnit,
27
+ PrecipitationUnit,
28
+ TimeFormat,
29
+ CellSelection,
30
+ LengthUnit,
31
+ )
32
+ from pyopenmeteo.params.pressure_levels import PressureVar, pressure_level, pressure_levels_range
33
+ from pyopenmeteo.params.solar import PanelOrientation
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ __all__ = [
38
+ # APIs
39
+ "ForecastAPI",
40
+ "HistoricalAPI",
41
+ "HistoricalForecastAPI",
42
+ "GeocodingAPI",
43
+ "ElevationAPI",
44
+ "Location",
45
+ "MarineAPI",
46
+ "ClimateAPI",
47
+ "AirQualityAPI",
48
+ "EnsembleAPI",
49
+ "FloodAPI",
50
+ "SatelliteRadiationAPI",
51
+ "SeasonalAPI",
52
+ # Exceptions
53
+ "MeteoPyError",
54
+ "APIError",
55
+ "RateLimitError",
56
+ "ServerError",
57
+ "GeocodingError",
58
+ "LocationNotFoundError",
59
+ "ValidationError",
60
+ "ConnectionError",
61
+ # Response
62
+ "WeatherResponse",
63
+ # Units
64
+ "TemperatureUnit",
65
+ "WindSpeedUnit",
66
+ "PrecipitationUnit",
67
+ "TimeFormat",
68
+ "CellSelection",
69
+ "LengthUnit",
70
+ # Pressure levels
71
+ "PressureVar",
72
+ "pressure_level",
73
+ "pressure_levels_range",
74
+ # Solar
75
+ "PanelOrientation",
76
+ # Version
77
+ "__version__",
78
+ ]
@@ -0,0 +1,26 @@
1
+ from pyopenmeteo.api.forecast import ForecastAPI
2
+ from pyopenmeteo.api.historical import HistoricalAPI, HistoricalForecastAPI
3
+ from pyopenmeteo.api.geo import GeocodingAPI, ElevationAPI, Location
4
+ from pyopenmeteo.api.marine import MarineAPI
5
+ from pyopenmeteo.api.climate import ClimateAPI
6
+ from pyopenmeteo.api.air_quality import AirQualityAPI
7
+ from pyopenmeteo.api.ensemble import EnsembleAPI
8
+ from pyopenmeteo.api.flood import FloodAPI
9
+ from pyopenmeteo.api.satellite import SatelliteRadiationAPI
10
+ from pyopenmeteo.api.seasonal import SeasonalAPI
11
+
12
+ __all__ = [
13
+ "ForecastAPI",
14
+ "HistoricalAPI",
15
+ "HistoricalForecastAPI",
16
+ "GeocodingAPI",
17
+ "ElevationAPI",
18
+ "Location",
19
+ "MarineAPI",
20
+ "ClimateAPI",
21
+ "AirQualityAPI",
22
+ "EnsembleAPI",
23
+ "FloodAPI",
24
+ "SatelliteRadiationAPI",
25
+ "SeasonalAPI",
26
+ ]
@@ -0,0 +1,142 @@
1
+ from datetime import date
2
+
3
+ from pyopenmeteo.core.base_api import BaseAPI
4
+ from pyopenmeteo.core.exceptions import ValidationError
5
+ from pyopenmeteo.core.request_builder import RequestBuilder
6
+ from pyopenmeteo.core.response import WeatherResponse
7
+ from pyopenmeteo.params.airquality_vars import CurrentAirQualityVars, HourlyAirQualityVars
8
+ from pyopenmeteo.params.units import TimeFormat
9
+
10
+
11
+ class AirQualityAPI(BaseAPI):
12
+ """Wraps the OpenMeteo Air Quality API.
13
+
14
+ Provides hourly air quality forecasts and current conditions including
15
+ pollutant concentrations, pollen counts, and air quality indices for
16
+ any location worldwide.
17
+
18
+ Example:
19
+ >>> from pyopenmeteo.params.airquality_vars import HourlyAirQualityVars
20
+ >>> api = AirQualityAPI()
21
+ >>> resp = api.get(
22
+ ... (48.85, 2.35),
23
+ ... hourly=[HourlyAirQualityVars.PM2_5],
24
+ ... forecast_days=3,
25
+ ... )
26
+ """
27
+
28
+ BASE_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"
29
+
30
+ def get(
31
+ self,
32
+ location: tuple[float, float] | list[float] | str,
33
+ *,
34
+ hourly: list[HourlyAirQualityVars | str] | None = None,
35
+ current: list[CurrentAirQualityVars | str] | None = None,
36
+ timeformat: TimeFormat | str | None = None,
37
+ timezone: str | None = None,
38
+ past_days: int | None = None,
39
+ forecast_days: int | None = None,
40
+ past_hours: int | None = None,
41
+ forecast_hours: int | None = None,
42
+ start_date: str | date | None = None,
43
+ end_date: str | date | None = None,
44
+ domains: str | None = None,
45
+ ) -> WeatherResponse:
46
+ """Fetch air quality forecast data.
47
+
48
+ Args:
49
+ location: ``(latitude, longitude)`` tuple/list or a place-name
50
+ string resolved via the Geocoding API.
51
+ hourly: Hourly air quality variables to request.
52
+ current: Current-conditions air quality variables to request.
53
+ Accepts the same variable set as ``hourly``.
54
+ timeformat: ``iso8601`` (default) or ``unixtime``.
55
+ timezone: tz database string like ``"Europe/Paris"`` or
56
+ ``"auto"`` to infer from location.
57
+ past_days: Include this many past days in the response (0–92).
58
+ forecast_days: Days ahead to forecast (1–7).
59
+ past_hours: Past hours to include (alternative to ``past_days``).
60
+ forecast_hours: Hours ahead to forecast (alternative to
61
+ ``forecast_days``).
62
+ start_date: Start of an explicit date range (``YYYY-MM-DD``).
63
+ end_date: End of an explicit date range (``YYYY-MM-DD``).
64
+ Required when ``start_date`` is given.
65
+ domains: Air quality domain to use. One of ``"auto"``,
66
+ ``"cams_europe"``, ``"cams_global_reanalysis"``, or
67
+ ``"cams_global_forecast"``.
68
+
69
+ Returns:
70
+ A WeatherResponse wrapping the raw API JSON.
71
+
72
+ Raises:
73
+ ValidationError: If parameters fail pre-flight checks.
74
+ LocationNotFoundError: If a place-name string cannot be geocoded.
75
+ APIError: On an HTTP-level error from the server.
76
+ RateLimitError: On HTTP 429.
77
+ ServerError: On HTTP 5xx.
78
+ ConnectionError: If the server is unreachable.
79
+ """
80
+ self._validate(
81
+ hourly=hourly,
82
+ current=current,
83
+ past_days=past_days,
84
+ forecast_days=forecast_days,
85
+ start_date=start_date,
86
+ end_date=end_date,
87
+ )
88
+
89
+ lat, lon = self.resolve_location(location)
90
+
91
+ params = RequestBuilder(
92
+ latitude=lat,
93
+ longitude=lon,
94
+ hourly=hourly,
95
+ current=current,
96
+ timeformat=timeformat,
97
+ timezone=timezone,
98
+ past_days=past_days,
99
+ forecast_days=forecast_days,
100
+ past_hours=past_hours,
101
+ forecast_hours=forecast_hours,
102
+ start_date=str(start_date) if start_date is not None else None,
103
+ end_date=str(end_date) if end_date is not None else None,
104
+ domains=domains,
105
+ ).build()
106
+
107
+ return self._get(params)
108
+
109
+ def _validate(
110
+ self,
111
+ hourly,
112
+ current,
113
+ past_days,
114
+ forecast_days,
115
+ start_date,
116
+ end_date,
117
+ ) -> None:
118
+ """Validate parameters before making the API request.
119
+
120
+ Args:
121
+ hourly: Hourly variables requested.
122
+ current: Current-conditions variables requested.
123
+ past_days: Number of past days to include.
124
+ forecast_days: Number of forecast days.
125
+ start_date: Explicit start date.
126
+ end_date: Explicit end date.
127
+
128
+ Raises:
129
+ ValidationError: If any parameter is invalid.
130
+ """
131
+ if not any([hourly, current]):
132
+ raise ValidationError(
133
+ "At least one of hourly or current must be specified."
134
+ )
135
+ if past_days is not None and not (0 <= past_days <= 92):
136
+ raise ValidationError("past_days must be between 0 and 92.")
137
+ if forecast_days is not None and not (1 <= forecast_days <= 7):
138
+ raise ValidationError("forecast_days must be between 1 and 7.")
139
+ if (start_date is None) != (end_date is None):
140
+ raise ValidationError(
141
+ "start_date and end_date must both be provided or both omitted."
142
+ )
@@ -0,0 +1,125 @@
1
+ from datetime import date
2
+
3
+ from pyopenmeteo.core.base_api import BaseAPI
4
+ from pyopenmeteo.core.exceptions import ValidationError
5
+ from pyopenmeteo.core.request_builder import RequestBuilder
6
+ from pyopenmeteo.core.response import WeatherResponse
7
+ from pyopenmeteo.params.climate_vars import ClimateModels, DailyClimateVars
8
+ from pyopenmeteo.params.units import CellSelection, PrecipitationUnit, TemperatureUnit, TimeFormat, WindSpeedUnit
9
+
10
+
11
+ class ClimateAPI(BaseAPI):
12
+ """Wraps the OpenMeteo Climate Change API.
13
+
14
+ Provides daily climate projections (1950–2100) based on CMIP6 models
15
+ downscaled to 10 km resolution. Requires an explicit date range and
16
+ daily variables.
17
+
18
+ Example:
19
+ >>> from pyopenmeteo.params.climate_vars import DailyClimateVars, ClimateModels
20
+ >>> api = ClimateAPI()
21
+ >>> resp = api.get(
22
+ ... (40.71, -74.01),
23
+ ... daily=[DailyClimateVars.TEMPERATURE_2M_MAX],
24
+ ... models=[ClimateModels.MRI_AGCM3_2_S],
25
+ ... start_date="2050-01-01",
26
+ ... end_date="2050-12-31",
27
+ ... )
28
+ """
29
+
30
+ BASE_URL = "https://climate-api.open-meteo.com/v1/climate"
31
+
32
+ def get(
33
+ self,
34
+ location: tuple[float, float] | list[float] | str,
35
+ *,
36
+ daily: list[DailyClimateVars | str],
37
+ start_date: str | date,
38
+ end_date: str | date,
39
+ models: list[ClimateModels | str] | ClimateModels | str | None = None,
40
+ temperature_unit: TemperatureUnit | str | None = None,
41
+ wind_speed_unit: WindSpeedUnit | str | None = None,
42
+ precipitation_unit: PrecipitationUnit | str | None = None,
43
+ timeformat: TimeFormat | str | None = None,
44
+ timezone: str | None = None,
45
+ cell_selection: CellSelection | str | None = None,
46
+ ) -> WeatherResponse:
47
+ """Fetch climate projection data.
48
+
49
+ Args:
50
+ location: ``(latitude, longitude)`` tuple/list or a place-name
51
+ string resolved via the Geocoding API.
52
+ daily: Daily variables to request. Required.
53
+ start_date: Start of the date range (``YYYY-MM-DD``). Required.
54
+ end_date: End of the date range (``YYYY-MM-DD``). Required.
55
+ models: One or more CMIP6 climate models to use. Accepts a single
56
+ value or a list.
57
+ temperature_unit: ``celsius`` (default) or ``fahrenheit``.
58
+ wind_speed_unit: ``kmh`` (default), ``ms``, ``mph``, or ``kn``.
59
+ precipitation_unit: ``mm`` (default) or ``inch``.
60
+ timeformat: ``iso8601`` (default) or ``unixtime``.
61
+ timezone: tz database string like ``"America/New_York"`` or
62
+ ``"auto"`` to infer from location.
63
+ cell_selection: ``land`` (default), ``sea``, or ``nearest``.
64
+
65
+ Returns:
66
+ A WeatherResponse wrapping the raw API JSON.
67
+
68
+ Raises:
69
+ ValidationError: If parameters fail pre-flight checks.
70
+ LocationNotFoundError: If a place-name string cannot be geocoded.
71
+ APIError: On an HTTP-level error from the server.
72
+ RateLimitError: On HTTP 429.
73
+ ServerError: On HTTP 5xx.
74
+ ConnectionError: If the server is unreachable.
75
+ """
76
+ self._validate(
77
+ daily=daily,
78
+ start_date=start_date,
79
+ end_date=end_date,
80
+ )
81
+
82
+ lat, lon = self.resolve_location(location)
83
+
84
+ if isinstance(models, (str, ClimateModels)):
85
+ models = [models]
86
+
87
+ params = RequestBuilder(
88
+ latitude=lat,
89
+ longitude=lon,
90
+ daily=daily,
91
+ start_date=str(start_date) if start_date is not None else None,
92
+ end_date=str(end_date) if end_date is not None else None,
93
+ models=models,
94
+ temperature_unit=temperature_unit,
95
+ wind_speed_unit=wind_speed_unit,
96
+ precipitation_unit=precipitation_unit,
97
+ timeformat=timeformat,
98
+ timezone=timezone,
99
+ cell_selection=cell_selection,
100
+ ).build()
101
+
102
+ return self._get(params)
103
+
104
+ def _validate(
105
+ self,
106
+ daily,
107
+ start_date,
108
+ end_date,
109
+ ) -> None:
110
+ """Validate parameters before making the API request.
111
+
112
+ Args:
113
+ daily: Daily variables requested.
114
+ start_date: Explicit start date.
115
+ end_date: Explicit end date.
116
+
117
+ Raises:
118
+ ValidationError: If any parameter is invalid.
119
+ """
120
+ if not daily:
121
+ raise ValidationError("daily variables are required for the Climate API.")
122
+ if start_date is None or end_date is None:
123
+ raise ValidationError(
124
+ "start_date and end_date are both required for the Climate API."
125
+ )
@@ -0,0 +1,161 @@
1
+ from datetime import date
2
+
3
+ from pyopenmeteo.core.base_api import BaseAPI
4
+ from pyopenmeteo.core.exceptions import ValidationError
5
+ from pyopenmeteo.core.request_builder import RequestBuilder
6
+ from pyopenmeteo.core.response import WeatherResponse
7
+ from pyopenmeteo.params.ensemble_vars import DailyEnsembleVars, EnsembleModels, HourlyEnsembleVars
8
+ from pyopenmeteo.params.units import CellSelection, PrecipitationUnit, TemperatureUnit, TimeFormat, WindSpeedUnit
9
+
10
+
11
+ class EnsembleAPI(BaseAPI):
12
+ """Wraps the OpenMeteo Ensemble Forecast API.
13
+
14
+ Provides probabilistic weather forecasts from ensemble model runs,
15
+ with up to 35 forecast days of hourly or daily data. A ``models``
16
+ selection is required.
17
+
18
+ Example:
19
+ >>> from pyopenmeteo.params.ensemble_vars import HourlyEnsembleVars, EnsembleModels
20
+ >>> api = EnsembleAPI()
21
+ >>> resp = api.get(
22
+ ... (48.85, 2.35),
23
+ ... hourly=[HourlyEnsembleVars.TEMPERATURE_2M],
24
+ ... models=EnsembleModels.ICON_EU_EPS,
25
+ ... forecast_days=7,
26
+ ... )
27
+ """
28
+
29
+ BASE_URL = "https://ensemble-api.open-meteo.com/v1/ensemble"
30
+
31
+ def get(
32
+ self,
33
+ location: tuple[float, float] | list[float] | str,
34
+ *,
35
+ hourly: list[HourlyEnsembleVars | str] | None = None,
36
+ daily: list[DailyEnsembleVars | str] | None = None,
37
+ models: list[EnsembleModels | str] | EnsembleModels | str,
38
+ temperature_unit: TemperatureUnit | str | None = None,
39
+ wind_speed_unit: WindSpeedUnit | str | None = None,
40
+ precipitation_unit: PrecipitationUnit | str | None = None,
41
+ timeformat: TimeFormat | str | None = None,
42
+ timezone: str | None = None,
43
+ past_days: int | None = None,
44
+ forecast_days: int | None = None,
45
+ past_hours: int | None = None,
46
+ forecast_hours: int | None = None,
47
+ start_date: str | date | None = None,
48
+ end_date: str | date | None = None,
49
+ cell_selection: CellSelection | str | None = None,
50
+ ) -> WeatherResponse:
51
+ """Fetch ensemble weather forecast data.
52
+
53
+ Args:
54
+ location: ``(latitude, longitude)`` tuple/list or a place-name
55
+ string resolved via the Geocoding API.
56
+ hourly: Hourly variables to request.
57
+ daily: Daily aggregate variables to request.
58
+ models: One or more ensemble models to use. Required. Accepts a
59
+ single value or a list.
60
+ temperature_unit: ``celsius`` (default) or ``fahrenheit``.
61
+ wind_speed_unit: ``kmh`` (default), ``ms``, ``mph``, or ``kn``.
62
+ precipitation_unit: ``mm`` (default) or ``inch``.
63
+ timeformat: ``iso8601`` (default) or ``unixtime``.
64
+ timezone: tz database string like ``"Europe/Paris"`` or ``"auto"``
65
+ to infer from location.
66
+ past_days: Include this many past days in the response (0–92).
67
+ forecast_days: Days ahead to forecast (1–35).
68
+ past_hours: Past hours to include (alternative to ``past_days``).
69
+ forecast_hours: Hours ahead to forecast (alternative to
70
+ ``forecast_days``).
71
+ start_date: Start of an explicit date range (``YYYY-MM-DD``).
72
+ end_date: End of an explicit date range (``YYYY-MM-DD``).
73
+ Required when ``start_date`` is given.
74
+ cell_selection: ``land`` (default), ``sea``, or ``nearest``.
75
+
76
+ Returns:
77
+ A WeatherResponse wrapping the raw API JSON.
78
+
79
+ Raises:
80
+ ValidationError: If parameters fail pre-flight checks.
81
+ LocationNotFoundError: If a place-name string cannot be geocoded.
82
+ APIError: On an HTTP-level error from the server.
83
+ RateLimitError: On HTTP 429.
84
+ ServerError: On HTTP 5xx.
85
+ ConnectionError: If the server is unreachable.
86
+ """
87
+ self._validate(
88
+ hourly=hourly,
89
+ daily=daily,
90
+ models=models,
91
+ past_days=past_days,
92
+ forecast_days=forecast_days,
93
+ start_date=start_date,
94
+ end_date=end_date,
95
+ )
96
+
97
+ lat, lon = self.resolve_location(location)
98
+
99
+ if isinstance(models, (str, EnsembleModels)):
100
+ models = [models]
101
+
102
+ params = RequestBuilder(
103
+ latitude=lat,
104
+ longitude=lon,
105
+ hourly=hourly,
106
+ daily=daily,
107
+ models=models,
108
+ temperature_unit=temperature_unit,
109
+ wind_speed_unit=wind_speed_unit,
110
+ precipitation_unit=precipitation_unit,
111
+ timeformat=timeformat,
112
+ timezone=timezone,
113
+ past_days=past_days,
114
+ forecast_days=forecast_days,
115
+ past_hours=past_hours,
116
+ forecast_hours=forecast_hours,
117
+ start_date=str(start_date) if start_date is not None else None,
118
+ end_date=str(end_date) if end_date is not None else None,
119
+ cell_selection=cell_selection,
120
+ ).build()
121
+
122
+ return self._get(params)
123
+
124
+ def _validate(
125
+ self,
126
+ hourly,
127
+ daily,
128
+ models,
129
+ past_days,
130
+ forecast_days,
131
+ start_date,
132
+ end_date,
133
+ ) -> None:
134
+ """Validate parameters before making the API request.
135
+
136
+ Args:
137
+ hourly: Hourly variables requested.
138
+ daily: Daily variables requested.
139
+ models: Ensemble model(s) requested.
140
+ past_days: Number of past days to include.
141
+ forecast_days: Number of forecast days.
142
+ start_date: Explicit start date.
143
+ end_date: Explicit end date.
144
+
145
+ Raises:
146
+ ValidationError: If any parameter is invalid.
147
+ """
148
+ if not any([hourly, daily]):
149
+ raise ValidationError(
150
+ "At least one of hourly or daily must be specified."
151
+ )
152
+ if not models:
153
+ raise ValidationError("models is required for the Ensemble API.")
154
+ if past_days is not None and not (0 <= past_days <= 92):
155
+ raise ValidationError("past_days must be between 0 and 92.")
156
+ if forecast_days is not None and not (1 <= forecast_days <= 35):
157
+ raise ValidationError("forecast_days must be between 1 and 35.")
158
+ if (start_date is None) != (end_date is None):
159
+ raise ValidationError(
160
+ "start_date and end_date must both be provided or both omitted."
161
+ )
@@ -0,0 +1,123 @@
1
+ from datetime import date
2
+
3
+ from pyopenmeteo.core.base_api import BaseAPI
4
+ from pyopenmeteo.core.exceptions import ValidationError
5
+ from pyopenmeteo.core.request_builder import RequestBuilder
6
+ from pyopenmeteo.core.response import WeatherResponse
7
+ from pyopenmeteo.params.flood_vars import DailyFloodVars, FloodModels
8
+
9
+
10
+ class FloodAPI(BaseAPI):
11
+ """Wraps the OpenMeteo Global Flood API.
12
+
13
+ Provides daily river discharge forecasts with up to 210 forecast days.
14
+ Ensemble mode returns individual ensemble member columns in the response.
15
+
16
+ Example:
17
+ >>> from pyopenmeteo.params.flood_vars import DailyFloodVars
18
+ >>> api = FloodAPI()
19
+ >>> resp = api.get(
20
+ ... (48.20, 16.37),
21
+ ... daily=[DailyFloodVars.RIVER_DISCHARGE],
22
+ ... forecast_days=30,
23
+ ... )
24
+ """
25
+
26
+ BASE_URL = "https://flood-api.open-meteo.com/v1/flood"
27
+
28
+ def get(
29
+ self,
30
+ location: tuple[float, float] | list[float] | str,
31
+ *,
32
+ daily: list[DailyFloodVars | str],
33
+ past_days: int | None = None,
34
+ forecast_days: int | None = None,
35
+ start_date: str | date | None = None,
36
+ end_date: str | date | None = None,
37
+ models: list[FloodModels | str] | FloodModels | str | None = None,
38
+ ensemble: bool | None = None,
39
+ ) -> WeatherResponse:
40
+ """Fetch river discharge flood forecast data.
41
+
42
+ Args:
43
+ location: ``(latitude, longitude)`` tuple/list or a place-name
44
+ string resolved via the Geocoding API.
45
+ daily: Daily flood variables to request. Required.
46
+ past_days: Include this many past days in the response (0–92).
47
+ forecast_days: Days ahead to forecast (1–210).
48
+ start_date: Start of an explicit date range (``YYYY-MM-DD``).
49
+ end_date: End of an explicit date range (``YYYY-MM-DD``).
50
+ Required when ``start_date`` is given.
51
+ models: One or more hydrological models to use. Accepts a single
52
+ value or a list.
53
+ ensemble: When ``True``, returns all ensemble members as separate
54
+ columns in the response.
55
+
56
+ Returns:
57
+ A WeatherResponse wrapping the raw API JSON.
58
+
59
+ Raises:
60
+ ValidationError: If parameters fail pre-flight checks.
61
+ LocationNotFoundError: If a place-name string cannot be geocoded.
62
+ APIError: On an HTTP-level error from the server.
63
+ RateLimitError: On HTTP 429.
64
+ ServerError: On HTTP 5xx.
65
+ ConnectionError: If the server is unreachable.
66
+ """
67
+ self._validate(
68
+ daily=daily,
69
+ past_days=past_days,
70
+ forecast_days=forecast_days,
71
+ start_date=start_date,
72
+ end_date=end_date,
73
+ )
74
+
75
+ lat, lon = self.resolve_location(location)
76
+
77
+ if isinstance(models, (str, FloodModels)):
78
+ models = [models]
79
+
80
+ params = RequestBuilder(
81
+ latitude=lat,
82
+ longitude=lon,
83
+ daily=daily,
84
+ past_days=past_days,
85
+ forecast_days=forecast_days,
86
+ start_date=str(start_date) if start_date is not None else None,
87
+ end_date=str(end_date) if end_date is not None else None,
88
+ models=models,
89
+ ensemble=ensemble,
90
+ ).build()
91
+
92
+ return self._get(params)
93
+
94
+ def _validate(
95
+ self,
96
+ daily,
97
+ past_days,
98
+ forecast_days,
99
+ start_date,
100
+ end_date,
101
+ ) -> None:
102
+ """Validate parameters before making the API request.
103
+
104
+ Args:
105
+ daily: Daily variables requested.
106
+ past_days: Number of past days to include.
107
+ forecast_days: Number of forecast days.
108
+ start_date: Explicit start date.
109
+ end_date: Explicit end date.
110
+
111
+ Raises:
112
+ ValidationError: If any parameter is invalid.
113
+ """
114
+ if not daily:
115
+ raise ValidationError("daily variables are required for the Flood API.")
116
+ if past_days is not None and not (0 <= past_days <= 92):
117
+ raise ValidationError("past_days must be between 0 and 92.")
118
+ if forecast_days is not None and not (1 <= forecast_days <= 210):
119
+ raise ValidationError("forecast_days must be between 1 and 210.")
120
+ if (start_date is None) != (end_date is None):
121
+ raise ValidationError(
122
+ "start_date and end_date must both be provided or both omitted."
123
+ )