import asyncio import logging from datetime import datetime, timedelta import httpx from web.database import SessionLocal from web.models import EvotorConnection, VkConnection, CachedStore logger = logging.getLogger("uvicorn.error") EVOTOR_STORES_URL = "https://api.evotor.ru/stores" EVOTOR_TOKEN_URL = "https://oauth.evotor.ru/oauth/token" VK_USERS_GET_URL = "https://api.vk.com/method/users.get" VK_API_VERSION = "5.131" # Refresh Evotor token if it expires within this window REFRESH_BEFORE_EXPIRY = timedelta(hours=2) async def _refresh_evotor_token(conn: EvotorConnection) -> str | None: """Attempt to refresh the Evotor access token. Returns new access token or None.""" from web.config import settings if not conn.refresh_token: return None try: async with httpx.AsyncClient() as client: resp = await client.post( EVOTOR_TOKEN_URL, data={ "grant_type": "refresh_token", "refresh_token": conn.refresh_token, }, auth=(settings.EVOTOR_CLIENT_ID, settings.EVOTOR_CLIENT_SECRET), timeout=15, ) if resp.status_code != 200: return None data = resp.json() return data if data.get("access_token") else None except Exception: return None async def check_evotor_connection(access_token: str) -> bool: try: async with httpx.AsyncClient() as client: response = await client.get( EVOTOR_STORES_URL, headers={"Authorization": f"Bearer {access_token}"}, timeout=15, ) return response.status_code == 200 except Exception: return False async def check_vk_connection(access_token: str) -> bool: try: async with httpx.AsyncClient() as client: resp = await client.get( VK_USERS_GET_URL, params={"access_token": access_token, "v": VK_API_VERSION}, timeout=10, ) if resp.status_code != 200: return False data = resp.json() return "error" not in data except Exception: return False async def run_health_checks() -> None: db = SessionLocal() try: now = datetime.utcnow() evotor_connections = db.query(EvotorConnection).all() for conn in evotor_connections: # Proactively refresh if token expires soon needs_refresh = ( conn.refresh_token and conn.token_expires_at and conn.token_expires_at - now < REFRESH_BEFORE_EXPIRY ) if needs_refresh: token_data = await _refresh_evotor_token(conn) if token_data: conn.access_token = token_data["access_token"] conn.refresh_token = token_data.get("refresh_token", conn.refresh_token) expires_in = token_data.get("expires_in") conn.token_expires_at = now + timedelta(seconds=expires_in) if expires_in else None logger.info("Refreshed Evotor token for user_id=%d", conn.user_id) is_online = await check_evotor_connection(conn.access_token) # If offline and not yet tried refresh, attempt it now if not is_online and conn.refresh_token and not needs_refresh: token_data = await _refresh_evotor_token(conn) if token_data: conn.access_token = token_data["access_token"] conn.refresh_token = token_data.get("refresh_token", conn.refresh_token) expires_in = token_data.get("expires_in") conn.token_expires_at = now + timedelta(seconds=expires_in) if expires_in else None is_online = await check_evotor_connection(conn.access_token) if is_online: logger.info("Evotor token refreshed after failed check for user_id=%d", conn.user_id) conn.is_online = is_online conn.last_checked_at = now vk_connections = db.query(VkConnection).all() for conn in vk_connections: conn.is_online = await check_vk_connection(conn.access_token) conn.last_checked_at = now db.commit() # Refresh catalog cache for online Evotor connections from web.config import settings refreshed_catalog = 0 for conn in evotor_connections: if not conn.is_online: continue cached = db.query(CachedStore).filter(CachedStore.user_id == conn.user_id).first() cache_age = (now - cached.fetched_at).total_seconds() if cached else None if cached is None or cache_age >= settings.CATALOG_REFRESH_INTERVAL_SECONDS: try: from web.evotor_api import refresh_catalog_cache await refresh_catalog_cache(conn.user_id, conn.access_token, db) refreshed_catalog += 1 except Exception: logger.exception("Failed to refresh catalog cache for user_id=%d", conn.user_id) logger.info( "Health checks completed: %d Evotor, %d VK, %d catalogs refreshed", len(evotor_connections), len(vk_connections), refreshed_catalog, ) except Exception: logger.exception("Error during health checks") db.rollback() finally: db.close() async def health_check_loop(interval: int) -> None: while True: await run_health_checks() await asyncio.sleep(interval)