REFACTOR external service and improve websocket training
This commit is contained in:
1
services/external/app/registry/__init__.py
vendored
Normal file
1
services/external/app/registry/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""City registry module for multi-city support"""
|
||||
163
services/external/app/registry/city_registry.py
vendored
Normal file
163
services/external/app/registry/city_registry.py
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
# services/external/app/registry/city_registry.py
|
||||
"""
|
||||
City Registry - Configuration-driven multi-city support
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import math
|
||||
|
||||
|
||||
class Country(str, Enum):
|
||||
SPAIN = "ES"
|
||||
FRANCE = "FR"
|
||||
|
||||
|
||||
class WeatherProvider(str, Enum):
|
||||
AEMET = "aemet"
|
||||
METEO_FRANCE = "meteo_france"
|
||||
OPEN_WEATHER = "open_weather"
|
||||
|
||||
|
||||
class TrafficProvider(str, Enum):
|
||||
MADRID_OPENDATA = "madrid_opendata"
|
||||
VALENCIA_OPENDATA = "valencia_opendata"
|
||||
BARCELONA_OPENDATA = "barcelona_opendata"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CityDefinition:
|
||||
"""City configuration with data source specifications"""
|
||||
city_id: str
|
||||
name: str
|
||||
country: Country
|
||||
latitude: float
|
||||
longitude: float
|
||||
radius_km: float
|
||||
|
||||
weather_provider: WeatherProvider
|
||||
weather_config: Dict[str, Any]
|
||||
traffic_provider: TrafficProvider
|
||||
traffic_config: Dict[str, Any]
|
||||
|
||||
timezone: str
|
||||
population: int
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class CityRegistry:
|
||||
"""Central registry of supported cities"""
|
||||
|
||||
CITIES: List[CityDefinition] = [
|
||||
CityDefinition(
|
||||
city_id="madrid",
|
||||
name="Madrid",
|
||||
country=Country.SPAIN,
|
||||
latitude=40.4168,
|
||||
longitude=-3.7038,
|
||||
radius_km=30.0,
|
||||
weather_provider=WeatherProvider.AEMET,
|
||||
weather_config={
|
||||
"station_ids": ["3195", "3129", "3197"],
|
||||
"municipality_code": "28079"
|
||||
},
|
||||
traffic_provider=TrafficProvider.MADRID_OPENDATA,
|
||||
traffic_config={
|
||||
"current_xml_url": "https://datos.madrid.es/egob/catalogo/...",
|
||||
"historical_base_url": "https://datos.madrid.es/...",
|
||||
"measurement_points_csv": "https://datos.madrid.es/..."
|
||||
},
|
||||
timezone="Europe/Madrid",
|
||||
population=3_200_000
|
||||
),
|
||||
CityDefinition(
|
||||
city_id="valencia",
|
||||
name="Valencia",
|
||||
country=Country.SPAIN,
|
||||
latitude=39.4699,
|
||||
longitude=-0.3763,
|
||||
radius_km=25.0,
|
||||
weather_provider=WeatherProvider.AEMET,
|
||||
weather_config={
|
||||
"station_ids": ["8416"],
|
||||
"municipality_code": "46250"
|
||||
},
|
||||
traffic_provider=TrafficProvider.VALENCIA_OPENDATA,
|
||||
traffic_config={
|
||||
"api_endpoint": "https://valencia.opendatasoft.com/api/..."
|
||||
},
|
||||
timezone="Europe/Madrid",
|
||||
population=800_000,
|
||||
enabled=False
|
||||
),
|
||||
CityDefinition(
|
||||
city_id="barcelona",
|
||||
name="Barcelona",
|
||||
country=Country.SPAIN,
|
||||
latitude=41.3851,
|
||||
longitude=2.1734,
|
||||
radius_km=30.0,
|
||||
weather_provider=WeatherProvider.AEMET,
|
||||
weather_config={
|
||||
"station_ids": ["0076"],
|
||||
"municipality_code": "08019"
|
||||
},
|
||||
traffic_provider=TrafficProvider.BARCELONA_OPENDATA,
|
||||
traffic_config={
|
||||
"api_endpoint": "https://opendata-ajuntament.barcelona.cat/..."
|
||||
},
|
||||
timezone="Europe/Madrid",
|
||||
population=1_600_000,
|
||||
enabled=False
|
||||
)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_enabled_cities(cls) -> List[CityDefinition]:
|
||||
"""Get all enabled cities"""
|
||||
return [city for city in cls.CITIES if city.enabled]
|
||||
|
||||
@classmethod
|
||||
def get_city(cls, city_id: str) -> Optional[CityDefinition]:
|
||||
"""Get city by ID"""
|
||||
for city in cls.CITIES:
|
||||
if city.city_id == city_id:
|
||||
return city
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_nearest_city(cls, latitude: float, longitude: float) -> Optional[CityDefinition]:
|
||||
"""Find nearest enabled city to coordinates"""
|
||||
enabled_cities = cls.get_enabled_cities()
|
||||
if not enabled_cities:
|
||||
return None
|
||||
|
||||
min_distance = float('inf')
|
||||
nearest_city = None
|
||||
|
||||
for city in enabled_cities:
|
||||
distance = cls._haversine_distance(
|
||||
latitude, longitude,
|
||||
city.latitude, city.longitude
|
||||
)
|
||||
if distance <= city.radius_km and distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_city = city
|
||||
|
||||
return nearest_city
|
||||
|
||||
@staticmethod
|
||||
def _haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""Calculate distance in km between two coordinates"""
|
||||
R = 6371
|
||||
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
|
||||
a = (math.sin(dlat/2) ** 2 +
|
||||
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
||||
math.sin(dlon/2) ** 2)
|
||||
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
|
||||
return R * c
|
||||
58
services/external/app/registry/geolocation_mapper.py
vendored
Normal file
58
services/external/app/registry/geolocation_mapper.py
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# services/external/app/registry/geolocation_mapper.py
|
||||
"""
|
||||
Geolocation Mapper - Maps tenant locations to cities
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
import structlog
|
||||
from .city_registry import CityRegistry, CityDefinition
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class GeolocationMapper:
|
||||
"""Maps tenant coordinates to nearest supported city"""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = CityRegistry()
|
||||
|
||||
def map_tenant_to_city(
|
||||
self,
|
||||
latitude: float,
|
||||
longitude: float
|
||||
) -> Optional[Tuple[CityDefinition, float]]:
|
||||
"""
|
||||
Map tenant coordinates to nearest city
|
||||
|
||||
Returns:
|
||||
Tuple of (CityDefinition, distance_km) or None if no match
|
||||
"""
|
||||
nearest_city = self.registry.find_nearest_city(latitude, longitude)
|
||||
|
||||
if not nearest_city:
|
||||
logger.warning(
|
||||
"No supported city found for coordinates",
|
||||
lat=latitude,
|
||||
lon=longitude
|
||||
)
|
||||
return None
|
||||
|
||||
distance = self.registry._haversine_distance(
|
||||
latitude, longitude,
|
||||
nearest_city.latitude, nearest_city.longitude
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Mapped tenant to city",
|
||||
lat=latitude,
|
||||
lon=longitude,
|
||||
city=nearest_city.name,
|
||||
distance_km=round(distance, 2)
|
||||
)
|
||||
|
||||
return (nearest_city, distance)
|
||||
|
||||
def validate_location_support(self, latitude: float, longitude: float) -> bool:
|
||||
"""Check if coordinates are supported"""
|
||||
result = self.map_tenant_to_city(latitude, longitude)
|
||||
return result is not None
|
||||
Reference in New Issue
Block a user