From 555cd13f09366c75d3a83a4ec266e3b8f701df11 Mon Sep 17 00:00:00 2001 From: anti Date: Tue, 28 Apr 2026 14:52:59 -0400 Subject: [PATCH] refactor(db): extract RealismMixin Moves the 8 synthetic-file + realism-config methods into sqlmodel_repo/realism.py. --- decnet/web/db/sqlmodel_repo/__init__.py | 152 +---------------------- decnet/web/db/sqlmodel_repo/realism.py | 157 ++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 150 deletions(-) create mode 100644 decnet/web/db/sqlmodel_repo/realism.py diff --git a/decnet/web/db/sqlmodel_repo/__init__.py b/decnet/web/db/sqlmodel_repo/__init__.py index bf429892..1e2e5891 100644 --- a/decnet/web/db/sqlmodel_repo/__init__.py +++ b/decnet/web/db/sqlmodel_repo/__init__.py @@ -48,8 +48,6 @@ from decnet.web.db.models import ( TopologyMutation, OrchestratorEmail, OrchestratorEvent, - RealismConfig, - SyntheticFile, CanaryBlob, CanaryToken, CanaryTrigger, @@ -69,6 +67,7 @@ from decnet.web.db.sqlmodel_repo.bounties import BountiesMixin from decnet.web.db.sqlmodel_repo.deckies import DeckiesMixin from decnet.web.db.sqlmodel_repo.fleet import FleetMixin from decnet.web.db.sqlmodel_repo.logs import LogsMixin +from decnet.web.db.sqlmodel_repo.realism import RealismMixin from decnet.web.db.sqlmodel_repo.swarm import SwarmMixin from decnet.web.db.sqlmodel_repo.webhooks import WebhooksMixin @@ -80,6 +79,7 @@ class SQLModelRepository( DeckiesMixin, FleetMixin, LogsMixin, + RealismMixin, SwarmMixin, WebhooksMixin, BaseRepository, @@ -2434,151 +2434,3 @@ class SQLModelRepository( await session.commit() return deleted - # ------------------------------------------------------------ realism - - async def record_synthetic_file(self, data: dict[str, Any]) -> str: - from decnet.web.db.models.realism import SYNTHETIC_FILE_BODY_LIMIT - if "last_body" in data and data["last_body"] is not None: - data = {**data, "last_body": data["last_body"][:SYNTHETIC_FILE_BODY_LIMIT]} - async with self._session() as session: - row = SyntheticFile(**data) - session.add(row) - await session.commit() - await session.refresh(row) - return row.uuid - - async def update_synthetic_file( - self, row_uuid: str, data: dict[str, Any], - ) -> None: - from decnet.web.db.models.realism import SYNTHETIC_FILE_BODY_LIMIT - if "last_body" in data and data["last_body"] is not None: - data = {**data, "last_body": data["last_body"][:SYNTHETIC_FILE_BODY_LIMIT]} - async with self._session() as session: - stmt = ( - update(SyntheticFile) - .where(SyntheticFile.uuid == row_uuid) - .values(**data) - ) - await session.execute(stmt) - await session.commit() - - async def list_synthetic_files( - self, - *, - decky_uuid: Optional[str] = None, - persona: Optional[str] = None, - content_class: Optional[str] = None, - limit: int = 100, - offset: int = 0, - ) -> list[dict[str, Any]]: - async with self._session() as session: - stmt = select(SyntheticFile) - if decky_uuid is not None: - stmt = stmt.where(SyntheticFile.decky_uuid == decky_uuid) - if persona is not None: - stmt = stmt.where(SyntheticFile.persona == persona) - if content_class is not None: - stmt = stmt.where(SyntheticFile.content_class == content_class) - stmt = ( - stmt.order_by(desc(SyntheticFile.last_modified)) - .offset(offset) - .limit(limit) - ) - result = await session.execute(stmt) - return [r.model_dump(mode="json") for r in result.scalars().all()] - - async def count_synthetic_files( - self, - *, - decky_uuid: Optional[str] = None, - persona: Optional[str] = None, - content_class: Optional[str] = None, - ) -> int: - from sqlalchemy import func as _f - async with self._session() as session: - stmt = select(_f.count(SyntheticFile.uuid)) - if decky_uuid is not None: - stmt = stmt.where(SyntheticFile.decky_uuid == decky_uuid) - if persona is not None: - stmt = stmt.where(SyntheticFile.persona == persona) - if content_class is not None: - stmt = stmt.where(SyntheticFile.content_class == content_class) - result = await session.execute(stmt) - return int(result.scalar() or 0) - - async def get_synthetic_file( - self, uuid: str, - ) -> Optional[dict[str, Any]]: - async with self._session() as session: - stmt = select(SyntheticFile).where(SyntheticFile.uuid == uuid) - result = await session.execute(stmt) - row = result.scalars().first() - if row is None: - return None - return row.model_dump(mode="json") - - async def get_realism_config( - self, key: str, - ) -> Optional[dict[str, Any]]: - async with self._session() as session: - stmt = select(RealismConfig).where(RealismConfig.key == key) - result = await session.execute(stmt) - row = result.scalars().first() - if row is None: - return None - return row.model_dump(mode="json") - - async def set_realism_config( - self, key: str, value: str, - ) -> None: - """Upsert one realism_config row. Last-write-wins.""" - async with self._session() as session: - stmt = select(RealismConfig).where(RealismConfig.key == key) - result = await session.execute(stmt) - row = result.scalars().first() - if row is None: - session.add(RealismConfig( - key=key, value=value, - updated_at=datetime.now(timezone.utc), - )) - else: - upd = ( - update(RealismConfig) - .where(RealismConfig.uuid == row.uuid) - .values(value=value, updated_at=datetime.now(timezone.utc)) - ) - await session.execute(upd) - await session.commit() - - async def pick_random_synthetic_file_for_edit( - self, - decky_uuid: str, - *, - max_age_days: int = 30, - ) -> Optional[dict[str, Any]]: - # Editable classes: anything whose body is plain text we can - # mutate idempotently. Binary canary artifacts are out — they - # rotate via a fresh plant, not an edit. - editable = ( - "note", "todo", "draft", "script", "log_cron", "log_daemon", - ) - from datetime import timedelta - cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) - async with self._session() as session: - stmt = ( - select(SyntheticFile) - .where( - SyntheticFile.decky_uuid == decky_uuid, - SyntheticFile.content_class.in_(editable), # type: ignore[attr-defined] - SyntheticFile.last_modified >= cutoff, - ) - # SQLite + MySQL both support func.random() / RAND() — - # SQLAlchemy's func.random() compiles per-dialect. - .order_by(func.random()) - .limit(1) - ) - result = await session.execute(stmt) - row = result.scalars().first() - if row is None: - return None - return row.model_dump(mode="json") diff --git a/decnet/web/db/sqlmodel_repo/realism.py b/decnet/web/db/sqlmodel_repo/realism.py new file mode 100644 index 00000000..ee3f0002 --- /dev/null +++ b/decnet/web/db/sqlmodel_repo/realism.py @@ -0,0 +1,157 @@ +"""Synthetic-file CRUD + realism config key/value store.""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from typing import Any, Optional + +from sqlalchemy import desc, func, select, update + +from decnet.web.db.models import RealismConfig, SyntheticFile +from decnet.web.db.models.realism import SYNTHETIC_FILE_BODY_LIMIT + + +class RealismMixin: + """Mixin: composed onto ``SQLModelRepository``.""" + + async def record_synthetic_file(self, data: dict[str, Any]) -> str: + if "last_body" in data and data["last_body"] is not None: + data = {**data, "last_body": data["last_body"][:SYNTHETIC_FILE_BODY_LIMIT]} + async with self._session() as session: + row = SyntheticFile(**data) + session.add(row) + await session.commit() + await session.refresh(row) + return row.uuid + + async def update_synthetic_file( + self, row_uuid: str, data: dict[str, Any], + ) -> None: + if "last_body" in data and data["last_body"] is not None: + data = {**data, "last_body": data["last_body"][:SYNTHETIC_FILE_BODY_LIMIT]} + async with self._session() as session: + stmt = ( + update(SyntheticFile) + .where(SyntheticFile.uuid == row_uuid) + .values(**data) + ) + await session.execute(stmt) + await session.commit() + + async def list_synthetic_files( + self, + *, + decky_uuid: Optional[str] = None, + persona: Optional[str] = None, + content_class: Optional[str] = None, + limit: int = 100, + offset: int = 0, + ) -> list[dict[str, Any]]: + async with self._session() as session: + stmt = select(SyntheticFile) + if decky_uuid is not None: + stmt = stmt.where(SyntheticFile.decky_uuid == decky_uuid) + if persona is not None: + stmt = stmt.where(SyntheticFile.persona == persona) + if content_class is not None: + stmt = stmt.where(SyntheticFile.content_class == content_class) + stmt = ( + stmt.order_by(desc(SyntheticFile.last_modified)) + .offset(offset) + .limit(limit) + ) + result = await session.execute(stmt) + return [r.model_dump(mode="json") for r in result.scalars().all()] + + async def count_synthetic_files( + self, + *, + decky_uuid: Optional[str] = None, + persona: Optional[str] = None, + content_class: Optional[str] = None, + ) -> int: + async with self._session() as session: + stmt = select(func.count(SyntheticFile.uuid)) + if decky_uuid is not None: + stmt = stmt.where(SyntheticFile.decky_uuid == decky_uuid) + if persona is not None: + stmt = stmt.where(SyntheticFile.persona == persona) + if content_class is not None: + stmt = stmt.where(SyntheticFile.content_class == content_class) + result = await session.execute(stmt) + return int(result.scalar() or 0) + + async def get_synthetic_file( + self, uuid: str, + ) -> Optional[dict[str, Any]]: + async with self._session() as session: + stmt = select(SyntheticFile).where(SyntheticFile.uuid == uuid) + result = await session.execute(stmt) + row = result.scalars().first() + if row is None: + return None + return row.model_dump(mode="json") + + async def get_realism_config( + self, key: str, + ) -> Optional[dict[str, Any]]: + async with self._session() as session: + stmt = select(RealismConfig).where(RealismConfig.key == key) + result = await session.execute(stmt) + row = result.scalars().first() + if row is None: + return None + return row.model_dump(mode="json") + + async def set_realism_config( + self, key: str, value: str, + ) -> None: + """Upsert one realism_config row. Last-write-wins.""" + async with self._session() as session: + stmt = select(RealismConfig).where(RealismConfig.key == key) + result = await session.execute(stmt) + row = result.scalars().first() + if row is None: + session.add(RealismConfig( + key=key, value=value, + updated_at=datetime.now(timezone.utc), + )) + else: + upd = ( + update(RealismConfig) + .where(RealismConfig.uuid == row.uuid) + .values(value=value, updated_at=datetime.now(timezone.utc)) + ) + await session.execute(upd) + await session.commit() + + async def pick_random_synthetic_file_for_edit( + self, + decky_uuid: str, + *, + max_age_days: int = 30, + ) -> Optional[dict[str, Any]]: + # Editable classes: anything whose body is plain text we can + # mutate idempotently. Binary canary artifacts are out — they + # rotate via a fresh plant, not an edit. + editable = ( + "note", "todo", "draft", "script", "log_cron", "log_daemon", + ) + cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days) + async with self._session() as session: + stmt = ( + select(SyntheticFile) + .where( + SyntheticFile.decky_uuid == decky_uuid, + SyntheticFile.content_class.in_(editable), # type: ignore[attr-defined] + SyntheticFile.last_modified >= cutoff, + ) + # SQLite + MySQL both support func.random() / RAND() — + # SQLAlchemy's func.random() compiles per-dialect. + .order_by(func.random()) + .limit(1) + ) + result = await session.execute(stmt) + row = result.scalars().first() + if row is None: + return None + return row.model_dump(mode="json")