Files
local-ci-cd-system/src/ci_orchestrator/credentials.py
T
Simone bd31a3f2f3 Phase A1: bootstrap Python ci_orchestrator package
- pyproject.toml (hatchling) with ruff/mypy/pytest dev deps
- src/ci_orchestrator/: config, credentials, backends (Protocol + WorkstationVmrunBackend), transport (WinRM via pypsrp, SSH via paramiko)
- CLI entry point with PoC 'wait-ready' subcommand (Windows + Linux guests)
- tests/python/: 35 unit tests, 83% coverage, all backends/transport mocked
- gitea/workflows/lint.yml: new 'python' job (ruff + mypy --strict + pytest --cov-fail-under=70)
- config.example.toml + README setup section + .gitignore Python entries

Neutral VmBackend Protocol prepared for Phase C ESXi extension.
See plans/implementation-plan-A-B.md step A1.
2026-05-14 18:34:02 +02:00

60 lines
1.8 KiB
Python

"""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"]