meteocatpy 0.0.7

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 (42) hide show
  1. package/.github/workflows/release.yml +33 -0
  2. package/.gitlab-ci.yml +46 -0
  3. package/.pre-commit-config.yaml +37 -0
  4. package/.releaserc +23 -0
  5. package/.releaserc.toml +14 -0
  6. package/AUTHORS.md +12 -0
  7. package/CHANGELOG.md +137 -0
  8. package/LICENSE +194 -0
  9. package/README.md +62 -0
  10. package/filetree.py +48 -0
  11. package/filetree.txt +48 -0
  12. package/meteocatpy/README.md +62 -0
  13. package/meteocatpy/__init__.py +27 -0
  14. package/meteocatpy/const.py +10 -0
  15. package/meteocatpy/data.py +140 -0
  16. package/meteocatpy/exceptions.py +35 -0
  17. package/meteocatpy/forecast.py +137 -0
  18. package/meteocatpy/helpers.py +46 -0
  19. package/meteocatpy/py.typed +0 -0
  20. package/meteocatpy/stations.py +71 -0
  21. package/meteocatpy/symbols.py +89 -0
  22. package/meteocatpy/town.py +61 -0
  23. package/meteocatpy/townstations.py +99 -0
  24. package/meteocatpy/variables.py +74 -0
  25. package/meteocatpy/version.py +2 -0
  26. package/package.json +23 -0
  27. package/poetry.lock +3313 -0
  28. package/pyproject.toml +72 -0
  29. package/releaserc.json +18 -0
  30. package/requirements.test.txt +3 -0
  31. package/setup.cfg +64 -0
  32. package/setup.py +10 -0
  33. package/tests/data_test.py +122 -0
  34. package/tests/import_test.py +18 -0
  35. package/tests/integration_test_complete.py +77 -0
  36. package/tests/integration_test_forecast.py +54 -0
  37. package/tests/integration_test_station_data.py +34 -0
  38. package/tests/integration_test_stations.py +32 -0
  39. package/tests/integration_test_symbols.py +68 -0
  40. package/tests/integration_test_town.py +32 -0
  41. package/tests/integration_test_town_stations.py +36 -0
  42. package/tests/integration_test_variables.py +32 -0
@@ -0,0 +1,62 @@
1
+ # Meteocat Python Package for Meteocat Home Assistant Integration
2
+
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![Python version compatibility](https://img.shields.io/pypi/pyversions/meteocatpy)](https://pypi.org/project/meteocatpy)
5
+ [![pipeline status](https://gitlab.com/figorr/meteocatpy/badges/master/pipeline.svg)](https://gitlab.com/figorr/meteocatpy/commits/master)
6
+
7
+
8
+ `meteocatpy` is a Python package to interact with the Meteocat API. Allows you to obtain meteorological data and lists of municipalities from the Meteocat API.
9
+
10
+ **NOTE:** Meteocat API requires to use an API_KEY, you should ask to (https://apidocs.meteocat.gencat.cat/documentacio/acces-ciutada-i-administracio/)
11
+
12
+ # Installation
13
+
14
+ You can install the package from PyPI using `pip`:
15
+ ```bash
16
+ pip install meteocatpy
17
+ ```
18
+
19
+ ```bash
20
+ from meteocatpy.town import MeteocatTown
21
+
22
+ # Replace 'tu_api_key' with your actual API key
23
+ API_KEY = "tu_api_key"
24
+ town_client = MeteocatTown(API_KEY)
25
+
26
+ # Get a list of municipalities (asynchronous call)
27
+ municipios_data = await town_client.get_municipis()
28
+ print(municipis)
29
+ ```
30
+
31
+ # Credits
32
+
33
+ This is a personal project.
34
+
35
+ Authors:
36
+ - Figorr
37
+
38
+ # Contributing
39
+
40
+ If you would like to contribute to this project, please open an issue or create a pull request. I'd be happy to review your contributions!
41
+
42
+ 1. [Check for open features/bugs](https://gitlab.com/figorr/meteocatpy/issues)
43
+ or [initiate a discussion on one](https://gitlab.com/figorr/meteocatpy/issues/new).
44
+ 2. [Fork the repository](https://gitlab.com/figorr/meteocatpy/forks/new).
45
+ 3. Install the dev environment: `make init`.
46
+ 4. Enter the virtual environment: `pipenv shell`
47
+ 5. Code your new feature or bug fix.
48
+ 6. Write a test that covers your new functionality.
49
+ 7. Update `README.md` with any new documentation.
50
+ 8. Run tests and ensure 100% code coverage for your contribution: `make coverage`
51
+ 9. Ensure you have no linting errors: `make lint`
52
+ 10. Ensure you have typed your code correctly: `make typing`
53
+ 11. Add yourself to `AUTHORS.md`.
54
+ 12. Submit a pull request!
55
+
56
+ # License
57
+
58
+ [Apache-2.0](LICENSE). By providing a contribution, you agree the contribution is licensed under Apache-2.0.
59
+
60
+ # API Reference
61
+
62
+ [See the docs 📚](https://apidocs.meteocat.gencat.cat/section/informacio-general/).
@@ -0,0 +1,27 @@
1
+ """METEOCAT API.
2
+
3
+ Python Package to collect data from Meteocat API and interact with Meteocat Home Assistant Integration
4
+ SPDX-License-Identifier: Apache-2.0
5
+
6
+ For more details about this API, please refer to the documentation at
7
+ https://gitlab.com/figorr/meteocatpy
8
+ """
9
+
10
+ # meteocatpy/__init__.py
11
+ from .town import MeteocatTown
12
+ from .forecast import MeteocatForecast
13
+ from .symbols import MeteocatSymbols
14
+ from .stations import MeteocatStations
15
+ from .townstations import MeteocatTownStations
16
+ from .data import MeteocatStationData
17
+ from .variables import MeteocatVariables
18
+
19
+ __all__ = [
20
+ "MeteocatTown",
21
+ "MeteocatForecast",
22
+ "MeteocatSymbols",
23
+ "MeteocatStations",
24
+ "MeteocatTownStations",
25
+ "MeteocatStationData",
26
+ "MeteocatVariables"
27
+ ]
@@ -0,0 +1,10 @@
1
+ """meteocatpy constants."""
2
+ BASE_URL = "https://api.meteo.cat"
3
+ MUNICIPIS_LIST_URL = "/referencia/v1/municipis"
4
+ MUNICIPIS_HORA_URL = "/pronostic/v1/municipalHoraria/{codi}"
5
+ MUNICIPIS_DIA_URL = "/pronostic/v1/municipal/{codi}"
6
+ SYMBOLS_URL = "/referencia/v1/simbols"
7
+ STATIONS_LIST_URL = "/xema/v1/estacions/metadades"
8
+ STATIONS_MUNICIPI_URL = "/xema/v1/representatives/metadades/municipis/{codi_municipi}/variables/{codi_variable}"
9
+ VARIABLES_URL = "/xema/v1/variables/mesurades/metadades"
10
+ STATION_DATA_URL = "/xema/v1/estacions/mesurades/{codiEstacio}/{any}/{mes}/{dia}"
@@ -0,0 +1,140 @@
1
+ import aiohttp
2
+ import logging
3
+ from datetime import datetime
4
+ from .variables import MeteocatVariables
5
+ from .const import BASE_URL, STATION_DATA_URL
6
+ from .exceptions import (
7
+ BadRequestError,
8
+ ForbiddenError,
9
+ TooManyRequestsError,
10
+ InternalServerError,
11
+ UnknownAPIError,
12
+ )
13
+
14
+ _LOGGER = logging.getLogger(__name__)
15
+
16
+ class MeteocatStationData:
17
+ """Clase para interactuar con los datos de estaciones de la API de Meteocat."""
18
+
19
+ def __init__(self, api_key: str):
20
+ """
21
+ Inicializa la clase MeteocatStationData.
22
+
23
+ Args:
24
+ api_key (str): Clave de API para autenticar las solicitudes.
25
+ """
26
+ self.api_key = api_key
27
+ self.headers = {
28
+ "Content-Type": "application/json",
29
+ "X-Api-Key": self.api_key,
30
+ }
31
+ self.variables = MeteocatVariables(api_key)
32
+
33
+ @staticmethod
34
+ def get_current_date():
35
+ """
36
+ Obtiene la fecha actual en formato numérico.
37
+
38
+ Returns:
39
+ tuple: Año (YYYY), mes (MM), día (DD) como enteros.
40
+ """
41
+ now = datetime.now()
42
+ return now.year, now.month, now.day
43
+
44
+ async def get_station_data(self, station_id: str):
45
+ """
46
+ Obtiene los datos meteorológicos de una estación desde la API de Meteocat.
47
+
48
+ Args:
49
+ station_id (str): Código de la estación.
50
+
51
+ Returns:
52
+ dict: Datos meteorológicos de la estación.
53
+ """
54
+ any, mes, dia = self.get_current_date() # Calcula la fecha actual
55
+ url = f"{BASE_URL}{STATION_DATA_URL}".format(
56
+ codiEstacio=station_id, any=any, mes=f"{mes:02d}", dia=f"{dia:02d}"
57
+ )
58
+
59
+ async with aiohttp.ClientSession() as session:
60
+ try:
61
+ async with session.get(url, headers=self.headers) as response:
62
+ if response.status == 200:
63
+ return await response.json()
64
+
65
+ # Gestionar errores según el código de estado
66
+ if response.status == 400:
67
+ raise BadRequestError(await response.json())
68
+ elif response.status == 403:
69
+ error_data = await response.json()
70
+ if error_data.get("message") == "Forbidden":
71
+ raise ForbiddenError(error_data)
72
+ elif error_data.get("message") == "Missing Authentication Token":
73
+ raise ForbiddenError(error_data)
74
+ elif response.status == 429:
75
+ raise TooManyRequestsError(await response.json())
76
+ elif response.status == 500:
77
+ raise InternalServerError(await response.json())
78
+ else:
79
+ raise UnknownAPIError(
80
+ f"Unexpected error {response.status}: {await response.text()}"
81
+ )
82
+
83
+ except aiohttp.ClientError as e:
84
+ raise UnknownAPIError(
85
+ message=f"Error al conectar con la API de Meteocat: {str(e)}",
86
+ status_code=0,
87
+ )
88
+
89
+ except Exception as ex:
90
+ raise UnknownAPIError(
91
+ message=f"Error inesperado: {str(ex)}",
92
+ status_code=0,
93
+ )
94
+
95
+ async def get_station_data_with_variables(self, station_id: str, force_update=False):
96
+ """
97
+ Obtiene los datos meteorológicos de una estación, organizados por variables.
98
+
99
+ Args:
100
+ station_id (str): Código de la estación.
101
+ force_update (bool): Si True, fuerza la actualización de las variables desde la API.
102
+
103
+ Returns:
104
+ dict: Datos organizados por variables.
105
+ """
106
+ # Obtener datos de la estación
107
+ station_data = await self.get_station_data(station_id)
108
+
109
+ # Registrar la respuesta para depuración
110
+ _LOGGER.debug("Datos de la estación (crudos): %s", station_data)
111
+
112
+ # Obtener las variables desde el caché o API
113
+ variables = await self.variables.get_variables(force_update=force_update)
114
+
115
+ # Crear una estructura para organizar los datos por variables
116
+ datos_por_variable = {}
117
+
118
+ # Recorrer cada estación en los datos devueltos
119
+ for estacion in station_data:
120
+ # Recorrer cada variable dentro de la estación
121
+ for variable in estacion.get("variables", []):
122
+ codi_variable = variable.get("codi")
123
+ variable_info = next((v for v in variables if v["codi"] == codi_variable), None)
124
+ if variable_info:
125
+ nombre_variable = variable_info["nom"]
126
+ if nombre_variable not in datos_por_variable:
127
+ datos_por_variable[nombre_variable] = []
128
+
129
+ # Agregar las lecturas de la variable
130
+ for lectura in variable.get("lectures", []):
131
+ datos_por_variable[nombre_variable].append(
132
+ {
133
+ "data": lectura["data"],
134
+ "valor": lectura["valor"],
135
+ "estat": lectura.get("estat", ""),
136
+ "base_horaria": lectura.get("baseHoraria", ""),
137
+ }
138
+ )
139
+
140
+ return datos_por_variable
@@ -0,0 +1,35 @@
1
+ """METEOCAT API exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ class MeteocatAPIError(Exception):
6
+ """Clase base para todos los errores de la API de Meteocat."""
7
+ def __init__(self, message: str, status_code: int, aws_info: dict = None):
8
+ super().__init__(message)
9
+ self.status_code = status_code
10
+ self.aws_info = aws_info
11
+
12
+ class BadRequestError(MeteocatAPIError):
13
+ """Error 400: Bad request."""
14
+ def __init__(self, message: str, aws_info: dict = None):
15
+ super().__init__(message, 400, aws_info)
16
+
17
+ class ForbiddenError(MeteocatAPIError):
18
+ """Error 403: Forbidden."""
19
+ def __init__(self, message: str, aws_info: dict = None):
20
+ super().__init__(message, 403, aws_info)
21
+
22
+ class TooManyRequestsError(MeteocatAPIError):
23
+ """Error 429: Too many requests."""
24
+ def __init__(self, message: str, aws_info: dict = None):
25
+ super().__init__(message, 429, aws_info)
26
+
27
+ class InternalServerError(MeteocatAPIError):
28
+ """Error 500: Internal server error."""
29
+ def __init__(self, message: str, aws_info: dict = None):
30
+ super().__init__(message, 500, aws_info)
31
+
32
+ class UnknownAPIError(MeteocatAPIError):
33
+ """Error desconocido de la API."""
34
+ def __init__(self, message: str, status_code: int, aws_info: dict = None):
35
+ super().__init__(message, status_code, aws_info)
@@ -0,0 +1,137 @@
1
+ import aiohttp
2
+ from .const import (
3
+ BASE_URL,
4
+ MUNICIPIS_HORA_URL,
5
+ MUNICIPIS_DIA_URL
6
+ )
7
+ from .exceptions import (
8
+ BadRequestError,
9
+ ForbiddenError,
10
+ TooManyRequestsError,
11
+ InternalServerError,
12
+ UnknownAPIError
13
+ )
14
+
15
+ class MeteocatForecast:
16
+ """Clase para interactuar con las predicciones de la API de Meteocat."""
17
+
18
+ def __init__(self, api_key: str):
19
+ """
20
+ Inicializa la clase MeteocatForecast.
21
+
22
+ Args:
23
+ api_key (str): Clave de API para autenticar las solicitudes.
24
+ """
25
+ self.api_key = api_key
26
+ self.headers = {
27
+ "Content-Type": "application/json",
28
+ "X-Api-Key": self.api_key,
29
+ }
30
+
31
+ async def _fetch_data(self, url: str):
32
+ """
33
+ Método genérico para realizar solicitudes a la API.
34
+
35
+ Args:
36
+ url (str): URL de la API a consultar.
37
+
38
+ Returns:
39
+ dict: Respuesta JSON de la API.
40
+ """
41
+ async with aiohttp.ClientSession() as session:
42
+ try:
43
+ async with session.get(url, headers=self.headers) as response:
44
+ if response.status == 200:
45
+ return await response.json()
46
+
47
+ # Gestionar errores según el código de estado
48
+ if response.status == 400:
49
+ raise BadRequestError(await response.json())
50
+ elif response.status == 403:
51
+ error_data = await response.json()
52
+ if error_data.get("message") == "Forbidden":
53
+ raise ForbiddenError(error_data)
54
+ elif error_data.get("message") == "Missing Authentication Token":
55
+ raise ForbiddenError(error_data)
56
+ elif response.status == 429:
57
+ raise TooManyRequestsError(await response.json())
58
+ elif response.status == 500:
59
+ raise InternalServerError(await response.json())
60
+ else:
61
+ raise UnknownAPIError(f"Unexpected error {response.status}: {await response.text()}")
62
+ except aiohttp.ClientError as e:
63
+ raise UnknownAPIError(
64
+ message=f"Error al conectar con la API de Meteocat: {str(e)}",
65
+ status_code=0,
66
+ )
67
+ except Exception as ex:
68
+ raise UnknownAPIError(
69
+ message=f"Error inesperado: {str(ex)}",
70
+ status_code=0,
71
+ )
72
+
73
+ async def get_prediccion_horaria(self, town_id: str):
74
+ """
75
+ Obtiene la predicción horaria a 72 horas para un municipio.
76
+
77
+ Args:
78
+ town_id (str): Código del municipio.
79
+
80
+ Returns:
81
+ dict: Predicción horaria para el municipio.
82
+ """
83
+ url = f"{BASE_URL}{MUNICIPIS_HORA_URL.format(codi=town_id)}"
84
+ return await self._fetch_data(url)
85
+
86
+ async def get_prediccion_diaria(self, town_id: str):
87
+ """
88
+ Obtiene la predicción diaria a 8 días para un municipio.
89
+
90
+ Args:
91
+ town_id (str): Código del municipio.
92
+
93
+ Returns:
94
+ dict: Predicción diaria para el municipio.
95
+ """
96
+ url = f"{BASE_URL}{MUNICIPIS_DIA_URL.format(codi=town_id)}"
97
+ return await self._fetch_data(url)
98
+
99
+ @staticmethod
100
+ def procesar_prediccion(prediccion_json):
101
+ """
102
+ Procesa el JSON de predicción y organiza los datos por variables.
103
+
104
+ Args:
105
+ prediccion_json (dict): JSON devuelto por la API de predicción.
106
+
107
+ Returns:
108
+ dict: Datos organizados por variables.
109
+ """
110
+ datos_por_variable = {}
111
+
112
+ # Iterar sobre los días en el JSON
113
+ for dia in prediccion_json.get("dies", []):
114
+ fecha = dia.get("data")
115
+
116
+ for variable, valores in dia.get("variables", {}).items():
117
+ if "valors" in valores: # Predicción horaria
118
+ for valor in valores["valors"]:
119
+ nombre_variable = variable
120
+ if nombre_variable not in datos_por_variable:
121
+ datos_por_variable[nombre_variable] = []
122
+ datos_por_variable[nombre_variable].append({
123
+ "fecha": valor["data"],
124
+ "valor": valor["valor"],
125
+ "unidad": valores.get("unitat", ""),
126
+ })
127
+ else: # Predicción diaria
128
+ nombre_variable = variable
129
+ if nombre_variable not in datos_por_variable:
130
+ datos_por_variable[nombre_variable] = []
131
+ datos_por_variable[nombre_variable].append({
132
+ "fecha": fecha,
133
+ "valor": valores["valor"],
134
+ "unidad": valores.get("unitat", ""),
135
+ })
136
+
137
+ return datos_por_variable
@@ -0,0 +1,46 @@
1
+ """Meteocat Helpers."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ import re
6
+ import unicodedata
7
+ from zoneinfo import ZoneInfo
8
+
9
+ TZ_UTC = ZoneInfo("UTC")
10
+
11
+
12
+ def dict_nested_value(data: dict[str, Any] | None, keys: list[str] | None) -> Any:
13
+ """Get value from dict with nested keys."""
14
+ if keys is None or len(keys) == 0:
15
+ return None
16
+ for key in keys or {}:
17
+ if data is not None:
18
+ data = data.get(key)
19
+ return data
20
+
21
+
22
+ def get_current_datetime(tz: ZoneInfo = TZ_UTC, replace: bool = True) -> datetime:
23
+ """Return current datetime in UTC."""
24
+ cur_dt = datetime.now(tz=tz)
25
+ if replace:
26
+ cur_dt = cur_dt.replace(minute=0, second=0, microsecond=0)
27
+ return cur_dt
28
+
29
+
30
+ def parse_api_timestamp(timestamp: str, tz: ZoneInfo = TZ_UTC) -> datetime:
31
+ """Parse API timestamp into datetime."""
32
+ return datetime.fromisoformat(timestamp).replace(tzinfo=tz)
33
+
34
+
35
+ def slugify(value: str, allow_unicode: bool = False) -> str:
36
+ """Convert string to a valid file name."""
37
+ if allow_unicode:
38
+ value = unicodedata.normalize("NFKC", value)
39
+ else:
40
+ value = (
41
+ unicodedata.normalize("NFKD", value)
42
+ .encode("ascii", "ignore")
43
+ .decode("ascii")
44
+ )
45
+ value = re.sub(r"[^\w\s]", "-", value.lower())
46
+ return re.sub(r"[-\s]+", "-", value).strip("-_")
File without changes
@@ -0,0 +1,71 @@
1
+ import aiohttp
2
+ from .const import BASE_URL, STATIONS_LIST_URL
3
+ from .exceptions import (
4
+ BadRequestError,
5
+ ForbiddenError,
6
+ TooManyRequestsError,
7
+ InternalServerError,
8
+ UnknownAPIError
9
+ )
10
+
11
+ class MeteocatStations:
12
+ """
13
+ Clase para interactuar con la API de Meteocat y obtener la lista de todas las estaciones."""
14
+
15
+ def __init__(self, api_key: str):
16
+ """
17
+ Inicializa la clase MeteocatStations.
18
+
19
+ Args:
20
+ api_key (str): Clave de API para autenticar las solicitudes.
21
+ """
22
+ self.api_key = api_key
23
+ self.headers = {
24
+ "Content-Type": "application/json",
25
+ "X-Api-Key": self.api_key,
26
+ }
27
+
28
+ async def get_stations(self):
29
+ """
30
+ Obtiene la lista de estaciones.
31
+
32
+ Returns:
33
+ dict: Lista de estaciones.
34
+ """
35
+ url = f"{BASE_URL}{STATIONS_LIST_URL}"
36
+ async with aiohttp.ClientSession() as session:
37
+ try:
38
+ async with session.get(url, headers=self.headers) as response:
39
+ if response.status == 200:
40
+ return await response.json()
41
+
42
+ # Gestionar errores según el código de estado
43
+ if response.status == 400:
44
+ raise BadRequestError(await response.json())
45
+ elif response.status == 403:
46
+ error_data = await response.json()
47
+ if error_data.get("message") == "Forbidden":
48
+ raise ForbiddenError(error_data)
49
+ elif error_data.get("message") == "Missing Authentication Token":
50
+ raise ForbiddenError(error_data)
51
+ elif response.status == 429:
52
+ raise TooManyRequestsError(await response.json())
53
+ elif response.status == 500:
54
+ raise InternalServerError(await response.json())
55
+ else:
56
+ raise UnknownAPIError(
57
+ f"Unexpected error {response.status}: {await response.text()}"
58
+ )
59
+
60
+ except aiohttp.ClientError as e:
61
+ raise UnknownAPIError(
62
+ message=f"Error al conectar con la API de Meteocat: {str(e)}",
63
+ status_code=0,
64
+ )
65
+
66
+ except Exception as ex:
67
+ raise UnknownAPIError(
68
+ message=f"Error inesperado: {str(ex)}",
69
+ status_code=0,
70
+ )
71
+
@@ -0,0 +1,89 @@
1
+ import aiohttp
2
+ from .const import BASE_URL, SYMBOLS_URL
3
+ from .exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
4
+
5
+
6
+ class MeteocatSymbols:
7
+ """Clase para interactuar con la API de símbolos de Meteocat."""
8
+
9
+ def __init__(self, api_key: str):
10
+ """
11
+ Inicializa la clase MeteocatSymbols.
12
+
13
+ Args:
14
+ api_key (str): Clave de API para autenticar las solicitudes.
15
+ """
16
+ self.api_key = api_key
17
+ self.headers = {
18
+ "Content-Type": "application/json",
19
+ "X-Api-Key": self.api_key,
20
+ }
21
+ self.symbols_map = {}
22
+
23
+ async def fetch_symbols(self):
24
+ url = f"{BASE_URL}{SYMBOLS_URL}"
25
+ async with aiohttp.ClientSession() as session:
26
+ try:
27
+ async with session.get(url, headers=self.headers) as response:
28
+ if response.status == 200:
29
+ data = await response.json()
30
+ print(data) # Esto te mostrará la estructura completa en la consola
31
+
32
+ # Asegurarse de que `data` sea una lista de categorías
33
+ if isinstance(data, list):
34
+ self.symbols_map = {}
35
+ for category in data:
36
+ if "valors" in category:
37
+ # Guardamos los valores de cada categoría
38
+ self.symbols_map[category["nom"]] = category["valors"]
39
+ return data # Devolvemos todo el conjunto de datos
40
+
41
+ else:
42
+ raise UnknownAPIError(f"Unexpected structure of data: {data}", status_code=response.status)
43
+
44
+ # Gestionar errores de respuesta
45
+ if response.status == 400:
46
+ raise BadRequestError(await response.json())
47
+ elif response.status == 403:
48
+ error_data = await response.json()
49
+ if error_data.get("message") == "Forbidden":
50
+ raise ForbiddenError(error_data)
51
+ elif error_data.get("message") == "Missing Authentication Token":
52
+ raise ForbiddenError(error_data)
53
+ elif response.status == 429:
54
+ raise TooManyRequestsError(await response.json())
55
+ elif response.status == 500:
56
+ raise InternalServerError(await response.json())
57
+ else:
58
+ raise UnknownAPIError(f"Unexpected error {response.status}: {await response.text()}", status_code=response.status)
59
+
60
+ except aiohttp.ClientError as e:
61
+ raise UnknownAPIError(
62
+ message=f"Error al conectar con la API de Meteocat: {str(e)}",
63
+ status_code=0,
64
+ )
65
+
66
+ except Exception as ex:
67
+ raise UnknownAPIError(
68
+ message=f"Error inesperado: {str(ex)}",
69
+ status_code=0,
70
+ )
71
+
72
+
73
+
74
+ def get_description(self, category: str, code: int) -> str:
75
+ """
76
+ Obtiene la descripción de un código de símbolo dentro de una categoría.
77
+
78
+ Args:
79
+ category (str): Nombre de la categoría (e.g., "cel").
80
+ code (int): Código del símbolo.
81
+
82
+ Returns:
83
+ str: Descripción del símbolo. Retorna 'Desconocido' si el código no está en el mapeo.
84
+ """
85
+ category_symbols = self.symbols_map.get(category, [])
86
+ for symbol in category_symbols:
87
+ if symbol["codi"] == str(code): # El código es devuelto como string por la API
88
+ return symbol["descripcio"]
89
+ return "Desconocido"