88 lines
2.8 KiB
Python
88 lines
2.8 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
|
|
log_file: Path | None
|
|
log_level: str
|
|
host: str
|
|
port: int
|
|
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"
|
|
)
|
|
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")
|
|
return cls(
|
|
environment=environment,
|
|
debug=environment == "development",
|
|
data_directory=data_directory,
|
|
database_path=database_path,
|
|
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")),
|
|
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",
|
|
log_file=None,
|
|
log_level="CRITICAL",
|
|
host="127.0.0.1",
|
|
port=8780,
|
|
)
|