2026-03-06 15:26:49 +03:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
2026-03-06 16:57:46 +03:00
|
|
|
from datetime import datetime, timedelta
|
2026-03-06 15:26:49 +03:00
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
from web.database import SessionLocal
|
2026-03-10 12:53:44 +03:00
|
|
|
from web.models import EvotorConnection, VkConnection, CachedStore
|
2026-03-06 15:26:49 +03:00
|
|
|
|
|
|
|
|
logger = logging.getLogger("uvicorn.error")
|
|
|
|
|
|
|
|
|
|
EVOTOR_STORES_URL = "https://api.evotor.ru/stores"
|
2026-03-06 16:57:46 +03:00
|
|
|
EVOTOR_TOKEN_URL = "https://oauth.evotor.ru/oauth/token"
|
2026-03-06 15:29:42 +03:00
|
|
|
VK_USERS_GET_URL = "https://api.vk.com/method/users.get"
|
|
|
|
|
VK_API_VERSION = "5.131"
|
2026-03-06 15:26:49 +03:00
|
|
|
|
2026-03-06 16:57:46 +03:00
|
|
|
# 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
|
|
|
|
|
|
2026-03-06 15:26:49 +03:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 15:29:42 +03:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 15:26:49 +03:00
|
|
|
async def run_health_checks() -> None:
|
|
|
|
|
db = SessionLocal()
|
|
|
|
|
try:
|
2026-03-06 16:57:46 +03:00
|
|
|
now = datetime.utcnow()
|
|
|
|
|
|
2026-03-06 15:29:42 +03:00
|
|
|
evotor_connections = db.query(EvotorConnection).all()
|
|
|
|
|
for conn in evotor_connections:
|
2026-03-06 16:57:46 +03:00
|
|
|
# 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
|
2026-03-06 15:29:42 +03:00
|
|
|
|
|
|
|
|
vk_connections = db.query(VkConnection).all()
|
|
|
|
|
for conn in vk_connections:
|
|
|
|
|
conn.is_online = await check_vk_connection(conn.access_token)
|
2026-03-06 16:57:46 +03:00
|
|
|
conn.last_checked_at = now
|
2026-03-06 15:29:42 +03:00
|
|
|
|
2026-03-06 15:26:49 +03:00
|
|
|
db.commit()
|
2026-03-10 12:53:44 +03:00
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
2026-03-06 15:29:42 +03:00
|
|
|
logger.info(
|
2026-03-10 12:53:44 +03:00
|
|
|
"Health checks completed: %d Evotor, %d VK, %d catalogs refreshed",
|
2026-03-06 15:29:42 +03:00
|
|
|
len(evotor_connections),
|
|
|
|
|
len(vk_connections),
|
2026-03-10 12:53:44 +03:00
|
|
|
refreshed_catalog,
|
2026-03-06 15:29:42 +03:00
|
|
|
)
|
2026-03-06 15:26:49 +03:00
|
|
|
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)
|