76 lines
1.9 KiB
Python
76 lines
1.9 KiB
Python
"""
|
|
control/base.py
|
|
---------------
|
|
Standalone SQLAlchemy foundation for the control plane.
|
|
|
|
`ControlBase` keeps control-plane table metadata fully separate from the
|
|
tenant-schema models declared on `app.db.Model`. The engine is built lazily
|
|
from the `CONTROL_DATABASE_URL` environment variable.
|
|
"""
|
|
|
|
import os
|
|
from contextlib import contextmanager
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
|
|
|
# Every control model subclasses this base — its own metadata collection.
|
|
ControlBase = declarative_base()
|
|
|
|
|
|
def get_control_database_url() -> str:
|
|
"""Return CONTROL_DATABASE_URL or raise a clear error if unset."""
|
|
url = os.environ.get('CONTROL_DATABASE_URL')
|
|
if not url:
|
|
raise RuntimeError(
|
|
"CONTROL_DATABASE_URL is not set. Example: "
|
|
"mysql+pymysql://jqc_control:password@localhost/jqc_control"
|
|
)
|
|
return url
|
|
|
|
|
|
_engine = None
|
|
_Session = None
|
|
|
|
|
|
def get_engine():
|
|
"""Lazily build (and cache) the control-plane engine."""
|
|
global _engine
|
|
if _engine is None:
|
|
_engine = create_engine(
|
|
get_control_database_url(),
|
|
pool_pre_ping=True,
|
|
pool_recycle=1800,
|
|
future=True,
|
|
)
|
|
return _engine
|
|
|
|
|
|
def get_sessionmaker():
|
|
"""Lazily build (and cache) the control-plane sessionmaker."""
|
|
global _Session
|
|
if _Session is None:
|
|
_Session = sessionmaker(
|
|
bind=get_engine(), future=True, expire_on_commit=False
|
|
)
|
|
return _Session
|
|
|
|
|
|
@contextmanager
|
|
def control_session():
|
|
"""Context-managed session — commits on success, rolls back on error.
|
|
|
|
Usage:
|
|
with control_session() as s:
|
|
s.add(obj)
|
|
"""
|
|
session = get_sessionmaker()()
|
|
try:
|
|
yield session
|
|
session.commit()
|
|
except Exception:
|
|
session.rollback()
|
|
raise
|
|
finally:
|
|
session.close()
|