from collections.abc import Generator from typing import Any, cast from sqlalchemy import CursorResult, Engine, create_engine from sqlalchemy.engine import Result from sqlalchemy.orm import Session from modelforge_api.persistence.runtime_engine import register_application_engine from modelforge_api.settings import get_settings def rows_affected(result: Result[Any]) -> int: """How many rows a DML statement actually changed. `Session.execute` is typed as returning `Result`, which carries no `rowcount`; a DML statement always returns a `CursorResult`, which does. SQLAlchemy 2.0.52 narrowed that return type, so reading `.rowcount` directly stopped type-checking — while continuing to work at runtime. The cast is where that knowledge lives, once, rather than at ten call sites. It matters more than it looks: every single-use claim in the platform is a conditional UPDATE whose row count decides the winner, and that is what makes enrolment tokens and lifecycle claims atomic instead of merely usually-correct. """ return cast("CursorResult[Any]", result).rowcount def build_engine(database_url: str | None = None) -> Engine: url = database_url or get_settings().database_url return register_application_engine(create_engine(url, pool_pre_ping=True)) engine = build_engine() def get_session() -> Generator[Session, None, None]: with Session(engine) as session: yield session