Applies the three findings from the /code-review high pass on this stack:
1. to_tsquery_str moved from app.feedquery to app.utils — it's a generic
tsquery sanitizer with no feed dependency; housing it in the feed
read-model coupled the Plex browse module to the feed domain. Both feed
and plex now import it from the neutral util module.
2. feedquery.decode_cursor no longer raises fastapi.HTTPException — it raises
a plain ValueError, and routes/feed.get_feed translates it to the HTTP 400.
The read-model no longer imports fastapi, matching its "no request state"
docstring.
3. Dropped the leading underscore on the helpers that are now a cross-module
public API: feedquery {filtered_query, sort_keys, order_by, keyset_predicate,
encode_cursor, decode_cursor}; plex.query {TS_CONFIG, FIELD_COLUMNS,
field_recheck, split_csv, added_cutoff, meta_filter_conds, apply_meta_filters,
like_escape}; plex.cards {movie_card, show_card, episode_card, leaf_card}.
Genuinely-internal helpers (_channel_url, _saved_expr, _ADDED_WINDOWS,
_lib_key) keep their underscore.
Pure code motion + rename. DB-lane suite 287 passed / 2 skipped; ruff clean.
39 lines
1.8 KiB
Python
39 lines
1.8 KiB
Python
"""Small cross-cutting helpers shared across modules (kept dependency-light on purpose)."""
|
|
import re
|
|
from datetime import date, datetime, timezone
|
|
|
|
|
|
def iso(dt: date | None) -> str | None:
|
|
"""Serialize a date/datetime for a JSON response: ISO-8601, or None when the column is
|
|
NULL. Single source of truth for the `x.isoformat() if x else None` idiom the routes had
|
|
hand-rolled ~17 times — so a future tz/format tweak lands in one place. Accepts `date`
|
|
too (datetime is a subclass) — e.g. MediaAsset.upload_date is a plain `date`."""
|
|
return dt.isoformat() if dt is not None else None
|
|
|
|
# Single source of truth for "looks like an email" — used by every endpoint that accepts an
|
|
# address (auth register/reset/demo, admin invites + demo whitelist, the setup wizard). Keep
|
|
# the strictness in one place so routes don't drift between this and a looser "@"-in check.
|
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
|
|
|
|
def valid_email(value: str | None) -> bool:
|
|
return bool(value and _EMAIL_RE.match(value))
|
|
|
|
|
|
def now_utc() -> datetime:
|
|
"""Current time as a timezone-aware UTC datetime."""
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def to_tsquery_str(q: str) -> str | None:
|
|
"""Turn free-text into a to_tsquery() string: word runs ANDed together, the LAST word
|
|
prefix-matched (`:*`) for as-you-type search — e.g. "ti amo magyar" → "ti & amo & magyar:*".
|
|
Only `\\w` runs containing an alphanumeric are kept, so no tsquery operators can leak in
|
|
(the string is built from sanitised tokens, never the raw query). Shared by the YouTube feed
|
|
search and the Plex library search, hence this neutral home rather than either domain module."""
|
|
tokens = [t for t in re.findall(r"\w+", q, flags=re.UNICODE) if any(c.isalnum() for c in t)]
|
|
if not tokens:
|
|
return None
|
|
tokens[-1] = tokens[-1] + ":*"
|
|
return " & ".join(tokens)
|