from __future__ import annotations import logging import time from dataclasses import dataclass import httpx logger = logging.getLogger(__name__) TOKEN_URL = "https://api.helpscout.net/v2/oauth2/token" REFRESH_BUFFER_SECONDS = 300 @dataclass class HelpScoutToken: access_token: str token_type: str = "bearer" expires_at: float = 0.0 class HelpScoutAuth: def __init__(self, app_id: str, app_secret: str): self._app_id = app_id self._app_secret = app_secret self._token: HelpScoutToken | None = None @property def access_token(self) -> str | None: if self._token: return self._token.access_token return None @property def auth_header(self) -> dict: return {"Authorization": f"Bearer {self.access_token or ''}"} @property def is_token_valid(self) -> bool: return self._token is not None and time.time() < self._token.expires_at - REFRESH_BUFFER_SECONDS async def ensure_token(self) -> str: if not self.is_token_valid: await self._fetch_token() return self._token.access_token async def force_refresh(self) -> str: logger.info("Help Scout: forcing token refresh due to 401") await self._fetch_token() return self._token.access_token async def _fetch_token(self) -> None: async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.post( TOKEN_URL, data={ "grant_type": "client_credentials", "client_id": self._app_id, "client_secret": self._app_secret, }, ) resp.raise_for_status() data = resp.json() expires_in = data.get("expires_in", 172800) self._token = HelpScoutToken( access_token=data["access_token"], token_type=data.get("token_type", "bearer"), expires_at=time.time() + expires_in, ) logger.info( "Help Scout token obtained: expires_in=%ds, expires_at=%s", expires_in, time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._token.expires_at)), )