init commit

This commit is contained in:
2025-11-02 01:14:41 -05:00
commit 7bf81109b3
31 changed files with 2387 additions and 0 deletions

129
app/utils/settings.py Normal file
View File

@@ -0,0 +1,129 @@
"""
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 = "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