diff --git a/README.md b/README.md index 31d55ed..b229305 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ **Meteole** is a Python library designed to simplify accessing weather data from the Météo-France APIs. It provides: - **Automated token management**: Simplify authentication with a single `application_id`. -- **Unified model usage**: AROME and ARPEGE forecasts with a consistent interface. +- **Unified model usage**: AROME, AROME INSTANTANE, ARPEGE, PIAF forecasts with a consistent interface. - **User-friendly parameter handling**: Intuitive management of key weather forecasting parameters. - **Seamless data integration**: Directly export forecasts as Pandas DataFrames - **Vigilance bulletins**: Retrieve real-time weather warnings across France. @@ -44,17 +44,17 @@ pip install meteole ### Step 1: Obtain an API token or key -Create an account on [the Météo-France API portal](https://portail-api.meteofrance.fr/). Next, subscribe to the desired APIs (Arome, Arpege, etc.). Retrieve the API token (or key) by going to “Mes APIs” and then “Générer token”. +Create an account on [the Météo-France API portal](https://portail-api.meteofrance.fr/). Next, subscribe to the desired APIs (Arome, Arpege, Arome Instantané, etc.). Retrieve the API token (or key) by going to “Mes APIs” and then “Générer token”. -### Step 2: Fetch Forecasts from AROME and ARPEGE +### Step 2: Fetch Forecasts -Meteole allows you to retrieve forecasts for a wide range of weather indicators. Here's how to get started with AROME and ARPEGE: +Meteole allows you to retrieve forecasts for a wide range of weather indicators. Here's how to get started: -| Characteristics | AROME | ARPEGE | -|------------------|----------------------|----------------------| -| Resolution | 1.3 km | 10 km | -| Update Frequency | Every 3 hours | Every 6 hours | -| Forecast Range | Up to 51 hours | Up to 114 hours | +| Characteristics | AROME | ARPEGE | AROME INSTANTANE | PIAF | +|------------------|----------------------|-----------------------------| -------------------------------| -------------------------------| +| Resolution | 1.3 km | 10 km | 1.3 km | 1.3 km | +| Update Frequency | Every 3 hours | Every 6 hours | Every 1 hour | Every 10 minutes | +| Forecast Range | Every hour, up to 51 hours | Every hour, up to 114 hours | Every 15 minutes, up to 360 minutes | Every 5 minutes, up to 195 minutes | *note : the date of the run cannot be more than 4 days in the past. Consequently, change the date of the run in the example below.* @@ -77,8 +77,10 @@ print(arome_client.INDICATORS) df_arome = arome_client.get_coverage( indicator="V_COMPONENT_OF_WIND_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", # Optional: if not, you have to fill coverage_id run="2025-01-10T00.00.00Z", # Optional: forecast start time - interval=None, # Optional: time range for predictions - forecast_horizons=[1, 2], # Optional: prediction times (in hours) + forecast_horizons=[ # Optional: prediction times (in hours) + dt.timedelta(hours=1), + dt.timedelta(hours=2), + ], heights=[10], # Optional: height above ground level pressures=None, # Optional: pressure level long = (-5.1413, 9.5602), # Optional: longitude @@ -89,7 +91,7 @@ df_arome = arome_client.get_coverage( ``` Note: The coverage_id can be used instead of indicator, run, and interval. -The usage of ARPEGE is identical to AROME, except that you initialize the `ArpegeForecast` class +The usage of ARPEGE, AROME INSTANTANE, PIAF is identical to AROME, except that you initialize the appropriate class ### Step 3: Explore Parameters and Indicators #### Discover Available Indicators diff --git a/docs/pages/coverage_parameters.md b/docs/pages/coverage_parameters.md index 68588c7..ba77294 100644 --- a/docs/pages/coverage_parameters.md +++ b/docs/pages/coverage_parameters.md @@ -1,6 +1,6 @@ Weather forecasts from Météo-France APIs are based on key parameters that vary from indicator to indicator, which makes them complex to use. -Understanding coverages is a must to have a comprehensive usage of Météo-France forecasting models like AROME or ARPEGE. +Understanding coverages is a must to have a comprehensive usage of Météo-France forecasting models like AROME, AROME INSTANTANE, ARPEGE or PIAF. ## Coverage_id @@ -38,7 +38,7 @@ When no interval is specified, it means coverage returns a single datapoint inst ## Others parameters ### Forecast_horizons The time of day to which the prediction corresponds must be specified. For example, for a run of 12:00, in 1 hour's time, we have the weather indicator prediction of 13:00. -The `get_coverage method` takes as (optional) parameter the list of desired forecast hours, named `forecast_horizons`. +The `get_coverage method` takes as (optional) parameter the list of desired forecast hours (in `dt.timedelta` format), named `forecast_horizons`. To get the list of available `forecast_horizons`, use the function `get_coverage_description` as described in the example below. diff --git a/docs/pages/how_to.md b/docs/pages/how_to.md index 2dc13ec..ef8c419 100644 --- a/docs/pages/how_to.md +++ b/docs/pages/how_to.md @@ -44,15 +44,15 @@ client.get_vignette() > More details about Vigilance Bulletin in [the official Meteo France Documentation](https://donneespubliques.meteofrance.fr/?fond=produit&id_produit=305&id_rubrique=50) -## Get AROME or ARPEGE data +## Get data -Meteole allows you to retrieve forecasts for a wide range of weather indicators. Here's how to get started with AROME and ARPEGE: +Meteole allows you to retrieve forecasts for a wide range of weather indicators. Here's how to get started with AROME, AROME INSTANTANE, ARPEGE or PIAF: -| Characteristics | AROME | ARPEGE | -|------------------|----------------------|----------------------| -| Resolution | 1.3 km | 10 km | -| Update Frequency | Every 3 hours | Every 6 hours | -| Forecast Range | Up to 51 hours | Up to 114 hours | +| Characteristics | AROME | ARPEGE | AROME INSTANTANE | PIAF | +|------------------|----------------------------|-----------------------------|--------------------------------| -------------------------------| +| Resolution | 1.3 km | 10 km | 1.3 km | 1.3 km | +| Update Frequency | Every 3 hours | Every 6 hours | Every 1 hour | Every 10 minutes | +| Forecast Range | Every hour, up to 51 hours | Every hour, up to 114 hours | Every 15 minutes, up to 360 minutes | Every 5 minutes, up to 195 minutes | *note : the date of the run cannot be more than 4 days in the past. Consequently, change the date of the run in the example below.* @@ -75,7 +75,10 @@ df_arome = arome_client.get_coverage( indicator="V_COMPONENT_OF_WIND_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", # Optional: if not, you have to fill coverage_id run="2025-01-10T00.00.00Z", # Optional: forecast start time interval=None, # Optional: time range for predictions - forecast_horizons=[1, 2], # Optional: prediction times (in hours) + forecast_horizons=[ + dt.timedelta(hours=1), + dt.timedelta(hours=2), + ], # Optional: prediction times (in hours) heights=[10], # Optional: height above ground level pressures=None, # Optional: pressure level long = (-5.1413, 9.5602), # Optional: longitude diff --git a/src/meteole/__init__.py b/src/meteole/__init__.py index 1d4015f..ce6d8e3 100644 --- a/src/meteole/__init__.py +++ b/src/meteole/__init__.py @@ -1,9 +1,11 @@ from importlib.metadata import version from meteole._arome import AromeForecast +from meteole._arome_instantane import AromePIForecast from meteole._arpege import ArpegeForecast +from meteole._piaf import PiafForecast from meteole._vigilance import Vigilance -__all__ = ["AromeForecast", "ArpegeForecast", "Vigilance"] +__all__ = ["AromeForecast", "AromePIForecast", "ArpegeForecast", "PiafForecast", "Vigilance"] __version__ = version("meteole") diff --git a/src/meteole/_arome_instantane.py b/src/meteole/_arome_instantane.py new file mode 100644 index 0000000..2c750f5 --- /dev/null +++ b/src/meteole/_arome_instantane.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import logging +from typing import final + +from meteole.clients import BaseClient, MeteoFranceClient +from meteole.forecast import WeatherForecast + +logger = logging.getLogger(__name__) + +AVAILABLE_AROME_TERRITORY: list[str] = [ + "FRANCE", +] + +AROMEPI_INSTANT_INDICATORS: list[str] = [ + "TPW_27315_HEIGHT__LEVEL_OF_ADIABATIC_CONDESATION", + "TPW_27415_HEIGHT__LEVEL_OF_ADIABATIC_CONDESATION ", # with a space at the end (cf API...) + "TPW_27465_HEIGHT__LEVEL_OF_ADIABATIC_CONDESATION", + "BRIGHTNESS_TEMPERATURE__GROUND_OR_WATER_SURFACE", + "CONVECTIVE_AVAILABLE_POTENTIAL_ENERGY__GROUND_OR_WATER_SURFACE", + "DIAG_GRELE__GROUND_OR_WATER_SURFACE", + "WIND_SPEED_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "RELATIVE_HUMIDITY__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "MOCON__GROUND_OR_WATER_SURFACE", + "LOW_CLOUD_COVER__GROUND_OR_WATER_SURFACE", + "SEVERE_PRECIPITATION_TYPE_15_MIN__GROUND_OR_WATER_SURFACE", + "PRECIPITATION_TYPE_15_MIN__GROUND_OR_WATER_SURFACE", + "PRESSURE__SEA_SURFACE", + "REFLECTIVITY_MAX_DBZ__GROUND_OR_WATER_SURFACE", + "DEW_POINT_TEMPERATURE__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "TOTAL_PRECIPITATION_RATE__GROUND_OR_WATER_SURFACE", + "TEMPERATURE__GROUND_OR_WATER_SURFACE", + "TEMPERATURE__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "U_COMPONENT_OF_WIND_GUST_15MIN__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "U_COMPONENT_OF_WIND_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "VISIBILITY_MINI_PRECIP_15MIN__GROUND_OR_WATER_SURFACE", + "VISIBILITY_MINI_15MIN__GROUND_OR_WATER_SURFACE", + "V_COMPONENT_OF_WIND_GUST_15MIN__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "V_COMPONENT_OF_WIND_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "WETB_TEMPERATURE__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", +] + + +AROMEPI_OTHER_INDICATORS: list[str] = [ + "TOTAL_WATER_PRECIPITATION__GROUND_OR_WATER_SURFACE", + "TOTAL_SNOW_PRECIPITATION__GROUND_OR_WATER_SURFACE", + "TOTAL_PRECIPITATION__GROUND_OR_WATER_SURFACE", + "WIND_SPEED_GUST_15MIN__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "WIND_SPEED_MAXIMUM_GUST__SPECIFIC_HEIGHT_LEVEL_ABOVE_GROUND", + "GRAUPEL__GROUND_OR_WATER_SURFACE", + "HAIL__GROUND_OR_WATER_SURFACE", + "SOLID_PRECIPITATION__GROUND_OR_WATER_SURFACE", +] + + +@final +class AromePIForecast(WeatherForecast): + """Access the AROME numerical weather forecast data from Meteo-France API. + + Doc: + - https://portail-api.meteofrance.fr/web/fr/api/arome + + Attributes: + territory: Covered area (e.g., FRANCE, ANTIL, ...). + precision: Precision value of the forecast. + capabilities: DataFrame containing details on all available coverage ids. + """ + + # Model constants + MODEL_NAME: str = "aromepi" + INDICATORS: list[str] = AROMEPI_INSTANT_INDICATORS + AROMEPI_OTHER_INDICATORS + INSTANT_INDICATORS: list[str] = AROMEPI_INSTANT_INDICATORS + BASE_ENTRY_POINT: str = "wcs/MF-NWP-HIGHRES-AROMEPI" + DEFAULT_TERRITORY: str = "FRANCE" + DEFAULT_PRECISION: float = 0.01 + CLIENT_CLASS: type[BaseClient] = MeteoFranceClient + + def _validate_parameters(self) -> None: + """Check the territory and the precision parameters. + + Raise: + ValueError: At least, one parameter is not good. + """ + if self.precision not in [0.01, 0.025]: + raise ValueError("Parameter `precision` must be in (0.01, 0.025). It is inferred from argument `territory`") + + if self.territory not in AVAILABLE_AROME_TERRITORY: + raise ValueError(f"Parameter `territory` must be in {AVAILABLE_AROME_TERRITORY}") diff --git a/src/meteole/_piaf.py b/src/meteole/_piaf.py new file mode 100644 index 0000000..39a9692 --- /dev/null +++ b/src/meteole/_piaf.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import logging +from typing import Any, final + +from meteole.clients import BaseClient, MeteoFranceClient +from meteole.forecast import WeatherForecast + +logger = logging.getLogger(__name__) + +AVAILABLE_PIAF_TERRITORY: list[str] = [ + "FRANCE", +] + +PIAF_INSTANT_INDICATORS: list[str] = [] + + +PIAF_OTHER_INDICATORS: list[str] = [ + "TOTAL_PRECIPITATION_RATE__GROUND_OR_WATER_SURFACE", +] + + +@final +class PiafForecast(WeatherForecast): + """Access the PIAF numerical weather forecast data from Meteo-France API. + + Doc: + - https://portail-api.meteofrance.fr/web/fr/api/arome + + Attributes: + territory: Covered area (e.g., FRANCE, ANTIL, ...). + precision: Precision value of the forecast. + capabilities: DataFrame containing details on all available coverage ids. + """ + + # Model constants + MODEL_NAME: str = "piaf" + INDICATORS: list[str] = PIAF_INSTANT_INDICATORS + PIAF_OTHER_INDICATORS + INSTANT_INDICATORS: list[str] = PIAF_INSTANT_INDICATORS + BASE_ENTRY_POINT: str = "wcs/MF-NWP-HIGHRES-PIAF" + DEFAULT_TERRITORY: str = "FRANCE" + DEFAULT_PRECISION: float = 0.01 + CLIENT_CLASS: type[BaseClient] = MeteoFranceClient + + def __init__( + self, + **kwargs: Any, + ): + """Initialize attributes. + + Args: + api_key: The API key for authentication. Defaults to None. + token: The API token for authentication. Defaults to None. + application_id: The Application ID for authentication. Defaults to None. + """ + super().__init__(api_base_url="https://api.meteofrance.fr/pro/", **kwargs) + + def _validate_parameters(self) -> None: + """Check the territory and the precision parameters. + + Raise: + ValueError: At least, one parameter is not good. + """ + if self.precision != 0.01: + raise ValueError("Parameter `precision` must be in 0.01.") + + if self.territory not in AVAILABLE_PIAF_TERRITORY: + raise ValueError(f"Parameter `territory` must be in {AVAILABLE_PIAF_TERRITORY}") diff --git a/src/meteole/clients.py b/src/meteole/clients.py index 2e51104..dc7ac5a 100644 --- a/src/meteole/clients.py +++ b/src/meteole/clients.py @@ -61,7 +61,6 @@ class MeteoFranceClient(BaseClient): """ # Class constants - API_BASE_URL: str = "https://public-api.meteofrance.fr/public/" TOKEN_URL: str = "https://portail-api.meteofrance.fr/token" # noqa: S105 GET_TOKEN_TIMEOUT_SEC: int = 10 INVALID_JWT_ERROR_CODE: str = "900901" @@ -71,6 +70,7 @@ def __init__( self, *, token: str | None = None, + api_base_url: str = "https://public-api.meteofrance.fr/public/", # need it as an argument since PIAF model has a different base URL api_key: str | None = None, application_id: str | None = None, certs_path: Path | None = None, @@ -84,6 +84,7 @@ def __init__( application_id: The application ID used for identification. certs_path: The path to a file or directory of trusted CA certificates for SSL verification. """ + self._api_base_url = api_base_url self._token = token self._api_key = api_key self._application_id = application_id @@ -108,7 +109,7 @@ def get(self, path: str, *, params: dict[str, Any] | None = None, max_retries: i Returns: The response returned by the API. """ - url: str = self.API_BASE_URL + path + url: str = self._api_base_url + path attempt: int = 0 logger.debug(f"GET {url}") diff --git a/src/meteole/forecast.py b/src/meteole/forecast.py index 093bd96..a2190b6 100644 --- a/src/meteole/forecast.py +++ b/src/meteole/forecast.py @@ -133,7 +133,9 @@ def get_coverage_description(self, coverage_id: str) -> dict[str, Any]: ]["gmlrgrid:generalGridAxis"] return { - "forecast_horizons": [int(time / 3600) for time in self._get_available_feature(grid_axis, "time")], + "forecast_horizons": [ + dt.timedelta(seconds=time) for time in self._get_available_feature(grid_axis, "time") + ], "heights": self._get_available_feature(grid_axis, "height"), "pressures": self._get_available_feature(grid_axis, "pressure"), } @@ -145,7 +147,7 @@ def get_coverage( long: tuple = FRANCE_METRO_LONGITUDES, heights: list[int] | None = None, pressures: list[int] | None = None, - forecast_horizons: list[int] | None = None, + forecast_horizons: list[dt.timedelta] | None = None, run: str | None = None, interval: str | None = None, coverage_id: str = "", @@ -159,7 +161,7 @@ def get_coverage( long: Minimum and maximum longitude. heights: Heights in meters. pressures: Pressures in hPa. - forecast_horizons: List of integers, representing the forecast horizons in hours. + forecast_horizons: List of timedelta, representing the forecast horizons in hours. run: The model inference timestamp. If None, defaults to the latest available run. Expected format: "YYYY-MM-DDTHH:MM:SSZ". interval: The aggregation period. Must be None for instant indicators; @@ -303,7 +305,7 @@ def _get_coverage_id( ) else: if not interval: - interval = "P1D" + interval = valid_intervals[0] logger.info( f"`interval=None` is invalid for non-instant indicators. Using default `interval={interval}`" ) @@ -320,9 +322,7 @@ def _get_coverage_id( return coverage_id - def _raise_if_invalid_or_fetch_default( - self, param_name: str, inputs: list[int] | None, availables: list[int] - ) -> list[int]: + def _raise_if_invalid_or_fetch_default(self, param_name: str, inputs: list | None, availables: list) -> list: """(Protected) Checks validity of `inputs`. @@ -469,7 +469,7 @@ def _grib_bytes_to_df( def _get_data_single_forecast( self, coverage_id: str, - forecast_horizon: int, + forecast_horizon: dt.timedelta, pressure: int | None, height: int | None, lat: tuple, @@ -483,7 +483,7 @@ def _get_data_single_forecast( coverage_id (str): the indicator. height (int): height in meters pressure (int): pressure in hPa - forecast_horizon (int): the forecast horizon in hours (how many hours ahead) + forecast_horizon (dt.timedelta): the forecast horizon (how much time ahead?) lat (tuple): minimum and maximum latitude long (tuple): minimum and maximum longitude temp_dir (str | None): Directory to store the temporary file. Defaults to None. @@ -496,7 +496,7 @@ def _get_data_single_forecast( coverage_id=coverage_id, height=height, pressure=pressure, - forecast_horizon_in_seconds=forecast_horizon * 3600, + forecast_horizon_in_seconds=int(forecast_horizon.total_seconds()), lat=lat, long=long, ) @@ -624,7 +624,7 @@ def get_combined_coverage( intervals: list[str | None] | None = None, lat: tuple = FRANCE_METRO_LATITUDES, long: tuple = FRANCE_METRO_LONGITUDES, - forecast_horizons: list[int] | None = None, + forecast_horizons: list[dt.timedelta] | None = None, temp_dir: str | None = None, ) -> pd.DataFrame: """ @@ -644,7 +644,7 @@ def get_combined_coverage( Defaults to 'P1D' for time-aggregated indicators. lat (tuple): The latitude range as (min_latitude, max_latitude). Defaults to FRANCE_METRO_LATITUDES. long (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. - forecast_horizons (list[int] | None): A list of forecast horizon values in hours. Defaults to None. + forecast_horizons (list[dt.timedelta] | None): A list of forecast horizon values in dt.timedelta. Defaults to None. temp_dir (str | None): Directory to store the temporary file. Defaults to None. Returns: @@ -680,7 +680,7 @@ def _get_combined_coverage_for_single_run( intervals: list[str | None] | None = None, lat: tuple = FRANCE_METRO_LATITUDES, long: tuple = FRANCE_METRO_LONGITUDES, - forecast_horizons: list[int] | None = None, + forecast_horizons: list[dt.timedelta] | None = None, temp_dir: str | None = None, ) -> pd.DataFrame: """(Protected) @@ -700,7 +700,7 @@ def _get_combined_coverage_for_single_run( Defaults to 'P1D' for time-aggregated indicators. lat (tuple): The latitude range as (min_latitude, max_latitude). Defaults to FRANCE_METRO_LATITUDES. long (tuple): The longitude range as (min_longitude, max_longitude). Defaults to FRANCE_METRO_LONGITUDES. - forecast_horizons (list[int] | None): A list of forecast horizon values in hours. Defaults to None. + forecast_horizons (list[dt.timedelta] | None): A list of forecast horizon values (as a dt.timedelta object). Defaults to None. temp_dir (str | None): Directory to store the temporary file. Defaults to None. Returns: @@ -778,7 +778,7 @@ def _check_params_length(params: list[Any] | None, arg_name: str) -> list[Any]: coverages, ) - def _get_forecast_horizons(self, coverage_ids: list[str]) -> list[list[int]]: + def _get_forecast_horizons(self, coverage_ids: list[str]) -> list[list[dt.timedelta]]: """(Protected) Retrieve the times for each coverage_id. @@ -788,7 +788,7 @@ def _get_forecast_horizons(self, coverage_ids: list[str]) -> list[list[int]]: Returns: List of times for each coverage ID. """ - indicator_times: list[list[int]] = [] + indicator_times: list[list[dt.timedelta]] = [] for coverage_id in coverage_ids: times = self.get_coverage_description(coverage_id)["forecast_horizons"] indicator_times.append(times) @@ -797,7 +797,7 @@ def _get_forecast_horizons(self, coverage_ids: list[str]) -> list[list[int]]: def find_common_forecast_horizons( self, list_coverage_id: list[str], - ) -> list[int]: + ) -> list[dt.timedelta]: """Find common forecast_horizons among coverage IDs. Args: @@ -821,7 +821,7 @@ def find_common_forecast_horizons( return sorted(common_forecast_horizons) - def _validate_forecast_horizons(self, coverage_ids: list[str], forecast_horizons: list[int]) -> list[str]: + def _validate_forecast_horizons(self, coverage_ids: list[str], forecast_horizons: list[dt.timedelta]) -> list[str]: """(Protected) Validate forecast_horizons for a list of coverage IDs. diff --git a/tests/test_forecasts.py b/tests/test_forecasts.py index ef45f32..cdddfe0 100644 --- a/tests/test_forecasts.py +++ b/tests/test_forecasts.py @@ -1,8 +1,6 @@ -import os import unittest -from pathlib import Path from unittest.mock import MagicMock, patch - +import datetime as dt import pandas as pd import pytest @@ -171,7 +169,7 @@ def test_get_data_single_forecast(self, mock_get_coverage_file, mock_grib_bytes_ coverage_id="coverage_1", height=None, pressure=None, - forecast_horizon=0, + forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), long=(-12, 16), ) @@ -197,7 +195,7 @@ def test_get_data_single_forecast_with_height( coverage_id="coverage_1", height=2, pressure=None, - forecast_horizon=0, + forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), long=(-12, 16), ) @@ -218,7 +216,11 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities "data": [19, 20, 21], # this column name varies depending on the coverage_id } ) - mock_get_coverage_description.return_value = {"heights": [2], "forecast_horizons": [0], "pressures": []} + mock_get_coverage_description.return_value = { + "heights": [2], + "forecast_horizons": [dt.timedelta(hours=0)], + "pressures": [], + } forecast = AromeForecast( self.client, @@ -229,7 +231,7 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities forecast.get_coverage( coverage_id="toto", heights=[2], - forecast_horizons=[0], + forecast_horizons=[dt.timedelta(hours=0)], lat=(37.5, 55.4), long=(-12, 16), ) @@ -238,7 +240,7 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities coverage_id="toto", height=2, pressure=None, - forecast_horizon=0, + forecast_horizon=dt.timedelta(hours=0), lat=(37.5, 55.4), long=(-12, 16), temp_dir=None, @@ -248,9 +250,17 @@ def test_get_coverage(self, mock_get_data_single_forecast, mock_get_capabilities def test_get_forecast_horizons(self, mock_get_coverage_description): def side_effect(coverage_id): if coverage_id == "id1": - return {"forecast_horizons": [0, 1, 2], "heights": [], "pressures": []} + return { + "forecast_horizons": [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2)], + "heights": [], + "pressures": [], + } elif coverage_id == "id2": - return {"forecast_horizons": [0, 2, 3], "heights": [], "pressures": []} + return { + "forecast_horizons": [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2)], + "heights": [], + "pressures": [], + } mock_get_coverage_description.side_effect = side_effect @@ -261,16 +271,23 @@ def side_effect(coverage_id): ) coverage_ids = ["id1", "id2"] - expected_result = [[0, 1, 2], [0, 2, 3]] + expected_result = [ + [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2)], + [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2)], + ] result = forecast._get_forecast_horizons(coverage_ids) self.assertEqual(result, expected_result) @patch("meteole._arome.AromeForecast._get_forecast_horizons") def test_find_common_forecast_horizons(self, mock_get_forecast_horizons): - mock_get_forecast_horizons.return_value = [[0, 1, 2, 3], [2, 3, 4, 5], [1, 2, 3, 6]] + mock_get_forecast_horizons.return_value = [ + [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2), dt.timedelta(hours=3)], + [dt.timedelta(hours=2), dt.timedelta(hours=3), dt.timedelta(hours=4), dt.timedelta(hours=5)], + [dt.timedelta(hours=1), dt.timedelta(hours=2), dt.timedelta(hours=3), dt.timedelta(hours=6)], + ] list_coverage_id = ["id1", "id2", "id3"] - expected_result = [2, 3] + expected_result = [dt.timedelta(hours=2), dt.timedelta(hours=3)] forecast = AromeForecast( self.client, @@ -282,10 +299,13 @@ def test_find_common_forecast_horizons(self, mock_get_forecast_horizons): @patch("meteole._arome.AromeForecast._get_forecast_horizons") def test_validate_forecast_horizons_valid(self, mock_get_forecast_horizons): - mock_get_forecast_horizons.return_value = [[0, 1, 2, 3], [2, 3, 4, 5]] + mock_get_forecast_horizons.return_value = [ + [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2), dt.timedelta(hours=3)], + [dt.timedelta(hours=2), dt.timedelta(hours=3), dt.timedelta(hours=4), dt.timedelta(hours=5)], + ] coverage_ids = ["id1", "id2"] - forecast_horizons = [2, 3] + forecast_horizons = [dt.timedelta(hours=2), dt.timedelta(hours=3)] expected_result = [] forecast = AromeForecast( @@ -298,10 +318,13 @@ def test_validate_forecast_horizons_valid(self, mock_get_forecast_horizons): @patch("meteole._arome.AromeForecast._get_forecast_horizons") def test_validate_forecast_horizons_invalid(self, mock_get_forecast_horizons): - mock_get_forecast_horizons.return_value = [[0, 1, 2, 3], [2, 3, 4, 5]] + mock_get_forecast_horizons.return_value = [ + [dt.timedelta(hours=0), dt.timedelta(hours=1), dt.timedelta(hours=2), dt.timedelta(hours=3)], + [dt.timedelta(hours=2), dt.timedelta(hours=3), dt.timedelta(hours=4), dt.timedelta(hours=5)], + ] coverage_ids = ["id1", "id2"] - forecast_horizons = [1, 2] + forecast_horizons = [dt.timedelta(hours=1), dt.timedelta(hours=2)] expected_result = ["id2"] forecast = AromeForecast( @@ -324,7 +347,7 @@ def test_get_combined_coverage( mock_get_coverage_id, ): mock_get_coverage_id.side_effect = lambda indicator, run, interval: f"{indicator}_{run}_{interval}" - mock_find_common_forecast_horizons.return_value = [0] + mock_find_common_forecast_horizons.return_value = [dt.timedelta(hours=0)] mock_validate_forecast_horizons.return_value = [] mock_get_coverage.side_effect = [ pd.DataFrame( @@ -332,7 +355,7 @@ def test_get_combined_coverage( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], } ), @@ -341,7 +364,7 @@ def test_get_combined_coverage( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data2": [30, 40], } ), @@ -357,14 +380,14 @@ def test_get_combined_coverage( intervals = ["", "P1D"] lat = (37.5, 55.4) long = (-12, 16) - forecast_horizons = [0] + forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( { "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], "data2": [30, 40], } @@ -393,7 +416,7 @@ def test_get_combined_coverage_invalid_forecast_horizons( mock_get_coverage_id, ): mock_get_coverage_id.side_effect = lambda indicator, run, interval: f"{indicator}_{run}_{interval}" - mock_find_common_forecast_horizons.return_value = [0] + mock_find_common_forecast_horizons.return_value = [dt.timedelta(hours=0)] mock_validate_forecast_horizons.return_value = [ "GEOMETRIC_HEIGHT__GROUND_OR_WATER_SURFACE_2024-12-13T00.00.00Z" ] @@ -408,7 +431,7 @@ def test_get_combined_coverage_invalid_forecast_horizons( intervals = ["", "P1D"] lat = (37.5, 55.4) long = (-12, 16) - forecast_horizons = [0] + forecast_horizons = [dt.timedelta(hours=0)] forecast = AromeForecast( self.client, @@ -435,7 +458,7 @@ def test_get_combined_coverage_multiple_runs( ): # Mock return values mock_get_coverage_id.side_effect = lambda indicator, run, interval: f"{indicator}_{run}_{interval}" - mock_find_common_forecast_horizons.return_value = [0] + mock_find_common_forecast_horizons.return_value = [dt.timedelta(hours=0)] mock_validate_forecast_horizons.return_value = [] mock_get_coverage.side_effect = [ pd.DataFrame( @@ -443,7 +466,7 @@ def test_get_combined_coverage_multiple_runs( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], } ), @@ -452,7 +475,7 @@ def test_get_combined_coverage_multiple_runs( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data2": [30, 40], } ), @@ -461,7 +484,7 @@ def test_get_combined_coverage_multiple_runs( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-14T00.00.00Z", "2024-12-14T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [100, 200], } ), @@ -470,7 +493,7 @@ def test_get_combined_coverage_multiple_runs( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-14T00.00.00Z", "2024-12-14T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data2": [300, 400], } ), @@ -486,14 +509,19 @@ def test_get_combined_coverage_multiple_runs( intervals = ["", "P1D"] lat = (37.5, 55.4) long = (-12, 16) - forecast_horizons = [0] + forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( { "latitude": [1, 2, 1, 2], "longitude": [3, 4, 3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z", "2024-12-14T00.00.00Z", "2024-12-14T00.00.00Z"], - "forecast_horizon": [0, 0, 0, 0], + "forecast_horizon": [ + dt.timedelta(hours=0), + dt.timedelta(hours=0), + dt.timedelta(hours=0), + dt.timedelta(hours=0), + ], "data1": [10, 20, 100, 200], "data2": [30, 40, 300, 400], } @@ -522,7 +550,7 @@ def test_get_combined_coverage_no_heights_or_pressures( mock_get_coverage_id, ): mock_get_coverage_id.side_effect = lambda indicator, run, interval: f"{indicator}_{run}_{interval}" - mock_find_common_forecast_horizons.return_value = [0] + mock_find_common_forecast_horizons.return_value = [dt.timedelta(hours=0)] mock_validate_forecast_horizons.return_value = [] mock_get_coverage.side_effect = [ pd.DataFrame( @@ -530,7 +558,7 @@ def test_get_combined_coverage_no_heights_or_pressures( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], } ), @@ -539,7 +567,7 @@ def test_get_combined_coverage_no_heights_or_pressures( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data2": [30, 40], } ), @@ -555,14 +583,14 @@ def test_get_combined_coverage_no_heights_or_pressures( intervals = ["", "P1D"] lat = (37.5, 55.4) long = (-12, 16) - forecast_horizons = [0] + forecast_horizons = [dt.timedelta(hours=0)] expected_result = pd.DataFrame( { "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], "data2": [30, 40], } @@ -599,7 +627,7 @@ def test_get_combined_coverage_no_optional_params( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], } ), @@ -608,7 +636,7 @@ def test_get_combined_coverage_no_optional_params( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data2": [30, 40], } ), @@ -625,7 +653,7 @@ def test_get_combined_coverage_no_optional_params( "latitude": [1, 2], "longitude": [3, 4], "run": ["2024-12-13T00.00.00Z", "2024-12-13T00.00.00Z"], - "forecast_horizon": [0, 0], + "forecast_horizon": [dt.timedelta(hours=0), dt.timedelta(hours=0)], "data1": [10, 20], "data2": [30, 40], } diff --git a/tutorial/Fetch_forecast_for_multiple_indicators.ipynb b/tutorial/Fetch_forecast_for_multiple_indicators.ipynb index 116b44f..70b920c 100644 --- a/tutorial/Fetch_forecast_for_multiple_indicators.ipynb +++ b/tutorial/Fetch_forecast_for_multiple_indicators.ipynb @@ -25,7 +25,7 @@ "source": [ "import random\n", "\n", - "from meteole import ArpegeForecast" + "from meteole import ArpegeForecast # or AromeForecast, AromePIForecast" ] }, { @@ -60,9 +60,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Init Client Arpege\n", - "\n", - "To get Arome Forecast, import `AromeForecast`" + "## Init Client" ] }, { @@ -71,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "client = ArpegeForecast(application_id=APP_ID)" + "client = ArpegeForecast(application_id=APP_ID) # or AromeForecast, AromePIForecast" ] }, { diff --git a/tutorial/Fetch_forecasts.ipynb b/tutorial/Fetch_forecasts.ipynb index 542cc7d..8751628 100644 --- a/tutorial/Fetch_forecasts.ipynb +++ b/tutorial/Fetch_forecasts.ipynb @@ -24,7 +24,7 @@ "source": [ "import random\n", "\n", - "from meteole import AromeForecast" + "from meteole import PiafForecast # or ArpegeForecast, PiafForecast, AromePIForecast" ] }, { @@ -52,16 +52,14 @@ "metadata": {}, "outputs": [], "source": [ - "APP_ID = \"\"" + "APP_ID = \"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Init Client Arome\n", - "\n", - "To get Arpege Forecast, import `ArpegeForecast`" + "## Init Client" ] }, { @@ -71,7 +69,7 @@ "outputs": [], "source": [ "# init client\n", - "arome = AromeForecast(application_id=APP_ID)" + "client = PiafForecast(application_id=APP_ID) # or ArpegeForecast, PiafForecast, AromePIForecast" ] }, { @@ -81,7 +79,7 @@ "outputs": [], "source": [ "# pick a random indicator\n", - "random_indicator = random.choice(arome.INDICATORS)\n", + "random_indicator = random.choice(client.INDICATORS)\n", "print(f\"Indicator: {random_indicator}\")" ] }, @@ -91,7 +89,7 @@ "metadata": {}, "outputs": [], "source": [ - "arome.INDICATORS" + "client.INDICATORS" ] }, { @@ -107,7 +105,7 @@ "metadata": {}, "outputs": [], "source": [ - "arome.get_coverage(random_indicator)" + "client.get_coverage(random_indicator)" ] }, { @@ -126,7 +124,7 @@ "outputs": [], "source": [ "# First parameters to create a coverage_id (run and interval)\n", - "df_capabilities = arome.get_capabilities()\n", + "df_capabilities = client.get_capabilities()\n", "\n", "list_run_valid = list(df_capabilities[df_capabilities[\"indicator\"] == random_indicator][\"run\"].unique())\n", "list_interval_valid = list(df_capabilities[df_capabilities[\"indicator\"] == random_indicator][\"interval\"].unique())\n", @@ -140,19 +138,12 @@ "outputs": [], "source": [ "# Then other parameters from a coverage_id\n", - "description = arome.get_coverage_description(list_coverage_id_valid[0])\n", + "description = client.get_coverage_description(list_coverage_id_valid[0])\n", "\n", "list_forecast_horizons_valid = description.get(\"forecast_horizons\", [])\n", "list_height_valid = description.get(\"heights\", [])\n", "list_pressure_id_valid = description.get(\"pressures\", [])" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": {