import asyncio import logging import httpx from app.lib.graph.auth import GraphAuthProvider _BASE_URL = "https://graph.microsoft.com/v1.0" logger = logging.getLogger(__name__) def _raise_with_detail(resp: httpx.Response) -> None: if resp.is_error: logger.error("Graph %s %s → %s: %s", resp.request.method, resp.request.url, resp.status_code, resp.text) resp.raise_for_status() class GraphClient: def __init__(self, auth: GraphAuthProvider) -> None: self._auth = auth self._client = httpx.AsyncClient(timeout=30.0) async def _headers(self) -> dict[str, str]: token = await asyncio.to_thread(self._auth.get_access_token) return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} async def get(self, path: str, params: dict | None = None) -> dict: resp = await self._client.get( f"{_BASE_URL}{path}", headers=await self._headers(), params=params ) _raise_with_detail(resp) return resp.json() async def get_raw(self, path: str) -> bytes: resp = await self._client.get( f"{_BASE_URL}{path}", headers=await self._headers() ) _raise_with_detail(resp) return resp.content async def post(self, path: str, body: dict) -> dict: resp = await self._client.post( f"{_BASE_URL}{path}", headers=await self._headers(), json=body ) _raise_with_detail(resp) return resp.json() async def patch(self, path: str, body: dict) -> dict: resp = await self._client.patch( f"{_BASE_URL}{path}", headers=await self._headers(), json=body ) _raise_with_detail(resp) return resp.json() async def delete(self, path: str) -> None: resp = await self._client.delete( f"{_BASE_URL}{path}", headers=await self._headers() ) _raise_with_detail(resp) async def aclose(self) -> None: await self._client.aclose()