Source code for animedex.auth.keyring_store

"""
OS-keyring implementation of :class:`~animedex.auth.store.TokenStore`.

Per ``plans/02 §7`` this is the production token store: tokens live
in the OS keyring (Secret Service on Linux, Keychain on macOS,
Credential Locker on Windows) so a stolen dotfile cannot exfiltrate
credentials. Enumeration (:meth:`keys`) is the one operation the
``keyring`` package does not expose uniformly; we keep an in-process
companion set populated as :meth:`set` and :meth:`delete` are called,
so :meth:`keys` returns what *this process* has registered. CLI
callers that need the full host-wide list use the OS keyring viewer.

Tests never touch the real keyring; the module pulls
:data:`_keyring` through an indirection so a fake module can be
swapped in.
"""

from __future__ import annotations

import keyring as _keyring
import keyring.errors as _keyring_errors
from typing import Iterable, Optional


_DEFAULT_SERVICE = "animedex"


[docs] class KeyringTokenStore: """A :class:`~animedex.auth.store.TokenStore` backed by the OS keyring. :param service: Keyring service namespace under which entries live; defaults to ``"animedex"``. Test code overrides this so production keyring entries stay untouched. :type service: str """
[docs] def __init__(self, *, service: str = _DEFAULT_SERVICE) -> None: self._service = service self._known_keys: set = set()
[docs] def set(self, backend: str, token: str) -> None: _keyring.set_password(self._service, backend, token) self._known_keys.add(backend)
[docs] def get(self, backend: str) -> Optional[str]: return _keyring.get_password(self._service, backend)
[docs] def delete(self, backend: str) -> None: # Per the TokenStore Protocol contract (animedex.auth.store.delete), # deleting a missing entry is not an error. Most OS keyring backends # (Secret Service on Linux, Credential Locker on Windows) raise # PasswordDeleteError when the entry is absent; swallow that to honour # the Protocol's idempotency guarantee. try: _keyring.delete_password(self._service, backend) except _keyring_errors.PasswordDeleteError: pass self._known_keys.discard(backend)
[docs] def keys(self) -> Iterable[str]: return list(self._known_keys)
[docs] def selftest() -> bool: """Smoke-test the keyring store at the import level only. Per ``plans/02 §7`` and ``plans/04 §2`` this selftest *must not* touch the real OS keyring: a CI environment may have no backend available, and writing a real entry from the diagnostic would be a side effect users do not expect. The function therefore only verifies that the ``keyring`` package imports and exposes the three call sites the store relies on. The behaviour itself is exercised in unit tests via a faked module. :return: ``True`` on success. :rtype: bool """ assert callable(getattr(_keyring, "set_password", None)) assert callable(getattr(_keyring, "get_password", None)) assert callable(getattr(_keyring, "delete_password", None)) return True