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 ..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/<alias_string>/<alias_category>", 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
+5 -20
View File
@@ -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/<int:tag_id>/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/<int:tag_id>/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
+5 -20
View File
@@ -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/<int:image_id>", 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:
+5 -20
View File
@@ -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)
+3 -16
View File
@@ -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()
+4 -18
View File
@@ -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 = (
+5 -20
View File
@@ -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/<int:image_id>/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
+7 -24
View File
@@ -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/<int:image_id>/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/<int:image_id>/tags/<int:tag_id>", 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"])
+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:
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())