RcmEmailAutomation / app /lib /graph /subscription.py
cmoss3's picture
Error handling, confidence scoring, daily brief, deduplicate webhook notifications
0a367ef
Raw
History Blame Contribute Delete
4.01 kB
import logging
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx
from app.config import Settings
from app.database import (
get_db,
get_active_subscription,
mark_subscription_inactive,
update_subscription_expiry,
upsert_subscription,
)
from app.lib.graph.client import GraphClient
logger = logging.getLogger(__name__)
_RENEWAL_MARGIN_MINUTES = 60
class SubscriptionManager:
def __init__(self, client: GraphClient, settings: Settings, db_path: Path) -> None:
self._client = client
self._settings = settings
self._db_path = db_path
async def ensure_subscription(self, folder_id: str) -> str:
async with get_db(self._db_path) as conn:
existing = await get_active_subscription(conn)
if existing:
expiry = datetime.fromisoformat(existing["expiry_datetime"].replace("Z", "+00:00"))
cutoff = datetime.now(timezone.utc) + timedelta(minutes=_RENEWAL_MARGIN_MINUTES)
if expiry > cutoff:
logger.info("Reusing existing subscription %s (expires %s)", existing["id"], expiry)
return existing["id"]
logger.info("Existing subscription %s is near expiry; creating new one", existing["id"])
return await self._create_subscription(folder_id)
async def _create_subscription(self, folder_id: str) -> str:
client_state = str(uuid.uuid4())
expiry = datetime.now(timezone.utc) + timedelta(
minutes=self._settings.subscription_expiry_minutes
)
expiry_str = expiry.strftime("%Y-%m-%dT%H:%M:%S.0000000Z")
body = {
"changeType": "created",
"notificationUrl": str(self._settings.notification_url),
"resource": f"users/{self._settings.mailbox_user}/mailFolders/{folder_id}/messages",
"expirationDateTime": expiry_str,
"clientState": client_state,
}
data = await self._client.post("/subscriptions", body)
sub_id = data["id"]
actual_expiry = data.get("expirationDateTime", expiry_str)
async with get_db(self._db_path) as conn:
await upsert_subscription(
conn,
sub_id=sub_id,
folder_id=folder_id,
folder_name=self._settings.mail_folder_name,
expiry_datetime=actual_expiry,
notification_url=str(self._settings.notification_url),
client_state=client_state,
)
logger.info("Created subscription %s expiring %s", sub_id, actual_expiry)
return sub_id
async def renew_subscription(self, sub_id: str) -> None:
expiry = datetime.now(timezone.utc) + timedelta(
minutes=self._settings.subscription_expiry_minutes
)
expiry_str = expiry.strftime("%Y-%m-%dT%H:%M:%S.0000000Z")
try:
data = await self._client.patch(
f"/subscriptions/{sub_id}",
{"expirationDateTime": expiry_str},
)
actual_expiry = data.get("expirationDateTime", expiry_str)
async with get_db(self._db_path) as conn:
await update_subscription_expiry(conn, sub_id, actual_expiry)
logger.info("Renewed subscription %s until %s", sub_id, actual_expiry)
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
logger.warning("Subscription %s not found on renewal; will recreate", sub_id)
async with get_db(self._db_path) as conn:
await mark_subscription_inactive(conn, sub_id)
raise
raise
async def get_client_state(self, sub_id: str) -> str | None:
async with get_db(self._db_path) as conn:
existing = await get_active_subscription(conn)
if existing and existing["id"] == sub_id:
return existing["client_state"]
return None