initial commit
This commit is contained in:
0
infrastructure/__init__.py
Normal file
0
infrastructure/__init__.py
Normal file
85
infrastructure/api_client.py
Normal file
85
infrastructure/api_client.py
Normal file
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
21
infrastructure/config.py
Normal file
21
infrastructure/config.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
api_url: str
|
||||
api_key: str
|
||||
task_name: str
|
||||
max_retries: int
|
||||
retry_base_delay: float
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "Config":
|
||||
return cls(
|
||||
api_url=os.getenv("RAILWAY_API_URL", "https://hub.ag3nts.org/verify"),
|
||||
api_key=os.environ["RAILWAY_API_KEY"],
|
||||
task_name=os.getenv("RAILWAY_TASK_NAME", "railway"),
|
||||
max_retries=int(os.getenv("RAILWAY_MAX_RETRIES", "10")),
|
||||
retry_base_delay=float(os.getenv("RAILWAY_RETRY_BASE_DELAY", "2.0")),
|
||||
)
|
||||
24
infrastructure/event_bus.py
Normal file
24
infrastructure/event_bus.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable
|
||||
|
||||
from domain.ports import EventBusPort
|
||||
|
||||
|
||||
class EventBus(EventBusPort):
|
||||
"""Prosta implementacja szyny zdarzeń (event bus).
|
||||
|
||||
Zgodnie z lekcją — architektura oparta o zdarzenia umożliwia:
|
||||
- monitorowanie działań agenta
|
||||
- podejmowanie akcji (np. kompresja kontekstu, logowanie)
|
||||
- subskrypcję zdarzeń z różnych komponentów
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._listeners: dict[str, list[Callable]] = defaultdict(list)
|
||||
|
||||
def emit(self, event: str, data: dict[str, Any]) -> None:
|
||||
for callback in self._listeners[event]:
|
||||
callback(data)
|
||||
|
||||
def on(self, event: str, callback: Callable) -> None:
|
||||
self._listeners[event].append(callback)
|
||||
40
infrastructure/logger_setup.py
Normal file
40
infrastructure/logger_setup.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from domain.ports import EventBusPort
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
|
||||
|
||||
def attach_event_logger(event_bus: EventBusPort) -> None:
|
||||
"""Podłącza monitorowanie zdarzeń API — zgodnie z lekcją:
|
||||
'warto zapisywać i monitorować wszystkie zdarzenia'."""
|
||||
|
||||
log = logging.getLogger("events")
|
||||
|
||||
def on_request(data: dict[str, Any]) -> None:
|
||||
log.info(">>> [%s] attempt=%d params=%s",
|
||||
data["action"], data["attempt"], data.get("params", {}))
|
||||
|
||||
def on_response(data: dict[str, Any]) -> None:
|
||||
log.info("<<< [%s] http=%d data=%s",
|
||||
data["action"], data["http_code"], data["data"])
|
||||
|
||||
def on_retry(data: dict[str, Any]) -> None:
|
||||
log.warning("... retry reason=%s delay=%.1fs", data["reason"], data["delay"])
|
||||
|
||||
def on_step(data: dict[str, Any]) -> None:
|
||||
log.info("=== STEP: %s", data.get("description", ""))
|
||||
|
||||
event_bus.on("api:request", on_request)
|
||||
event_bus.on("api:response", on_response)
|
||||
event_bus.on("api:retry", on_retry)
|
||||
event_bus.on("workflow:step", on_step)
|
||||
Reference in New Issue
Block a user