testsAndMisc/python_pkg/wsg_grabber/store_verdicts.py
Krzysztof kuhy Rudnicki adf2e402df Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading `meta/` against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That makes the source 3.14-only at the
syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy
chose it deliberately over holding ruff a release back (2026-08-29).

Also drops a comment in `wsg_grabber/models.py` that the bump makes false: it
explained why the file used ``str, Enum`` instead of ``StrEnum`` under mypy at
3.10, sitting directly above a ``class Verdict(StrEnum)`` that had already
stopped obeying it.

ruff, ruff-format, mypy --strict and pylint all clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:50:39 +02:00

168 lines
4.7 KiB
Python

"""The review trail: verdicts, and everything undo needs to reverse them.
Split out of :mod:`python_pkg.wsg_grabber.store` to stay under this repo's
500-line-per-file cap, along the seam that matters: this module owns what the
user decided, the other owns how a file got downloaded.
The trail lives in the index rather than in memory, which is what makes undo
unbounded and lets it survive quitting the reviewer.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from python_pkg.wsg_grabber.models import ReviewedItem, ReviewItem, Verdict
from python_pkg.wsg_grabber.states import FileState
if TYPE_CHECKING:
from pathlib import Path
import sqlite3
def _now() -> str:
"""Return the current UTC time as an ISO-8601 string.
Returns:
str: Timestamp suitable for the index's text date columns.
"""
return datetime.now(tz=UTC).isoformat()
def _scalar(conn: sqlite3.Connection, sql: str, params: list[str]) -> int:
"""Run a single-value query and return it as an int.
Args:
conn: Open connection.
sql: Query yielding exactly one column.
params: Bound parameters.
Returns:
int: The first column of the first row.
"""
return int(conn.execute(sql, params).fetchone()[0])
def record_verdict(
conn: sqlite3.Connection,
md5_b64: str,
state: FileState,
reviewed_name: str | None = None,
) -> None:
"""Store the user's decision about a file.
Args:
conn: Open connection.
md5_b64: File identity.
state: Either ``KEPT`` or ``PASSED``.
reviewed_name: Name the file now has in keep/ or trash/. Persisting it
is what lets the verdict be undone in a later session.
"""
conn.execute(
"""
UPDATE files
SET state = ?, reviewed_at = ?, reviewed_name = ?
WHERE md5 = ?
""",
(state.value, _now(), reviewed_name, md5_b64),
)
def reviewed_count(conn: sqlite3.Connection) -> int:
"""Return how many verdicts could still be taken back.
Args:
conn: Open connection.
Returns:
int: Count of reviewed files whose destination name is known.
"""
return _scalar(
conn,
"""
SELECT COUNT(*) FROM files
WHERE state IN (?, ?) AND reviewed_name IS NOT NULL
""",
[FileState.KEPT.value, FileState.PASSED.value],
)
def newest_verdict(
conn: sqlite3.Connection,
directory: Path,
) -> ReviewedItem | None:
"""Return the most recent verdict that can be undone.
Reading this from the index rather than from memory is what makes undo
survive quitting, and unbounded: every verdict ever recorded is a candidate,
newest first.
Args:
conn: Open connection.
directory: Where an undone file should be put back.
Returns:
ReviewedItem | None: The verdict to reverse, or None when there is none.
"""
row = conn.execute(
"""
SELECT md5, local_name, reviewed_name, orig_name, ext, fsize,
width, height, state
FROM files
WHERE state IN (?, ?) AND reviewed_name IS NOT NULL
ORDER BY reviewed_at DESC, md5 DESC
LIMIT 1
""",
(FileState.KEPT.value, FileState.PASSED.value),
).fetchone()
if row is None:
return None
return ReviewedItem(
item=ReviewItem(
md5=row["md5"],
path=directory / row["local_name"],
orig_name=f"{row['orig_name']}{row['ext']}",
fsize=row["fsize"],
width=row["width"],
height=row["height"],
),
choice=Verdict.KEEP if row["state"] == FileState.KEPT.value else Verdict.SKIP,
reviewed_name=row["reviewed_name"],
)
def forget_verdict(conn: sqlite3.Connection, md5_b64: str) -> None:
"""Make a verdict un-undoable without changing it.
Used when the file is no longer where the verdict left it, so the same
entry does not keep surfacing as the next thing to undo.
Args:
conn: Open connection.
md5_b64: File identity.
"""
conn.execute(
"UPDATE files SET reviewed_name = NULL WHERE md5 = ?",
(md5_b64,),
)
def restore_for_review(conn: sqlite3.Connection, md5_b64: str, name: str) -> None:
"""Undo a verdict, returning the file to the review queue.
Args:
conn: Open connection.
md5_b64: File identity.
name: Filename it has again inside the incoming directory.
"""
conn.execute(
"""
UPDATE files
SET state = ?, local_name = ?, reviewed_at = NULL,
reviewed_name = NULL
WHERE md5 = ?
""",
(FileState.READY.value, name, md5_b64),
)