"""OAuth helpers for connecting Alba Core to PropertyMe.
These helpers build URLs and token request payloads only. They do not store
secrets; real credentials belong in .env or a secret manager.
"""
from __future__ import annotations
import secrets
from dataclasses import dataclass
from typing import Mapping
from urllib.parse import urlencode
[docs]
@dataclass(frozen=True, slots=True)
class PropertyMeOAuthConfig:
"""OAuth settings required to start or refresh a PropertyMe session."""
client_id: str
client_secret: str
redirect_uri: str
auth_url: str
token_url: str
scope: str
[docs]
def is_ready(self) -> bool:
"""Return True when all required OAuth fields are present."""
return all(
[
self.client_id.strip(),
self.client_secret.strip(),
self.redirect_uri.strip(),
self.auth_url.strip(),
self.token_url.strip(),
self.scope.strip(),
]
)
[docs]
def build_authorization_url(config: PropertyMeOAuthConfig, state: str | None = None) -> tuple[str, str]:
"""Build the PropertyMe authorisation URL and state token."""
if not config.is_ready():
raise ValueError("PropertyMe OAuth config is incomplete")
state = state or secrets.token_urlsafe(24)
query = urlencode(
{
"response_type": "code",
"state": state,
"client_id": config.client_id,
"scope": config.scope,
"redirect_uri": config.redirect_uri,
}
)
return f"{config.auth_url}?{query}", state
[docs]
def token_exchange_payload(config: PropertyMeOAuthConfig, code: str) -> Mapping[str, str]:
"""Return the form payload for exchanging an auth code for tokens."""
if not config.is_ready():
raise ValueError("PropertyMe OAuth config is incomplete")
return {
"grant_type": "authorization_code",
"code": code,
"client_id": config.client_id,
"client_secret": config.client_secret,
"redirect_uri": config.redirect_uri,
}
[docs]
def refresh_payload(config: PropertyMeOAuthConfig, refresh_token: str) -> Mapping[str, str]:
"""Return the form payload for refreshing an access token."""
if not config.is_ready():
raise ValueError("PropertyMe OAuth config is incomplete")
return {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": config.client_id,
"client_secret": config.client_secret,
}