build(backend): close the lint gaps the E32 audit measured
ruff gains RUF100, PLW1510 and flake8-bugbear, every hit fixed rather than ignored. B008 stays on with FastAPI's Depends/Query declared immutable. basedpyright moves to standard mode, pytest to --strict-markers, and a hand run of pytest now requires the database unless SIFTLODE_TEST_REQUIRE_DB=0. PLW1510 found a real bug: the three ffprobe probes in plex/stream.py never looked at the exit status, so a probe that failed was read as the file's truth (an HDR source sent down the HW-safe path, a multi-audio file collapsed to one track, a subtitle offset taken from garbage) and the warning each except branch promises never fired. They now run with check=True.
This commit is contained in:
@@ -436,7 +436,7 @@ def explore_channel(
|
||||
channel = db.get(Channel, channel_id)
|
||||
result = explore_ingest_page(db, yt, channel, page_token)
|
||||
except YouTubeError as exc:
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
return {
|
||||
"ok": True,
|
||||
"channel_id": channel_id,
|
||||
@@ -597,7 +597,7 @@ def subscribe(
|
||||
if "subscriptionDuplicate" in exc.reason.split(","):
|
||||
yt_sub_id = ""
|
||||
else:
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
# Enrich a stub channel (title/thumbnail/subscriber count) so it shows up properly right
|
||||
# away, on BOTH the normal and the desync path — a resurfaced stub still needs metadata to
|
||||
# render. Keep the client open (we're still inside the `with`); a metadata failure here
|
||||
@@ -658,7 +658,7 @@ def unsubscribe(
|
||||
except YouTubeError as exc:
|
||||
# 422 (a real, explainable error → speaking modal), not 502 which the client treats as
|
||||
# a transient "connection lost" gateway blip.
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
db.delete(sub)
|
||||
db.commit()
|
||||
return {"ok": True, "unsubscribed": channel_id}
|
||||
|
||||
@@ -170,7 +170,7 @@ def add_pin(
|
||||
# between `resolve_ref` confirming it and this INSERT (a Plex sync dropping a removed
|
||||
# item), which trips the FK. Semantically that is the same "no such thing" the pre-check
|
||||
# returns above — re-raising handed the user a 500 they could do nothing with.
|
||||
raise not_found("item")
|
||||
raise not_found("item") from None
|
||||
return {"ok": True, "id": winner.id, "created": False}
|
||||
return {"ok": True, "id": pin.id, "created": True}
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ def set_credential(
|
||||
try:
|
||||
cleaned = dlcreds.validate_cookies(body.cookies)
|
||||
except dlcreds.InvalidCookies as e:
|
||||
raise HTTPException(status_code=400, detail={"code": "cookies", "reason": str(e)})
|
||||
raise HTTPException(status_code=400, detail={"code": "cookies", "reason": str(e)}) from e
|
||||
dlcreds.upsert_credential(db, user.id, host_key, cleaned)
|
||||
audit.record(
|
||||
db,
|
||||
@@ -407,7 +407,7 @@ def enqueue_download(
|
||||
try:
|
||||
job = service.enqueue(db, user.id, source_kind, source_ref, spec, profile_id, display_name)
|
||||
except quota.QuotaExceeded as e:
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e))
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e)) from e
|
||||
return _serialize_job(db, job)
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ def upload_download(
|
||||
display_name=upload.display_name,
|
||||
)
|
||||
except quota.QuotaExceeded as e:
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e))
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e)) from e
|
||||
return _serialize_job(db, job)
|
||||
|
||||
|
||||
@@ -547,9 +547,9 @@ def enqueue_edit(
|
||||
try:
|
||||
job = service.enqueue_edit(db, user.id, source_job, spec, display_name)
|
||||
except quota.QuotaExceeded as e:
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e))
|
||||
raise HTTPException(status_code=422, detail=_quota_detail(e)) from e
|
||||
except service.EditError as e:
|
||||
raise HTTPException(status_code=400, detail={"code": "edit", "reason": e.reason})
|
||||
raise HTTPException(status_code=400, detail={"code": "edit", "reason": e.reason}) from e
|
||||
return _serialize_job(db, job)
|
||||
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ def get_feed(
|
||||
# Expose each key column on the row so we can build the next cursor from the last item.
|
||||
query = query.add_columns(*[k["expr"].label(f"_k{i}") for i, k in enumerate(keys)])
|
||||
if vals is not None:
|
||||
for k, v in zip(keys, vals):
|
||||
for k, v in zip(keys, vals, strict=True):
|
||||
k["value"] = v
|
||||
if ranked and window is not None:
|
||||
rows = windowed_relevance_rows(db, query, keys, window, cursor_id, limit + 1)
|
||||
@@ -431,7 +431,7 @@ def get_video_detail(
|
||||
):
|
||||
items = yt.get_videos([video_id])
|
||||
except YouTubeError as exc:
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
if not items:
|
||||
raise not_found("video")
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ def sync_youtube(
|
||||
return {"ok": True, **pull_user_playlists(db, user)}
|
||||
except YouTubeError as exc:
|
||||
db.rollback()
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
|
||||
|
||||
@router.post("/{playlist_id}/revert-youtube")
|
||||
@@ -369,10 +369,10 @@ def revert_youtube(
|
||||
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PULL):
|
||||
return {"ok": True, **repull_playlist(db, user, pl)}
|
||||
except PlaylistGone:
|
||||
raise not_found("playlist")
|
||||
raise not_found("playlist") from None
|
||||
except YouTubeError as exc:
|
||||
db.rollback()
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
|
||||
|
||||
def _pushable(db: Session, user: User, playlist_id: int) -> Playlist:
|
||||
@@ -398,7 +398,7 @@ def push_plan(
|
||||
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PUSH):
|
||||
plan = plan_merge(db, user, pl)
|
||||
except YouTubeError as exc:
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
plan["remaining_today"] = quota.remaining_today(db)
|
||||
plan["affordable"] = plan["units_estimate"] <= plan["remaining_today"]
|
||||
return plan
|
||||
@@ -436,10 +436,10 @@ def push(
|
||||
raise _refused(429, "quota_exceeded")
|
||||
return {"ok": True, **merge_playlist(db, user, pl, live=live)}
|
||||
except PlaylistGone:
|
||||
raise not_found("playlist")
|
||||
raise not_found("playlist") from None
|
||||
except YouTubeError as exc:
|
||||
db.rollback()
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
|
||||
|
||||
@router.get("/{playlist_id}")
|
||||
@@ -539,7 +539,7 @@ def delete_playlist(
|
||||
with YouTubeClient(db, user) as yt:
|
||||
yt.delete_playlist(pl.yt_playlist_id)
|
||||
except YouTubeError as exc:
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
# The playlist first, as every item write: the cascade deletes the items before the list row.
|
||||
lock_playlists(db, pl.id)
|
||||
db.delete(pl) # items cascade
|
||||
|
||||
@@ -428,7 +428,7 @@ def create_collection(
|
||||
db.commit()
|
||||
except (PlexError, PlexNotConfigured) as e:
|
||||
db.rollback()
|
||||
raise _write_or_pending_error(wrote, e)
|
||||
raise _write_or_pending_error(wrote, e) from e
|
||||
return _collection_card(col)
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ def collection_add_item(
|
||||
db.commit()
|
||||
except (PlexError, PlexNotConfigured) as e:
|
||||
db.rollback()
|
||||
raise _write_or_pending_error(wrote, e)
|
||||
raise _write_or_pending_error(wrote, e) from e
|
||||
return _collection_card(col)
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ def collection_remove_item(
|
||||
db.commit()
|
||||
except (PlexError, PlexNotConfigured) as e:
|
||||
db.rollback()
|
||||
raise _write_or_pending_error(wrote, e)
|
||||
raise _write_or_pending_error(wrote, e) from e
|
||||
return _collection_card(col)
|
||||
|
||||
|
||||
@@ -502,7 +502,7 @@ def collection_rename(
|
||||
db.commit()
|
||||
except (PlexError, PlexNotConfigured) as e:
|
||||
db.rollback()
|
||||
raise _write_or_pending_error(wrote, e)
|
||||
raise _write_or_pending_error(wrote, e) from e
|
||||
return _collection_card(col)
|
||||
|
||||
|
||||
|
||||
@@ -124,8 +124,14 @@ def _recently_added(
|
||||
# share one), so the merge doesn't depend on object identity staying alive.
|
||||
top_movies = [o for _, kind, o in top if kind == "movie"]
|
||||
top_shows = [o for _, kind, o in top if kind == "show"]
|
||||
mcards = {m.rating_key: c for m, c in zip(top_movies, _movie_cards(db, uid, top_movies, lang))}
|
||||
scards = {s.rating_key: c for s, c in zip(top_shows, _show_cards(db, uid, top_shows, lang))}
|
||||
mcards = {
|
||||
m.rating_key: c
|
||||
for m, c in zip(top_movies, _movie_cards(db, uid, top_movies, lang), strict=True)
|
||||
}
|
||||
scards = {
|
||||
s.rating_key: c
|
||||
for s, c in zip(top_shows, _show_cards(db, uid, top_shows, lang), strict=True)
|
||||
}
|
||||
return [mcards[o.rating_key] if kind == "movie" else scards[o.rating_key] for _, kind, o in top]
|
||||
|
||||
|
||||
@@ -351,10 +357,12 @@ def _because_you_watched(
|
||||
ordered_shows = [shows[rk] for rk in rks if rk in shows]
|
||||
# rating_key is globally unique in Plex, so it keys the serialized cards back safely (no id()).
|
||||
mcards = {
|
||||
m.rating_key: c for m, c in zip(ordered_movies, _movie_cards(db, uid, ordered_movies, lang))
|
||||
m.rating_key: c
|
||||
for m, c in zip(ordered_movies, _movie_cards(db, uid, ordered_movies, lang), strict=True)
|
||||
}
|
||||
scards = {
|
||||
s.rating_key: c for s, c in zip(ordered_shows, _show_cards(db, uid, ordered_shows, lang))
|
||||
s.rating_key: c
|
||||
for s, c in zip(ordered_shows, _show_cards(db, uid, ordered_shows, lang), strict=True)
|
||||
}
|
||||
items: list[dict] = []
|
||||
for rk in rks:
|
||||
|
||||
@@ -87,7 +87,7 @@ def create_view(
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise refused(409, "view_name_taken", name=name)
|
||||
raise refused(409, "view_name_taken", name=name) from None
|
||||
return _serialize(view)
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ def update_view(
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise refused(409, "view_name_taken", name=view.name)
|
||||
raise refused(409, "view_name_taken", name=view.name) from None
|
||||
return _serialize(view)
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def run_job_now(
|
||||
try:
|
||||
result = trigger_job(job_id, actor_id=user.id)
|
||||
except KeyError:
|
||||
raise not_found("job")
|
||||
raise not_found("job") from None
|
||||
if result == "already_running":
|
||||
raise refused(409, "job_running")
|
||||
return {"ok": True, "id": job_id, "started": True}
|
||||
|
||||
@@ -331,7 +331,7 @@ def search_youtube(
|
||||
# bare 500 hidden behind the FE's generic "search failed" (same rationale as B-core-1).
|
||||
try:
|
||||
collected.extend(_ingest_candidates(db, yt, user, candidates))
|
||||
except Exception as exc: # noqa: BLE001 — any ingest failure, not just YouTubeError
|
||||
except Exception as exc: # any ingest failure, not just YouTubeError
|
||||
db.rollback()
|
||||
# The term is NOT in this line. A search string is exactly what the privacy
|
||||
# policy keeps out of the log unless the user turned Teljes/TRACE on for
|
||||
|
||||
@@ -39,7 +39,7 @@ def sync_subscriptions(user: User = Depends(require_human), db: Session = Depend
|
||||
# The onboarding wizard's first call: an unguarded failure here was a raw 500 and the
|
||||
# blocking "Server error" dialog, with nothing the user could act on (E9 F1).
|
||||
db.rollback()
|
||||
raise youtube_refusal(exc)
|
||||
raise youtube_refusal(exc) from exc
|
||||
result["quota_used_estimate"] = m.used
|
||||
result["quota_remaining_today"] = m.remaining
|
||||
return {"ok": True, **result}
|
||||
|
||||
@@ -75,7 +75,7 @@ def create_tag(
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise refused(409, "tag_name_taken", name=name)
|
||||
raise refused(409, "tag_name_taken", name=name) from None
|
||||
return {
|
||||
"ok": True,
|
||||
"id": tag.id,
|
||||
@@ -113,7 +113,7 @@ def update_tag(
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise refused(409, "tag_name_taken", name=tag.name)
|
||||
raise refused(409, "tag_name_taken", name=tag.name) from None
|
||||
return {
|
||||
"ok": True,
|
||||
"id": tag.id,
|
||||
|
||||
Reference in New Issue
Block a user