""" Environment-aware settings for Code of Conquest. - Loads environment variables from OS and `.env` (OS wins). - Provides repo-relative default paths for data storage. - Validates a few key fields (env, model backend). - Ensures important directories exist on first load. - Exposes a tiny singleton: get_settings(). Style: - Python 3.11+ - Dataclasses (no Pydantic) - Docstrings + inline comments """ from __future__ import annotations import os from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Optional from dotenv import load_dotenv class Environment(str, Enum): DEV = "dev" TEST = "test" PROD = "prod" def _repo_root_from_here() -> Path: """ Resolve the repository root by walking up from this file. This file lives at: project/app/core/utils/settings.py So parents[3] should be the repo root: parents[0] = utils parents[1] = core parents[2] = app parents[3] = project root """ here = Path(__file__).resolve() repo_root = here.parents[3] return repo_root @dataclass class Settings: """ Settings container for Code of Conquest. Load order: 1) OS environment 2) .env file (at repo root) 3) Defaults below Paths default into the repo under ./data unless overridden. """ # --- Core Tunables--- env: Environment = Environment.DEV log_level: str = "INFO" flask_secret_key: str = os.getenv("FLASK_SECRET_KEY","change-me-for-prod") # APPWRITE Things appwrite_endpoint: str = os.getenv("APPWRITE_ENDPOINT","NOT SET") appwrite_project_id: str = os.getenv("APPWRITE_PROJECT_ID","NOT SET") appwrite_api_key: str = os.getenv("APPWRITE_API_KEY","NOT SET") app_name: str = "Code of Conquest" app_version: str = "v 0.0.1" # --- Paths (default under ./data) --- repo_root: Path = field(default_factory=_repo_root_from_here) # --- Build paths for convenience (not env-controlled directly) --- def __post_init__(self) -> None: # Basic validation if self.env not in (Environment.DEV, Environment.TEST, Environment.PROD): raise ValueError(f"Invalid COC_ENV: {self.env}") @staticmethod def _ensure_dir(path: Path) -> None: if path is None: return if not path.exists(): path.mkdir(parents=True, exist_ok=True) # ---- Singleton loader ---- _settings_singleton: Optional[Settings] = None def get_settings() -> Settings: """ Load settings from environment and `.env` once, then reuse. OS env always takes precedence over `.env`. Returns: Settings: A process-wide singleton instance. """ global _settings_singleton if _settings_singleton is not None: return _settings_singleton # Load .env from repo root repo_root = _repo_root_from_here() dotenv_path = repo_root / ".env" load_dotenv(dotenv_path=dotenv_path, override=False) # Environment env_str = os.getenv("COC_ENV", "dev").strip().lower() if env_str == "dev": env_val = Environment.DEV elif env_str == "test": env_val = Environment.TEST elif env_str == "prod": env_val = Environment.PROD else: raise ValueError(f"COC_ENV must be one of dev|test|prod, got '{env_str}'") # Construct settings _settings_singleton = Settings( env=env_val, log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper(), ) return _settings_singleton