69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Mapping, Optional, Union
|
|
|
|
ConfigPath = Union[str, Path]
|
|
|
|
|
|
def _default_credentials_path() -> Path:
|
|
"""Return the default path for the credential file."""
|
|
xdg = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
return (xdg / "brecal" / "credentials.json").expanduser()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Credentials:
|
|
"""Holds username/password pairs for the BreCal API."""
|
|
|
|
username: str
|
|
password: str
|
|
|
|
@classmethod
|
|
def from_mapping(cls, data: Mapping[str, Any]) -> "Credentials":
|
|
"""Create credentials from a mapping (dict, TOML config, etc.)."""
|
|
username = _coalesce_key(
|
|
data, ("username", "user_name", "user"), required="username"
|
|
)
|
|
password = _coalesce_key(
|
|
data, ("password", "pass", "secret"), required="password"
|
|
)
|
|
if not isinstance(username, str) or not username.strip():
|
|
raise ValueError("BreCal credentials require a non-empty username.")
|
|
if not isinstance(password, str) or not password:
|
|
raise ValueError("BreCal credentials require a non-empty password.")
|
|
return cls(username=username.strip(), password=password)
|
|
|
|
@classmethod
|
|
def load(cls, path: Optional[ConfigPath] = None) -> "Credentials":
|
|
"""Load credentials from a JSON file."""
|
|
file_path = Path(path) if path else _default_credentials_path()
|
|
text = file_path.read_text(encoding="utf-8")
|
|
data = json.loads(text)
|
|
return cls.from_mapping(data)
|
|
|
|
@classmethod
|
|
def from_env(
|
|
cls, username_var: str = "BRECAL_USERNAME", password_var: str = "BRECAL_PASSWORD"
|
|
) -> "Credentials":
|
|
"""Load credentials from environment variables."""
|
|
username = os.getenv(username_var)
|
|
password = os.getenv(password_var)
|
|
if not username or not password:
|
|
raise EnvironmentError(
|
|
f"Missing credentials in env vars {username_var}/{password_var}"
|
|
)
|
|
return cls(username=username, password=password)
|
|
|
|
|
|
def _coalesce_key(
|
|
data: Mapping[str, Any], keys: tuple[str, ...], *, required: str
|
|
) -> Any:
|
|
for key in keys:
|
|
if key in data:
|
|
return data[key]
|
|
raise KeyError(f"Missing '{required}' in credentials mapping.")
|