Finalize diagnostics, logging controls, and email test support

This commit is contained in:
2026-03-01 22:34:07 +13:00
parent 12d3777e76
commit d1c9acbb8d
19 changed files with 2578 additions and 99 deletions

View File

@@ -1,11 +1,16 @@
from typing import Any, Dict, Optional
import logging
import time
import httpx
from ..logging_config import sanitize_headers, sanitize_value
class ApiClient:
def __init__(self, base_url: Optional[str], api_key: Optional[str] = None):
self.base_url = base_url.rstrip("/") if base_url else None
self.api_key = api_key
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
def configured(self) -> bool:
return bool(self.base_url)
@@ -13,42 +18,66 @@ class ApiClient:
def headers(self) -> Dict[str, str]:
return {"X-Api-Key": self.api_key} if self.api_key else {}
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
async def _request(
self,
method: str,
path: str,
*,
params: Optional[Dict[str, Any]] = None,
payload: Optional[Dict[str, Any]] = None,
) -> Optional[Any]:
if not self.base_url:
self.logger.warning("client request skipped method=%s path=%s reason=not-configured", method, path)
return None
url = f"{self.base_url}{path}"
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(url, headers=self.headers(), params=params)
response.raise_for_status()
started_at = time.perf_counter()
self.logger.debug(
"outbound request started method=%s url=%s params=%s payload=%s headers=%s",
method,
url,
sanitize_value(params),
sanitize_value(payload),
sanitize_headers(self.headers()),
)
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.request(
method,
url,
headers=self.headers(),
params=params,
json=payload,
)
response.raise_for_status()
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
self.logger.debug(
"outbound request completed method=%s url=%s status=%s duration_ms=%s",
method,
url,
response.status_code,
duration_ms,
)
if not response.content:
return None
return response.json()
except Exception:
duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
self.logger.exception(
"outbound request failed method=%s url=%s duration_ms=%s",
method,
url,
duration_ms,
)
raise
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
return await self._request("GET", path, params=params)
async def post(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Optional[Any]:
if not self.base_url:
return None
url = f"{self.base_url}{path}"
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(url, headers=self.headers(), json=payload)
response.raise_for_status()
return response.json()
return await self._request("POST", path, payload=payload)
async def put(self, path: str, payload: Optional[Dict[str, Any]] = None) -> Optional[Any]:
if not self.base_url:
return None
url = f"{self.base_url}{path}"
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.put(url, headers=self.headers(), json=payload)
response.raise_for_status()
if not response.content:
return None
return response.json()
return await self._request("PUT", path, payload=payload)
async def delete(self, path: str) -> Optional[Any]:
if not self.base_url:
return None
url = f"{self.base_url}{path}"
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.delete(url, headers=self.headers())
response.raise_for_status()
if not response.content:
return None
return response.json()
return await self._request("DELETE", path)