Add Nominatim rate limiting and caching enhancements across commands. Introduce NominatimRateLimiter for API compliance, update geocoding methods to utilize rate-limited calls, and extend caching duration for geocoding results to 30 days. Refactor command implementations to improve efficiency and prevent duplicate API requests.

This commit is contained in:
agessaman
2025-11-26 14:34:45 -08:00
parent a6d2065379
commit 92f063b4bc
10 changed files with 293 additions and 100 deletions
@@ -8,6 +8,7 @@ import re
import requests
from datetime import datetime, timedelta
from geopy.geocoders import Nominatim
from ...utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder
from ..base_command import BaseCommand
from ...models import MeshMessage
@@ -53,8 +54,8 @@ class GlobalWxCommand(BaseCommand):
self.logger.warning(f"Invalid precipitation_unit '{self.precipitation_unit}', using 'inch'")
self.precipitation_unit = 'inch'
# Initialize geocoder
self.geolocator = Nominatim(user_agent="meshcore-bot")
# Initialize geocoder (will use rate-limited helpers for actual calls)
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
@@ -241,7 +242,9 @@ class GlobalWxCommand(BaseCommand):
self.logger.debug(f"Using cached geocoding for {location}")
# Get address details with reverse geocoding
try:
reverse_location = self.geolocator.reverse(f"{cached_lat}, {cached_lon}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{cached_lat}, {cached_lon}", timeout=10
)
if reverse_location:
address_info = reverse_location.raw.get('address', {})
# Store the full geocode result for display name
@@ -254,11 +257,15 @@ class GlobalWxCommand(BaseCommand):
geocode_result = None
# Strategy 1: Try as-is
geocode_result = self.geolocator.geocode(location)
geocode_result = rate_limited_nominatim_geocode_sync(
self.bot, location, timeout=10
)
# Strategy 2: If no result and no country specified, try with default country
if not geocode_result and ',' not in location:
geocode_result = self.geolocator.geocode(f"{location}, {self.default_country}")
geocode_result = rate_limited_nominatim_geocode_sync(
self.bot, f"{location}, {self.default_country}", timeout=10
)
if not geocode_result:
return None, None, None, None
+20 -19
View File
@@ -10,6 +10,7 @@ import requests_cache
from retry_requests import retry
from datetime import datetime
from geopy.geocoders import Nominatim
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder
from .base_command import BaseCommand
from ..models import MeshMessage
@@ -41,8 +42,8 @@ class AqiCommand(BaseCommand):
# Get timezone from config
self.timezone = self.bot.config.get('Bot', 'timezone', fallback='America/Los_Angeles')
# Initialize geocoder
self.geolocator = Nominatim(user_agent="meshcore-bot")
# Initialize geocoder (will use rate-limited helpers for actual calls)
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
@@ -384,7 +385,7 @@ class AqiCommand(BaseCommand):
mapped_location = zip_code_mappings[zip_code]
self.logger.debug(f"Using specific mapping for ZIP {zip_code}: {mapped_location}")
try:
result = self.geolocator.geocode(mapped_location)
result = rate_limited_nominatim_geocode_sync(self.bot, mapped_location, timeout=10)
if result and result.address:
location_result = result
self.logger.debug(f"Found mapped location for ZIP {zip_code}: {result.address}")
@@ -397,10 +398,10 @@ class AqiCommand(BaseCommand):
try:
if isinstance(query, dict):
# Use structured query
result = self.geolocator.geocode(query=query)
result = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
else:
# Use text-based query
result = self.geolocator.geocode(query)
result = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
if result and result.address:
# Check if it's a US location
@@ -425,7 +426,7 @@ class AqiCommand(BaseCommand):
# Get detailed address info via reverse geocoding
try:
reverse_location = self.geolocator.reverse(f"{lat}, {lon}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{lat}, {lon}", timeout=10)
if reverse_location:
address_info = reverse_location.raw.get('address', {})
else:
@@ -608,7 +609,7 @@ class AqiCommand(BaseCommand):
self.logger.debug(f"Using cached geocoding for {city}")
# Still need to do reverse geocoding for address details
try:
reverse_location = self.geolocator.reverse(f"{cached_lat}, {cached_lon}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{cached_lat}, {cached_lon}", timeout=10)
if reverse_location:
return cached_lat, cached_lon, reverse_location.raw.get('address', {})
except:
@@ -629,14 +630,14 @@ class AqiCommand(BaseCommand):
if is_country:
# Handle international cities
location = self.geolocator.geocode(f"{city_name}, {state_or_country}")
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city_name}, {state_or_country}", timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city_name}, {state_or_country}", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -644,14 +645,14 @@ class AqiCommand(BaseCommand):
return location.latitude, location.longitude, location.raw.get('address', {})
else:
# Handle US city, state format
location = self.geolocator.geocode(f"{city_name}, {state_or_country}, USA")
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city_name}, {state_or_country}, USA", timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city_name}, {state_or_country}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -676,14 +677,14 @@ class AqiCommand(BaseCommand):
# If it's a major city with multiple locations, try the major ones first
if city.lower() in major_city_mappings:
for major_city_query in major_city_mappings[city.lower()]:
location = self.geolocator.geocode(major_city_query)
location = rate_limited_nominatim_geocode_sync(self.bot, major_city_query, timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -691,14 +692,14 @@ class AqiCommand(BaseCommand):
return location.latitude, location.longitude, location.raw.get('address', {})
# First try with default state
location = self.geolocator.geocode(f"{city}, {self.default_state}, USA")
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city}, {self.default_state}, USA", timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city}, {self.default_state}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -708,14 +709,14 @@ class AqiCommand(BaseCommand):
# Try neighborhood-specific queries for major cities
neighborhood_queries = self.get_neighborhood_queries(city)
for query in neighborhood_queries:
location = self.geolocator.geocode(query)
location = rate_limited_nominatim_geocode_sync(self.bot, query, timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(query, location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -723,14 +724,14 @@ class AqiCommand(BaseCommand):
return location.latitude, location.longitude, location.raw.get('address', {})
# Try without state as final fallback
location = self.geolocator.geocode(f"{city}, USA")
location = rate_limited_nominatim_geocode_sync(self.bot, f"{city}, USA", timeout=10)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(self.bot, f"{location.latitude}, {location.longitude}", timeout=10)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
+4 -3
View File
@@ -393,9 +393,10 @@ class PrefixCommand(BaseCommand):
location_str = abbreviate_location(city, 20)
else:
# Fallback to basic geocoding
from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="meshcore-bot")
location = geolocator.reverse(f"{row['latitude']}, {row['longitude']}")
from ..utils import rate_limited_nominatim_reverse_sync
location = rate_limited_nominatim_reverse_sync(
self.bot, f"{row['latitude']}, {row['longitude']}", timeout=10
)
if location:
address = location.raw.get('address', {})
# Try neighborhood first, then city, then town, etc.
+19 -27
View File
@@ -10,6 +10,7 @@ import time
import hashlib
import pytz
from geopy.geocoders import Nominatim
from ..utils import rate_limited_nominatim_geocode, rate_limited_nominatim_reverse, get_nominatim_geocoder
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple, Dict
from .base_command import BaseCommand
@@ -50,8 +51,8 @@ class SolarforecastCommand(BaseCommand):
# Get default state from config for city disambiguation
self.default_state = self.bot.config.get('Weather', 'default_state', fallback='WA')
# Initialize geocoder
self.geolocator = Nominatim(user_agent="meshcore-bot")
# Initialize geocoder (will use rate-limited helpers for actual calls)
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
@@ -352,10 +353,8 @@ class SolarforecastCommand(BaseCommand):
if cached_lat and cached_lon:
return cached_lat, cached_lon
loop = asyncio.get_event_loop()
location = await loop.run_in_executor(
None,
lambda: self.geolocator.geocode(f"{zipcode}, USA", timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, f"{zipcode}, USA", timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
@@ -391,9 +390,8 @@ class SolarforecastCommand(BaseCommand):
if cached_lat and cached_lon:
return cached_lat, cached_lon
location = await loop.run_in_executor(
None,
lambda q=major_city_query: self.geolocator.geocode(q, timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, major_city_query, timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
@@ -406,9 +404,8 @@ class SolarforecastCommand(BaseCommand):
if cached_lat and cached_lon:
return cached_lat, cached_lon
location = await loop.run_in_executor(
None,
lambda: self.geolocator.geocode(state_query, timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, state_query, timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(state_query, location.latitude, location.longitude)
@@ -420,27 +417,24 @@ class SolarforecastCommand(BaseCommand):
if cached_lat and cached_lon:
return cached_lat, cached_lon
location = await loop.run_in_executor(
None,
lambda: self.geolocator.geocode(cache_query, timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, cache_query, timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(cache_query, location.latitude, location.longitude)
return location.latitude, location.longitude
# Try without state
location = await loop.run_in_executor(
None,
lambda: self.geolocator.geocode(f"{city_clean}, USA", timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, f"{city_clean}, USA", timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(f"{city_clean}, USA", location.latitude, location.longitude)
return location.latitude, location.longitude
# Try international
location = await loop.run_in_executor(
None,
lambda: self.geolocator.geocode(city_clean, timeout=self.url_timeout)
location = await rate_limited_nominatim_geocode(
self.bot, city_clean, timeout=self.url_timeout
)
if location:
self.db_manager.cache_geocoding(city_clean, location.latitude, location.longitude)
@@ -480,9 +474,8 @@ class SolarforecastCommand(BaseCommand):
# For coordinates, always do reverse geocoding
if location_type == "coordinates":
location = await loop.run_in_executor(
None,
lambda: self.geolocator.reverse(f"{lat}, {lon}", timeout=self.url_timeout)
location = await rate_limited_nominatim_reverse(
self.bot, f"{lat}, {lon}", timeout=self.url_timeout
)
if location and location.raw:
address = location.raw.get('address', {})
@@ -499,9 +492,8 @@ class SolarforecastCommand(BaseCommand):
# For city/zipcode, use original if it worked, or reverse geocode
if location_type in ["city", "zipcode"]:
# Try reverse geocoding to get confirmed city name
location = await loop.run_in_executor(
None,
lambda: self.geolocator.reverse(f"{lat}, {lon}", timeout=self.url_timeout)
location = await rate_limited_nominatim_reverse(
self.bot, f"{lat}, {lon}", timeout=self.url_timeout
)
if location and location.raw:
address = location.raw.get('address', {})
+35 -13
View File
@@ -10,6 +10,7 @@ import requests
import xml.dom.minidom
from datetime import datetime, timedelta
from geopy.geocoders import Nominatim
from ..utils import rate_limited_nominatim_geocode_sync, rate_limited_nominatim_reverse_sync, get_nominatim_geocoder
import maidenhead as mh
from .base_command import BaseCommand
from ..models import MeshMessage
@@ -44,8 +45,9 @@ class WxCommand(BaseCommand):
# Get default state from config for city disambiguation
self.default_state = self.bot.config.get('Weather', 'default_state', fallback='WA')
# Initialize geocoder
self.geolocator = Nominatim(user_agent="meshcore-bot")
# Initialize geocoder (will use rate-limited helpers for actual calls)
# Keep geolocator for backwards compatibility, but prefer rate-limited helpers
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
@@ -318,8 +320,10 @@ class WxCommand(BaseCommand):
def zipcode_to_lat_lon(self, zipcode: str) -> tuple:
"""Convert zipcode to latitude and longitude"""
try:
# Use Nominatim to geocode the zipcode
location = self.geolocator.geocode(f"{zipcode}, USA")
# Use rate-limited Nominatim to geocode the zipcode
location = rate_limited_nominatim_geocode_sync(
self.bot, f"{zipcode}, USA", timeout=10
)
if location:
return location.latitude, location.longitude
else:
@@ -338,7 +342,9 @@ class WxCommand(BaseCommand):
self.logger.debug(f"Using cached geocoding for {city}")
# Still need to do reverse geocoding for address details
try:
reverse_location = self.geolocator.reverse(f"{cached_lat}, {cached_lon}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{cached_lat}, {cached_lon}", timeout=10
)
if reverse_location:
return cached_lat, cached_lon, reverse_location.raw.get('address', {})
except:
@@ -354,14 +360,18 @@ class WxCommand(BaseCommand):
state = city_parts[1]
# Try the specific city, state combination first
location = self.geolocator.geocode(f"{city_name}, {state}, USA")
location = rate_limited_nominatim_geocode_sync(
self.bot, f"{city_name}, {state}, USA", timeout=10
)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city_name}, {state}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -386,14 +396,18 @@ class WxCommand(BaseCommand):
# If it's a major city with multiple locations, try the major ones first
if city.lower() in major_city_mappings:
for major_city_query in major_city_mappings[city.lower()]:
location = self.geolocator.geocode(major_city_query)
location = rate_limited_nominatim_geocode_sync(
self.bot, major_city_query, timeout=10
)
if location:
# Cache the result
self.db_manager.cache_geocoding(major_city_query, location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -401,14 +415,18 @@ class WxCommand(BaseCommand):
return location.latitude, location.longitude, location.raw.get('address', {})
# First try with default state
location = self.geolocator.geocode(f"{city}, {self.default_state}, USA")
location = rate_limited_nominatim_geocode_sync(
self.bot, f"{city}, {self.default_state}, USA", timeout=10
)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city}, {self.default_state}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
@@ -416,14 +434,18 @@ class WxCommand(BaseCommand):
return location.latitude, location.longitude, location.raw.get('address', {})
else:
# Try without state as fallback
location = self.geolocator.geocode(f"{city}, USA")
location = rate_limited_nominatim_geocode_sync(
self.bot, f"{city}, USA", timeout=10
)
if location:
# Cache the result
self.db_manager.cache_geocoding(f"{city}, USA", location.latitude, location.longitude)
# Use reverse geocoding to get detailed address info
try:
reverse_location = self.geolocator.reverse(f"{location.latitude}, {location.longitude}")
reverse_location = rate_limited_nominatim_reverse_sync(
self.bot, f"{location.latitude}, {location.longitude}", timeout=10
)
if reverse_location:
return location.latitude, location.longitude, reverse_location.raw.get('address', {})
except:
+5 -1
View File
@@ -25,7 +25,7 @@ from meshcore import EventType
from meshcore_cli.meshcore_cli import send_cmd, send_chan_msg
# Import our modules
from .rate_limiter import RateLimiter, BotTxRateLimiter
from .rate_limiter import RateLimiter, BotTxRateLimiter, NominatimRateLimiter
from .message_handler import MessageHandler
from .command_manager import CommandManager
from .channel_manager import ChannelManager
@@ -90,6 +90,10 @@ class MeshCoreBot:
self.bot_tx_rate_limiter = BotTxRateLimiter(
self.config.getfloat('Bot', 'bot_tx_rate_limit_seconds', fallback=1.0)
)
# Nominatim rate limiter: 1.1 seconds between requests (Nominatim policy: max 1 req/sec)
self.nominatim_rate_limiter = NominatimRateLimiter(
self.config.getfloat('Bot', 'nominatim_rate_limit_seconds', fallback=1.1)
)
self.tx_delay_ms = self.config.getint('Bot', 'tx_delay_ms', fallback=250)
# Initialize translator for localization BEFORE CommandManager
+4 -4
View File
@@ -91,8 +91,8 @@ class DBManager:
self.logger.error(f"Error getting cached geocoding: {e}")
return None, None
def cache_geocoding(self, query: str, latitude: float, longitude: float, cache_hours: int = 24):
"""Cache geocoding result for future use"""
def cache_geocoding(self, query: str, latitude: float, longitude: float, cache_hours: int = 720):
"""Cache geocoding result for future use (default: 30 days)"""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
@@ -148,8 +148,8 @@ class DBManager:
return None
return None
def cache_json(self, cache_key: str, cache_value: Dict, cache_type: str, cache_hours: int = 24):
"""Cache a JSON value for future use"""
def cache_json(self, cache_key: str, cache_value: Dict, cache_type: str, cache_hours: int = 720):
"""Cache a JSON value for future use (default: 30 days for geolocation)"""
try:
json_str = json.dumps(cache_value)
self.cache_value(cache_key, json_str, cache_type, cache_hours)
+42
View File
@@ -55,3 +55,45 @@ class BotTxRateLimiter:
wait_time = self.time_until_next_tx()
if wait_time > 0:
await asyncio.sleep(wait_time + 0.05) # Small buffer
class NominatimRateLimiter:
"""Rate limiting for Nominatim geocoding API requests
Nominatim policy: Maximum 1 request per second
We'll be conservative and use 1.1 seconds to ensure compliance
"""
def __init__(self, seconds: float = 1.1):
self.seconds = seconds
self.last_request = 0
self._lock = None # Will be set to asyncio.Lock if needed
def can_request(self) -> bool:
"""Check if we can make a Nominatim request"""
return time.time() - self.last_request >= self.seconds
def time_until_next(self) -> float:
"""Get time until next allowed request"""
elapsed = time.time() - self.last_request
return max(0, self.seconds - elapsed)
def record_request(self):
"""Record that we made a Nominatim request"""
self.last_request = time.time()
async def wait_for_request(self):
"""Wait until we can make a Nominatim request (async)"""
import asyncio
while not self.can_request():
wait_time = self.time_until_next()
if wait_time > 0:
await asyncio.sleep(wait_time + 0.05) # Small buffer
def wait_for_request_sync(self):
"""Wait until we can make a Nominatim request (synchronous)"""
import time as time_module
while not self.can_request():
wait_time = self.time_until_next()
if wait_time > 0:
time_module.sleep(wait_time + 0.05) # Small buffer
+17 -28
View File
@@ -12,6 +12,7 @@ from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from pathlib import Path
from meshcore import EventType
from .utils import rate_limited_nominatim_reverse_sync
@@ -1276,13 +1277,10 @@ class RepeaterManager:
return existing_data.get('state'), existing_data.get('country')
try:
from geopy.geocoders import Nominatim
# Initialize geocoder with proper timeout
geolocator = Nominatim(user_agent="meshcore-bot", timeout=10)
# Perform reverse geocoding
location = geolocator.reverse(f"{latitude}, {longitude}")
# Use rate-limited Nominatim reverse geocoding
location = rate_limited_nominatim_reverse_sync(
self.bot, f"{latitude}, {longitude}", timeout=10
)
if location:
address = location.raw.get('address', {})
@@ -1322,13 +1320,10 @@ class RepeaterManager:
return existing_data.get('city')
try:
from geopy.geocoders import Nominatim
# Initialize geocoder with proper timeout
geolocator = Nominatim(user_agent="meshcore-bot", timeout=10)
# Perform reverse geocoding
location = geolocator.reverse(f"{latitude}, {longitude}")
# Use rate-limited Nominatim reverse geocoding
location = rate_limited_nominatim_reverse_sync(
self.bot, f"{latitude}, {longitude}", timeout=10
)
if location:
address = location.raw.get('address', {})
@@ -1400,17 +1395,11 @@ class RepeaterManager:
self.logger.debug(f"Using cached location data for {latitude}, {longitude}")
return cached_result
from geopy.geocoders import Nominatim
# Initialize geocoder with proper user agent and timeout
geolocator = Nominatim(
user_agent="meshcore-bot-geolocation-update",
timeout=10 # 10 second timeout
)
# Perform reverse geocoding
# Use rate-limited Nominatim reverse geocoding
self.logger.debug(f"Calling Nominatim reverse geocoding for {latitude}, {longitude}")
location = geolocator.reverse(f"{latitude}, {longitude}")
location = rate_limited_nominatim_reverse_sync(
self.bot, f"{latitude}, {longitude}", timeout=10
)
if location:
address = location.raw.get('address', {})
@@ -1451,8 +1440,8 @@ class RepeaterManager:
else:
self.logger.warning(f"Geocoding API returned no location for {latitude}, {longitude}")
# Cache the result for 24 hours to avoid duplicate API calls
self.db_manager.cache_json(cache_key, location_info, "geolocation", cache_hours=24)
# Cache the result for 30 days - geolocation data is very stable
self.db_manager.cache_json(cache_key, location_info, "geolocation", cache_hours=720)
return location_info
@@ -3473,8 +3462,8 @@ class RepeaterManager:
"""Get cached geocoding result for a query"""
return self.db_manager.get_cached_geocoding(query)
def cache_geocoding(self, query: str, latitude: float, longitude: float, cache_hours: int = 24):
"""Cache geocoding result for future use"""
def cache_geocoding(self, query: str, latitude: float, longitude: float, cache_hours: int = 720):
"""Cache geocoding result for future use (default: 30 days)"""
self.db_manager.cache_geocoding(query, latitude, longitude, cache_hours)
def cleanup_geocoding_cache(self):
+135
View File
@@ -363,3 +363,138 @@ def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
# Earth's radius in kilometers
earth_radius = 6371.0
return earth_radius * c
def get_nominatim_geocoder(user_agent: str = "meshcore-bot", timeout: int = 10):
"""
Get a Nominatim geocoder instance with proper User-Agent.
Args:
user_agent: User-Agent string for Nominatim (required by their policy)
timeout: Request timeout in seconds
Returns:
Nominatim geocoder instance
"""
from geopy.geocoders import Nominatim
return Nominatim(user_agent=user_agent, timeout=timeout)
async def rate_limited_nominatim_geocode(bot, query: str, timeout: int = 10):
"""
Perform rate-limited Nominatim geocoding (forward geocoding).
Args:
bot: Bot instance (must have nominatim_rate_limiter attribute)
query: Location query string
timeout: Request timeout in seconds
Returns:
Geocoding result or None
"""
if not hasattr(bot, 'nominatim_rate_limiter'):
# Fallback if rate limiter not initialized
geolocator = get_nominatim_geocoder(timeout=timeout)
return geolocator.geocode(query, timeout=timeout)
# Wait for rate limiter
await bot.nominatim_rate_limiter.wait_for_request()
# Make the request
geolocator = get_nominatim_geocoder(timeout=timeout)
result = geolocator.geocode(query, timeout=timeout)
# Record the request
bot.nominatim_rate_limiter.record_request()
return result
async def rate_limited_nominatim_reverse(bot, coordinates: str, timeout: int = 10):
"""
Perform rate-limited Nominatim reverse geocoding.
Args:
bot: Bot instance (must have nominatim_rate_limiter attribute)
coordinates: Coordinates string in format "lat, lon"
timeout: Request timeout in seconds
Returns:
Reverse geocoding result or None
"""
if not hasattr(bot, 'nominatim_rate_limiter'):
# Fallback if rate limiter not initialized
geolocator = get_nominatim_geocoder(timeout=timeout)
return geolocator.reverse(coordinates, timeout=timeout)
# Wait for rate limiter
await bot.nominatim_rate_limiter.wait_for_request()
# Make the request
geolocator = get_nominatim_geocoder(timeout=timeout)
result = geolocator.reverse(coordinates, timeout=timeout)
# Record the request
bot.nominatim_rate_limiter.record_request()
return result
def rate_limited_nominatim_geocode_sync(bot, query: str, timeout: int = 10):
"""
Perform rate-limited Nominatim geocoding (synchronous version).
Args:
bot: Bot instance (must have nominatim_rate_limiter attribute)
query: Location query string
timeout: Request timeout in seconds
Returns:
Geocoding result or None
"""
if not hasattr(bot, 'nominatim_rate_limiter'):
# Fallback if rate limiter not initialized
geolocator = get_nominatim_geocoder(timeout=timeout)
return geolocator.geocode(query, timeout=timeout)
# Wait for rate limiter
bot.nominatim_rate_limiter.wait_for_request_sync()
# Make the request
geolocator = get_nominatim_geocoder(timeout=timeout)
result = geolocator.geocode(query, timeout=timeout)
# Record the request
bot.nominatim_rate_limiter.record_request()
return result
def rate_limited_nominatim_reverse_sync(bot, coordinates: str, timeout: int = 10):
"""
Perform rate-limited Nominatim reverse geocoding (synchronous version).
Args:
bot: Bot instance (must have nominatim_rate_limiter attribute)
coordinates: Coordinates string in format "lat, lon"
timeout: Request timeout in seconds
Returns:
Reverse geocoding result or None
"""
if not hasattr(bot, 'nominatim_rate_limiter'):
# Fallback if rate limiter not initialized
geolocator = get_nominatim_geocoder(timeout=timeout)
return geolocator.reverse(coordinates, timeout=timeout)
# Wait for rate limiter
bot.nominatim_rate_limiter.wait_for_request_sync()
# Make the request
geolocator = get_nominatim_geocoder(timeout=timeout)
result = geolocator.reverse(coordinates, timeout=timeout)
# Record the request
bot.nominatim_rate_limiter.record_request()
return result