140 lines
4.8 KiB
Python
140 lines
4.8 KiB
Python
"""Configuration-backed product profile for a reusable agent console."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
|
|
class AppProfileError(RuntimeError):
|
|
"""Safe error for an invalid product profile."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AppProfile:
|
|
product_name: str
|
|
short_name: str
|
|
page_title: str
|
|
page_icon: str
|
|
header_title: str
|
|
header_description: str
|
|
login_kicker: str
|
|
login_title: str
|
|
login_description: str
|
|
login_footer: str
|
|
primary_color: str
|
|
text_color: str
|
|
muted_color: str
|
|
border_color: str
|
|
|
|
|
|
_ENV_FIELD_NAMES = {
|
|
"product_name": "AGENT_CONSOLE_NAME",
|
|
"short_name": "AGENT_CONSOLE_SHORT_NAME",
|
|
"page_title": "AGENT_CONSOLE_PAGE_TITLE",
|
|
"page_icon": "AGENT_CONSOLE_PAGE_ICON",
|
|
"header_title": "AGENT_CONSOLE_HEADER_TITLE",
|
|
"header_description": "AGENT_CONSOLE_HEADER_DESCRIPTION",
|
|
"login_kicker": "AGENT_CONSOLE_LOGIN_KICKER",
|
|
"login_title": "AGENT_CONSOLE_LOGIN_TITLE",
|
|
"login_description": "AGENT_CONSOLE_LOGIN_DESCRIPTION",
|
|
"login_footer": "AGENT_CONSOLE_LOGIN_FOOTER",
|
|
"primary_color": "AGENT_CONSOLE_PRIMARY_COLOR",
|
|
"text_color": "AGENT_CONSOLE_TEXT_COLOR",
|
|
"muted_color": "AGENT_CONSOLE_MUTED_COLOR",
|
|
"border_color": "AGENT_CONSOLE_BORDER_COLOR",
|
|
}
|
|
|
|
|
|
def _string(section: Mapping[str, Any], key: str, fallback: str = "") -> str:
|
|
return str(section.get(key) or fallback).strip()
|
|
|
|
|
|
def resolve_profile_path(default_path: Path) -> Path:
|
|
configured = os.environ.get("AGENT_CONSOLE_PROFILE_PATH", "").strip()
|
|
if not configured:
|
|
return default_path
|
|
path = Path(configured).expanduser()
|
|
return path if path.is_absolute() else default_path.parent / path
|
|
|
|
|
|
def _dotenv_value(name: str, path: Path | None) -> str:
|
|
"""Read one simple KEY=value runtime setting without importing a dotenv lib."""
|
|
|
|
if path is None:
|
|
return ""
|
|
try:
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
except (OSError, UnicodeError):
|
|
return ""
|
|
prefix = f"{name}="
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith(prefix):
|
|
return stripped[len(prefix) :].strip().strip('"').strip("'")
|
|
return ""
|
|
|
|
|
|
def _apply_environment_overrides(profile: AppProfile, env_file: Path | None) -> AppProfile:
|
|
"""Apply deployment-specific presentation values without a code change."""
|
|
|
|
values = {
|
|
field_name: (
|
|
os.environ.get(env_name, "").strip()
|
|
or _dotenv_value(env_name, env_file)
|
|
or getattr(profile, field_name)
|
|
)
|
|
for field_name, env_name in _ENV_FIELD_NAMES.items()
|
|
}
|
|
return AppProfile(**values)
|
|
|
|
|
|
def load_app_profile(default_path: Path, env_file: Path | None = None) -> AppProfile:
|
|
"""Load the selectable product skin without coupling it to a PoC name."""
|
|
|
|
path = resolve_profile_path(default_path)
|
|
try:
|
|
payload: Any = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, ValueError):
|
|
raise AppProfileError(f"애플리케이션 프로필을 읽지 못했습니다: {path}") from None
|
|
if not isinstance(payload, Mapping):
|
|
raise AppProfileError("애플리케이션 프로필 형식이 올바르지 않습니다.")
|
|
product = payload.get("product")
|
|
theme = payload.get("theme")
|
|
if not isinstance(product, Mapping) or not isinstance(theme, Mapping):
|
|
raise AppProfileError("애플리케이션 프로필에 product와 theme 객체가 필요합니다.")
|
|
|
|
profile = _apply_environment_overrides(AppProfile(
|
|
product_name=_string(product, "name"),
|
|
short_name=_string(product, "short_name"),
|
|
page_title=_string(product, "page_title"),
|
|
page_icon=_string(product, "page_icon", "🤖"),
|
|
header_title=_string(product, "header_title"),
|
|
header_description=_string(product, "header_description"),
|
|
login_kicker=_string(product, "login_kicker"),
|
|
login_title=_string(product, "login_title"),
|
|
login_description=_string(product, "login_description"),
|
|
login_footer=_string(product, "login_footer"),
|
|
primary_color=_string(theme, "primary_color"),
|
|
text_color=_string(theme, "text_color"),
|
|
muted_color=_string(theme, "muted_color"),
|
|
border_color=_string(theme, "border_color"),
|
|
), env_file)
|
|
required = (
|
|
profile.product_name,
|
|
profile.short_name,
|
|
profile.page_title,
|
|
profile.header_title,
|
|
profile.login_title,
|
|
profile.primary_color,
|
|
profile.text_color,
|
|
profile.muted_color,
|
|
profile.border_color,
|
|
)
|
|
if not all(required):
|
|
raise AppProfileError("애플리케이션 프로필의 필수 표시값 또는 색상이 비어 있습니다.")
|
|
return profile
|