"""Service to handle task result notifications with retries and diagnostics.""" from typing import Dict, Any import httpx from pydantic import HttpUrl from tenacity import retry, stop_after_attempt, wait_exponential class NotificationService: """Service for sending notifications with retry logic.""" def __init__(self, max_retries: int = 5, timeout: float = 30.0): """Create a reusable HTTP client. timeout can be a float or seconds; we prefer a more generous read timeout on Spaces. """ # Increase timeouts to accommodate slower evaluators timeout_cfg = httpx.Timeout( connect=10.0, read=max(60.0, float(timeout)), write=30.0, pool=10.0, ) # Follow redirects to ensure we validate the final response code self.client = httpx.AsyncClient(timeout=timeout_cfg, follow_redirects=True, headers={ "User-Agent": "TDS-Project1-API/1.0" }) self.max_retries = max_retries @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=1, max=60), reraise=True ) async def notify_evaluation(self, url: str | HttpUrl, task_result: Dict[str, Any]) -> None: """ Send task result to evaluation URL with retries. Args: url: The evaluation URL to send results to (str or HttpUrl) task_result: The task result data to send Raises: httpx.HTTPError: If the request fails after all retries """ # Convert HttpUrl to string if needed url_str = str(url) if not isinstance(url, str) else url try: response = await self.client.post( url_str, json=task_result, headers={"Content-Type": "application/json"} ) except httpx.RequestError as e: # Provide clearer diagnostics for network-level errors print(f"[Notify] Request error to {url_str}: {e}") raise # Enforce strict 200 OK per requirement (not just any 2xx) if response.status_code != 200: body_preview = (response.text or "").strip() if len(body_preview) > 512: body_preview = body_preview[:512] + "..." print(f"[Notify] Non-200 from evaluator: {response.status_code}. Body: {body_preview}") raise httpx.HTTPStatusError( f"Expected 200 OK, got {response.status_code}", request=response.request, response=response ) async def close(self): """Close the HTTP client.""" await self.client.aclose()