"""Provisioner base class and registry (T029).""" from __future__ import annotations import asyncio import logging from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: from user_store import ServiceCredential, User, UserStore logger = logging.getLogger(__name__) @dataclass class ProvisionResult: """Result of a provisioning attempt.""" success: bool service_username: str | None = None service_user_id: str | None = None service_url: str | None = None error: str | None = None class ServiceProvisioner(ABC): """Abstract base for service account provisioners.""" @property @abstractmethod def service_name(self) -> str: """Unique service key (e.g. 'vikunja', 'karakeep').""" @property def capabilities(self) -> list[str]: """Human-readable capability labels.""" return [] @property def requires_consent(self) -> bool: """Whether the user must explicitly opt-in before provisioning.""" return True @abstractmethod async def provision(self, user: User, store: UserStore) -> ProvisionResult: """Create a service account for *user* and store credentials. Implementations MUST: - Generate credentials (username, password, API token) - Store the token via ``store.store_credential(...)`` - Log to ``store.log_provisioning(...)`` - Attempt rollback on partial failure """ @abstractmethod async def health_check(self, credential: ServiceCredential) -> bool: """Return True if *credential* is still valid and usable.""" # ── Provisioner registry ───────────────────────────────────────── class ProvisionerRegistry: """Central registry of service provisioners.""" def __init__(self) -> None: self._provisioners: dict[str, ServiceProvisioner] = {} self._locks: dict[str, asyncio.Lock] = {} def register(self, provisioner: ServiceProvisioner) -> None: self._provisioners[provisioner.service_name] = provisioner logger.info("Registered provisioner: %s", provisioner.service_name) def get(self, service_name: str) -> ServiceProvisioner | None: return self._provisioners.get(service_name) @property def available(self) -> dict[str, ServiceProvisioner]: return dict(self._provisioners) def get_lock(self, user_id: str, service: str) -> asyncio.Lock: """Per-user, per-service provisioning lock to prevent races.""" key = f"{user_id}:{service}" if key not in self._locks: self._locks[key] = asyncio.Lock() return self._locks[key] provisioner_registry = ProvisionerRegistry()