From f7f75fcac685a12c19af0ae7ab7e6823e783adb0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 15 May 2026 15:48:02 -0400 Subject: [PATCH] refactor(fc2c-i): sweep blueprints + ml tasks onto shared get_session (covers tasks/ml.py, a survey gap) --- backend/app/api/aliases.py | 22 ++++------------------ backend/app/api/allowlist.py | 25 +++++-------------------- backend/app/api/gallery.py | 25 +++++-------------------- backend/app/api/import_admin.py | 25 +++++-------------------- backend/app/api/ml_admin.py | 19 +++---------------- backend/app/api/settings.py | 22 ++++------------------ backend/app/api/suggestions.py | 25 +++++-------------------- backend/app/api/tags.py | 31 +++++++------------------------ backend/app/tasks/ml.py | 12 ++++-------- 9 files changed, 42 insertions(+), 164 deletions(-) diff --git a/backend/app/api/aliases.py b/backend/app/api/aliases.py index 3b1d2f6..3c77510 100644 --- a/backend/app/api/aliases.py +++ b/backend/app/api/aliases.py @@ -2,27 +2,15 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..services.ml.aliases import AliasService aliases_bp = Blueprint("aliases", __name__, url_prefix="/api") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - @aliases_bp.route("/aliases", methods=["GET"]) async def list_aliases(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: rows = await AliasService(session).list_all() return jsonify( [ @@ -43,8 +31,7 @@ async def create_alias(): required = {"alias_string", "alias_category", "canonical_tag_id"} if not body or not required.issubset(body): return jsonify({"error": f"required: {sorted(required)}"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: await AliasService(session).create( body["alias_string"], body["alias_category"], @@ -58,8 +45,7 @@ async def create_alias(): "/aliases//", methods=["DELETE"] ) async def remove_alias(alias_string: str, alias_category: str): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: await AliasService(session).remove(alias_string, alias_category) await session.commit() return "", 204 diff --git a/backend/app/api/allowlist.py b/backend/app/api/allowlist.py index 69889c8..53f38bb 100644 --- a/backend/app/api/allowlist.py +++ b/backend/app/api/allowlist.py @@ -2,28 +2,16 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..models import TagAllowlist from ..services.ml.allowlist import AllowlistService allowlist_bp = Blueprint("allowlist", __name__, url_prefix="/api") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - @allowlist_bp.route("/allowlist", methods=["GET"]) async def list_allowlist(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: rows = await AllowlistService(session).list_all() return jsonify( [ @@ -40,8 +28,7 @@ async def list_allowlist(): @allowlist_bp.route("/tags//allowlist", methods=["GET"]) async def get_one(tag_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: row = await session.get(TagAllowlist, tag_id) if row is None: return jsonify({"error": "not on allowlist"}), 404 @@ -58,8 +45,7 @@ async def patch_threshold(tag_id: int): mc = float(body["min_confidence"]) if not (0 < mc <= 1): return jsonify({"error": "min_confidence must be in (0, 1]"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: await AllowlistService(session).update_threshold(tag_id, mc) await session.commit() return "", 204 @@ -67,8 +53,7 @@ async def patch_threshold(tag_id: int): @allowlist_bp.route("/tags//allowlist", methods=["DELETE"]) async def remove(tag_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: await AllowlistService(session).remove(tag_id) await session.commit() return "", 204 diff --git a/backend/app/api/gallery.py b/backend/app/api/gallery.py index e824297..5401368 100644 --- a/backend/app/api/gallery.py +++ b/backend/app/api/gallery.py @@ -2,22 +2,11 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..services.gallery_service import GalleryService gallery_bp = Blueprint("gallery", __name__, url_prefix="/api/gallery") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - @gallery_bp.route("/scroll", methods=["GET"]) async def scroll(): @@ -29,8 +18,7 @@ async def scroll(): tag_id_raw = request.args.get("tag_id") tag_id = int(tag_id_raw) if tag_id_raw else None - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = GalleryService(session) try: page = await svc.scroll(cursor=cursor, limit=limit, tag_id=tag_id) @@ -63,8 +51,7 @@ async def scroll(): async def timeline(): tag_id_raw = request.args.get("tag_id") tag_id = int(tag_id_raw) if tag_id_raw else None - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = GalleryService(session) buckets = await svc.timeline(tag_id=tag_id) return jsonify([{"year": b.year, "month": b.month, "count": b.count} for b in buckets]) @@ -79,8 +66,7 @@ async def jump(): return jsonify({"error": "year and month query params required"}), 400 tag_id_raw = request.args.get("tag_id") tag_id = int(tag_id_raw) if tag_id_raw else None - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = GalleryService(session) cursor = await svc.jump_cursor(year=year, month=month, tag_id=tag_id) return jsonify({"cursor": cursor}) @@ -88,8 +74,7 @@ async def jump(): @gallery_bp.route("/image/", methods=["GET"]) async def image_detail(image_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = GalleryService(session) payload = await svc.get_image_with_tags(image_id) if payload is None: diff --git a/backend/app/api/import_admin.py b/backend/app/api/import_admin.py index 21e11c3..e4be69a 100644 --- a/backend/app/api/import_admin.py +++ b/backend/app/api/import_admin.py @@ -5,22 +5,11 @@ from datetime import UTC, datetime, timedelta from quart import Blueprint, jsonify, request from sqlalchemy import delete, select, update -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..models import ImportBatch, ImportTask import_admin_bp = Blueprint("import_admin", __name__, url_prefix="/api/import") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - @import_admin_bp.route("/trigger", methods=["POST"]) async def trigger_scan(): @@ -39,8 +28,7 @@ async def trigger_scan(): @import_admin_bp.route("/status", methods=["GET"]) async def status(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: active = ( await session.execute( select(ImportBatch) @@ -72,8 +60,7 @@ async def list_tasks(): cursor_raw = request.args.get("cursor") cursor_id = int(cursor_raw) if cursor_raw else None - Session = _session_factory() - async with Session() as session: + async with get_session() as session: stmt = select(ImportTask).order_by(ImportTask.created_at.desc(), ImportTask.id.desc()) if status_filter: stmt = stmt.where(ImportTask.status == status_filter) @@ -107,8 +94,7 @@ async def list_tasks(): @import_admin_bp.route("/retry-failed", methods=["POST"]) async def retry_failed(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: failed_ids = ( await session.execute(select(ImportTask.id).where(ImportTask.status == "failed")) ).scalars().all() @@ -140,8 +126,7 @@ async def clear_completed(): datetime.now(UTC) - timedelta(days=int(age_days)) if age_days else None ) - Session = _session_factory() - async with Session() as session: + async with get_session() as session: stmt = delete(ImportTask).where(ImportTask.status.in_(status_filter)) if cutoff is not None: stmt = stmt.where(ImportTask.finished_at < cutoff) diff --git a/backend/app/api/ml_admin.py b/backend/app/api/ml_admin.py index 3d7bbe5..a346175 100644 --- a/backend/app/api/ml_admin.py +++ b/backend/app/api/ml_admin.py @@ -2,22 +2,11 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..models import MLSettings ml_admin_bp = Blueprint("ml_admin", __name__, url_prefix="/api/ml") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - _EDITABLE = ( "suggestion_threshold_artist", @@ -33,8 +22,7 @@ _EDITABLE = ( async def get_settings(): from sqlalchemy import select - Session = _session_factory() - async with Session() as session: + async with get_session() as session: s = ( await session.execute(select(MLSettings).where(MLSettings.id == 1)) ).scalar_one() @@ -59,8 +47,7 @@ async def patch_settings(): body = await request.get_json() if not isinstance(body, dict): return jsonify({"error": "body must be an object"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: s = ( await session.execute(select(MLSettings).where(MLSettings.id == 1)) ).scalar_one() diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 7d2d842..f5f5354 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -3,22 +3,11 @@ from quart import Blueprint, jsonify, request from sqlalchemy import func, select -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..models import ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag settings_bp = Blueprint("settings", __name__, url_prefix="/api") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - _EDITABLE_FIELDS = ( "min_width", @@ -33,8 +22,7 @@ _EDITABLE_FIELDS = ( @settings_bp.route("/settings/import", methods=["GET"]) async def get_import_settings(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: row = ( await session.execute(select(ImportSettings).where(ImportSettings.id == 1)) ).scalar_one() @@ -55,8 +43,7 @@ async def update_import_settings(): if not isinstance(body, dict): return jsonify({"error": "body must be a JSON object"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: row = ( await session.execute(select(ImportSettings).where(ImportSettings.id == 1)) ).scalar_one() @@ -70,8 +57,7 @@ async def update_import_settings(): @settings_bp.route("/system/stats", methods=["GET"]) async def system_stats(): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: total_images = (await session.execute(select(func.count(ImageRecord.id)))).scalar_one() total_tags = (await session.execute(select(func.count(Tag.id)))).scalar_one() storage_bytes = ( diff --git a/backend/app/api/suggestions.py b/backend/app/api/suggestions.py index 9e00aea..09d70e2 100644 --- a/backend/app/api/suggestions.py +++ b/backend/app/api/suggestions.py @@ -2,28 +2,16 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..services.ml.allowlist import AllowlistService from ..services.ml.suggestions import SuggestionService suggestions_bp = Blueprint("suggestions", __name__, url_prefix="/api") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - @suggestions_bp.route("/images//suggestions", methods=["GET"]) async def get_suggestions(image_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: sl = await SuggestionService(session).for_image(image_id) return jsonify( { @@ -53,8 +41,7 @@ async def accept_suggestion(image_id: int): if not body or "tag_id" not in body: return jsonify({"error": "tag_id required"}), 400 tag_id = body["tag_id"] - Session = _session_factory() - async with Session() as session: + async with get_session() as session: newly_added = await AllowlistService(session).accept(image_id, tag_id) await session.commit() if newly_added: @@ -72,8 +59,7 @@ async def alias_suggestion(image_id: int): required = {"alias_string", "alias_category", "canonical_tag_id"} if not body or not required.issubset(body): return jsonify({"error": f"required: {sorted(required)}"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: newly_added = await AllowlistService(session).add_alias_and_accept( image_id, body["alias_string"], @@ -95,8 +81,7 @@ async def dismiss_suggestion(image_id: int): body = await request.get_json() if not body or "tag_id" not in body: return jsonify({"error": "tag_id required"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: await AllowlistService(session).dismiss(image_id, body["tag_id"]) await session.commit() return "", 204 diff --git a/backend/app/api/tags.py b/backend/app/api/tags.py index 8498c4f..941754d 100644 --- a/backend/app/api/tags.py +++ b/backend/app/api/tags.py @@ -2,23 +2,12 @@ from quart import Blueprint, jsonify, request -from ..extensions import make_engine, make_session_factory +from ..extensions import get_session from ..models import TagKind from ..services.tag_service import TagService, TagValidationError tags_bp = Blueprint("tags", __name__, url_prefix="/api") -_engine = None -_Session = None - - -def _session_factory(): - global _engine, _Session - if _engine is None: - _engine = make_engine() - _Session = make_session_factory(_engine) - return _Session - def _coerce_kind(raw: str | None) -> TagKind | None: if raw is None: @@ -38,8 +27,7 @@ async def autocomplete(): except ValueError: return jsonify({"error": "limit must be an integer"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) hits = await svc.autocomplete(q, kind=kind, limit=limit) @@ -69,8 +57,7 @@ async def create_tag(): return jsonify({"error": f"invalid kind {body['kind']!r}"}), 400 fandom_id = body.get("fandom_id") - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) try: tag = await svc.find_or_create(name, kind, fandom_id=fandom_id) @@ -84,8 +71,7 @@ async def create_tag(): @tags_bp.route("/images//tags", methods=["GET"]) async def list_tags_for_image(image_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) tags = await svc.list_for_image(image_id) return jsonify( @@ -108,8 +94,7 @@ async def add_tag_to_image(image_id: int): return jsonify({"error": "tag_id required"}), 400 source = body.get("source", "manual") - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) await svc.add_to_image(image_id, body["tag_id"], source=source) await session.commit() @@ -118,8 +103,7 @@ async def add_tag_to_image(image_id: int): @tags_bp.route("/images//tags/", methods=["DELETE"]) async def remove_tag_from_image(image_id: int, tag_id: int): - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) await svc.remove_from_image(image_id, tag_id) await session.commit() @@ -131,8 +115,7 @@ async def rename_tag(tag_id: int): body = await request.get_json() if not body or "name" not in body: return jsonify({"error": "name required"}), 400 - Session = _session_factory() - async with Session() as session: + async with get_session() as session: svc = TagService(session) try: tag = await svc.rename(tag_id, body["name"]) diff --git a/backend/app/tasks/ml.py b/backend/app/tasks/ml.py index 29a9816..13d4394 100644 --- a/backend/app/tasks/ml.py +++ b/backend/app/tasks/ml.py @@ -305,13 +305,11 @@ def _confidence_for_tag(session, tag, preds: dict) -> float | None: def recompute_centroid(self, tag_id: int) -> bool: import asyncio - from ..extensions import make_engine, make_session_factory + from ..extensions import get_session from ..services.ml.centroids import CentroidService async def _run() -> bool: - engine = make_engine() - Session = make_session_factory(engine) - async with Session() as session: + async with get_session() as session: svc = CentroidService(session) result = await svc.recompute_for_tag(tag_id) await session.commit() @@ -325,13 +323,11 @@ def recompute_centroids(self) -> int: """Daily: find drifted centroids, enqueue recompute_centroid for each.""" import asyncio - from ..extensions import make_engine, make_session_factory + from ..extensions import get_session from ..services.ml.centroids import CentroidService async def _list() -> list[int]: - engine = make_engine() - Session = make_session_factory(engine) - async with Session() as session: + async with get_session() as session: return await CentroidService(session).list_drifted() drifted = asyncio.run(_list())