refactor(backend): select() and an inline now everywhere, held by a test

CLAUDE.md's canonical table names SQLAlchemy 2.0 select() and an inline
datetime.now(timezone.utc), and both had drifted: five db.query(...) calls
remained in routes/auth.py and routes/me.py (seven before the demo went),
and four private now helpers stood in sync/videos.py, utils.py (imported
as _now by explore and maintenance), scheduler.py (_now_iso) and the e2e
seed. All are the canonical form now.

Ruff cannot ban a method call on an instance or a function by name, so
tests/test_canonical_patterns.py reads every app module's AST and fails on
a .query(...) call or a now/_now/now_utc helper — seen red on a planted file.
This commit is contained in:
2026-10-05 00:19:39 +02:00
parent ab76166878
commit 24c47d12bd
10 changed files with 111 additions and 59 deletions
+12 -4
View File
@@ -399,7 +399,7 @@ async def callback(
return RedirectResponse(url=_access_notice_url("requested", email, req.invite))
return RedirectResponse(url=_access_notice_url("denied"))
user = db.query(User).filter(User.google_sub == sub).one_or_none()
user = db.execute(select(User).where(User.google_sub == sub)).scalar_one_or_none()
if user is None:
# No account carries this Google identity yet. If one already exists for this MAILBOX —
# e.g. an email+password registration — adopt the Google identity onto it instead of
@@ -412,7 +412,11 @@ async def callback(
# (ids 13 and 14). Exact spelling still wins when both exist, so an account that already
# matches literally is never passed over for a canonical sibling.
canon = canonical_email(email)
candidates = db.query(User).filter(User.email_canonical == canon).order_by(User.id).all()
candidates = (
db.execute(select(User).where(User.email_canonical == canon).order_by(User.id))
.scalars()
.all()
)
existing = next((u for u in candidates if u.email == email), None) or next(
iter(candidates), None
)
@@ -516,7 +520,9 @@ def _sync_google_email(db: Session, user: User, email: str) -> None:
return
if canonical_email(user.email) == canonical_email(email):
return # same mailbox, their spelling — leave it alone
clash = db.query(User).filter(User.email == email, User.id != user.id).one_or_none()
clash = db.execute(
select(User).where(User.email == email, User.id != user.id)
).scalar_one_or_none()
if clash is None:
user.email = email
@@ -575,7 +581,9 @@ def _complete_link(
user = db.get(User, link_uid)
if user is None:
return RedirectResponse(url="/?link=error")
other = db.query(User).filter(User.google_sub == sub, User.id != user.id).one_or_none()
other = db.execute(
select(User).where(User.google_sub == sub, User.id != user.id)
).scalar_one_or_none()
if other is not None:
return RedirectResponse(url="/?link=conflict")
if user.google_sub is not None and user.google_sub != sub:
+1 -1
View File
@@ -40,7 +40,7 @@ def my_accounts(
"""The accounts that have authenticated in this browser session (switchable without a
new Google sign-in). The active one is flagged."""
ids = request.session.get("account_ids") or [user.id]
rows = {u.id: u for u in db.query(User).filter(User.id.in_(ids)).all()}
rows = {u.id: u for u in db.execute(select(User).where(User.id.in_(ids))).scalars()}
out = []
for uid in ids: # preserve recency order from the session
u = rows.get(uid)
+4 -8
View File
@@ -106,10 +106,6 @@ def apply_interval(db, job_id: str, minutes: int) -> int:
return minutes
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _record(name: str, **fields) -> None:
with _activity_lock:
_activity.setdefault(name, {}).update(fields)
@@ -187,7 +183,7 @@ def _job(name: str, fn) -> None:
db = SessionLocal()
actor_id = _manual_actor.get() # set only for a manual "run now" trigger
_record(
name, last_started=_now_iso(), last_error=None, progress=None
name, last_started=datetime.now(timezone.utc).isoformat(), last_error=None, progress=None
) # running already claimed
def sink(current, total, phase):
@@ -199,7 +195,7 @@ def _job(name: str, fn) -> None:
_record(
name,
status="skipped",
last_finished=_now_iso(),
last_finished=datetime.now(timezone.utc).isoformat(),
last_result="paused",
result=_PAUSED,
progress=None,
@@ -216,7 +212,7 @@ def _job(name: str, fn) -> None:
_record(
name,
status="ok",
last_finished=_now_iso(),
last_finished=datetime.now(timezone.utc).isoformat(),
last_result=summary,
result=data,
progress=None,
@@ -236,7 +232,7 @@ def _job(name: str, fn) -> None:
_record(
name,
status="error",
last_finished=_now_iso(),
last_finished=datetime.now(timezone.utc).isoformat(),
last_error=err,
result=failure,
progress=None,
+4 -5
View File
@@ -8,7 +8,7 @@ stay private to users who explored (or subscribe to) the channel — the feed qu
otherwise the cleanup job hard-deletes the channel and its untouched ephemeral videos after a
grace period (mirrors the maintenance job's grace-then-delete safety)."""
from datetime import timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import delete, or_, select
from sqlalchemy.orm import Session
@@ -24,7 +24,6 @@ from app.models import (
VideoState,
)
from app.sync.videos import insert_stubs, stub_from_playlist_item, apply_video_details
from app.utils import now_utc as _now
from app.youtube.client import YouTubeClient, YouTubeError, is_playlist_gone
log = logs.get_logger(__name__)
@@ -59,7 +58,7 @@ def _enrich_new(db: Session, yt: YouTubeClient, video_ids: list[str]) -> int:
if not videos:
return 0
items = {it["id"]: it for it in yt.get_videos([v.id for v in videos])}
now = _now()
now = datetime.now(timezone.utc)
for video in videos:
item = items.get(video.id)
if item is None:
@@ -115,7 +114,7 @@ def purge_unkept_explored(db: Session) -> dict:
or surfaced by the user's own search (SearchFind) — these survive even if the channel goes.
A channel row is dropped only once it holds no videos at all. 0 quota."""
grace_days = sysconfig.get_int(db, "explore_grace_days")
cutoff = _now() - timedelta(days=grace_days)
cutoff = datetime.now(timezone.utc) - timedelta(days=grace_days)
expired = db.execute(
delete(ExploredChannel).where(ExploredChannel.created_at < cutoff)
@@ -166,7 +165,7 @@ def purge_unkept_search(db: Session, grace_days: int | None = None) -> dict:
SearchFinds. `grace_days=0` purges immediately (the admin "purge now" path); None uses config.
0 quota."""
grace = sysconfig.get_int(db, "search_grace_days") if grace_days is None else grace_days
cutoff = _now() - timedelta(days=grace)
cutoff = datetime.now(timezone.utc) - timedelta(days=grace)
state_exists = select(VideoState.id).where(VideoState.video_id == Video.id).exists()
pl_exists = select(PlaylistItem.id).where(PlaylistItem.video_id == Video.id).exists()
sub_exists = select(Subscription.id).where(Subscription.channel_id == Video.channel_id).exists()
+3 -4
View File
@@ -20,7 +20,7 @@ Quota: phase 1 costs ~(flagged / 50) units, phase 2 ~(batch / 50). Both respect
backfill reserve as the other jobs so interactive syncs never starve.
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
@@ -31,7 +31,6 @@ from app.models import Playlist, PlaylistItem, Video
from app.notifications import create_notification
from app.sync.runner import get_service_user
from app.sync.videos import apply_video_details, parse_dt
from app.utils import now_utc as _now
from app.youtube.client import YouTubeClient
log = logs.get_logger(__name__)
@@ -60,7 +59,7 @@ def _is_abandoned_upcoming(item: dict) -> bool:
scheduled = parse_dt(live.get("scheduledStartTime"))
if scheduled is None:
return False
return _now() - scheduled > timedelta(days=2)
return datetime.now(timezone.utc) - scheduled > timedelta(days=2)
def _evaluate(video: Video, item: dict | None) -> str | None:
@@ -225,7 +224,7 @@ def run_maintenance(db: Session) -> dict:
return {"reason": "no service user"}
# One `now` for the whole run: each phase taking its own let a video phase 1 had just recovered
# (stamped with phase 1's) pass phase 2's "not checked this run" test and be fetched twice.
now = _now()
now = datetime.now(timezone.utc)
with YouTubeClient(db, user) as yt:
phase1 = _recheck_flagged(db, yt, now)
phase2 = (
+12 -12
View File
@@ -22,10 +22,6 @@ log = logs.get_logger(__name__)
_DURATION_RE = re.compile(r"P(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", re.IGNORECASE)
def _now() -> datetime:
return datetime.now(timezone.utc)
def parse_iso8601_duration(value: str | None) -> int | None:
if not value:
return None
@@ -80,7 +76,7 @@ def apply_rss_feed(db: Session, channel: Channel, entries: list[dict]) -> int:
time. DB-only — must run on the session's own thread; the network fetch
(fetch_channel_feed) is kept separate so callers like run_rss_poll can parallelise it."""
inserted = insert_stubs(db, entries)
channel.last_rss_at = _now()
channel.last_rss_at = datetime.now(timezone.utc)
db.add(channel)
db.commit()
return inserted
@@ -96,7 +92,9 @@ def stub_from_playlist_item(item: dict, channel_id: str) -> dict | None:
# published_at is NOT NULL (migration 0068); fall back to now() for the rare item that carries no
# date at all, so the bulk stub insert never writes a NULL. The enrich pass replaces it if the
# video's own snippet later yields a real publish date.
published = parse_dt(content.get("videoPublishedAt") or snippet.get("publishedAt")) or _now()
published = parse_dt(
content.get("videoPublishedAt") or snippet.get("publishedAt")
) or datetime.now(timezone.utc)
return {
"id": video_id,
"channel_id": channel_id,
@@ -124,12 +122,14 @@ def backfill_channel_recent(db: Session, yt: YouTubeClient, channel: Channel) ->
"""First pass: newest videos up to the configured count or age cutoff. Records a
resume cursor for the later deep backfill."""
if not channel.uploads_playlist_id:
channel.recent_synced_at = _now()
channel.recent_synced_at = datetime.now(timezone.utc)
db.add(channel)
db.commit()
return 0
cutoff = _now() - timedelta(days=sysconfig.get_int(db, "backfill_recent_max_days"))
cutoff = datetime.now(timezone.utc) - timedelta(
days=sysconfig.get_int(db, "backfill_recent_max_days")
)
collected: list[dict] = []
page_token: str | None = None
next_cursor: str | None = None
@@ -164,7 +164,7 @@ def backfill_channel_recent(db: Session, yt: YouTubeClient, channel: Channel) ->
break
next_cursor = page_token # whole page consumed, more pages remain
channel.recent_synced_at = _now()
channel.recent_synced_at = datetime.now(timezone.utc)
channel.backfill_cursor = next_cursor
channel.backfill_done = done
inserted = insert_stubs(db, collected)
@@ -264,7 +264,7 @@ def apply_video_details(video: Video, item: dict) -> None:
if snippet_published:
video.published_at = snippet_published
elif not video.published_at:
video.published_at = _now()
video.published_at = datetime.now(timezone.utc)
video.thumbnail_url = best_thumbnail(snippet.get("thumbnails")) or video.thumbnail_url
video.duration_seconds = parse_iso8601_duration(content.get("duration"))
video.view_count = int(stats["viewCount"]) if stats.get("viewCount") else None
@@ -296,7 +296,7 @@ def _apply_video_batch(
if not videos:
return 0
items = {it["id"]: it for it in yt.get_videos([v.id for v in videos])}
now = _now()
now = datetime.now(timezone.utc)
applied = 0
for video in videos:
item = items.get(video.id)
@@ -312,7 +312,7 @@ def _apply_video_batch(
def _mark_enriched(video: Video) -> None:
# Unavailable (deleted/private): stamp so we don't keep retrying.
video.enriched_at = _now()
video.enriched_at = datetime.now(timezone.utc)
def enrich_pending(db: Session, yt: YouTubeClient, limit: int | None = None) -> int:
+1 -6
View File
@@ -1,7 +1,7 @@
"""Small cross-cutting helpers shared across modules (kept dependency-light on purpose)."""
import re
from datetime import date, datetime, timezone
from datetime import date
def iso(dt: date | None) -> str | None:
@@ -55,11 +55,6 @@ def canonical_email(value: str | None) -> str:
return f"{local}@gmail.com" if local else email
def now_utc() -> datetime:
"""Current time as a timezone-aware UTC datetime."""
return datetime.now(timezone.utc)
# The shortest last word that is prefix-matched. A 1–2 letter prefix matches a large share of the
# library and ts_rank then scores every match: "m" took ~5–8 s on prod against ~100 ms for a whole
# word (E31 S4e). A shorter last word is matched whole instead ("tv", "4k" still find themselves).