Ig0tU commited on
Commit
4833096
·
1 Parent(s): 849592b

Performance optimization: Added caching (24h loc, 5m weather) and connection pooling

Browse files
main.py CHANGED
@@ -11,7 +11,9 @@ from models.weather_models import (
11
  RequestModel,
12
  )
13
 
14
- app = FastAPI(title="Weatherstack-Compatible API")
 
 
15
 
16
 
17
  @app.get("/health", include_in_schema=False)
 
11
  RequestModel,
12
  )
13
 
14
+ from services.api_client import lifespan
15
+
16
+ app = FastAPI(title="Weatherstack-Compatible API", lifespan=lifespan)
17
 
18
 
19
  @app.get("/health", include_in_schema=False)
requirements.txt CHANGED
@@ -8,3 +8,4 @@ timezonefinder
8
  pytz
9
  streamlit
10
  pandas
 
 
8
  pytz
9
  streamlit
10
  pandas
11
+ async-lru
services/api_client.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from typing import Optional
3
+ import httpx
4
+
5
+ _client: Optional[httpx.AsyncClient] = None
6
+
7
+
8
+ @asynccontextmanager
9
+ async def lifespan(app):
10
+ global _client
11
+ _client = httpx.AsyncClient()
12
+ yield
13
+ await _client.aclose()
14
+
15
+
16
+ def get_client() -> httpx.AsyncClient:
17
+ global _client
18
+ if _client is None:
19
+ raise RuntimeError("HTTP Client not initialized. Ensure lifespan is used.")
20
+ return _client
services/location_service.py CHANGED
@@ -3,11 +3,14 @@ from timezonefinder import TimezoneFinder
3
  import pytz
4
  from datetime import datetime
5
  from typing import Dict, Any, Optional
 
 
6
  from models.weather_models import LocationModel
7
 
8
  tf = TimezoneFinder()
9
 
10
 
 
11
  async def get_location_details(query: str) -> Optional[LocationModel]:
12
  # Basic Lat/Lon check
13
  if "," in query:
@@ -22,24 +25,24 @@ async def get_location_details(query: str) -> Optional[LocationModel]:
22
  url = f"https://nominatim.openstreetmap.org/search?q={query}&format=json&limit=1&addressdetails=1"
23
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
24
 
25
- async with httpx.AsyncClient() as client:
26
- response = await client.get(url, headers=headers)
27
- if response.status_code == 200 and response.json():
28
- data = response.json()[0]
29
- lat = float(data["lat"])
30
- lon = float(data["lon"])
31
- address = data.get("address", {})
32
-
33
- return await get_location_from_coords(
34
- lat,
35
- lon,
36
- name=address.get("city")
37
- or address.get("town")
38
- or address.get("village")
39
- or data["display_name"].split(",")[0],
40
- country=address.get("country", ""),
41
- region=address.get("state") or address.get("region", ""),
42
- )
43
 
44
  return None
45
 
@@ -59,19 +62,19 @@ async def get_location_from_coords(
59
  if not name:
60
  url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
61
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
62
- async with httpx.AsyncClient() as client:
63
- res = await client.get(url, headers=headers)
64
- if res.status_code == 200:
65
- data = res.json()
66
- address = data.get("address", {})
67
- name = (
68
- address.get("city")
69
- or address.get("town")
70
- or address.get("village")
71
- or "Unknown"
72
- )
73
- country = address.get("country", "Unknown")
74
- region = address.get("state") or address.get("region", "Unknown")
75
 
76
  return LocationModel(
77
  name=name or "Unknown",
@@ -90,36 +93,36 @@ async def get_autocomplete_suggestions(query: str) -> list:
90
  url = f"https://nominatim.openstreetmap.org/search?q={query}&format=json&limit=10&addressdetails=1"
91
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
92
 
93
- async with httpx.AsyncClient() as client:
94
- response = await client.get(url, headers=headers)
95
- if response.status_code == 200:
96
- data = response.json()
97
- suggestions = []
98
- for item in data:
99
- address = item.get("address", {})
100
- lat = float(item["lat"])
101
- lon = float(item["lon"])
102
- timezone_id = tf.timezone_at(lng=lon, lat=lat) or "UTC"
103
- tz = pytz.timezone(timezone_id)
104
- now = datetime.now(tz)
105
- offset_hours = now.utcoffset().total_seconds() / 3600
106
-
107
- from models.weather_models import AutocompleteLocation
108
-
109
- suggestions.append(
110
- AutocompleteLocation(
111
- id=int(item["place_id"]),
112
- name=address.get("city")
113
- or address.get("town")
114
- or address.get("village")
115
- or item["display_name"].split(",")[0],
116
- country=address.get("country", ""),
117
- region=address.get("state") or address.get("region", ""),
118
- lat=str(round(lat, 3)),
119
- lon=str(round(lon, 3)),
120
- timezone_id=timezone_id,
121
- utc_offset=f"{offset_hours:.1f}",
122
- )
123
  )
124
- return suggestions
 
125
  return []
 
3
  import pytz
4
  from datetime import datetime
5
  from typing import Dict, Any, Optional
6
+ from async_lru import alru_cache
7
+ from services.api_client import get_client
8
  from models.weather_models import LocationModel
9
 
10
  tf = TimezoneFinder()
11
 
12
 
13
+ @alru_cache(maxsize=1000)
14
  async def get_location_details(query: str) -> Optional[LocationModel]:
15
  # Basic Lat/Lon check
16
  if "," in query:
 
25
  url = f"https://nominatim.openstreetmap.org/search?q={query}&format=json&limit=1&addressdetails=1"
26
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
27
 
28
+ client = get_client()
29
+ response = await client.get(url, headers=headers)
30
+ if response.status_code == 200 and response.json():
31
+ data = response.json()[0]
32
+ lat = float(data["lat"])
33
+ lon = float(data["lon"])
34
+ address = data.get("address", {})
35
+
36
+ return await get_location_from_coords(
37
+ lat,
38
+ lon,
39
+ name=address.get("city")
40
+ or address.get("town")
41
+ or address.get("village")
42
+ or data["display_name"].split(",")[0],
43
+ country=address.get("country", ""),
44
+ region=address.get("state") or address.get("region", ""),
45
+ )
46
 
47
  return None
48
 
 
62
  if not name:
63
  url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
64
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
65
+ client = get_client()
66
+ res = await client.get(url, headers=headers)
67
+ if res.status_code == 200:
68
+ data = res.json()
69
+ address = data.get("address", {})
70
+ name = (
71
+ address.get("city")
72
+ or address.get("town")
73
+ or address.get("village")
74
+ or "Unknown"
75
+ )
76
+ country = address.get("country", "Unknown")
77
+ region = address.get("state") or address.get("region", "Unknown")
78
 
79
  return LocationModel(
80
  name=name or "Unknown",
 
93
  url = f"https://nominatim.openstreetmap.org/search?q={query}&format=json&limit=10&addressdetails=1"
94
  headers = {"User-Agent": "WeatherstackReplacementAPI/1.0"}
95
 
96
+ client = get_client()
97
+ response = await client.get(url, headers=headers)
98
+ if response.status_code == 200:
99
+ data = response.json()
100
+ suggestions = []
101
+ for item in data:
102
+ address = item.get("address", {})
103
+ lat = float(item["lat"])
104
+ lon = float(item["lon"])
105
+ timezone_id = tf.timezone_at(lng=lon, lat=lat) or "UTC"
106
+ tz = pytz.timezone(timezone_id)
107
+ now = datetime.now(tz)
108
+ offset_hours = now.utcoffset().total_seconds() / 3600
109
+
110
+ from models.weather_models import AutocompleteLocation
111
+
112
+ suggestions.append(
113
+ AutocompleteLocation(
114
+ id=int(item["place_id"]),
115
+ name=address.get("city")
116
+ or address.get("town")
117
+ or address.get("village")
118
+ or item["display_name"].split(",")[0],
119
+ country=address.get("country", ""),
120
+ region=address.get("state") or address.get("region", ""),
121
+ lat=str(round(lat, 3)),
122
+ lon=str(round(lon, 3)),
123
+ timezone_id=timezone_id,
124
+ utc_offset=f"{offset_hours:.1f}",
 
125
  )
126
+ )
127
+ return suggestions
128
  return []
services/weather_service.py CHANGED
@@ -2,6 +2,8 @@ import httpx
2
  import asyncio
3
  from datetime import datetime
4
  from typing import Optional, Dict, Any, List
 
 
5
  from models.weather_models import CurrentWeatherModel, AstroModel, AirQualityModel
6
  from utils.conversions import (
7
  convert_temperature,
@@ -37,6 +39,7 @@ WMO_TO_WEATHERSTACK = {
37
  AIR_QUALITY_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"
38
 
39
 
 
40
  async def fetch_current_weather(
41
  lat: float, lon: float, unit: str = "m"
42
  ) -> CurrentWeatherModel:
@@ -55,13 +58,15 @@ async def fetch_current_weather(
55
  "timezone": "auto",
56
  }
57
 
58
- async with httpx.AsyncClient() as client:
59
- weather_res, aq_res = await asyncio.gather(
60
- client.get(OPEN_METEO_URL, params=params),
61
- client.get(AIR_QUALITY_URL, params=aq_params),
62
- )
63
 
64
- if weather_res.status_code == 200:
 
 
 
 
 
 
 
65
  data = weather_res.json()
66
  current = data["current"]
67
  daily = data.get("daily", {})
@@ -154,6 +159,7 @@ def get_wind_dir(degrees: float) -> str:
154
  return directions[idx]
155
 
156
 
 
157
  async def fetch_weather_forecast(
158
  lat: float, lon: float, days: int, unit: str = "m"
159
  ) -> Dict[str, Any]:
@@ -167,9 +173,10 @@ async def fetch_weather_forecast(
167
  "timezone": "auto",
168
  }
169
 
170
- async with httpx.AsyncClient() as client:
171
- response = await client.get(OPEN_METEO_URL, params=params)
172
- if response.status_code == 200:
 
173
  data = response.json()
174
  daily = data["daily"]
175
 
@@ -217,6 +224,7 @@ async def fetch_weather_forecast(
217
  HISTORICAL_API_URL = "https://archive-api.open-meteo.com/v1/archive"
218
 
219
 
 
220
  async def fetch_historical_weather(
221
  lat: float, lon: float, date: str, unit: str = "m"
222
  ) -> Dict[str, Any]:
@@ -231,9 +239,10 @@ async def fetch_historical_weather(
231
  "timezone": "auto",
232
  }
233
 
234
- async with httpx.AsyncClient() as client:
235
- response = await client.get(HISTORICAL_API_URL, params=params)
236
- if response.status_code == 200:
 
237
  data = response.json()
238
  daily = data["daily"]
239
 
 
2
  import asyncio
3
  from datetime import datetime
4
  from typing import Optional, Dict, Any, List
5
+ from async_lru import alru_cache
6
+ from services.api_client import get_client
7
  from models.weather_models import CurrentWeatherModel, AstroModel, AirQualityModel
8
  from utils.conversions import (
9
  convert_temperature,
 
39
  AIR_QUALITY_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"
40
 
41
 
42
+ @alru_cache(maxsize=100, ttl=300)
43
  async def fetch_current_weather(
44
  lat: float, lon: float, unit: str = "m"
45
  ) -> CurrentWeatherModel:
 
58
  "timezone": "auto",
59
  }
60
 
 
 
 
 
 
61
 
62
+ client = get_client()
63
+
64
+ weather_res, aq_res = await asyncio.gather(
65
+ client.get(OPEN_METEO_URL, params=params),
66
+ client.get(AIR_QUALITY_URL, params=aq_params),
67
+ )
68
+
69
+ if weather_res.status_code == 200:
70
  data = weather_res.json()
71
  current = data["current"]
72
  daily = data.get("daily", {})
 
159
  return directions[idx]
160
 
161
 
162
+ @alru_cache(maxsize=100, ttl=300)
163
  async def fetch_weather_forecast(
164
  lat: float, lon: float, days: int, unit: str = "m"
165
  ) -> Dict[str, Any]:
 
173
  "timezone": "auto",
174
  }
175
 
176
+
177
+ client = get_client()
178
+ response = await client.get(OPEN_METEO_URL, params=params)
179
+ if response.status_code == 200:
180
  data = response.json()
181
  daily = data["daily"]
182
 
 
224
  HISTORICAL_API_URL = "https://archive-api.open-meteo.com/v1/archive"
225
 
226
 
227
+ @alru_cache(maxsize=100, ttl=300)
228
  async def fetch_historical_weather(
229
  lat: float, lon: float, date: str, unit: str = "m"
230
  ) -> Dict[str, Any]:
 
239
  "timezone": "auto",
240
  }
241
 
242
+
243
+ client = get_client()
244
+ response = await client.get(HISTORICAL_API_URL, params=params)
245
+ if response.status_code == 200:
246
  data = response.json()
247
  daily = data["daily"]
248