Files
xiaobaifupan/next/backend/bootstrap/settings.py
T

113 lines
4.2 KiB
Python

from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
class ConfigurationError(ValueError):
pass
def _resolve_path(raw_value: str, default: Path) -> Path:
candidate = Path(raw_value).expanduser() if raw_value.strip() else default
if not candidate.is_absolute():
candidate = PROJECT_ROOT / candidate
return candidate.resolve()
def _parse_port(raw_value: str) -> int:
try:
port = int(raw_value)
except ValueError as exc:
raise ConfigurationError("APP_PORT must be an integer") from exc
if not 1 <= port <= 65535:
raise ConfigurationError("APP_PORT must be between 1 and 65535")
return port
@dataclass(frozen=True, slots=True)
class Settings:
environment: str
debug: bool
data_directory: Path
database_path: Path
mentor_skills_directory: Path
private_mentor_skills_directory: Path
log_file: Path | None
log_level: str
host: str
port: int
encryption_key: str | None
frontend_dist_directory: Path = PROJECT_ROOT / "frontend" / "dist"
timezone: str = "Asia/Shanghai"
@classmethod
def from_environment(cls) -> Settings:
environment = os.getenv("APP_ENV", "development").strip().lower()
if environment not in {"development", "test", "production"}:
raise ConfigurationError("APP_ENV must be development, test, or production")
timezone = os.getenv("APP_TIMEZONE", "Asia/Shanghai").strip()
if timezone != "Asia/Shanghai":
raise ConfigurationError("APP_TIMEZONE must be Asia/Shanghai")
data_directory = _resolve_path(os.getenv("APP_DATA_DIR", ""), PROJECT_ROOT / "data")
database_path = _resolve_path(
os.getenv("APP_DATABASE_PATH", ""), data_directory / "xiaobai.db"
)
mentor_skills_directory = _resolve_path(
os.getenv("APP_MENTOR_SKILLS_DIR", ""), PROJECT_ROOT / "config" / "mentor-skills"
)
private_mentor_skills_directory = _resolve_path(
os.getenv("APP_PRIVATE_MENTOR_SKILLS_DIR", ""),
data_directory / "private-mentor-skills",
)
frontend_dist_directory = _resolve_path(
os.getenv("APP_FRONTEND_DIST", ""), PROJECT_ROOT / "frontend" / "dist"
)
log_file = _resolve_path(
os.getenv("APP_LOG_FILE", ""),
data_directory / "logs" / "application.log",
)
log_level = os.getenv("APP_LOG_LEVEL", "INFO").strip().upper()
if log_level not in {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}:
raise ConfigurationError("APP_LOG_LEVEL is invalid")
encryption_key = os.getenv("APP_ENCRYPTION_KEY", "").strip() or None
if environment == "production" and encryption_key is None:
raise ConfigurationError("APP_ENCRYPTION_KEY is required in production")
return cls(
environment=environment,
debug=environment == "development",
data_directory=data_directory,
database_path=database_path,
mentor_skills_directory=mentor_skills_directory,
private_mentor_skills_directory=private_mentor_skills_directory,
frontend_dist_directory=frontend_dist_directory,
log_file=log_file,
log_level=log_level,
host=os.getenv("APP_HOST", "127.0.0.1").strip() or "127.0.0.1",
port=_parse_port(os.getenv("APP_PORT", "8780")),
encryption_key=encryption_key,
timezone=timezone,
)
@classmethod
def for_test(cls, data_directory: Path) -> Settings:
root = data_directory.resolve()
return cls(
environment="test",
debug=False,
data_directory=root,
database_path=root / "xiaobai-test.db",
mentor_skills_directory=PROJECT_ROOT / "config" / "mentor-skills",
private_mentor_skills_directory=root / "private-mentor-skills",
frontend_dist_directory=root / "frontend-dist",
log_file=None,
log_level="CRITICAL",
host="127.0.0.1",
port=8780,
encryption_key=None,
)