rq only parses the cron when the scheduler loads it, then logs and drops
the job while cron_config had already logged it as scheduled. Validate it
up front and log an error instead. croniter was a transitive rq dependency
and is now declared directly, with its typeshed stubs for mypy.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Skip CA files that can't be read instead of failing startup, type the
PKI test fixture, and declare certifi as a direct dependency.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* chore(deps): bump redis to 8.1 and fakeredis to 2.38
redis-py 6.2.0 -> 8.1.0 and fakeredis 2.30.1 -> 2.38.0. RQ 2.12
(redis!=6,>=3.5) and fakeredis (redis>=4.3) both accept redis 8.
fakeredis 2.38 types FakeServer's version argument as a tuple, so pass
(7,) instead of 7. Behaviour is unchanged since fakeredis normalises an
int to a one-element tuple.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx
* chore(deps): drop types-redis in favour of redis-py's own types
types-redis only covers redis 4.6 and shadowed the inline types that
redis 8 ships, so mypy was checking calls against a stale API. Removing
it also drops its transitive stub packages (types-cffi, types-pyopenssl,
types-setuptools), which nothing else uses.
redis-py's types differ from the stubs in three ways:
- Redis, AsyncRedis and Pipeline are not generic, so drop the [str] and
[bytes] parameters.
- Responses are typed bytes | str regardless of decode_responses, so cast
at the few call sites that rely on the decoded (or raw) type.
- watch, unwatch, multi, aclose and execute_command are unannotated, so
add redis to untyped_calls_exclude, as is done for rq and apprise.
Also replace setex, deprecated in redis 8, with set(..., ex=...).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx
* fix(device-auth): replace deprecated GETSET with SET ... GET
code-review step. GETSET is deprecated since Redis 6.2 in favour of
SET with GET. Using it with ex= also sets the poll key's TTL in the same
command, instead of a separate EXPIRE that a crash could skip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014wzuPg8vDEjsQ8xqvZgFwx
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
1.1.12 predates Python 3.14 support and has NULL-dereference crashes
(CONPY-318, CONPY-306) fixed in 1.1.13 and 1.1.14. The image moved to
Python 3.14 in 5.3.0, which matches reports of alembic segfaulting inside
_mariadb.cpython-314 at startup.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Forward notifications to any of the 100+ services Apprise reaches instead
of the two hand-coded webhook formats. The channel form lists every
Apprise service with the fields it takes, and the backend builds the
Apprise URL from them.
- New `apprise` channel type, admin only: Apprise opens its own
connections, past the SSRF guard. The owner's role is checked again at
every delivery.
- GET /api/notification-channels/apprise-services describes each service
from Apprise's plugin details: URL tokens as fields, options as
advanced fields. A field is required only when every URL template
needs it.
- A channel stores the service and its fields sealed; the URL is built
from the template those fields fill at each send. Local schemas, the
options that read files and the ones RomM sets are never offered.
Apprise follows no redirects, does not retry and its timeouts are
capped.
- The RomM JSON webhook keeps its payload and HMAC signature; its
Discord and ntfy formats go, Apprise covers them. Email is unchanged.
- The v2 dialog picks the type from one searchable list and renders the
service's fields, options folded under "Advanced options".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
uvicorn 0.35 predates official Python 3.14 support (added in 0.38), and
uvicorn-worker 0.4 requires uvicorn>=0.36. The lock also moves
websockets from 12.0 to 17.1, since uvicorn's standard extra now needs
websockets>=13 for its default websockets-sansio implementation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013rrhKiFFB4CKzyVDXf3oHT
`~= 2.3` allowed any release below 3.0, so a later `uv lock` could pick
2.4 while Trunk stayed on 2.3.0 and quietly reintroduce the skew this
change exists to remove. `~= 2.3.0` holds the same minor line as Trunk.
The resolved version is unchanged at 2.3.1; only the recorded specifier
moves in the lockfile.
The config comment now states what the current value is rather than what
the previous one did.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SPpv6W2ygP9tQ61fTQxdYp
`pyproject.toml` asked for `mypy ~= 1.19` (lock: 1.19.1) while
`.trunk/trunk.yaml` runs `[email protected]`, so `uv run mypy` and the build
were a major version apart and could disagree on the same file.
Moving the dependency up rather than pinning Trunk down keeps CI
enforcement exactly as it is today and changes only the local side. The
two agree: over the whole backend, 2.3.1 and 2.3.0 produce byte-identical
output (151 errors in 39 files).
The lockfile moves mypy and its three transitives (ast-serialize, librt,
pathspec). None is reachable from application code, so the runtime
dependency set is unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SPpv6W2ygP9tQ61fTQxdYp
* test(backend): stop the suite emitting warnings
The backend suite reported 4838 warnings. Three sources, all now clear:
- 18 uses of the deprecated HTTP_422_UNPROCESSABLE_ENTITY, renamed to
HTTP_422_UNPROCESSABLE_CONTENT.
- Unawaited coroutines in the streaming and scan tests. A bare MagicMock
drops the coroutine handed to spawn_sync_task, and a bare AsyncMock's
return_value is itself an AsyncMock, so the to_dict() call on it leaked
out of the scan handler.
- About 4780 from sentry-sdk 2.32 and opentelemetry 1.36 calling
asyncio.iscoroutinefunction, which Python 3.14 deprecates and 3.16
removes. Bumping both in the lock clears them.
The remaining starlette warning wants httpx2 in place of the httpx 0.27 the
metadata providers pin, so it is filtered rather than fixed: a second HTTP
stack for the test client is a larger change than the warning justifies.
4815 passed, 2 skipped, 0 warnings.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
* test(backend): address review on the warning fixes
Greptile flagged the pytest.ini rationale as over the two-line comment limit,
and the new scan fake as untyped. Both fixed: the filter comment is one
non-obvious fact, and the fake now takes `scan_stats` as a typed keyword-only
parameter, which is a real contract rather than `**kwargs`.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
---------
Co-authored-by: Claude Sonnet 5 <[email protected]>
RomM runs `rq cron` as a long-lived process, and 2.12 fixes the case
that process is most exposed to: CronScheduler.heartbeat() now re-creates
its Redis hash when the key has expired, so a scheduler whose host slept
past the 120s TTL comes back registered instead of running unlisted. The
TTL itself moves to DEFAULT_CRON_SCHEDULER_TTL, shared with the registry
cleanup threshold that prunes it, so the two can no longer drift apart.
2.12 also adds a CronJobRegistry, which makes a cron job's history
readable without an active scheduler. Nothing reads it yet; the task
endpoints report queue and job state, not cron history.
The one API removed, CronScheduler.save_jobs_data(), is internal to the
scheduler loop and is not called here. cron_config.py only calls
cron.register().
The constraint stays under 3.0 for the reasons the 2.11 bump recorded:
that release moves get_current_job() to contextvars, reworks dependency
handling behind a ReadyJobRegistry, and changes the
Worker.handle_job_success() signature, which RomMWorker subclasses around.
Claude-Session: https://claude.ai/code/session_01EXV6RTfrRjgH88P9CrtYPQ
Co-authored-by: Claude Opus 5 <[email protected]>
The runtime moved to 3.14 but the pinned versions did not, so packages
without cp314 wheels silently resolved to their pure-Python universal
wheels. Bump each to the first release that ships cp314 wheels, all
within the existing pyproject specs:
- SQLAlchemy 2.0.41 -> 2.0.52 (cyextension, and greenlet is back so
sqlalchemy.ext.asyncio is functional rather than merely importable)
- multidict, propcache, frozenlist (aiohttp/yarl accelerators)
- wrapt 1.17.2 -> 1.17.3 (OpenTelemetry instrumentation proxies)
- coverage 7.9.2 -> 7.16.0 (CTracer, and CI runs the suite under --cov)
- mysql-connector-python 9.3.0 -> 9.7.0 (C extension protocol)
- debugpy 1.8.14 -> 1.8.21 (pydevd accelerators)
- pyinstrument 5.0.3 -> 5.1.3 (no longer compiled from sdist on every
`uv sync --all-extras`, which broke the no-toolchain dev setup)
- anyio 4.9.0 -> 4.14.2, dropping a PEP 765 SyntaxWarning printed on
every process start
Run pytest and the migrations workflow when the runtime or the lock
changes: nothing in the 3.14 bump lives under `backend/`, so neither
workflow triggered on it. `e2e.yml` already listed those paths.
Make `.python-version` the single home for the version: `uv python
install` and `actions/setup-python` both read it. In the dev Dockerfile
that also fixes the interpreter layer not being invalidated when the pin
changes, and the sigil install now derives site-packages from the venv.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Bumps the runtime everywhere it is pinned: requires-python, the
.python-version uv reads, both Dockerfiles (with the python:3.14-alpine3.23
and uv 3.14 image digests), the six workflows that install Python, and the
contributor docs.
Several locked C-extension versions had no cp314 wheels, so the Alpine
image fell back to building them from source and failed on the missing
C++ and Rust toolchains. Those packages are upgraded within their existing
pyproject ranges: pydantic 2.11.7 -> 2.13.5 (pydantic-core 2.46.5), uvloop
0.22.1, grpcio 1.83.1, httptools 0.8.0, inflate64 1.0.4, markupsafe 3.0.3,
pyyaml 6.0.3 and streaming-form-data 1.20.0. The lockfile also drops the
3.13-only resolution branches.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The override let vcrpy 8.2.1 past the rolling 7-day exclude-newer window
on the day after its release. The window has since caught up, and the
fixed 2026-06-17 cutoff was now holding vcrpy back from 8.3.0. The
aiohttp 3.14 compatibility fix the comment referred to landed in 8.2.0
and is present in every later release, so nothing needs the override.
Upgrade vcrpy to 8.3.0 in the lock so the change exercises what the
removal permits; all cassette-backed tests pass on it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The rolling seven-day window has reached rq 2.11.0, so `rq ~= 2.11`
resolves without an exclusion of its own.
Co-Authored-By: Claude Opus 5 <[email protected]>
rq-scheduler has one release since 2023, 109 open issues, and RomM ran a
fork of it for Redis username and SSL options that upstream never took.
Everything it was here for now ships with RQ.
Its two failure modes were the reason for the workarounds in 9e9f59c04.
It reads a job's function name before taking the job out of its registry,
so one unreadable job stalls it for good; RQ's scheduler removes every id
it looked at whether or not the job could be read. And cancelling a
delayed job needed both `Scheduler.cancel` and `Job.cancel`, because the
first left the status alone and the second left the registry entry, which
the scheduler then queued anyway; `Job.cancel` on its own now clears the
scheduled registry.
Delayed jobs, which is how the watcher defers a rescan, go onto the queue
with `Queue.enqueue_in` and are released by the worker running with
`--with-scheduler`. Periodic tasks are declared in `tasks/cron_config.py`
and loaded by an `rq cron` process, replacing `rqscheduler`. A task is
registered only when it is enabled and has a cron string, so the
schedule-then-unschedule dance around the env toggles is gone, and with
it `PeriodicTask.init/schedule/unschedule`.
Jobs are enqueued by task name through `run_task_by_name`, resolved
against a catalog in `tasks/registry.py`. Nothing pickles a task instance
into Redis any more, which is what made a payload unreadable across
upgrades in the first place. The catalog also gives the tasks endpoint
one home for what a task is, rather than a list of its own.
Scan discovery keys off the type a job carries rather than the scheduled
rescan's function name, since every task now shares one entry point. That
also drops SCAN_LIBRARY_TASK_FUNC, which only existed to name a function
across an import cycle.
The watcher's dedupe worked on `job.args[0]` while scans are enqueued with
keyword arguments, so it never matched and every filesystem change
scheduled another scan. It reads `platform_ids` from the payload now, and
deliberately ignores a scan already running: that one may have walked past
the folder that just changed.
Startup clears what the old scheduler left in Redis, once.
Co-Authored-By: Claude Opus 5 <[email protected]>
The lockfile sat on 2.9.0 while the constraint allowed anything under
3.0, so the floor now names the version the code is tested against.
2.11 carries what the scheduler work ahead needs: RQScheduler acquires
and refreshes its lock before enqueueing, each CronJob has a name and
keeps the ids of the jobs it created, and calling create_cron() twice no
longer duplicates jobs. 2.10 added webhook notifications and a stable
scheduler identity, and 2.9.1 covers redis-py >= 8.
2.11.0 was published inside the rolling 7-day window, so it needs a
per-package exclusion until 2026-08-24, when the window reaches it and
the entry can go.
The constraint stays under 3.0 deliberately. That release moves
get_current_job() to contextvars, reworks dependency handling behind a
ReadyJobRegistry, and changes the Worker.handle_job_success() signature,
which RomMWorker subclasses around.
Co-Authored-By: Claude Opus 5 <[email protected]>
Drop the `exclude-newer-package` pin that froze starlette resolution at
1.0.1, and widen the version specifier to allow the 1.6.x line.
Co-Authored-By: Claude Opus 5 <[email protected]>
RAHasher can't read the block-compressed RVZ/WIA containers Dolphin
creates, so GameCube and Wii games stored as .rvz/.wia never matched on
RetroAchievements ("Not a Gamecube disc" / "Not a supported Wii file").
Add utils/rvz_hasher.py, which reconstructs only the disc byte ranges
the RA hash covers, mirroring rcheevos:
- An RVZ/WIA reader with random access over raw-data and partition
groups (NONE/bzip2/LZMA/LZMA2/Zstandard codecs, RVZ packing with the
lagged-Fibonacci junk PRNG, and stored hash exceptions).
- GameCube: MD5 of the boot header, apploader and main.dol segments.
- Wii: the hash additionally covers encrypted cluster bytes, so the
reader recalculates the H0/H1/H2 hash tree, applies the container's
hash exceptions and re-encrypts clusters with the embedded title key
(AES-128-CBC) to reproduce the exact retail disc bytes.
RAHasherService tries the native path for .rvz/.wia on the GameCube and
Wii platforms and falls back to RAHasher on any failure.
Verified against Mario Kart: Double Dash!! (USA) and Super Mario Galaxy
(USA) RVZ dumps: the computed hashes are present in the official RA
hash lists (games 7693 and 189), and RAHasher 1.8.3 produces identical
hashes when run on disc images reconstructed by the same reader.
Closes#3649Closes#3650
Co-Authored-By: Claude Fable 5 <[email protected]>