refactor(fc2c-i): sweep blueprints + ml tasks onto shared get_session (covers tasks/ml.py, a survey gap)

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