"""Credential store abstraction. Wraps the ``keyring`` library so callers do not depend on it directly. Phase B note: the same interface is used on both Windows (Credential Manager) and Linux (Secret Service / file-based vault); see ``plans/implementation-plan-A-B.md`` step B3. """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class Credential: """Username + secret pair retrieved from a credential store.""" username: str password: str class CredentialStore(Protocol): """Read-only credential lookup.""" def get(self, target: str) -> Credential: """Return the credential associated with ``target``. Raises :class:`KeyError` if the target is unknown. """ ... class KeyringCredentialStore: """Default :class:`CredentialStore` backed by the ``keyring`` library.""" def __init__(self, service: str = "ci") -> None: self._service = service def get(self, target: str) -> Credential: try: import keyring except ImportError as exc: # pragma: no cover - dep declared raise RuntimeError( "keyring is not installed; run `pip install keyring`." ) from exc cred = keyring.get_credential(target, None) if cred is None: # Fallback: some backends only support get_password and need the # username to be the same as the target. password = keyring.get_password(self._service, target) if password is None: raise KeyError(f"Credential '{target}' not found in keyring.") return Credential(username=target, password=password) return Credential(username=cred.username, password=cred.password) __all__ = ["Credential", "CredentialStore", "KeyringCredentialStore"]