import time import logging from typing import Any import requests from domain.entities import ApiResponse from domain.ports import RailwayApiPort, EventBusPort from infrastructure.config import Config logger = logging.getLogger(__name__) class RailwayApiClient(RailwayApiPort): """Implementacja klienta API kolejowego z obsługą: - Retry z backoff przy błędach 503 (symulacja przeciążenia) - Respektowanie limitów zapytań (429) z retry_after - Logowanie każdego wywołania i odpowiedzi - Emitowanie zdarzeń przez EventBus """ def __init__(self, config: Config, event_bus: EventBusPort) -> None: self._config = config self._event_bus = event_bus self._session = requests.Session() self._session.headers.update({"Content-Type": "application/json"}) def send_action(self, action: str, **params: Any) -> ApiResponse: payload = { "apikey": self._config.api_key, "task": self._config.task_name, "answer": {"action": action, **params}, } for attempt in range(1, self._config.max_retries + 1): self._event_bus.emit("api:request", { "action": action, "params": params, "attempt": attempt, }) try: resp = self._session.post(self._config.api_url, json=payload, timeout=30) except requests.RequestException as exc: logger.error("Request failed: %s", exc) self._wait(self._config.retry_base_delay * attempt) continue data = resp.json() api_resp = ApiResponse( ok=data.get("ok", False), data=data, http_code=resp.status_code, headers=dict(resp.headers), ) self._event_bus.emit("api:response", { "action": action, "http_code": resp.status_code, "data": data, }) if api_resp.is_server_error: delay = self._config.retry_base_delay * attempt logger.warning("503 Server outage (attempt %d/%d), retrying in %.1fs", attempt, self._config.max_retries, delay) self._event_bus.emit("api:retry", {"reason": "503", "delay": delay}) self._wait(delay) continue if api_resp.is_rate_limited: delay = api_resp.retry_after + 1 logger.warning("429 Rate limited, waiting %ds", delay) self._event_bus.emit("api:retry", {"reason": "429", "delay": delay}) self._wait(delay) continue return api_resp raise RuntimeError(f"Action '{action}' failed after {self._config.max_retries} retries") def _wait(self, seconds: float) -> None: logger.info("Waiting %.1fs...", seconds) time.sleep(seconds)