Files
mono/shared/config.py
giles f42042ccb7 Monorepo: consolidate 7 repos into one
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
2026-02-24 19:44:17 +00:00

85 lines
2.7 KiB
Python

# suma_browser/config.py
from __future__ import annotations
import asyncio
import os
from types import MappingProxyType
from typing import Any, Optional
import copy
import yaml
# Default config path (override with APP_CONFIG_FILE)
_DEFAULT_CONFIG_PATH = os.environ.get(
"APP_CONFIG_FILE",
os.path.join(os.getcwd(), "config/app-config.yaml"),
)
# Module state
_init_lock = asyncio.Lock()
_data_frozen: Any = None # read-only view (mappingproxy / tuples / frozensets)
_data_plain: Any = None # plain builtins for pretty-print / logging
# ---------------- utils ----------------
def _freeze(obj: Any) -> Any:
"""Deep-freeze containers to read-only equivalents."""
if isinstance(obj, dict):
# freeze children first, then wrap dict in mappingproxy
return MappingProxyType({k: _freeze(v) for k, v in obj.items()})
if isinstance(obj, list):
return tuple(_freeze(v) for v in obj)
if isinstance(obj, set):
return frozenset(_freeze(v) for v in obj)
if isinstance(obj, tuple):
return tuple(_freeze(v) for v in obj)
return obj
# ---------------- API ----------------
async def init_config(path: Optional[str] = None, *, force: bool = False) -> None:
"""
Load YAML exactly as-is and cache both a frozen (read-only) and a plain copy.
Idempotent; pass force=True to reload.
"""
global _data_frozen, _data_plain
if _data_frozen is not None and not force:
return
async with _init_lock:
if _data_frozen is not None and not force:
return
cfg_path = path or _DEFAULT_CONFIG_PATH
if not os.path.exists(cfg_path):
raise FileNotFoundError(f"Config file not found: {cfg_path}")
with open(cfg_path, "r", encoding="utf-8") as f:
raw = yaml.safe_load(f) # whatever the YAML root is
# store plain as loaded; store frozen for normal use
_data_plain = raw
_data_frozen = _freeze(raw)
def config() -> Any:
"""
Return the read-only (frozen) config. Call init_config() first.
"""
if _data_frozen is None:
raise RuntimeError("init_config() has not been awaited yet.")
return _data_frozen
def as_plain() -> Any:
"""
Return a deep copy of the plain config for safe external use/pretty printing.
"""
if _data_plain is None:
raise RuntimeError("init_config() has not been awaited yet.")
return copy.deepcopy(_data_plain)
def pretty() -> str:
"""
YAML pretty string without mappingproxy noise.
"""
if _data_plain is None:
raise RuntimeError("init_config() has not been awaited yet.")
return yaml.safe_dump(_data_plain, sort_keys=False, allow_unicode=True)