meteo-lt-pkg 0.5.0__py3-none-any.whl → 0.5.0b0__py3-none-any.whl
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.
- meteo_lt/__init__.py +15 -1
- meteo_lt/api.py +51 -29
- meteo_lt/client.py +91 -8
- meteo_lt/const.py +1 -4
- meteo_lt/models.py +48 -24
- meteo_lt/utils.py +13 -10
- meteo_lt/warnings.py +15 -45
- {meteo_lt_pkg-0.5.0.dist-info → meteo_lt_pkg-0.5.0b0.dist-info}/METADATA +11 -11
- meteo_lt_pkg-0.5.0b0.dist-info/RECORD +12 -0
- meteo_lt_pkg-0.5.0.dist-info/RECORD +0 -12
- {meteo_lt_pkg-0.5.0.dist-info → meteo_lt_pkg-0.5.0b0.dist-info}/WHEEL +0 -0
- {meteo_lt_pkg-0.5.0.dist-info → meteo_lt_pkg-0.5.0b0.dist-info}/licenses/LICENSE +0 -0
- {meteo_lt_pkg-0.5.0.dist-info → meteo_lt_pkg-0.5.0b0.dist-info}/top_level.txt +0 -0
meteo_lt/__init__.py
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
"""init.py"""
|
|
2
2
|
|
|
3
3
|
from .api import MeteoLtAPI
|
|
4
|
-
from .models import
|
|
4
|
+
from .models import (
|
|
5
|
+
Coordinates,
|
|
6
|
+
LocationBase,
|
|
7
|
+
Place,
|
|
8
|
+
ForecastTimestamp,
|
|
9
|
+
Forecast,
|
|
10
|
+
WeatherWarning,
|
|
11
|
+
HydroStation,
|
|
12
|
+
HydroObservation,
|
|
13
|
+
HydroObservationData,
|
|
14
|
+
)
|
|
5
15
|
|
|
6
16
|
__all__ = [
|
|
7
17
|
"MeteoLtAPI",
|
|
8
18
|
"Coordinates",
|
|
19
|
+
"LocationBase",
|
|
9
20
|
"Place",
|
|
10
21
|
"ForecastTimestamp",
|
|
11
22
|
"Forecast",
|
|
12
23
|
"WeatherWarning",
|
|
24
|
+
"HydroStation",
|
|
25
|
+
"HydroObservation",
|
|
26
|
+
"HydroObservationData",
|
|
13
27
|
]
|
meteo_lt/api.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Main API class script"""
|
|
2
2
|
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
|
-
from .models import
|
|
6
|
-
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from .models import (
|
|
6
|
+
Forecast,
|
|
7
|
+
Place,
|
|
8
|
+
WeatherWarning,
|
|
9
|
+
HydroStation,
|
|
10
|
+
HydroObservationData,
|
|
11
|
+
)
|
|
12
|
+
from .utils import find_nearest_location
|
|
7
13
|
from .client import MeteoLtClient
|
|
8
14
|
from .warnings import WeatherWarningsProcessor
|
|
9
15
|
|
|
@@ -21,7 +27,12 @@ class MeteoLtAPI:
|
|
|
21
27
|
await self.client.__aenter__()
|
|
22
28
|
return self
|
|
23
29
|
|
|
24
|
-
async def __aexit__(
|
|
30
|
+
async def __aexit__(
|
|
31
|
+
self,
|
|
32
|
+
exc_type: Optional[type],
|
|
33
|
+
exc_val: Optional[Exception],
|
|
34
|
+
exc_tb: Optional[object],
|
|
35
|
+
) -> None:
|
|
25
36
|
"""Async context manager exit"""
|
|
26
37
|
await self.client.__aexit__(exc_type, exc_val, exc_tb)
|
|
27
38
|
|
|
@@ -29,31 +40,32 @@ class MeteoLtAPI:
|
|
|
29
40
|
"""Close the API client and cleanup resources"""
|
|
30
41
|
await self.client.close()
|
|
31
42
|
|
|
32
|
-
async def fetch_places(self):
|
|
43
|
+
async def fetch_places(self) -> None:
|
|
33
44
|
"""Gets all places from API"""
|
|
34
45
|
self.places = await self.client.fetch_places()
|
|
35
46
|
|
|
36
|
-
async def get_nearest_place(self, latitude, longitude):
|
|
47
|
+
async def get_nearest_place(self, latitude: float, longitude: float) -> Optional[Place]:
|
|
37
48
|
"""Finds nearest place using provided coordinates"""
|
|
38
49
|
if not self.places:
|
|
39
50
|
await self.fetch_places()
|
|
40
|
-
return
|
|
51
|
+
return find_nearest_location(latitude, longitude, self.places)
|
|
41
52
|
|
|
42
53
|
async def get_forecast_with_warnings(
|
|
43
|
-
self,
|
|
44
|
-
|
|
54
|
+
self,
|
|
55
|
+
latitude: Optional[float] = None,
|
|
56
|
+
longitude: Optional[float] = None,
|
|
57
|
+
place_code: Optional[str] = None,
|
|
58
|
+
) -> Forecast:
|
|
45
59
|
"""Get forecast with weather warnings for a location"""
|
|
46
60
|
if place_code is None:
|
|
47
61
|
if latitude is None or longitude is None:
|
|
48
|
-
raise ValueError(
|
|
49
|
-
"Either place_code or both latitude and longitude must be provided"
|
|
50
|
-
)
|
|
62
|
+
raise ValueError("Either place_code or both latitude and longitude must be provided")
|
|
51
63
|
place = await self.get_nearest_place(latitude, longitude)
|
|
52
64
|
place_code = place.code
|
|
53
65
|
|
|
54
66
|
return await self.get_forecast(place_code, include_warnings=True)
|
|
55
67
|
|
|
56
|
-
async def get_forecast(self, place_code, include_warnings=True):
|
|
68
|
+
async def get_forecast(self, place_code: str, include_warnings: bool = True) -> Forecast:
|
|
57
69
|
"""Retrieves forecast data from API"""
|
|
58
70
|
forecast = await self.client.fetch_forecast(place_code)
|
|
59
71
|
|
|
@@ -62,26 +74,36 @@ class MeteoLtAPI:
|
|
|
62
74
|
|
|
63
75
|
return forecast
|
|
64
76
|
|
|
65
|
-
async def get_weather_warnings(
|
|
66
|
-
self, administrative_division: str = None
|
|
67
|
-
) -> List[WeatherWarning]:
|
|
77
|
+
async def get_weather_warnings(self, administrative_division: str = None) -> List[WeatherWarning]:
|
|
68
78
|
"""Fetches weather warnings from meteo.lt JSON API"""
|
|
69
|
-
return await self.warnings_processor.get_weather_warnings(
|
|
70
|
-
administrative_division
|
|
71
|
-
)
|
|
79
|
+
return await self.warnings_processor.get_weather_warnings(administrative_division)
|
|
72
80
|
|
|
73
|
-
async def _enrich_forecast_with_warnings(self, forecast: Forecast):
|
|
81
|
+
async def _enrich_forecast_with_warnings(self, forecast: Forecast) -> None:
|
|
74
82
|
"""Enrich forecast timestamps with relevant weather warnings"""
|
|
75
|
-
if
|
|
76
|
-
not forecast
|
|
77
|
-
or not forecast.place
|
|
78
|
-
or not forecast.place.administrative_division
|
|
79
|
-
):
|
|
83
|
+
if not forecast or not forecast.place or not forecast.place.administrative_division:
|
|
80
84
|
return
|
|
81
85
|
|
|
82
|
-
warnings = await self.get_weather_warnings(
|
|
83
|
-
forecast.place.administrative_division
|
|
84
|
-
)
|
|
86
|
+
warnings = await self.get_weather_warnings(forecast.place.administrative_division)
|
|
85
87
|
|
|
86
88
|
if warnings:
|
|
87
89
|
self.warnings_processor.enrich_forecast_with_warnings(forecast, warnings)
|
|
90
|
+
|
|
91
|
+
async def get_hydro_stations(self) -> List[HydroStation]:
|
|
92
|
+
"""Get list of all hydrological stations"""
|
|
93
|
+
return await self.client.fetch_hydro_stations()
|
|
94
|
+
|
|
95
|
+
async def get_nearest_hydro_station(self, latitude: float, longitude: float) -> Optional[HydroStation]:
|
|
96
|
+
"""Find the nearest hydrological station to given coordinates"""
|
|
97
|
+
stations = await self.get_hydro_stations()
|
|
98
|
+
if not stations:
|
|
99
|
+
return None
|
|
100
|
+
return find_nearest_location(latitude, longitude, stations)
|
|
101
|
+
|
|
102
|
+
async def get_hydro_observation_data(
|
|
103
|
+
self,
|
|
104
|
+
station_code: str,
|
|
105
|
+
observation_type: str = "measured",
|
|
106
|
+
date: str = "latest",
|
|
107
|
+
) -> HydroObservationData:
|
|
108
|
+
"""Get hydrological observation data for a station"""
|
|
109
|
+
return await self.client.fetch_hydro_observation_data(station_code, observation_type, date)
|
meteo_lt/client.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""MeteoLt API client for external API calls"""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import List, Optional
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
5
|
|
|
6
6
|
import aiohttp
|
|
7
7
|
|
|
8
|
-
from .models import
|
|
8
|
+
from .models import (
|
|
9
|
+
Place,
|
|
10
|
+
Forecast,
|
|
11
|
+
HydroStation,
|
|
12
|
+
HydroObservationData,
|
|
13
|
+
HydroObservation,
|
|
14
|
+
)
|
|
9
15
|
from .const import BASE_URL, WARNINGS_URL, TIMEOUT, ENCODING
|
|
10
16
|
|
|
11
17
|
|
|
@@ -25,11 +31,16 @@ class MeteoLtClient:
|
|
|
25
31
|
)
|
|
26
32
|
return self
|
|
27
33
|
|
|
28
|
-
async def __aexit__(
|
|
34
|
+
async def __aexit__(
|
|
35
|
+
self,
|
|
36
|
+
exc_type: Optional[type],
|
|
37
|
+
exc_val: Optional[Exception],
|
|
38
|
+
exc_tb: Optional[Any],
|
|
39
|
+
) -> None:
|
|
29
40
|
"""Async context manager exit"""
|
|
30
41
|
await self.close()
|
|
31
42
|
|
|
32
|
-
async def close(self):
|
|
43
|
+
async def close(self) -> None:
|
|
33
44
|
"""Close the client session if we own it"""
|
|
34
45
|
if self._session and self._owns_session:
|
|
35
46
|
await self._session.close()
|
|
@@ -55,14 +66,12 @@ class MeteoLtClient:
|
|
|
55
66
|
async def fetch_forecast(self, place_code: str) -> Forecast:
|
|
56
67
|
"""Retrieves forecast data from API"""
|
|
57
68
|
session = await self._get_session()
|
|
58
|
-
async with session.get(
|
|
59
|
-
f"{BASE_URL}/places/{place_code}/forecasts/long-term"
|
|
60
|
-
) as response:
|
|
69
|
+
async with session.get(f"{BASE_URL}/places/{place_code}/forecasts/long-term") as response:
|
|
61
70
|
response.encoding = ENCODING
|
|
62
71
|
response_json = await response.json()
|
|
63
72
|
return Forecast.from_dict(response_json)
|
|
64
73
|
|
|
65
|
-
async def fetch_weather_warnings(self) ->
|
|
74
|
+
async def fetch_weather_warnings(self) -> Dict[str, Any]:
|
|
66
75
|
"""Fetches raw weather warnings data from meteo.lt JSON API"""
|
|
67
76
|
session = await self._get_session()
|
|
68
77
|
|
|
@@ -78,3 +87,77 @@ class MeteoLtClient:
|
|
|
78
87
|
async with session.get(latest_file_url) as response:
|
|
79
88
|
text_data = await response.text()
|
|
80
89
|
return json.loads(text_data)
|
|
90
|
+
|
|
91
|
+
async def fetch_hydro_stations(self) -> List[HydroStation]:
|
|
92
|
+
"""Get list of all hydrological stations."""
|
|
93
|
+
session = await self._get_session()
|
|
94
|
+
async with session.get(f"{BASE_URL}/hydro-stations") as resp:
|
|
95
|
+
if resp.status == 200:
|
|
96
|
+
response = await resp.json()
|
|
97
|
+
stations = []
|
|
98
|
+
for station_data in response:
|
|
99
|
+
stations.append(
|
|
100
|
+
HydroStation(
|
|
101
|
+
code=station_data.get("code"),
|
|
102
|
+
name=station_data.get("name"),
|
|
103
|
+
water_body=station_data.get("waterBody"),
|
|
104
|
+
coordinates=station_data.get("coordinates", {}),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return stations
|
|
108
|
+
else:
|
|
109
|
+
raise Exception(f"API returned status {resp.status}")
|
|
110
|
+
|
|
111
|
+
async def fetch_hydro_station(self, station_code: str) -> HydroStation:
|
|
112
|
+
"""Get information about a specific hydrological station."""
|
|
113
|
+
session = await self._get_session()
|
|
114
|
+
async with session.get(f"{BASE_URL}/hydro-stations/{station_code}") as resp:
|
|
115
|
+
if resp.status == 200:
|
|
116
|
+
response = await resp.json()
|
|
117
|
+
return HydroStation(
|
|
118
|
+
code=response.get("code"),
|
|
119
|
+
name=response.get("name"),
|
|
120
|
+
water_body=response.get("waterBody"),
|
|
121
|
+
coordinates=response.get("coordinates", {}),
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
raise Exception(f"API returned status {resp.status}")
|
|
125
|
+
|
|
126
|
+
async def fetch_hydro_observation_data(
|
|
127
|
+
self,
|
|
128
|
+
station_code: str,
|
|
129
|
+
observation_type: str = "measured",
|
|
130
|
+
date: str = "latest",
|
|
131
|
+
) -> HydroObservationData:
|
|
132
|
+
"""Get hydrological observation data for a station."""
|
|
133
|
+
session = await self._get_session()
|
|
134
|
+
async with session.get(
|
|
135
|
+
f"{BASE_URL}/hydro-stations/{station_code}/observations/{observation_type}/{date}"
|
|
136
|
+
) as resp:
|
|
137
|
+
if resp.status == 200:
|
|
138
|
+
response = await resp.json()
|
|
139
|
+
station = HydroStation(
|
|
140
|
+
code=response["station"].get("code"),
|
|
141
|
+
name=response["station"].get("name"),
|
|
142
|
+
water_body=response["station"].get("waterBody"),
|
|
143
|
+
coordinates=response["station"].get("coordinates", {}),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
observations = []
|
|
147
|
+
for obs_data in response.get("observations", []):
|
|
148
|
+
observations.append(
|
|
149
|
+
HydroObservation(
|
|
150
|
+
observation_datetime=obs_data.get("observationTimeUtc"),
|
|
151
|
+
water_level=obs_data.get("waterLevel"),
|
|
152
|
+
water_temperature=obs_data.get("waterTemperature"),
|
|
153
|
+
water_discharge=obs_data.get("waterDischarge"),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return HydroObservationData(
|
|
158
|
+
station=station,
|
|
159
|
+
observations_data_range=response.get("observationsDataRange"),
|
|
160
|
+
observations=observations,
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
raise Exception(f"API returned status {resp.status}")
|
meteo_lt/const.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"""const.py"""
|
|
2
2
|
|
|
3
3
|
BASE_URL = "https://api.meteo.lt/v1"
|
|
4
|
-
WARNINGS_URL =
|
|
5
|
-
"https://www.meteo.lt/app/mu-plugins/Meteo/Components/"
|
|
6
|
-
"WeatherWarningsNew/list_JSON.php"
|
|
7
|
-
)
|
|
4
|
+
WARNINGS_URL = "https://www.meteo.lt/app/mu-plugins/Meteo/Components/" "WeatherWarningsNew/list_JSON.php"
|
|
8
5
|
TIMEOUT = 30
|
|
9
6
|
ENCODING = "utf-8"
|
|
10
7
|
|
meteo_lt/models.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field, fields
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
-
from typing import List, Optional
|
|
5
|
+
from typing import List, Optional, Dict, Any, Type
|
|
6
6
|
|
|
7
7
|
from .const import COUNTY_MUNICIPALITIES
|
|
8
8
|
|
|
@@ -16,17 +16,12 @@ class Coordinates:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
|
-
class
|
|
20
|
-
"""
|
|
19
|
+
class LocationBase:
|
|
20
|
+
"""Base class for locations with coordinates"""
|
|
21
21
|
|
|
22
22
|
code: str
|
|
23
23
|
name: str
|
|
24
|
-
administrative_division: str = field(
|
|
25
|
-
metadata={"json_key": "administrativeDivision"}
|
|
26
|
-
)
|
|
27
|
-
country_code: str = field(metadata={"json_key": "countryCode"})
|
|
28
24
|
coordinates: Coordinates
|
|
29
|
-
counties: List[str] = field(init=False)
|
|
30
25
|
|
|
31
26
|
@property
|
|
32
27
|
def latitude(self):
|
|
@@ -38,13 +33,19 @@ class Place:
|
|
|
38
33
|
"""Longitude from coordinates"""
|
|
39
34
|
return self.coordinates.longitude
|
|
40
35
|
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Place(LocationBase):
|
|
39
|
+
"""Places"""
|
|
40
|
+
|
|
41
|
+
administrative_division: str = field(metadata={"json_key": "administrativeDivision"})
|
|
42
|
+
country_code: str = field(metadata={"json_key": "countryCode"})
|
|
43
|
+
counties: List[str] = field(init=False)
|
|
44
|
+
|
|
41
45
|
def __post_init__(self):
|
|
42
46
|
self.counties = []
|
|
43
47
|
for county, municipalities in COUNTY_MUNICIPALITIES.items():
|
|
44
|
-
if (
|
|
45
|
-
self.administrative_division.replace(" savivaldybė", "")
|
|
46
|
-
in municipalities
|
|
47
|
-
):
|
|
48
|
+
if self.administrative_division.replace(" savivaldybė", "") in municipalities:
|
|
48
49
|
self.counties.append(county)
|
|
49
50
|
|
|
50
51
|
|
|
@@ -60,6 +61,32 @@ class WeatherWarning:
|
|
|
60
61
|
end_time: Optional[str] = None
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
@dataclass
|
|
65
|
+
class HydroStation(LocationBase):
|
|
66
|
+
"""Hydrological station data."""
|
|
67
|
+
|
|
68
|
+
water_body: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class HydroObservation:
|
|
73
|
+
"""Single hydrological observation."""
|
|
74
|
+
|
|
75
|
+
observation_datetime: Optional[str] = None
|
|
76
|
+
water_level: Optional[float] = None # cm
|
|
77
|
+
water_temperature: Optional[float] = None # °C
|
|
78
|
+
water_discharge: Optional[float] = None # m3/s
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class HydroObservationData:
|
|
83
|
+
"""Observation data response."""
|
|
84
|
+
|
|
85
|
+
station: HydroStation
|
|
86
|
+
observations_data_range: Optional[dict] = None
|
|
87
|
+
observations: List[HydroObservation] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
63
90
|
@dataclass
|
|
64
91
|
class ForecastTimestamp:
|
|
65
92
|
"""ForecastTimestamp"""
|
|
@@ -85,16 +112,12 @@ class Forecast:
|
|
|
85
112
|
place: Place
|
|
86
113
|
forecast_created: str = field(metadata={"json_key": "forecastCreationTimeUtc"})
|
|
87
114
|
current_conditions: ForecastTimestamp
|
|
88
|
-
forecast_timestamps: List[ForecastTimestamp] = field(
|
|
89
|
-
metadata={"json_key": "forecastTimestamps"}
|
|
90
|
-
)
|
|
115
|
+
forecast_timestamps: List[ForecastTimestamp] = field(metadata={"json_key": "forecastTimestamps"})
|
|
91
116
|
|
|
92
117
|
def __post_init__(self):
|
|
93
118
|
"""Post-initialization processing."""
|
|
94
119
|
|
|
95
|
-
current_hour = datetime.now(timezone.utc).replace(
|
|
96
|
-
minute=0, second=0, microsecond=0
|
|
97
|
-
)
|
|
120
|
+
current_hour = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
|
|
98
121
|
# Current conditions are equal to current hour record
|
|
99
122
|
for forecast in self.forecast_timestamps:
|
|
100
123
|
if (
|
|
@@ -118,9 +141,9 @@ class Forecast:
|
|
|
118
141
|
]
|
|
119
142
|
|
|
120
143
|
|
|
121
|
-
def from_dict(cls, data:
|
|
144
|
+
def from_dict(cls: Type, data: Dict[str, Any]) -> Any:
|
|
122
145
|
"""Utility function to convert a dictionary to a dataclass instance."""
|
|
123
|
-
init_args = {}
|
|
146
|
+
init_args: Dict[str, Any] = {}
|
|
124
147
|
for f in fields(cls):
|
|
125
148
|
if not f.init:
|
|
126
149
|
continue # Skip fields that are not part of the constructor
|
|
@@ -133,11 +156,9 @@ def from_dict(cls, data: dict):
|
|
|
133
156
|
value = from_dict(f.type, value)
|
|
134
157
|
elif isinstance(value, list) and hasattr(f.type.__args__[0], "from_dict"):
|
|
135
158
|
value = [from_dict(f.type.__args__[0], item) for item in value]
|
|
136
|
-
elif f.name in ("datetime", "forecast_created"):
|
|
159
|
+
elif f.name in ("datetime", "forecast_created", "observation_datetime") and value:
|
|
137
160
|
# Convert datetime to ISO 8601 format
|
|
138
|
-
dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S").replace(
|
|
139
|
-
tzinfo=timezone.utc
|
|
140
|
-
)
|
|
161
|
+
dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
|
141
162
|
value = dt.isoformat()
|
|
142
163
|
|
|
143
164
|
init_args[f.name] = value
|
|
@@ -149,3 +170,6 @@ Place.from_dict = classmethod(from_dict)
|
|
|
149
170
|
ForecastTimestamp.from_dict = classmethod(from_dict)
|
|
150
171
|
Forecast.from_dict = classmethod(from_dict)
|
|
151
172
|
WeatherWarning.from_dict = classmethod(from_dict)
|
|
173
|
+
HydroStation.from_dict = classmethod(from_dict)
|
|
174
|
+
HydroObservation.from_dict = classmethod(from_dict)
|
|
175
|
+
HydroObservationData.from_dict = classmethod(from_dict)
|
meteo_lt/utils.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""utils.py"""
|
|
2
2
|
|
|
3
3
|
from math import radians, sin, cos, sqrt, atan2
|
|
4
|
+
from typing import TypeVar, List
|
|
4
5
|
|
|
6
|
+
LocationT = TypeVar("LocationT") # Type variable for location objects with latitude/longitude
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
7
10
|
"""Calculate the great-circle distance between two points on the Earth's surface."""
|
|
8
11
|
# Convert latitude and longitude from degrees to radians
|
|
9
12
|
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
|
@@ -17,18 +20,18 @@ def haversine(lat1, lon1, lat2, lon2):
|
|
|
17
20
|
return r * c
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
def
|
|
21
|
-
"""Find the nearest
|
|
22
|
-
|
|
23
|
+
def find_nearest_location(latitude: float, longitude: float, locations: List[LocationT]) -> LocationT:
|
|
24
|
+
"""Find the nearest location from a list of locations based on the given latitude and longitude."""
|
|
25
|
+
nearest_location = None
|
|
23
26
|
min_distance = float("inf")
|
|
24
27
|
|
|
25
|
-
for
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
distance = haversine(latitude, longitude,
|
|
28
|
+
for location in locations:
|
|
29
|
+
location_lat = location.latitude
|
|
30
|
+
location_lon = location.longitude
|
|
31
|
+
distance = haversine(latitude, longitude, location_lat, location_lon)
|
|
29
32
|
|
|
30
33
|
if distance < min_distance:
|
|
31
34
|
min_distance = distance
|
|
32
|
-
|
|
35
|
+
nearest_location = location
|
|
33
36
|
|
|
34
|
-
return
|
|
37
|
+
return nearest_location
|
meteo_lt/warnings.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
6
|
|
|
7
7
|
from .models import Forecast, WeatherWarning
|
|
8
8
|
from .const import COUNTY_MUNICIPALITIES
|
|
@@ -15,24 +15,18 @@ class WeatherWarningsProcessor:
|
|
|
15
15
|
def __init__(self, client: MeteoLtClient):
|
|
16
16
|
self.client = client
|
|
17
17
|
|
|
18
|
-
async def get_weather_warnings(
|
|
19
|
-
self, administrative_division: str = None
|
|
20
|
-
) -> List[WeatherWarning]:
|
|
18
|
+
async def get_weather_warnings(self, administrative_division: str = None) -> List[WeatherWarning]:
|
|
21
19
|
"""Fetches and processes weather warnings"""
|
|
22
20
|
warnings_data = await self.client.fetch_weather_warnings()
|
|
23
21
|
warnings = self._parse_warnings_data(warnings_data)
|
|
24
22
|
|
|
25
23
|
# Filter by administrative division if specified
|
|
26
24
|
if administrative_division:
|
|
27
|
-
warnings = [
|
|
28
|
-
w
|
|
29
|
-
for w in warnings
|
|
30
|
-
if self._warning_affects_area(w, administrative_division)
|
|
31
|
-
]
|
|
25
|
+
warnings = [w for w in warnings if self._warning_affects_area(w, administrative_division)]
|
|
32
26
|
|
|
33
27
|
return warnings
|
|
34
28
|
|
|
35
|
-
def _parse_warnings_data(self, warnings_data:
|
|
29
|
+
def _parse_warnings_data(self, warnings_data: Optional[Dict[str, Any]]) -> List[WeatherWarning]:
|
|
36
30
|
"""Parse raw warnings data into WeatherWarning objects"""
|
|
37
31
|
warnings = []
|
|
38
32
|
|
|
@@ -49,9 +43,7 @@ class WeatherWarningsProcessor:
|
|
|
49
43
|
for area_group in phenomenon_group.get("area_groups", []):
|
|
50
44
|
for alert in area_group.get("single_alerts", []):
|
|
51
45
|
# Skip alerts with no phenomenon or empty descriptions
|
|
52
|
-
if not alert.get("phenomenon") or not alert.get(
|
|
53
|
-
"description", {}
|
|
54
|
-
).get("lt"):
|
|
46
|
+
if not alert.get("phenomenon") or not alert.get("description", {}).get("lt"):
|
|
55
47
|
continue
|
|
56
48
|
|
|
57
49
|
# Create warnings for each area in the group
|
|
@@ -62,7 +54,7 @@ class WeatherWarningsProcessor:
|
|
|
62
54
|
|
|
63
55
|
return warnings
|
|
64
56
|
|
|
65
|
-
def _create_warning_from_alert(self, alert:
|
|
57
|
+
def _create_warning_from_alert(self, alert: Dict[str, Any], area: Dict[str, Any]) -> WeatherWarning:
|
|
66
58
|
"""Create a WeatherWarning from alert data"""
|
|
67
59
|
county = area.get("name", "Unknown")
|
|
68
60
|
phenomenon = alert.get("phenomenon", "")
|
|
@@ -89,15 +81,9 @@ class WeatherWarningsProcessor:
|
|
|
89
81
|
end_time=alert.get("t_to"),
|
|
90
82
|
)
|
|
91
83
|
|
|
92
|
-
def _warning_affects_area(
|
|
93
|
-
self, warning: WeatherWarning, administrative_division: str
|
|
94
|
-
) -> bool:
|
|
84
|
+
def _warning_affects_area(self, warning: WeatherWarning, administrative_division: str) -> bool:
|
|
95
85
|
"""Check if warning affects specified administrative division"""
|
|
96
|
-
admin_lower = (
|
|
97
|
-
administrative_division.lower()
|
|
98
|
-
.replace(" savivaldybė", "")
|
|
99
|
-
.replace(" sav.", "")
|
|
100
|
-
)
|
|
86
|
+
admin_lower = administrative_division.lower().replace(" savivaldybė", "").replace(" sav.", "")
|
|
101
87
|
|
|
102
88
|
# Check if the administrative division matches the warning county
|
|
103
89
|
if admin_lower in warning.county.lower():
|
|
@@ -107,28 +93,20 @@ class WeatherWarningsProcessor:
|
|
|
107
93
|
if warning.county in COUNTY_MUNICIPALITIES:
|
|
108
94
|
municipalities = COUNTY_MUNICIPALITIES[warning.county]
|
|
109
95
|
for municipality in municipalities:
|
|
110
|
-
mun_clean = (
|
|
111
|
-
municipality.lower()
|
|
112
|
-
.replace(" savivaldybė", "")
|
|
113
|
-
.replace(" sav.", "")
|
|
114
|
-
)
|
|
96
|
+
mun_clean = municipality.lower().replace(" savivaldybė", "").replace(" sav.", "")
|
|
115
97
|
if admin_lower in mun_clean or mun_clean in admin_lower:
|
|
116
98
|
return True
|
|
117
99
|
|
|
118
100
|
return False
|
|
119
101
|
|
|
120
|
-
def enrich_forecast_with_warnings(
|
|
121
|
-
self, forecast: Forecast, warnings: List[WeatherWarning]
|
|
122
|
-
):
|
|
102
|
+
def enrich_forecast_with_warnings(self, forecast: Forecast, warnings: List[WeatherWarning]) -> None:
|
|
123
103
|
"""Enrich forecast timestamps with relevant weather warnings"""
|
|
124
104
|
if not warnings:
|
|
125
105
|
return
|
|
126
106
|
|
|
127
107
|
# For each forecast timestamp, find applicable warnings
|
|
128
108
|
for timestamp in forecast.forecast_timestamps:
|
|
129
|
-
timestamp.warnings = self._get_warnings_for_timestamp(
|
|
130
|
-
timestamp.datetime, warnings
|
|
131
|
-
)
|
|
109
|
+
timestamp.warnings = self._get_warnings_for_timestamp(timestamp.datetime, warnings)
|
|
132
110
|
|
|
133
111
|
# Also add warnings to current conditions if available
|
|
134
112
|
if hasattr(forecast, "current_conditions") and forecast.current_conditions:
|
|
@@ -136,14 +114,10 @@ class WeatherWarningsProcessor:
|
|
|
136
114
|
forecast.current_conditions.datetime, warnings
|
|
137
115
|
)
|
|
138
116
|
|
|
139
|
-
def _get_warnings_for_timestamp(
|
|
140
|
-
self, timestamp_str: str, warnings: List[WeatherWarning]
|
|
141
|
-
) -> List[WeatherWarning]:
|
|
117
|
+
def _get_warnings_for_timestamp(self, timestamp_str: str, warnings: List[WeatherWarning]) -> List[WeatherWarning]:
|
|
142
118
|
"""Get warnings that are active for a specific timestamp"""
|
|
143
119
|
try:
|
|
144
|
-
timestamp = datetime.fromisoformat(timestamp_str).replace(
|
|
145
|
-
tzinfo=timezone.utc
|
|
146
|
-
)
|
|
120
|
+
timestamp = datetime.fromisoformat(timestamp_str).replace(tzinfo=timezone.utc)
|
|
147
121
|
applicable_warnings = []
|
|
148
122
|
|
|
149
123
|
for warning in warnings:
|
|
@@ -151,12 +125,8 @@ class WeatherWarningsProcessor:
|
|
|
151
125
|
continue
|
|
152
126
|
|
|
153
127
|
try:
|
|
154
|
-
start_time = datetime.fromisoformat(
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
end_time = datetime.fromisoformat(
|
|
158
|
-
warning.end_time.replace("Z", "+00:00")
|
|
159
|
-
)
|
|
128
|
+
start_time = datetime.fromisoformat(warning.start_time.replace("Z", "+00:00"))
|
|
129
|
+
end_time = datetime.fromisoformat(warning.end_time.replace("Z", "+00:00"))
|
|
160
130
|
|
|
161
131
|
# Check if timestamp falls within warning period
|
|
162
132
|
if start_time <= timestamp <= end_time:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meteo_lt-pkg
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0b0
|
|
4
4
|
Summary: A library to fetch weather data from api.meteo.lt
|
|
5
5
|
Author-email: Brunas <brunonas@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/Brunas/meteo_lt-pkg
|
|
@@ -12,17 +12,17 @@ Classifier: Development Status :: 4 - Beta
|
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: aiohttp
|
|
15
|
+
Requires-Dist: aiohttp>=3.13
|
|
16
16
|
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: pytest; extra == "dev"
|
|
18
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
19
|
-
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
20
|
-
Requires-Dist: black; extra == "dev"
|
|
21
|
-
Requires-Dist: coverage; extra == "dev"
|
|
22
|
-
Requires-Dist: flake8; extra == "dev"
|
|
23
|
-
Requires-Dist: pyflakes; extra == "dev"
|
|
24
|
-
Requires-Dist: pylint; extra == "dev"
|
|
25
|
-
Requires-Dist: build; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=1.3; extra == "dev"
|
|
20
|
+
Requires-Dist: black>=26.0; extra == "dev"
|
|
21
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: flake8>=7.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pyflakes>=3.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pylint>=4.0; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
26
26
|
Dynamic: license-file
|
|
27
27
|
|
|
28
28
|
# Meteo.Lt Lithuanian weather forecast package
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
meteo_lt/__init__.py,sha256=HRQ5O9YMn6h2bXkhx6yuPF6voo6SiUZDrY8RZFeLaks,456
|
|
2
|
+
meteo_lt/api.py,sha256=TJrtP2ScPhT30QR9rHs2NzPibQgbkppMDrPh9JiyBoU,4086
|
|
3
|
+
meteo_lt/client.py,sha256=LwFRr1GDuVoeFEKj31w0pwmPQegwM-o1prZdtOop5r0,6291
|
|
4
|
+
meteo_lt/const.py,sha256=Mz_8F5k_dVQlGNdYdGQOXqaEye4G5Z0xoBw971O8vBM,2690
|
|
5
|
+
meteo_lt/models.py,sha256=8mUQek55pVMxjuLheW6T8oGwinf0VJdkC1Y8cWv7TIo,5667
|
|
6
|
+
meteo_lt/utils.py,sha256=D2SDbWx0o_iKwJP1tCrHLovWWmT0HUu4C9gNSQEmiDw,1339
|
|
7
|
+
meteo_lt/warnings.py,sha256=6wYzinhBzXC3apR38LSEGPwsxiVTrVgLvDWql9-w0B0,6141
|
|
8
|
+
meteo_lt_pkg-0.5.0b0.dist-info/licenses/LICENSE,sha256=3IGi6xn6NUdXGvcdwD0MUbhy3Yz5NRnUjJrwKanFAD4,1073
|
|
9
|
+
meteo_lt_pkg-0.5.0b0.dist-info/METADATA,sha256=5fBukQnZvFbMZu6XQa4PRAZTxAwivyIFCipoz3uHHhY,10382
|
|
10
|
+
meteo_lt_pkg-0.5.0b0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
meteo_lt_pkg-0.5.0b0.dist-info/top_level.txt,sha256=-aEdc9FzHhcIH4_0TNdKNxuvDnS3chKoJy6LK9Ud-G4,9
|
|
12
|
+
meteo_lt_pkg-0.5.0b0.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
meteo_lt/__init__.py,sha256=TP3DhEXEpHdI6bevRjxb0cbVrEWbI8mEocYc1DwA_KI,255
|
|
2
|
-
meteo_lt/api.py,sha256=3RCNUV9guscSADrcLRbsRomhp9rpPDrMVdCmKXsohmw,3006
|
|
3
|
-
meteo_lt/client.py,sha256=j5F1Ag00EnEI8ErLz0Q2lYqWfO8YgqYlsAHfhzkGyWE,2798
|
|
4
|
-
meteo_lt/const.py,sha256=1xeQ2wErqPREnYfK4OogadlVIu0p2gGoILsOcsIfp8Q,2702
|
|
5
|
-
meteo_lt/models.py,sha256=ki0kVe_uUVOMSC2VQsW5i2pd9A06wvuupn7jgSwyQ-0,4838
|
|
6
|
-
meteo_lt/utils.py,sha256=SL7ZeTEfdrdQoe_DOJaiA2zVxt08ujyFJ24JxJm1Hks,1081
|
|
7
|
-
meteo_lt/warnings.py,sha256=XpHMOkwqXB_mgQccyPdOlmRFcOV58Dkwtva0kNDxdpQ,6520
|
|
8
|
-
meteo_lt_pkg-0.5.0.dist-info/licenses/LICENSE,sha256=3IGi6xn6NUdXGvcdwD0MUbhy3Yz5NRnUjJrwKanFAD4,1073
|
|
9
|
-
meteo_lt_pkg-0.5.0.dist-info/METADATA,sha256=-PAVjCqFFvvdg8kEvn96UJM5ZjRCbOWLwGlbzjE1wpc,10328
|
|
10
|
-
meteo_lt_pkg-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
-
meteo_lt_pkg-0.5.0.dist-info/top_level.txt,sha256=-aEdc9FzHhcIH4_0TNdKNxuvDnS3chKoJy6LK9Ud-G4,9
|
|
12
|
-
meteo_lt_pkg-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|