Skip to content

Commit

Permalink
Merge pull request #27 from MAIF/feature/arome_instantane
Browse files Browse the repository at this point in the history
Additional models : Arome Instantane, PIAF
  • Loading branch information
GratienDSX authored Jan 31, 2025
2 parents 6b58ec2 + 554a2c0 commit c308150
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 105 deletions.
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.*

Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/coverage_parameters.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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.

Expand Down
19 changes: 11 additions & 8 deletions docs/pages/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/meteole/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
88 changes: 88 additions & 0 deletions src/meteole/_arome_instantane.py
Original file line number Diff line number Diff line change
@@ -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}")
68 changes: 68 additions & 0 deletions src/meteole/_piaf.py
Original file line number Diff line number Diff line change
@@ -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}")
5 changes: 3 additions & 2 deletions src/meteole/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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}")

Expand Down
Loading

0 comments on commit c308150

Please sign in to comment.