diff --git a/.github/workflows/ci-cd-mkdocs.yml b/.github/workflows/ci-cd-mkdocs.yml index f928cf5..fca9d4a 100644 --- a/.github/workflows/ci-cd-mkdocs.yml +++ b/.github/workflows/ci-cd-mkdocs.yml @@ -36,7 +36,7 @@ with: path: "docs/site/" publish: - if: success() && github.ref == 'refs/heads/main' + if: success() && startsWith(github.ref, 'refs/tags') name: Publish doc needs: build permissions: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e06ea91..f3420de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,20 +27,20 @@ repos: - id: check-added-large-files args: [--maxkb=500] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.8.6 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.1 hooks: - id: mypy args: [--config-file=pyproject.toml] files: src additional_dependencies: [pydantic~=2.0,types-pytz,types-requests,types-python-dateutil] - repo: https://github.com/gitleaks/gitleaks - rev: v8.21.2 + rev: v8.22.1 hooks: - id: gitleaks - repo: https://github.com/pypa/pip-audit @@ -49,12 +49,12 @@ repos: - id: pip-audit args: [--skip-editable] - repo: https://github.com/compilerla/conventional-pre-commit - rev: v3.6.0 + rev: v4.0.0 hooks: - id: conventional-pre-commit stages: [commit-msg] args: [feat, fix, ci, chore, test, docs] - repo: https://github.com/kynan/nbstripout - rev: 0.7.1 + rev: 0.8.1 hooks: - id: nbstripout diff --git a/README.md b/README.md index eb746ad..fcf73e0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
-
+
@@ -120,7 +120,7 @@ vigi.get_vignette()
-To have more documentation from MeteoFrance in Vigilance Bulletin :
+To have more documentation from Meteo-France in Vigilance Bulletin :
- [Meteo France Documentation](https://donneespubliques.meteofrance.fr/?fond=produit&id_produit=305&id_rubrique=50)
## Contributing
diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml
index 271e661..3a867ce 100644
--- a/docs/mkdocs.yaml
+++ b/docs/mkdocs.yaml
@@ -5,23 +5,23 @@ repo_name: MAIF/meteole
site_author: OSSbyMAIF Team
docs_dir: pages
theme:
- name: 'material'
+ name: material
logo: assets/img/svg/meteole-fond-clair.svg
- favicon: assets/img/svg/meteole-git.svg
+ favicon: assets/img/png/meteole-git.png
palette:
# Palette toggle for automatic mode
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Switch to light mode
- primary: white
- accent: red
+ primary: light green
+ accent: lime
# Palette toggle for light mode
- media: "(prefers-color-scheme: light)"
scheme: default
- primary: white
- accent: red
+ primary: light green
+ accent: lime
toggle:
icon: material/brightness-7
name: Switch to dark mode
@@ -30,13 +30,13 @@ theme:
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
- accent: red
+ accent: lime
toggle:
icon: material/brightness-4
name: Switch to system preference
font:
- text: 'Roboto'
- code: 'Roboto Mono'
+ text: Urbanist
+ code: Source Code Pro
language: en
features:
- content.tabs.link
@@ -81,6 +81,4 @@ nav:
- User Guide:
- How to: how_to.md
- Advanced User Guide:
- - Coverages: coverage_parameters.md
-extra_css:
- - assets/css/mkdocs_extra.css
\ No newline at end of file
+ - Coverages: coverage_parameters.md
\ No newline at end of file
diff --git a/docs/pages/assets/img/png/meteole-git.png b/docs/pages/assets/img/png/meteole-git.png
new file mode 100644
index 0000000..25f7bc4
Binary files /dev/null and b/docs/pages/assets/img/png/meteole-git.png differ
diff --git a/docs/pages/assets/img/svg/meteole-git.svg b/docs/pages/assets/img/svg/meteole-git.svg
deleted file mode 100644
index 552bbfb..0000000
--- a/docs/pages/assets/img/svg/meteole-git.svg
+++ /dev/null
@@ -1 +0,0 @@
-This attachment was removed because it contains data that could pose a security risk.
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 018897d..98577a4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,14 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "meteole"
-version = "0.0.1"
+version = "0.1.0b1"
requires-python = ">3.8.0"
description = "A Python client library for forecast model APIs (e.g., Météo-France)."
readme = "README.md"
license = {text = "Apache-2.0"}
authors = [
{name = "ThomasBouche"},
- {name = "develop-cs"},
{name = "GratienDSX"},
]
diff --git a/src/meteole/__init__.py b/src/meteole/__init__.py
index 3d093e8..1d4015f 100644
--- a/src/meteole/__init__.py
+++ b/src/meteole/__init__.py
@@ -1,4 +1,3 @@
-import logging
from importlib.metadata import version
from meteole._arome import AromeForecast
@@ -8,21 +7,3 @@
__all__ = ["AromeForecast", "ArpegeForecast", "Vigilance"]
__version__ = version("meteole")
-
-
-def setup_logger():
- """Setup logger with proper StreamHandler and formatter"""
- logger = logging.getLogger(__name__)
- logger.setLevel(logging.INFO)
-
- handler = logging.StreamHandler()
- handler.setLevel(logging.INFO)
-
- # formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- formatter = logging.Formatter("%(message)s")
- handler.setFormatter(formatter)
-
- return logger.addHandler(handler)
-
-
-logger = setup_logger()
diff --git a/src/meteole/_arome.py b/src/meteole/_arome.py
index 145bb17..5600c23 100644
--- a/src/meteole/_arome.py
+++ b/src/meteole/_arome.py
@@ -1,16 +1,10 @@
-"""The interface for the observational data from the meteo-France API.
-
-See :
-- https://portail-api.meteofrance.fr/web/fr/api/arome
-"""
-
from __future__ import annotations
import logging
from typing import final
from meteole.clients import BaseClient, MeteoFranceClient
-from meteole.forecast import Forecast
+from meteole.forecast import WeatherForecast
logger = logging.getLogger(__name__)
@@ -50,8 +44,17 @@
@final
-class AromeForecast(Forecast):
- """Access the AROME numerical forecast data."""
+class AromeForecast(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 = "arome"
@@ -62,9 +65,14 @@ class AromeForecast(Forecast):
DEFAULT_PRECISION: float = 0.01
CLIENT_CLASS: type[BaseClient] = MeteoFranceClient
- def _validate_parameters(self):
- """Assert the parameters are valid."""
+ 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/_arpege.py b/src/meteole/_arpege.py
index f676c45..3616203 100644
--- a/src/meteole/_arpege.py
+++ b/src/meteole/_arpege.py
@@ -1,15 +1,9 @@
-"""The interface for the observational data from the meteo-France API.
-
-See :
-- https://portail-api.meteofrance.fr/web/fr/api/arpege
-"""
-
from __future__ import annotations
from typing import Any, final
from meteole.clients import BaseClient, MeteoFranceClient
-from meteole.forecast import Forecast
+from meteole.forecast import WeatherForecast
AVAILABLE_ARPEGE_TERRITORY: list[str] = ["EUROPE", "GLOBE", "ATOURX", "EURAT"]
@@ -68,8 +62,17 @@
@final
-class ArpegeForecast(Forecast):
- """Access the ARPEGE numerical forecast data."""
+class ArpegeForecast(WeatherForecast):
+ """Access the ARPEGE numerical weather forecast data from Meteo-France API.
+
+ Doc:
+ - https://portail-api.meteofrance.fr/web/fr/api/arpege
+
+ 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 = "arpege"
@@ -87,24 +90,20 @@ def __init__(
territory: str = "EUROPE",
**kwargs: Any,
):
- """
- Initializes an ArpegeForecast object for accessing ARPEGE forecast data.
+ """Initializes an ArpegeForecast object.
The `precision` of the forecast is inferred from the specified `territory`.
Args:
- territory (str, optional): The ARPEGE territory to fetch. Defaults to "EUROPE".
- api_key (str | None, optional): The API key for authentication. Defaults to None.
- token (str | None, optional): The API token for authentication. Defaults to None.
- application_id (str | None, optional): The Application ID for authentication. Defaults to None.
- cache_dir (str | None, optional): Path to the cache directory. Defaults to None.
- If not provided, the cache directory is set to "/tmp/cache".
+ territory: The ARPEGE territory to fetch. Defaults to "EUROPE".
+ 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.
Notes:
- See `MeteoFranceClient` for additional details on the parameters `api_key`, `token`,
and `application_id`.
- Available territories are listed in the `AVAILABLE_TERRITORY` constant.
-
"""
super().__init__(
client=client,
@@ -113,7 +112,11 @@ def __init__(
**kwargs,
)
- def _validate_parameters(self):
- """Assert the parameters are valid."""
+ def _validate_parameters(self) -> None:
+ """Check the territory and the precision parameters.
+
+ Raise:
+ ValueError: At least, one parameter is not good.
+ """
if self.territory not in AVAILABLE_ARPEGE_TERRITORY:
raise ValueError(f"The parameter precision must be in {AVAILABLE_ARPEGE_TERRITORY}")
diff --git a/src/meteole/_vigilance.py b/src/meteole/_vigilance.py
index 0216013..6f5b695 100644
--- a/src/meteole/_vigilance.py
+++ b/src/meteole/_vigilance.py
@@ -17,14 +17,13 @@
@final
class Vigilance:
- """Wrapper around the meteo-France API for the vigilance data.
- Ressources are:
+ """Easy access to the vigilance data of Meteo-France.
+
+ Resources are:
- textesvigilance
- cartevigilance
- Documentation
- -------------
- See:
+ Docs:
- https://portail-api.meteofrance.fr/web/fr/api/DonneesPubliquesVigilance
- https://donneespubliques.meteofrance.fr/client/document/descriptiftechnique_vigilancemetropole_
donneespubliques_v4_20230911_307.pdf
@@ -49,7 +48,7 @@ def __init__(
client: BaseClient | None = None,
**kwargs: Any,
) -> None:
- """TODO"""
+ """Initialize attributes"""
if client is not None:
self._client = client
else:
@@ -57,11 +56,10 @@ def __init__(
self._client = MeteoFranceClient(**kwargs)
def get_bulletin(self) -> dict[str, Any]:
- """
- Retrieve the vigilance bulletin.
+ """Retrieve the vigilance bulletin.
Returns:
- dict: a Dict representing the vigilance bulletin
+ Dictionary representing the vigilance bulletin
"""
path: str = self.VIGILANCE_BASE_PATH + self.API_VERSION + "/textesvigilance/encours"
@@ -72,7 +70,7 @@ def get_bulletin(self) -> dict[str, Any]:
return resp.json()
except MissingDataError as e:
- if "no matching blob" in e.message:
+ if "no matching blob" in str(e):
logger.warning("Ongoing vigilance requires no publication")
else:
logger.error(f"Unexpected error: {e}")
@@ -82,11 +80,10 @@ def get_bulletin(self) -> dict[str, Any]:
return {}
def get_map(self) -> dict[str, Any]:
- """
- Get the vigilance map with predicted risk displayed.
+ """Get the vigilance map with predicted risk displayed.
Returns:
- dict: a Dict with the predicted risk.
+ Dictionary with the predicted risk.
"""
path: str = self.VIGILANCE_BASE_PATH + self.API_VERSION + "/cartevigilance/encours"
logger.debug(f"GET {path}")
@@ -96,12 +93,12 @@ def get_map(self) -> dict[str, Any]:
return resp.json()
def get_phenomenon(self) -> tuple[pd.DataFrame, pd.DataFrame]:
- """
- Get risk prediction by phenomenon and by domain.
+ """Get risk prediction by phenomenon and by domain.
Returns:
- pd.DataFrame: a DataFrame with phenomenon by id
- pd.DataFrame: a DataFrame with phenomenon by domain
+ Tuple of:
+ A DataFrame with phenomenon by id
+ A DataFrame with phenomenon by domain
"""
df_carte = pd.DataFrame(self.get_map())
periods_data = df_carte.loc["periods", "product"]
@@ -128,9 +125,7 @@ def get_phenomenon(self) -> tuple[pd.DataFrame, pd.DataFrame]:
return df_phenomenon, df_timelaps
def get_vignette(self) -> None:
- """
- Get png.
- """
+ """Get png file."""
path: str = self.VIGILANCE_BASE_PATH + self.API_VERSION + "/vignettenationale-J-et-J1/encours"
logger.debug(f"GET {path}")
diff --git a/src/meteole/clients.py b/src/meteole/clients.py
index 698b323..2e51104 100644
--- a/src/meteole/clients.py
+++ b/src/meteole/clients.py
@@ -20,24 +20,36 @@
class HttpStatus(int, Enum):
"""Http status codes"""
- OK: int = 200
- BAD_REQUEST: int = 400
- UNAUTHORIZED: int = 401
- FORBIDDEN: int = 403
- NOT_FOUND: int = 404
- TOO_MANY_REQUESTS: int = 429
- INTERNAL_ERROR: int = 500
- BAD_GATEWAY: int = 502
- UNAVAILABLE: int = 503
- GATEWAY_TIMEOUT: int = 504
+ OK = 200
+ BAD_REQUEST = 400
+ UNAUTHORIZED = 401
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+ TOO_MANY_REQUESTS = 429
+ INTERNAL_ERROR = 500
+ BAD_GATEWAY = 502
+ UNAVAILABLE = 503
+ GATEWAY_TIMEOUT = 504
class BaseClient(ABC):
- """TODO"""
+ """(Abstract)
+
+ Base class for weather forecast provider clients.
+ """
@abstractmethod
def get(self, path: str, *, params: dict[str, Any] | None = None, max_retries: int = 5) -> Response:
- """TODO"""
+ """Retrieve some data with retry capability.
+
+ Args:
+ path: Path to a resource.
+ params: The query parameters of the request.
+ max_retries: The maximum number of retry attempts in case of failure.
+
+ Returns:
+ The response returned by the API.
+ """
raise NotImplementedError
@@ -46,17 +58,11 @@ class MeteoFranceClient(BaseClient):
This class handles the connection setup and token refreshment required for
authenticating and making requests to the Meteo France API.
-
- Attributes:
- api_key (str | None): The API key for accessing the Meteo France API.
- token (str | None): The authentication token for accessing the API.
- application_id (str | None): The application ID used for identification.
- verify (Path | None): The path to a file or directory of trusted CA certificates for SSL verification.
"""
# Class constants
API_BASE_URL: str = "https://public-api.meteofrance.fr/public/"
- TOKEN_URL: str = "https://portail-api.meteofrance.fr/token"
+ TOKEN_URL: str = "https://portail-api.meteofrance.fr/token" # noqa: S105
GET_TOKEN_TIMEOUT_SEC: int = 10
INVALID_JWT_ERROR_CODE: str = "900901"
RETRY_DELAY_SEC: int = 10
@@ -70,13 +76,13 @@ def __init__(
certs_path: Path | None = None,
) -> None:
"""
- Initializes the MeteoFranceClient object.
+ Initialize attributes.
Args:
- api_key (str | None): The API key for accessing the Meteo France API.
- token (str | None): The authentication token for accessing the API.
- application_id (str | None): The application ID used for identification.
- verify (Path | None): The path to a file or directory of trusted CA certificates for SSL verification.
+ token: The authentication token for accessing the API.
+ api_key: The API key for accessing the Meteo France API.
+ 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._token = token
self._api_key = api_key
@@ -92,16 +98,15 @@ def __init__(
def get(self, path: str, *, params: dict[str, Any] | None = None, max_retries: int = 5) -> Response:
"""
- Makes a GET request to the API with optional retries.
+ Make a GET request to the API with optional retries.
Args:
- url (str): The URL to send the GET request to.
- params (dict, optional): The query parameters to include in the request. Defaults to None.
- max_retries (int, optional): The maximum number of retry attempts in case of failure. Defaults to 5.
+ path: Path to a resource.
+ params: The query parameters of the request.
+ max_retries: The maximum number of retry attempts in case of failure.
Returns:
- requests.Response: The response returned by the API.
-
+ The response returned by the API.
"""
url: str = self.API_BASE_URL + path
attempt: int = 0
@@ -157,8 +162,10 @@ def get(self, path: str, *, params: dict[str, Any] | None = None, max_retries: i
raise GenericMeteofranceApiError(f"Failed to get a successful response from API after {attempt} retries")
def _connect(self):
- """Connect to the MeteoFrance API.
+ """(Protected)
+ Connect to the Meteo-France API.
+ Note:
If the API key is provided, it is used to authenticate the user.
If the token is provided, it is used to authenticate the user.
If the application ID is provided, a token is requested from the API.
@@ -183,11 +190,15 @@ def _connect(self):
self._session.headers.update({"Authorization": f"Bearer {self._token}"})
def _get_token(self) -> str:
- """request a token from the meteo-France API.
+ """(Protected)
+ Request a token from the Meteo-France API.
The token lasts 1 hour, and is used to authenticate the user.
If a new token is requested before the previous one expires, the previous one is invalidated.
A local cache is used to avoid requesting a new token at each run of the script.
+
+ Rerturns:
+ A JWT.
"""
if self._token_expired is False and self._token is not None:
# Use cached token
@@ -216,11 +227,13 @@ def _get_token(self) -> str:
return token
def _is_token_expired(self, response: Response) -> bool:
- """Check if the token is expired.
+ """(Protected)
+ Check if the token is expired.
- Returns
- -------
- bool
+ Args:
+ response: A request's response from the API.
+
+ Returns:
True if the token is expired, False otherwise.
"""
result: bool = False
diff --git a/src/meteole/errors.py b/src/meteole/errors.py
index ebb8527..e596eb7 100644
--- a/src/meteole/errors.py
+++ b/src/meteole/errors.py
@@ -1,53 +1,61 @@
+"""Error implementations"""
+
from __future__ import annotations
+from typing import Any, MutableMapping
+
import xmltodict
class GenericMeteofranceApiError(Exception):
- """Exception raised errors in the input parameters where a required field is missing.
+ """Exception raised when a required field is missing in the input parameters.
Args:
- message (str): Human-readable string descipting the exceptetion.
- description (str): More detailed description of the error."""
+ message: Human-readable string describing the exception.
+ description: More detailed description of the error.
+ """
- def init(self, text: str) -> None:
+ def __init__(self, text: str) -> None:
"""Initialize the exception with an error message parsed from an XML
string.
Args:
- text (str): XML string containing the error details,
+ text: XML string containing the error details,
expected to follow a specific schema with 'am:fault' as the root
element and 'am:message' and 'am:description' as child elements."""
+ try:
+ # Parse error message with xmltodict
+ data: MutableMapping[str, Any] = xmltodict.parse(text)
+ msg_content: str = data["am:fault"]["am:message"]
+ description: str = data["am:fault"]["am:description"]
+ message: str = f"{msg_content}\n {description}"
+ except Exception:
+ message = text
- # parse the error message with xmltodict
- data = xmltodict.parse(text)
- message = data["am:fault"]["am:message"]
- description = data["am:fault"]["am:description"]
- self.message = f"{message}\n {description}"
- super().__init__(self.message)
+ super().__init__(message)
class MissingDataError(Exception):
"""Exception raised errors in the input data is missing"""
- def init(self, text: str) -> None:
+ def __init__(self, text: str) -> None:
"""Initialize the exception with an error message parsed from an XML
string.
Args:
- text (str): XML string containing the error details,
+ text: XML string containing the error details,
expected to follow a specific schema with 'am:fault' as the root
element and 'am:message' and 'am:description' as child elements."""
- # parse the error message with xmltodict
try:
- data = xmltodict.parse(text)
- exception = data["mw:fault"]["mw:description"]["ns0:ExceptionReport"]["ns0:Exception"]
- code = exception["@exceptionCode"]
- locator = exception["@locator"]
- exception_text = exception["ns0:ExceptionText"]
- message = f"Error code: {code}\nLocator: {locator}\nText: {exception_text}"
+ # Parse error message with xmltodict
+ data: MutableMapping[str, Any] = xmltodict.parse(text)
+ exception: dict[Any, Any] = data["mw:fault"]["mw:description"]["ns0:ExceptionReport"]["ns0:Exception"]
+ code: str = exception["@exceptionCode"]
+ locator: str = exception["@locator"]
+ exception_text: str = exception["ns0:ExceptionText"]
+ message: str = f"Error code: {code}\nLocator: {locator}\nText: {exception_text}"
except Exception:
message = text
- self.message = message
- super().__init__(self.message)
+
+ super().__init__(message)
diff --git a/src/meteole/forecast.py b/src/meteole/forecast.py
index 8b66b89..62bec8d 100644
--- a/src/meteole/forecast.py
+++ b/src/meteole/forecast.py
@@ -19,14 +19,17 @@
logger = logging.getLogger(__name__)
-class Forecast(ABC):
- """(Abstract class)
- Provides a unified interface to query AROME and ARPEGE endpoints
-
- Attributes
- ----------
- capabilities: pandas.DataFrame
- coverage dataframe containing the details of all available coverage_ids
+class WeatherForecast(ABC):
+ """(Abstract)
+ Base class for weather forecast models.
+
+ Note: Currently, this class is highly related to Meteo-France models.
+ This will not be the case in the future.
+
+ Attributes:
+ territory: Covered area (e.g., FRANCE, ANTIL, ...).
+ precision: Precision value of the forecast.
+ capabilities: DataFrame containing details on all available coverage ids.
"""
# Class constants
@@ -53,7 +56,14 @@ def __init__(
precision: float = DEFAULT_PRECISION,
**kwargs: Any,
):
- """Init the Forecast object."""
+ """Initialize attributes.
+
+ Args:
+ territory: The ARPEGE territory to fetch.
+ 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.
+ """
self.territory = territory # "FRANCE", "ANTIL", or others (see API doc)
self.precision = precision
self._validate_parameters()
@@ -72,14 +82,26 @@ def __init__(
@property
def capabilities(self) -> pd.DataFrame:
- """TODO"""
+ """Getter method of the capabilities attribute.
+
+ Returns:
+ DataFrame of details on all available coverage ids.
+ """
if self._capabilities is None:
self._capabilities = self._build_capabilities()
return self._capabilities
@property
- def indicators(self) -> pd.DataFrame:
- """TODO"""
+ def indicators(self) -> list[str]:
+ """Getter method of the indicators.
+
+ Indicators are identifying the kind of a predicted value (i.e., measurement).
+
+ Warning: This method is deprecated as INDICATORS is now a class constant.
+
+ Returns:
+ List of covered indicators.
+ """
warn(
"The 'indicators' attribute is deprecated, it will be removed soon. "
"Use 'INDICATORS' instead (class constant).",
@@ -89,35 +111,42 @@ def indicators(self) -> pd.DataFrame:
return self.INDICATORS
@abstractmethod
- def _validate_parameters(self):
- """Assert parameters are valid."""
- pass
+ def _validate_parameters(self) -> None:
+ """Check the territory and the precision parameters.
+
+ Raise:
+ ValueError: At least, one parameter is not good.
+ """
+ raise NotImplementedError
def get_capabilities(self) -> pd.DataFrame:
- "Returns the coverage dataframe containing the details of all available coverage_ids"
+ """Explicit "getter method" of the capabilities attribute.
+
+ Returns:
+ DataFrame of details on all available coverage ids.
+ """
return self.capabilities
- def get_coverage_description(self, coverage_id: str) -> dict:
- """This endpoint returns the available axis (times, heights) to properly query coverage
+ def get_coverage_description(self, coverage_id: str) -> dict[str, Any]:
+ """Return the available axis (times, heights) of a coverage.
- TODO: other informations can be fetched from this endpoint, not yet implemented.
+ TODO: Other informations can be fetched, not yet implemented.
Args:
- coverage_id (str): use :meth:`get_capabilities()` to list all available coverage_id
- """
+ coverage_id: An id of a coverage, use get_capabilities() to get them.
- # Get coverage description
+ Returns:
+ A dictionary containing more info on the coverage.
+ """
description = self._get_coverage_description(coverage_id)
grid_axis = description["wcs:CoverageDescriptions"]["wcs:CoverageDescription"]["gml:domainSet"][
"gmlrgrid:ReferenceableGridByVectors"
]["gmlrgrid:generalGridAxis"]
return {
- "forecast_horizons": [
- int(time / 3600) for time in self.__class__._get_available_feature(grid_axis, "time")
- ],
- "heights": self.__class__._get_available_feature(grid_axis, "height"),
- "pressures": self.__class__._get_available_feature(grid_axis, "pressure"),
+ "forecast_horizons": [int(time / 3600) 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"),
}
def get_coverage(
@@ -132,24 +161,30 @@ def get_coverage(
interval: str | None = None,
coverage_id: str = "",
) -> pd.DataFrame:
- """Returns the data associated with the coverage_id for the selected parameters.
+ """Return the coverage data (i.e., the weather forecast data).
Args:
- coverage_id (str): coverage_id, get the list using :meth:`get_capabilities`
- lat (tuple): minimum and maximum latitude
- long (tuple): minimum and maximum longitude
- heights (list): heights in meters
- pressures (list): pressures in hPa
- forecast_horizons (list): list of integers, representing the forecast horizon in hours
+ indicator: Indicator of a coverage to retrieve.
+ lat: Minimum and maximum latitude.
+ long: Minimum and maximum longitude.
+ heights: Heights in meters.
+ pressures: Pressures in hPa.
+ forecast_horizons: List of integers, 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;
+ raises an error if specified. Defaults to "P1D" for time-aggregated indicators such
+ as TOTAL_PRECIPITATION.
+ coverage_id: An id of a coverage, use get_capabilities() to get them.
Returns:
pd.DataFrame: The complete run for the specified execution.
"""
- # ensure we only have one of coverage_id, indicator
+ # Ensure we only have one of coverage_id, indicator
if not bool(indicator) ^ bool(coverage_id):
raise ValueError("Argument `indicator` or `coverage_id` need to be set (only one of them)")
- if indicator:
+ if indicator is not None:
coverage_id = self._get_coverage_id(indicator, run, interval)
logger.info(f"Using `coverage_id={coverage_id}`")
@@ -179,7 +214,12 @@ def get_coverage(
return pd.concat(df_list, axis=0).reset_index(drop=True)
def _build_capabilities(self) -> pd.DataFrame:
- "Returns the coverage dataframe containing the details of all available coverage_ids"
+ """(Protected)
+ Fetch and build the model capabilities.
+
+ Returns:
+ DataFrame all the details.
+ """
logger.info("Fetching all available coverages...")
@@ -221,14 +261,14 @@ def _get_coverage_id(
run: str | None = None,
interval: str | None = None,
) -> str:
- """
- Retrieves a `coverage_id` from the capabilities based on the provided parameters.
+ """(Protected)
+ Retrieve a `coverage_id` from the capabilities based on the provided parameters.
Args:
- indicator (str): The indicator to retrieve. This parameter is required.
- run (str | None, optional): The model inference timestamp. If None, defaults to the latest available run.
+ indicator: The indicator to retrieve. This parameter is required.
+ run: The model inference timestamp. If None, defaults to the latest available run.
Expected format: "YYYY-MM-DDTHH:MM:SSZ". Defaults to None.
- interval (str | None, optional): The aggregation period. Must be None for instant indicators;
+ interval: The aggregation period. Must be None for instant indicators;
raises an error if specified. Defaults to "P1D" for time-aggregated indicators such as
TOTAL_PRECIPITATION.
@@ -291,7 +331,8 @@ def _get_coverage_id(
def _raise_if_invalid_or_fetch_default(
self, param_name: str, inputs: list[int] | None, availables: list[int]
) -> list[int]:
- """Checks validity of `inputs`.
+ """(Protected)
+ Checks validity of `inputs`.
Checks if the elements in `inputs` are in `availables` and raises a ValueError if not.
If `inputs` is empty or None, uses the first element from `availables` as the default value.
@@ -319,8 +360,12 @@ def _raise_if_invalid_or_fetch_default(
logger.info(f"Using `{param_name}={inputs}`")
return inputs
- def _fetch_capabilities(self) -> dict:
- """The Capabilities of the AROME/ARPEGE service."""
+ def _fetch_capabilities(self) -> dict[Any, Any]:
+ """Fetch the model capabilities.
+
+ Returns:
+ Raw capabilities (dictionary).
+ """
url = f"{self._model_base_path}/{self._entry_point}/GetCapabilities"
params = {
@@ -345,10 +390,11 @@ def _fetch_capabilities(self) -> dict:
logger.error(f"Response: {xml}")
raise e
- def _get_coverage_description(self, coverage_id: str) -> dict:
- """Get the description of a coverage.
+ def _get_coverage_description(self, coverage_id: str) -> dict[Any, Any]:
+ """(Protected)
+ Get the description of a coverage.
- .. warning::
+ Warning:
The return value is the raw XML data.
Not yet parsed to be usable.
In the future, it should be possible to use it to
@@ -371,7 +417,7 @@ def _get_coverage_description(self, coverage_id: str) -> dict:
return xmltodict.parse(response.text)
def _grib_bytes_to_df(self, grib_str: bytes) -> pd.DataFrame:
- """
+ """(Protected)
Converts GRIB data (in binary format) into a pandas DataFrame.
This method writes the binary GRIB data to a temporary file, reads it using
@@ -417,7 +463,8 @@ def _get_data_single_forecast(
lat: tuple,
long: tuple,
) -> pd.DataFrame:
- """Returns the forecasts for a given time and indicator.
+ """(Protected)
+ Return the forecast's data for a given time and indicator.
Args:
coverage_id (str): the indicator.
@@ -486,7 +533,7 @@ def _get_coverage_file(
lat: tuple = (37.5, 55.4),
long: tuple = (-12, 16),
) -> bytes:
- """
+ """(Protected)
Retrieves raster data for a specified model prediction and saves it to a file.
If no `filepath` is provided, the file is saved to a default cache directory under
@@ -537,14 +584,28 @@ def _get_coverage_file(
return response.content
@staticmethod
- def _get_available_feature(grid_axis, feature_name):
- features = []
- feature_grid_axis = [
- ax for ax in grid_axis if ax["gmlrgrid:GeneralGridAxis"]["gmlrgrid:gridAxesSpanned"] == feature_name
+ def _get_available_feature(grid_axis: list[dict[str, Any]], feature_name: str) -> list[int]:
+ """(Protected)
+ Retrieve available features.
+
+ TODO: add more details here.
+
+ Args:
+ grid_axis: ?
+ feature_name: ?
+
+ Returns:
+ List of available feature.
+ """
+ feature_grid_axis: list[dict[str, Any]] = [
+ ax for ax in grid_axis if str(ax["gmlrgrid:GeneralGridAxis"]["gmlrgrid:gridAxesSpanned"]) == feature_name
]
- if feature_grid_axis:
- features = feature_grid_axis[0]["gmlrgrid:GeneralGridAxis"]["gmlrgrid:coefficients"].split(" ")
- features = [int(feature) for feature in features]
+
+ features: list[int] = (
+ list(map(int, feature_grid_axis[0]["gmlrgrid:GeneralGridAxis"]["gmlrgrid:coefficients"].split(" ")))
+ if len(feature_grid_axis) > 0
+ else []
+ )
return features
def get_combined_coverage(
@@ -570,8 +631,9 @@ def get_combined_coverage(
runs (list[str]): A list of runs for each indicator. Format should be "YYYY-MM-DDTHH:MM:SSZ".
heights (list[int] | None): A list of heights in meters to filter by (default is None).
pressures (list[int] | None): A list of pressures in hPa to filter by (default is None).
- intervals (list[str] | None): A list of aggregation periods (default is None). Must be `None` or "" for instant indicators;
- otherwise, raises an exception. Defaults to 'P1D' for time-aggregated indicators.
+ intervals (list[str] | None): A list of aggregation periods (default is None).
+ Must be `None` or "" for instant indicators ; otherwise, raises an exception.
+ 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.
@@ -581,9 +643,8 @@ def get_combined_coverage(
Raises:
ValueError: If the length of `heights` does not match the length of `indicator_names`.
-
"""
- if not runs:
+ if runs is None:
runs = [None]
coverages = [
self._get_combined_coverage_for_single_run(
@@ -611,7 +672,7 @@ def _get_combined_coverage_for_single_run(
long: tuple = FRANCE_METRO_LONGITUDES,
forecast_horizons: list[int] | None = None,
) -> pd.DataFrame:
- """
+ """(Protected)
Get a combined DataFrame of coverage data for a given run considering a list of indicators.
This method retrieves and aggregates coverage data for specified indicators, with options
@@ -623,8 +684,9 @@ def _get_combined_coverage_for_single_run(
run (str): A single runs for each indicator. Format should be "YYYY-MM-DDTHH:MM:SSZ".
heights (list[int] | None): A list of heights in meters to filter by (default is None).
pressures (list[int] | None): A list of pressures in hPa to filter by (default is None).
- intervals (Optional[list[str]]): A list of aggregation periods (default is None). Must be `None` or "" for instant indicators;
- otherwise, raises an exception. Defaults to 'P1D' for time-aggregated indicators.
+ intervals (Optional[list[str]]): A list of aggregation periods (default is None).
+ Must be `None` or "" for instant indicators ; otherwise, raises an exception.
+ 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.
@@ -634,16 +696,29 @@ def _get_combined_coverage_for_single_run(
Raises:
ValueError: If the length of `heights` does not match the length of `indicator_names`.
-
"""
- def _check_params_length(params: list | None, arg_name: str) -> list:
- """assert length is ok or raise"""
+ def _check_params_length(params: list[Any] | None, arg_name: str) -> list[Any]:
+ """(Protected)
+ Assert length is ok or raise an error.
+
+ Args:
+ params: list of parameters.
+ arg_name: argument name.
+
+ Returns:
+ The given parameters unchanged.
+
+ Raises:
+ ValueError: The length of {arg_name} must match the length of indicator_names.
+ """
if params is None:
return [None] * len(indicator_names)
if len(params) != len(indicator_names):
raise ValueError(
- f"The length of {arg_name} must match the length of indicator_names. If you want multiple {arg_name} for a single indicator, create multiple entries in `indicator_names`."
+ f"The length of {arg_name} must match the length of indicator_names."
+ f" If you want multiple {arg_name} for a single indicator, create multiple"
+ " entries in `indicator_names`."
)
return params
@@ -691,14 +766,16 @@ def _check_params_length(params: list | None, arg_name: str) -> list:
)
def _get_forecast_horizons(self, coverage_ids: list[str]) -> list[list[int]]:
- """
+ """(Protected)
Retrieve the times for each coverage_id.
- Parameters:
- coverage_ids (list[str]): List of coverage IDs.
+
+ Args:
+ coverage_ids: List of coverage IDs.
+
Returns:
- list[list[int]]: List of times for each coverage ID.
+ List of times for each coverage ID.
"""
- indicator_times = []
+ indicator_times: list[list[int]] = []
for coverage_id in coverage_ids:
times = self.get_coverage_description(coverage_id)["forecast_horizons"]
indicator_times.append(times)
@@ -708,13 +785,16 @@ def find_common_forecast_horizons(
self,
list_coverage_id: list[str],
) -> list[int]:
- """
- Find common forecast_horizons among coverage IDs.
- indicator_names (list[str]): List of indicator names.
- run (Optional[str]): Identifies the model inference. Defaults to latest if None. Format "YYYY-MM-DDTHH:MM:SSZ".
- intervals (Optional[list[str]]): List of aggregation periods. Must be None for instant indicators, otherwise raises. Defaults to P1D for time-aggregated indicators like TOTAL_PRECIPITATION.
+ """Find common forecast_horizons among coverage IDs.
+
+ Args:
+ indicator_names: List of indicator names.
+ run: Identifies the model inference. Defaults to latest if None. Format "YYYY-MM-DDTHH:MM:SSZ".
+ intervals: List of aggregation periods. Must be None for instant indicators, otherwise raises.
+ Defaults to P1D for time-aggregated indicators like TOTAL_PRECIPITATION.
+
Returns:
- list[int]: Common forecast_horizons
+ List of common forecast_horizons.
"""
indicator_forecast_horizons = self._get_forecast_horizons(list_coverage_id)
@@ -729,13 +809,15 @@ 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]:
- """
+ """(Protected)
Validate forecast_horizons for a list of coverage IDs.
- Parameters:
- coverage_ids (list[str]): List of coverage IDs.
- forecast_horizons (list[int]): List of time forecasts to validate.
+
+ Args:
+ coverage_ids: List of coverage IDs.
+ forecast_horizons: List of time forecasts to validate.
+
Returns:
- list[str]: List of invalid coverage IDs.
+ List of invalid coverage IDs.
"""
indicator_forecast_horizons = self._get_forecast_horizons(coverage_ids)