import logging from dataclasses import dataclass, field from sqlalchemy.ext.asyncio import AsyncSession from app.collectors.registry import ( get_opportunity_collectors, ) from app.services.opportunity_sync import ( OpportunitySyncResult, OpportunitySyncService, ) logger = logging.getLogger(__name__) @dataclass(slots=True, frozen=True) class OpportunitySyncFailure: source_slug: str error_type: str message: str @dataclass(slots=True) class OpportunityBatchSyncResult: results: list[OpportunitySyncResult] = field( default_factory=list ) failures: list[OpportunitySyncFailure] = field( default_factory=list ) @property def total_sources(self) -> int: return len(self.results) + len( self.failures ) @property def successful_sources(self) -> int: return len(self.results) @property def failed_sources(self) -> int: return len(self.failures) @property def collected(self) -> int: return sum( result.collected for result in self.results ) @property def created(self) -> int: return sum( result.created for result in self.results ) @property def updated(self) -> int: return sum( result.updated for result in self.results ) @property def failed_opportunities(self) -> int: return sum( result.failed for result in self.results ) @property def is_successful(self) -> bool: return not self.failures class OpportunityBatchSyncService: def __init__( self, session: AsyncSession, ) -> None: self.session = session self.sync_service = OpportunitySyncService( session ) async def synchronize_all( self, ) -> OpportunityBatchSyncResult: batch_result = ( OpportunityBatchSyncResult() ) collectors = ( get_opportunity_collectors() ) for collector in collectors: source_slug = ( collector.source_slug ) logger.info( "Starting opportunity synchronization " "for source '%s'.", source_slug, ) try: sync_result = ( await self.sync_service .synchronize(collector) ) except Exception as error: failure = OpportunitySyncFailure( source_slug=source_slug, error_type=type(error).__name__, message=str(error), ) batch_result.failures.append( failure ) logger.exception( "Opportunity synchronization " "failed for source '%s'.", source_slug, ) continue batch_result.results.append( sync_result ) logger.info( ( "Opportunity synchronization " "completed for source '%s': " "collected=%s created=%s " "updated=%s failed=%s." ), source_slug, sync_result.collected, sync_result.created, sync_result.updated, sync_result.failed, ) logger.info( ( "Opportunity batch synchronization " "completed: sources=%s successful=%s " "failed=%s collected=%s created=%s " "updated=%s." ), batch_result.total_sources, batch_result.successful_sources, batch_result.failed_sources, batch_result.collected, batch_result.created, batch_result.updated, ) return batch_result