from typing import Any, Dict, Optional, Union import aiohttp import logging import json import csv import io from app.services.service_manager import ServiceManager from app.schemas.file import FileInDB from sqlalchemy.orm import Session logger = logging.getLogger(__name__) class BaseExternalService: def __init__(self, base_url: str, api_key: Optional[str] = None): self.base_url = base_url self.api_key = api_key self.session = None self.service_manager = ServiceManager() self._services = {} async def _get_session(self) -> aiohttp.ClientSession: if self.session is None or self.session.closed: self.session = aiohttp.ClientSession() return self.session async def _make_request( self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, data: Optional[Dict[str, Any]] = None, content_type: str = "application/json", binary: bool = False ) -> Union[Dict[str, Any], str, bytes]: session = await self._get_session() url = f"{self.base_url}{endpoint}" if self.api_key: headers = headers or {} headers["Authorization"] = f"Bearer {self.api_key}" try: async with session.request(method, url, params=params, headers=headers, json=data) as response: response.raise_for_status() # Get the actual content type from the response response_content_type = response.headers.get('content-type', '').lower() logger.info(f"Making request to {url}") if binary: return await response.read() # Get the raw response text first raw_response = await response.text() # Only try to parse as JSON if the content type indicates JSON if 'application/json' in response_content_type or 'text/json' in response_content_type: try: # First try to parse the response directly return await response.json() except Exception as e: try: # If that fails, try parsing the raw text as JSON (in case it's double-encoded) return json.loads(raw_response) except Exception as e: logger.error(f"Failed to parse JSON response: {e}") return raw_response return raw_response except aiohttp.ClientError as e: logger.error(f"Request failed: {e}") raise except Exception as e: logger.error(f"Unexpected error during API request: {str(e)}") raise async def close(self): """Close the aiohttp session if it exists""" if self.session and not self.session.closed: await self.session.close() self.session = None logger.info(f"Closed session for {self.__class__.__name__}") def get_service(self, name: str) -> Any: """Get a service by name with lazy loading""" if name not in self._services: self._services[name] = self.service_manager.get_service(name) return self._services[name] @property def file_service(self): """Convenience property for file service""" return self.get_service('file') async def save_file(self, db: Session, file_data: Union[bytes, list[dict]], file_name: str, subdir: str, file_type: Optional[str] = None) -> FileInDB: """Save a file using the FileService""" if isinstance(file_data, list): # Convert list of dictionaries to CSV bytes output = io.StringIO() writer = csv.DictWriter(output, fieldnames=file_data[0].keys()) writer.writeheader() writer.writerows(file_data) file_data = output.getvalue().encode('utf-8') file_type = file_type or 'text/csv' # Use FileService to save the file file_service = self.get_service('file') return await file_service.save_file( db=db, file_data=file_data, filename=file_name, subdir=subdir, file_type=file_type )