Initial commit - production deployment
This commit is contained in:
205
shared/clients/nominatim_client.py
Executable file
205
shared/clients/nominatim_client.py
Executable file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Nominatim Client for geocoding and address search
|
||||
"""
|
||||
|
||||
import structlog
|
||||
import httpx
|
||||
from typing import Optional, List, Dict, Any
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class NominatimClient:
|
||||
"""
|
||||
Client for Nominatim geocoding service.
|
||||
|
||||
Provides address search and geocoding capabilities for the bakery onboarding flow.
|
||||
"""
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
self.config = config
|
||||
self.nominatim_url = getattr(
|
||||
config,
|
||||
"NOMINATIM_SERVICE_URL",
|
||||
"http://nominatim-service:8080"
|
||||
)
|
||||
self.timeout = 30
|
||||
|
||||
async def search_address(
|
||||
self,
|
||||
query: str,
|
||||
country_codes: str = "es",
|
||||
limit: int = 5,
|
||||
addressdetails: bool = True
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for addresses matching a query.
|
||||
|
||||
Args:
|
||||
query: Address search query (e.g., "Calle Mayor 1, Madrid")
|
||||
country_codes: Limit search to country codes (default: "es" for Spain)
|
||||
limit: Maximum number of results (default: 5)
|
||||
addressdetails: Include detailed address breakdown (default: True)
|
||||
|
||||
Returns:
|
||||
List of geocoded results with lat, lon, and address details
|
||||
|
||||
Example:
|
||||
results = await nominatim.search_address("Calle Mayor 1, Madrid")
|
||||
if results:
|
||||
lat = results[0]["lat"]
|
||||
lon = results[0]["lon"]
|
||||
display_name = results[0]["display_name"]
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.nominatim_url}/search",
|
||||
params={
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"countrycodes": country_codes,
|
||||
"addressdetails": 1 if addressdetails else 0,
|
||||
"limit": limit
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
results = response.json()
|
||||
logger.info(
|
||||
"Address search completed",
|
||||
query=query,
|
||||
results_count=len(results)
|
||||
)
|
||||
return results
|
||||
else:
|
||||
logger.error(
|
||||
"Nominatim search failed",
|
||||
query=query,
|
||||
status_code=response.status_code,
|
||||
response=response.text
|
||||
)
|
||||
return []
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error("Nominatim search timeout", query=query)
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error("Nominatim search error", query=query, error=str(e))
|
||||
return []
|
||||
|
||||
async def geocode_address(
|
||||
self,
|
||||
street: str,
|
||||
city: str,
|
||||
postal_code: Optional[str] = None,
|
||||
country: str = "Spain"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Geocode a structured address to coordinates.
|
||||
|
||||
Args:
|
||||
street: Street name and number
|
||||
city: City name
|
||||
postal_code: Optional postal code
|
||||
country: Country name (default: "Spain")
|
||||
|
||||
Returns:
|
||||
Dict with lat, lon, and display_name, or None if not found
|
||||
|
||||
Example:
|
||||
location = await nominatim.geocode_address(
|
||||
street="Calle Mayor 1",
|
||||
city="Madrid",
|
||||
postal_code="28013"
|
||||
)
|
||||
if location:
|
||||
lat, lon = location["lat"], location["lon"]
|
||||
"""
|
||||
# Build structured query
|
||||
query_parts = [street, city]
|
||||
if postal_code:
|
||||
query_parts.append(postal_code)
|
||||
query_parts.append(country)
|
||||
|
||||
query = ", ".join(query_parts)
|
||||
|
||||
results = await self.search_address(query, limit=1)
|
||||
if results:
|
||||
return results[0]
|
||||
return None
|
||||
|
||||
async def reverse_geocode(
|
||||
self,
|
||||
latitude: float,
|
||||
longitude: float
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Reverse geocode coordinates to an address.
|
||||
|
||||
Args:
|
||||
latitude: Latitude coordinate
|
||||
longitude: Longitude coordinate
|
||||
|
||||
Returns:
|
||||
Dict with address details, or None if not found
|
||||
|
||||
Example:
|
||||
address = await nominatim.reverse_geocode(40.4168, -3.7038)
|
||||
if address:
|
||||
city = address["address"]["city"]
|
||||
street = address["address"]["road"]
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.nominatim_url}/reverse",
|
||||
params={
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
"format": "json",
|
||||
"addressdetails": 1
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info(
|
||||
"Reverse geocoding completed",
|
||||
lat=latitude,
|
||||
lon=longitude
|
||||
)
|
||||
return result
|
||||
else:
|
||||
logger.error(
|
||||
"Nominatim reverse geocoding failed",
|
||||
lat=latitude,
|
||||
lon=longitude,
|
||||
status_code=response.status_code
|
||||
)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Reverse geocoding error",
|
||||
lat=latitude,
|
||||
lon=longitude,
|
||||
error=str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
async def health_check(self) -> bool:
|
||||
"""
|
||||
Check if Nominatim service is healthy.
|
||||
|
||||
Returns:
|
||||
True if service is responding, False otherwise
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
response = await client.get(f"{self.nominatim_url}/status")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.warning("Nominatim health check failed", error=str(e))
|
||||
return False
|
||||
Reference in New Issue
Block a user