"""NAS realestate 내부 계약 — X-Internal-Key + retry.""" from __future__ import annotations import asyncio import logging import httpx logger = logging.getLogger(__name__) _RETRY_STATUSES = {429, 500, 502, 503, 504} _TARGETS_TIMEOUT = 15.0 _INGEST_TIMEOUT = 180.0 # 첫 실행 bulk upsert/매칭/알림이 느림 (초 단위 수십) class NASClient: def __init__(self, base_url: str, api_key: str): self._base_url = base_url.rstrip("/") self._api_key = api_key self._client = httpx.AsyncClient() async def close(self) -> None: await self._client.aclose() async def get_targets(self) -> dict: return await self._request("GET", "/api/internal/realestate/targets", timeout=_TARGETS_TIMEOUT, max_attempts=3) async def post_ingest(self, fetched_at: str, batches: list[dict]) -> dict: # ingest는 재시도 안 함(max_attempts=1) — 타임아웃 후 재전송 시 # NAS가 같은 batch를 재처리/재알림할 위험. 실패 시 다음 사이클에 재시도. return await self._request( "POST", "/api/internal/realestate/listings-ingest", json={"fetched_at": fetched_at, "batches": batches}, timeout=_INGEST_TIMEOUT, max_attempts=1) async def _request(self, method: str, path: str, *, timeout: float, max_attempts: int, **kwargs) -> dict: url = f"{self._base_url}{path}" headers = {"X-Internal-Key": self._api_key} for attempt in range(max_attempts): try: resp = await self._client.request(method, url, headers=headers, timeout=timeout, **kwargs) if resp.status_code in _RETRY_STATUSES and attempt < max_attempts - 1: await asyncio.sleep(2 ** attempt) continue resp.raise_for_status() return resp.json() except httpx.TimeoutException: if attempt < max_attempts - 1: await asyncio.sleep(2 ** attempt) continue raise raise RuntimeError("retry exhausted")