Wothmag07's picture
Doc-MCP Application
1e6a9db
"""Application configuration helpers."""
from __future__ import annotations
from functools import lru_cache
import os
from pathlib import Path
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
PROJECT_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_VAULT_BASE = PROJECT_ROOT / "data" / "vaults"
class AppConfig(BaseModel):
"""Runtime configuration loaded from environment variables."""
model_config = ConfigDict(frozen=True)
jwt_secret_key: Optional[str] = Field(
default=None,
description="HMAC secret for JWT signing (required for JWT/HTTP auth)",
)
enable_local_mode: bool = Field(
default=True,
description="Allow local-dev token bypass when running locally",
)
local_dev_token: Optional[str] = Field(
default="local-dev-token",
description="Static token accepted in local mode for development",
)
vault_base_path: Path = Field(..., description="Base directory for per-user vaults")
hf_oauth_client_id: Optional[str] = Field(
None, description="Hugging Face OAuth client ID (optional)"
)
hf_oauth_client_secret: Optional[str] = Field(
None, description="Hugging Face OAuth client secret (optional)"
)
hf_space_url: str = Field(
default="http://localhost:5173",
description="Base URL of the HF Space or local dev server"
)
@field_validator("vault_base_path", mode="before")
@classmethod
def _normalize_vault_path(cls, value: str | Path | None) -> Path:
if value is None or value == "":
raise ValueError("VAULT_BASE_PATH is required")
if isinstance(value, Path):
path = value
else:
path = Path(value)
# Resolve relative vault paths from the project root so data/ is placed
# alongside backend rather than inside it.
path = path.expanduser()
if not path.is_absolute():
path = PROJECT_ROOT / path
return path.resolve()
@field_validator("jwt_secret_key", mode="before")
@classmethod
def _ensure_secret(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
cleaned = value.strip()
if not cleaned:
raise ValueError(
"JWT_SECRET_KEY cannot be empty; unset the variable to disable JWT auth in local mode"
)
if len(cleaned) < 16:
raise ValueError("JWT_SECRET_KEY must be at least 16 characters")
return cleaned
def _read_env(key: str, default: Optional[str] = None) -> Optional[str]:
return os.getenv(key, default)
@lru_cache(maxsize=1)
def get_config() -> AppConfig:
"""Load and cache application configuration."""
jwt_secret = _read_env("JWT_SECRET_KEY")
vault_base = _read_env("VAULT_BASE_PATH", str(DEFAULT_VAULT_BASE))
hf_client_id = _read_env("HF_OAUTH_CLIENT_ID")
hf_client_secret = _read_env("HF_OAUTH_CLIENT_SECRET")
hf_space_url = _read_env("HF_SPACE_URL", "http://localhost:5173")
enable_local_mode = _read_env("ENABLE_LOCAL_MODE", "true").lower() not in {
"0",
"false",
"no",
}
local_dev_token = _read_env("LOCAL_DEV_TOKEN", "local-dev-token")
config = AppConfig(
jwt_secret_key=jwt_secret,
enable_local_mode=enable_local_mode,
local_dev_token=local_dev_token,
vault_base_path=vault_base,
hf_oauth_client_id=hf_client_id,
hf_oauth_client_secret=hf_client_secret,
hf_space_url=hf_space_url,
)
# Ensure vault base directory exists for downstream services.
config.vault_base_path.mkdir(parents=True, exist_ok=True)
return config
def reload_config() -> AppConfig:
"""Clear cached config (useful for tests) and reload."""
get_config.cache_clear()
return get_config()
__all__ = ["AppConfig", "get_config", "reload_config", "PROJECT_ROOT", "DEFAULT_VAULT_BASE"]