Source code for fenn.remote.credentials
"""Local credential storage for the Fenn remote service.
A single credential lives in ``~/.fenn/credentials`` as JSON::
{"api_key": "fk_live_...", "host": null}
Resolution order for :func:`resolve_api_key`:
1. ``FENN_API_KEY`` environment variable
2. the stored credential
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from fenn.exceptions import CredentialsError
ENV_API_KEY = "FENN_API_KEY"
def _home() -> Path:
home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
return Path(home) if home else Path.home()
CREDENTIALS_DIR = _home() / ".fenn"
CREDENTIALS_PATH = CREDENTIALS_DIR / "credentials"
[docs]
@dataclass(frozen=True)
class Credentials:
api_key: str
host: Optional[str] = None
def _read_store() -> dict:
if not CREDENTIALS_PATH.is_file():
return {}
try:
with open(CREDENTIALS_PATH, "r", encoding="utf-8") as fh:
data = json.load(fh)
except (OSError, json.JSONDecodeError) as exc:
raise CredentialsError(
f"Could not read credentials file {CREDENTIALS_PATH}: {exc}"
) from exc
if not isinstance(data, dict):
raise CredentialsError(
f"Malformed credentials file {CREDENTIALS_PATH}; "
"delete it and run `fenn auth login` again."
)
return data
def _write_store(data: dict) -> None:
CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
with open(CREDENTIALS_PATH, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
try:
os.chmod(CREDENTIALS_PATH, 0o600)
except OSError:
# Best effort — not supported on all platforms (e.g. Windows).
pass
[docs]
def write_credentials(api_key: str, *, host: Optional[str] = None) -> Path:
"""Persist ``api_key`` and return the credentials file path."""
_write_store({"api_key": api_key, "host": host})
return CREDENTIALS_PATH
[docs]
def load_credentials() -> Optional[Credentials]:
"""Return the stored :class:`Credentials`, or ``None`` if unset."""
store = _read_store()
if not isinstance(store, dict) or not store.get("api_key"):
return None
return Credentials(
api_key=str(store["api_key"]),
host=store.get("host") or None,
)
[docs]
def delete_credentials() -> bool:
"""Remove the stored credential. Returns True if one existed."""
if not CREDENTIALS_PATH.is_file():
return False
if load_credentials() is None:
return False
_write_store({})
return True
[docs]
def resolve_api_key() -> Credentials:
"""Resolve an API key from env > credentials file.
Raises:
CredentialsError: when no key can be found anywhere.
"""
env_key = os.environ.get(ENV_API_KEY)
if env_key:
return Credentials(api_key=env_key)
creds = load_credentials()
if creds is None:
raise CredentialsError(
f"No API key found. Set {ENV_API_KEY} or run `fenn auth login`."
)
return creds
[docs]
def mask_key(api_key: str) -> str:
"""Return a redacted form safe for terminal output."""
if len(api_key) <= 12:
return "*" * len(api_key)
return f"{api_key[:8]}...{api_key[-4:]}"