86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
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)
|