""" Carmodoo Sync Agent - Syncs makers/models from Carmodoo to AutonetSellCar Backend Also performs nightly soldout checks. """ import asyncio import os import logging import httpx from datetime import datetime, time from dotenv import load_dotenv from .carmodoo_client import CarmodooClient, CarmodooConfig # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('sync_agent') class SyncAgent: def __init__(self): load_dotenv() # Carmodoo config self.carmodoo_config = CarmodooConfig( user_id=os.getenv('CARMODOO_USER_ID', ''), password=os.getenv('CARMODOO_PASSWORD', ''), ) # Backend API self.api_url = os.getenv('API_SERVER_URL', 'http://autonet-backend:8000/api') self.api_key = os.getenv('AGENT_API_KEY', '') self.carmodoo: CarmodooClient = None self.http_client: httpx.AsyncClient = None async def start(self): """Initialize connections""" logger.info("Starting Sync Agent...") self.carmodoo = CarmodooClient(self.carmodoo_config) await self.carmodoo.create_session() self.http_client = httpx.AsyncClient(timeout=30.0) # Login to Carmodoo if not await self.carmodoo.login(): logger.error("Failed to login to Carmodoo") return False logger.info("Sync Agent started successfully") return True async def stop(self): """Cleanup connections""" if self.carmodoo: await self.carmodoo.close() if self.http_client: await self.http_client.aclose() logger.info("Sync Agent stopped") async def sync_makers(self): """Sync car makers from Carmodoo to Backend""" logger.info("Syncing car makers...") makers = await self.carmodoo.get_car_makers() logger.info(f"Found {len(makers)} makers from Carmodoo") synced = 0 for maker in makers: try: response = await self.http_client.post( f"{self.api_url}/cars/makers/", json={ "code": maker.code, "name": maker.name, } ) if response.status_code in [200, 201]: synced += 1 except Exception as e: logger.error(f"Error syncing maker {maker.code}: {e}") logger.info(f"Synced {synced}/{len(makers)} makers") return makers async def sync_models(self, makers): """Sync car models for all makers""" logger.info("Syncing car models...") total_models = 0 synced = 0 for maker in makers: await asyncio.sleep(0.5) # Rate limiting models = await self.carmodoo.get_car_models(maker.code) total_models += len(models) # Get maker ID from backend try: response = await self.http_client.get(f"{self.api_url}/cars/makers/") if response.status_code == 200: backend_makers = response.json() maker_id = None for bm in backend_makers: if bm['code'] == maker.code: maker_id = bm['id'] break if maker_id: for model in models: try: resp = await self.http_client.post( f"{self.api_url}/cars/models/", json={ "code": model.code, "maker_id": maker_id, "name": model.name, } ) if resp.status_code in [200, 201]: synced += 1 except Exception as e: logger.error(f"Error syncing model {model.code}: {e}") except Exception as e: logger.error(f"Error getting makers from backend: {e}") logger.debug(f"Maker {maker.name}: {len(models)} models") logger.info(f"Synced {synced}/{total_models} models") async def run_sync(self): """Run full sync""" if not await self.start(): return try: # Sync makers makers = await self.sync_makers() # Sync models await self.sync_models(makers) logger.info("Sync completed successfully!") except Exception as e: logger.error(f"Sync error: {e}") finally: await self.stop() async def check_soldout(self): """Check all cars for soldout status""" logger.info("Starting soldout check...") if not await self.start(): return try: # Get all active cars from backend response = await self.http_client.get( f"{self.api_url}/cars", params={"admin": True, "page_size": 1000, "status": "active"} ) if response.status_code != 200: logger.error(f"Failed to get cars: {response.status_code}") return data = response.json() cars = data.get("cars", []) logger.info(f"Checking {len(cars)} cars for soldout status...") soldout_count = 0 checked = 0 for car in cars: if car.get("soldout"): continue # Already soldout source_id = car.get("source_id") car_id = car.get("id") if not source_id: continue # Check if car exists on Carmodoo is_available = await self._check_car_on_carmodoo(source_id) checked += 1 if not is_available: # Mark as soldout via API try: resp = await self.http_client.post( f"{self.api_url}/cars/{car_id}/soldout" ) if resp.status_code == 200: soldout_count += 1 logger.info(f"Car {car_id} ({car.get('car_name')}) marked as SOLD OUT") except Exception as e: logger.error(f"Failed to mark car {car_id} as soldout: {e}") # Rate limiting if checked % 10 == 0: logger.info(f"Progress: {checked}/{len(cars)} checked, {soldout_count} sold out") await asyncio.sleep(1) logger.info(f"Soldout check completed: {checked} checked, {soldout_count} sold out") except Exception as e: logger.error(f"Soldout check error: {e}") finally: await self.stop() async def _check_car_on_carmodoo(self, source_id: str) -> bool: """Check if car exists on Carmodoo""" try: # Try to get car info from Carmodoo url = f"https://dealer.carmodoo.com/car/carPopView.html" params = {"carNo": source_id} async with httpx.AsyncClient(timeout=30.0) as client: response = await client.get(url, params=params) if response.status_code == 404: return False content = response.text.lower() sold_keywords = ["판매완료", "판매 완료", "삭제된", "없는 차량", "존재하지 않"] for keyword in sold_keywords: if keyword in content: return False return True except Exception as e: logger.error(f"Error checking car {source_id}: {e}") return True # Assume available on error async def run_scheduled(self): """Run agent with scheduled tasks""" logger.info("Starting scheduled agent...") # Run initial sync await self.run_sync() # Schedule nightly soldout check at 3:00 AM while True: now = datetime.now() target_time = time(3, 0) # 3:00 AM # Calculate seconds until next 3:00 AM target_datetime = datetime.combine(now.date(), target_time) if now.time() >= target_time: # Already past 3 AM today, schedule for tomorrow from datetime import timedelta target_datetime += timedelta(days=1) seconds_until = (target_datetime - now).total_seconds() logger.info(f"Next soldout check in {seconds_until / 3600:.1f} hours at {target_datetime}") await asyncio.sleep(seconds_until) # Run soldout check logger.info("Running scheduled soldout check...") await self.check_soldout() async def main(): agent = SyncAgent() # Check command line args import sys if len(sys.argv) > 1: if sys.argv[1] == "sync": await agent.run_sync() elif sys.argv[1] == "soldout": await agent.check_soldout() elif sys.argv[1] == "scheduled": await agent.run_scheduled() else: # Default: run scheduled await agent.run_scheduled() if __name__ == '__main__': asyncio.run(main())