# ================================================================ # services/data/app/external/base_client.py # ================================================================ """Base HTTP client for external APIs - Enhanced for AEMET""" import httpx from typing import Dict, Any, Optional import structlog from datetime import datetime logger = structlog.get_logger() class BaseAPIClient: def __init__(self, base_url: str, api_key: Optional[str] = None): self.base_url = base_url self.api_key = api_key self.timeout = httpx.Timeout(30.0) async def _get(self, endpoint: str, params: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """Make GET request""" try: url = f"{self.base_url}{endpoint}" # Add API key to params for AEMET (not headers) request_params = params or {} if self.api_key: request_params["api_key"] = self.api_key # Add headers if provided request_headers = headers or {} logger.debug("Making API request", url=url, params=request_params) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get(url, params=request_params, headers=request_headers) response.raise_for_status() # Log response for debugging response_data = response.json() logger.debug("API response received", status_code=response.status_code, response_keys=list(response_data.keys()) if isinstance(response_data, dict) else "non-dict") return response_data except httpx.HTTPStatusError as e: logger.error("HTTP error", status_code=e.response.status_code, url=url, response_text=e.response.text[:200]) return None except httpx.RequestError as e: logger.error("Request error", error=str(e), url=url) return None except Exception as e: logger.error("Unexpected error", error=str(e), url=url) return None async def _fetch_url_directly(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """Fetch data directly from a full URL (for AEMET datos URLs)""" try: request_headers = headers or {} logger.debug("Making direct URL request", url=url) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get(url, headers=request_headers) response.raise_for_status() # Handle encoding issues common with Spanish data sources try: response_data = response.json() except UnicodeDecodeError: logger.warning("UTF-8 decode failed, trying alternative encodings", url=url) # Try common Spanish encodings for encoding in ['latin-1', 'windows-1252', 'iso-8859-1']: try: text_content = response.content.decode(encoding) import json response_data = json.loads(text_content) logger.info("Successfully decoded with encoding", encoding=encoding) break except (UnicodeDecodeError, json.JSONDecodeError): continue else: logger.error("Failed to decode response with any encoding", url=url) return None logger.debug("Direct URL response received", status_code=response.status_code, data_type=type(response_data), data_length=len(response_data) if isinstance(response_data, (list, dict)) else "unknown") return response_data except httpx.HTTPStatusError as e: logger.error("HTTP error in direct fetch", status_code=e.response.status_code, url=url) return None except httpx.RequestError as e: logger.error("Request error in direct fetch", error=str(e), url=url) return None except Exception as e: logger.error("Unexpected error in direct fetch", error=str(e), url=url) return None async def _post(self, endpoint: str, data: Optional[Dict] = None, headers: Optional[Dict] = None) -> Optional[Dict[str, Any]]: """Make POST request""" try: url = f"{self.base_url}{endpoint}" request_headers = headers or {} if self.api_key: request_headers["Authorization"] = f"Bearer {self.api_key}" async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.post(url, json=data, headers=request_headers) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: logger.error("HTTP error", status_code=e.response.status_code, url=url) return None except httpx.RequestError as e: logger.error("Request error", error=str(e), url=url) return None except Exception as e: logger.error("Unexpected error", error=str(e), url=url) return None