meteocatpy 0.0.7 → 0.0.8
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.
- package/.github/workflows/release.yml +33 -33
- package/.gitlab-ci.yml +46 -46
- package/.pre-commit-config.yaml +37 -37
- package/.releaserc +23 -23
- package/.releaserc.toml +14 -14
- package/AUTHORS.md +12 -12
- package/CHANGELOG.md +145 -137
- package/README.md +61 -61
- package/filetree.py +48 -48
- package/filetree.txt +48 -48
- package/meteocatpy/README.md +61 -61
- package/meteocatpy/__init__.py +27 -27
- package/meteocatpy/const.py +10 -10
- package/meteocatpy/data.py +93 -140
- package/meteocatpy/exceptions.py +35 -35
- package/meteocatpy/forecast.py +137 -137
- package/meteocatpy/helpers.py +46 -46
- package/meteocatpy/stations.py +71 -71
- package/meteocatpy/symbols.py +89 -89
- package/meteocatpy/town.py +61 -61
- package/meteocatpy/townstations.py +99 -99
- package/meteocatpy/variables.py +74 -74
- package/meteocatpy/version.py +2 -2
- package/package.json +23 -23
- package/poetry.lock +3313 -3313
- package/pyproject.toml +72 -72
- package/releaserc.json +17 -17
- package/requirements.test.txt +3 -3
- package/setup.cfg +64 -64
- package/setup.py +10 -10
- package/tests/data_test.py +122 -122
- package/tests/import_test.py +18 -18
- package/tests/integration_test_complete.py +76 -76
- package/tests/integration_test_forecast.py +54 -54
- package/tests/integration_test_station_data.py +33 -33
- package/tests/integration_test_stations.py +31 -31
- package/tests/integration_test_symbols.py +68 -68
- package/tests/integration_test_town.py +32 -32
- package/tests/integration_test_town_stations.py +36 -36
- package/tests/integration_test_variables.py +32 -32
package/meteocatpy/data.py
CHANGED
|
@@ -1,140 +1,93 @@
|
|
|
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
|
|
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
|
+
)
|
package/meteocatpy/exceptions.py
CHANGED
|
@@ -1,35 +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)
|
|
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)
|
package/meteocatpy/forecast.py
CHANGED
|
@@ -1,137 +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
|
|
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
|