fix(fc2a): satisfy ruff 0.15.13 lint — UP017, UP042, I001

Ruff lint surfaced 23 violations across three rules; all addressed:

UP017 (Use datetime.UTC alias):
  Replaced 13 sites of datetime.now(timezone.utc) with datetime.now(UTC),
  also adjusted from-imports accordingly. UTC is a Python 3.11+ alias for
  timezone.utc that ruff's pyupgrade rules prefer.

UP042 (StrEnum):
  Replaced `class TagKind(str, Enum)` and `class SkipReason(str, Enum)`
  with `class Foo(StrEnum)`. StrEnum was added in Python 3.11 stdlib and
  is the modern idiom. Behavior is equivalent for our usage (the .value
  attribute, str(member) semantics).

I001 (Import sorting):
  Added `known-first-party = ["backend"]` to ruff.toml's [lint.isort] so
  ruff groups `backend.*` imports correctly. Without it, ruff treated
  them as third-party and demanded a different grouping. The existing
  import order is stdlib → third-party → first-party → local relative,
  which ruff now accepts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 14:11:35 -04:00
parent 117873423b
commit b4e0d680f1
10 changed files with 31 additions and 26 deletions
+2 -2
View File
@@ -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()
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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"
+5 -5
View File
@@ -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()
+3 -3
View File
@@ -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)
+5
View File
@@ -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"
+2 -2
View File
@@ -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",
+4 -4
View File
@@ -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()
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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",