meteocatpy 0.0.11 → 0.0.13

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 (40) hide show
  1. package/.github/workflows/release.yml +33 -33
  2. package/.gitlab-ci.yml +46 -46
  3. package/.pre-commit-config.yaml +37 -37
  4. package/.releaserc +23 -23
  5. package/.releaserc.toml +14 -14
  6. package/AUTHORS.md +12 -12
  7. package/CHANGELOG.md +187 -171
  8. package/README.md +61 -61
  9. package/filetree.py +48 -48
  10. package/filetree.txt +48 -48
  11. package/meteocatpy/README.md +61 -61
  12. package/meteocatpy/__init__.py +27 -27
  13. package/meteocatpy/const.py +10 -10
  14. package/meteocatpy/data.py +93 -93
  15. package/meteocatpy/exceptions.py +35 -35
  16. package/meteocatpy/forecast.py +137 -137
  17. package/meteocatpy/helpers.py +46 -46
  18. package/meteocatpy/stations.py +71 -71
  19. package/meteocatpy/symbols.py +89 -89
  20. package/meteocatpy/town.py +61 -61
  21. package/meteocatpy/townstations.py +99 -99
  22. package/meteocatpy/variables.py +77 -77
  23. package/meteocatpy/version.py +2 -2
  24. package/package.json +23 -23
  25. package/poetry.lock +3313 -3313
  26. package/pyproject.toml +72 -72
  27. package/releaserc.json +17 -17
  28. package/requirements.test.txt +3 -3
  29. package/setup.cfg +64 -64
  30. package/setup.py +10 -10
  31. package/tests/data_test.py +122 -122
  32. package/tests/import_test.py +18 -18
  33. package/tests/integration_test_complete.py +76 -76
  34. package/tests/integration_test_forecast.py +54 -54
  35. package/tests/integration_test_station_data.py +33 -33
  36. package/tests/integration_test_stations.py +31 -31
  37. package/tests/integration_test_symbols.py +68 -68
  38. package/tests/integration_test_town.py +32 -32
  39. package/tests/integration_test_town_stations.py +36 -36
  40. package/tests/integration_test_variables.py +32 -32
@@ -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
@@ -1,46 +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("-_")
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("-_")
@@ -1,71 +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
-
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
+