diff --git a/backend/app/api/import_admin.py b/backend/app/api/import_admin.py index 7eb5884..21e11c3 100644 --- a/backend/app/api/import_admin.py +++ b/backend/app/api/import_admin.py @@ -1,6 +1,6 @@ """Import admin API: trigger scan, list tasks, retry, clear.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from quart import Blueprint, jsonify, request from sqlalchemy import delete, select, update @@ -137,7 +137,7 @@ async def clear_completed(): return jsonify({"error": "status must be a list"}), 400 cutoff = ( - datetime.now(timezone.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() diff --git a/backend/app/models/tag.py b/backend/app/models/tag.py index 2ca9d39..14be164 100644 --- a/backend/app/models/tag.py +++ b/backend/app/models/tag.py @@ -7,7 +7,7 @@ different kinds and the same character name can exist in different fandoms. """ from datetime import datetime -from enum import Enum +from enum import StrEnum from sqlalchemy import ( CheckConstraint, @@ -25,7 +25,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base -class TagKind(str, Enum): +class TagKind(StrEnum): artist = "artist" character = "character" fandom = "fandom" diff --git a/backend/app/services/importer.py b/backend/app/services/importer.py index fb1b378..abc3754 100644 --- a/backend/app/services/importer.py +++ b/backend/app/services/importer.py @@ -11,7 +11,7 @@ by the task. The Quart side uses an async session via the same models. import hashlib import shutil from dataclasses import dataclass -from enum import Enum +from enum import StrEnum from pathlib import Path from PIL import Image @@ -25,7 +25,7 @@ from ..utils.slug import slugify from .thumbnailer import Thumbnailer -class SkipReason(str, Enum): +class SkipReason(StrEnum): too_small = "too_small" too_transparent = "too_transparent" single_color = "single_color" diff --git a/backend/app/tasks/import_file.py b/backend/app/tasks/import_file.py index e9b5be2..e350909 100644 --- a/backend/app/tasks/import_file.py +++ b/backend/app/tasks/import_file.py @@ -2,7 +2,7 @@ updates the ImportTask state machine + ImportBatch counters atomically. """ -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from sqlalchemy import create_engine, select, update @@ -34,7 +34,7 @@ def import_media_file(self, import_task_id: int) -> dict: return {"status": "missing", "task_id": import_task_id} task.status = "processing" - task.started_at = datetime.now(timezone.utc) + task.started_at = datetime.now(UTC) session.add(task) session.commit() @@ -55,7 +55,7 @@ def import_media_file(self, import_task_id: int) -> dict: except Exception as exc: # pragma: no cover — pipeline crash task.status = "failed" task.error = f"{type(exc).__name__}: {exc}" - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) session.execute( update(ImportBatch) .where(ImportBatch.id == task.batch_id) @@ -86,7 +86,7 @@ def import_media_file(self, import_task_id: int) -> dict: counter_col_name = "failed" counter_col = ImportBatch.failed - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) session.execute( update(ImportBatch) .where(ImportBatch.id == task.batch_id) @@ -114,7 +114,7 @@ def import_media_file(self, import_task_id: int) -> dict: .where(ImportBatch.id == task.batch_id) .values( status="complete", - finished_at=datetime.now(timezone.utc), + finished_at=datetime.now(UTC), ) ) session.commit() diff --git a/backend/app/tasks/maintenance.py b/backend/app/tasks/maintenance.py index f6e3d99..376d46f 100644 --- a/backend/app/tasks/maintenance.py +++ b/backend/app/tasks/maintenance.py @@ -1,6 +1,6 @@ """Periodic maintenance: recover stuck import tasks, garbage-collect old finished tasks.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import create_engine, delete, select, update from sqlalchemy.orm import sessionmaker @@ -29,7 +29,7 @@ def recover_interrupted_tasks() -> int: forever) without resetting slow-but-still-running jobs. """ SessionLocal = _sync_session_factory() - cutoff = datetime.now(timezone.utc) - timedelta(minutes=STUCK_THRESHOLD_MINUTES) + cutoff = datetime.now(UTC) - timedelta(minutes=STUCK_THRESHOLD_MINUTES) with SessionLocal() as session: stuck_ids = session.execute( select(ImportTask.id) @@ -63,7 +63,7 @@ def cleanup_old_tasks() -> int: rather than an archive. Matches IR's default. """ SessionLocal = _sync_session_factory() - cutoff = datetime.now(timezone.utc) - timedelta(days=OLD_TASK_DAYS) + cutoff = datetime.now(UTC) - timedelta(days=OLD_TASK_DAYS) with SessionLocal() as session: result = session.execute( delete(ImportTask) diff --git a/ruff.toml b/ruff.toml index b8603a4..5ded561 100644 --- a/ruff.toml +++ b/ruff.toml @@ -19,5 +19,10 @@ ignore = [ "alembic/versions/*.py" = ["E", "F", "I", "UP"] "tests/*" = ["F401"] +[lint.isort] +# Tell ruff that `backend` is our first-party module so imports get +# grouped correctly: stdlib, third-party, first-party, then local relative. +known-first-party = ["backend"] + [format] quote-style = "double" diff --git a/tests/test_api_gallery.py b/tests/test_api_gallery.py index 5293687..25746ee 100644 --- a/tests/test_api_gallery.py +++ b/tests/test_api_gallery.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest @@ -18,7 +18,7 @@ async def client(app): async def _seed(db, count: int = 3): - base = datetime.now(timezone.utc) + base = datetime.now(UTC) for i in range(count): r = ImageRecord( path=f"/images/x/{i}.jpg", diff --git a/tests/test_api_import_admin.py b/tests/test_api_import_admin.py index 62461a4..b64c415 100644 --- a/tests/test_api_import_admin.py +++ b/tests/test_api_import_admin.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest @@ -46,7 +46,7 @@ async def test_list_tasks_with_status_filter(client, db): for status in ("complete", "failed", "complete"): db.add(ImportTask( batch_id=batch.id, source_path="/x", task_type="media", status=status, - finished_at=datetime.now(timezone.utc), + finished_at=datetime.now(UTC), )) await db.commit() @@ -74,14 +74,14 @@ async def test_clear_completed(client, db): batch = ImportBatch(triggered_by="manual", source_path="/import", scan_mode="quick") db.add(batch) await db.flush() - long_ago = datetime.now(timezone.utc) - timedelta(days=30) + long_ago = datetime.now(UTC) - timedelta(days=30) db.add(ImportTask( batch_id=batch.id, source_path="/x", task_type="media", status="complete", finished_at=long_ago, )) db.add(ImportTask( batch_id=batch.id, source_path="/y", task_type="media", - status="complete", finished_at=datetime.now(timezone.utc), + status="complete", finished_at=datetime.now(UTC), )) await db.commit() diff --git a/tests/test_gallery_service.py b/tests/test_gallery_service.py index 3d6d263..9a5fe70 100644 --- a/tests/test_gallery_service.py +++ b/tests/test_gallery_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest @@ -12,7 +12,7 @@ from backend.app.services.gallery_service import ( def _now(): - return datetime.now(timezone.utc) + return datetime.now(UTC) async def _seed_images(db, count: int, sha_prefix: str = "0") -> list[ImageRecord]: @@ -37,7 +37,7 @@ async def _seed_images(db, count: int, sha_prefix: str = "0") -> list[ImageRecor def test_cursor_roundtrip(): - ts = datetime(2026, 5, 14, 12, 30, 0, tzinfo=timezone.utc) + ts = datetime(2026, 5, 14, 12, 30, 0, tzinfo=UTC) encoded = encode_cursor(ts, 42) back_ts, back_id = decode_cursor(encoded) assert back_ts == ts diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index b38adb3..ad70df9 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -2,7 +2,7 @@ function runs synchronously inside the same DB transaction as the test. """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import pytest @@ -26,7 +26,7 @@ def _make_batch(session) -> int: def test_recover_interrupted_only_old(db_sync): batch_id = _make_batch(db_sync) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) fresh = ImportTask( batch_id=batch_id, source_path="/import/a.jpg", task_type="media", @@ -52,7 +52,7 @@ def test_recover_interrupted_only_old(db_sync): def test_cleanup_old_deletes_finished_old(db_sync): batch_id = _make_batch(db_sync) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) old_complete = ImportTask( batch_id=batch_id, source_path="/import/a.jpg", task_type="media",