ci / lint-test-leaks (pull_request) Canceled after 0s
- cayceops core: env-only config (CAYCE_OPS_*), engine/session, ORM models with state machines (proposal/mail/content) and audited writes - provider-agnostic LLM interface (null provider default, stdlib openai-compat) - Typer CLI with lazy component loading (scanner/executor/maild/publisher/monitor) - alembic 0001 initial schema; sqlite-portable JSON columns - tests: 7 passing on throwaway sqlite; fixtures invented, never real data - ci: ruff + alembic smoke + pytest + gitleaks(full history) in .gitea/workflows + .github/workflows; local gate = make check - deploy: docker-compose (postgres 16) + hardened systemd units/timers - docs: DECISIONS D1-D9; README project (grant text retired)
82 lines
2.0 KiB
Python
82 lines
2.0 KiB
Python
"""Database engine, sessions and schema lifecycle helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
|
|
from sqlalchemy import create_engine, text
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from cayceops.models import Base
|
|
|
|
_engine = None
|
|
_factory: sessionmaker | None = None
|
|
|
|
|
|
def engine(url: str | None = None):
|
|
"""Return (and cache) the process SQLAlchemy engine."""
|
|
global _engine
|
|
if _engine is None:
|
|
from cayceops.config import load
|
|
|
|
_engine = create_engine(url or load().database_url, future=True)
|
|
return _engine
|
|
|
|
|
|
def reset_engine() -> None:
|
|
"""Drop the cached engine (used by tests and CLI re-entry)."""
|
|
global _engine, _factory
|
|
if _engine is not None:
|
|
_engine.dispose()
|
|
_engine = None
|
|
_factory = None
|
|
|
|
|
|
def session_factory():
|
|
global _factory
|
|
if _factory is None:
|
|
_factory = sessionmaker(bind=engine(), expire_on_commit=False, future=True)
|
|
return _factory
|
|
|
|
|
|
@contextmanager
|
|
def session_scope() -> Iterator[Session]:
|
|
"""Transactional scope: commit on success, rollback on error."""
|
|
s = session_factory()()
|
|
try:
|
|
yield s
|
|
s.commit()
|
|
except Exception:
|
|
s.rollback()
|
|
raise
|
|
finally:
|
|
s.close()
|
|
|
|
|
|
def init_db() -> None:
|
|
"""Create all tables from metadata (dev/test convenience).
|
|
|
|
Production uses Alembic migrations (``cayceops db upgrade``).
|
|
"""
|
|
Base.metadata.create_all(engine())
|
|
|
|
|
|
def upgrade_db() -> None:
|
|
"""Run Alembic migrations to head."""
|
|
from alembic.config import Config as AlembicConfig
|
|
|
|
from alembic import command
|
|
from cayceops.config import load
|
|
|
|
cfg = AlembicConfig("alembic.ini")
|
|
cfg.set_main_option("script_location", "alembic")
|
|
cfg.set_main_option("sqlalchemy.url", load().database_url)
|
|
command.upgrade(cfg, "head")
|
|
|
|
|
|
def current_revision() -> str | None:
|
|
with engine().connect() as conn:
|
|
row = conn.execute(text("SELECT version_num FROM alembic_version")).first()
|
|
return row[0] if row else None
|