Skip to content

X2: mokkari 4.8.0 + full-size candidate cover url - #218

Merged
ajslater merged 2 commits into
developfrom
claude/cbx-520-mokkari-480
Sep 21, 2026
Merged

ajslater merged 2 commits into
developfrom
claude/cbx-520-mokkari-480

Conversation

@ajslater

Copy link
Copy Markdown
Owner

comicbox 5.2.0, PR X2 of 2. Implements §6–§9 of
tasks/comicbox-5.2.0-plan.md. Independent of
#217 (X1), which mints the v5.2.0
NEWS section this appends to.

mokkari 4.8.0

4.8.0
shipped all three upstream asks from tasks/metron-rate-limit-plan.md,
now ticked there.

U1 (#167) — Session(rate_limiter=...)

comicbox's PacedSession subclass overrode the private
_execute_http_request and disabled _check_rate_limit. It collapses to
GateRateLimiter: a registration through the public hook. PacedSession
is deleted.

RateGate stays; mokkari's HeaderPacedRateLimiter is deliberately
not adopted, and the module docstring now carries the point-by-point
comparison so nobody simplifies it later. The short version: the
reference limiter does not wait at all before the first response (N
threads burst), blocks every caller on a 429 instead of releasing one
per freed slot, has no per_minute ceiling, no stats, and raises on the
daily window by comparing the server's epoch reset against the local
clock — the clock-drift trap the gate was designed around, and which
5.1.1's Skipped(reason="quota_reserved") abort semantics depend on not
hitting.

One private reach remains: Session._http, for a requests response
hook. The rate_limiter hook carries no URL, no status code and no raw
headers, and 5.1.2's telemetry — per-endpoint counts and "responses
without rate-limit headers, by status", reported to Metron's maintainer —
needs all three. The hook fires inside _http.request, which preserves
the old override's observe → cooldown → release order. Pacing no longer
depends on any private seam. A public accessor is filed as follow-up U7;
per the repo's convention I have not offered upstream a patch.

U2 (#168) — bounded pagination 429 retries

With a limiter set, a 429 mid-pagination retries the page through the
limiter without sleeping
, so the wait is the gate's rebuilt window
rather than two stacked delays. No comicbox code compensated for the old
unbounded loop, so nothing was deleted. Two consequences are now tested:
the retry does not call mokkari.session.time.sleep, and a spent daily
quota (RateGate.acquire → OnlineLookupAbortedError, not a
RateLimitError) propagates out of a paginated call instead of being
swallowed — the 5.1.1 abort semantics now hold mid-pagination too.

U3 (#169) — pooled requests.Session

One TLS handshake per run instead of per request. Two consequences
handled: close_shared_sessions() releases the pool at the end of every
Runner.run() (read out of sys.modules, so an offline run still never
imports mokkari) and from the new OnlineSession.close() / context
manager; and an os.register_at_fork handler drops inherited sessions in
a child, rebinding the lock rather than taking it — the thread that held
it at fork time does not exist in the child.

CandidateSummary.cover_url_full

Display only. The matcher keeps hashing cover_url: hashing a larger
image moves every score and would need a fresh calibration run.

  • ComicVine picks largest-first (original_url → medium_url) and
    never falls back to small_url or thumbnail — a record with only
    those tiers has nothing larger to show, and None is what a frontend
    keys "no hover" on.
  • Metron serves one full-size image per issue, so both urls are the same,
    and both are None with no image.
  • Zero API budget: the six ComicVine Images variants ride in the search
    response comicbox already paid for.

Accounting change

record_http_request loses its blocked_seconds parameter; the gate
wait comes in on the new record_gate_wait. _ApiCounts.requests now
counts responses received, which is what the server's own logs show;
a send that never answered appears only under connection_failures,
where it used to be double-counted under its endpoint too.

Test migration

Every test that patched mokkari.session.requests.request was
intercepting nothing under 4.8.0 — sends go through Session._http. They
now mount a requests.adapters.BaseAdapter (new
tests/util/metron_transport.py), which runs the whole real stack:
requests.Session.send, comicbox's response hook, mokkari's cookie
policy and the pool, exactly as production does.

Verification

make fix && make lint && make ty && make complexity
  • make lint → 0 errors, 0 warnings, 0 notes
  • make ty → All checks passed!
  • make complexity → no functions over 15; radon cc --min C and
    mi --min B both silent
make test → exit=0
2208 passed, 1 skipped, 17 warnings in 44.71s

uv.lock resolves mokkari 4.8.0. CI reports skipping on PRs into
develop by design; the local sequence above is the verification.

Deliberate non-adoption

HeaderPacedRateLimiter (above); even spacing of sends (the gate's
sliding log is already exact to DRF's, and spacing would slow small
batches for nothing); Session.last_cache_status (diagnostic only,
unchanged from the 4.6.0 audit).

Public surface codex Wave 2 imports

CandidateSummary.cover_url_full, OnlineSession.close().

🤖 Generated with Claude Code

ajslater and others added 2 commits September 20, 2026 18:47
… url

mokkari 4.8.0 shipped all three upstream asks from
tasks/metron-rate-limit-plan.md.

U1 (#167) — `Session(rate_limiter=...)`. comicbox's `PacedSession`
subclass, which overrode the private `_execute_http_request` and disabled
`_check_rate_limit`, collapses to `GateRateLimiter`: a registration
through the public hook. `RateGate` stays rather than mokkari's
`HeaderPacedRateLimiter`; the paced_session module docstring has the
point-by-point comparison so nobody "simplifies" it later. One private
reach remains, `Session._http`, for the per-endpoint and header-less
telemetry the hook cannot see -- a `requests` response hook, which also
keeps the observe/cooldown/release order the override had.

U2 (#168) — bounded pagination 429 retries. With a limiter set a page is
retried through it without sleeping, so a paginated call now waits on the
gate's rebuilt window, and a spent daily quota aborts mid-pagination
instead of being slept out.

U3 (#169) — a pooled `requests.Session`: one TLS handshake per run
instead of per request. `close_shared_sessions()` releases the pool at
the end of a run and from the new `OnlineSession.close()` / context
manager; an `os.register_at_fork` handler drops inherited sessions in a
child rather than letting two processes share sockets.

Also:

- `CandidateSummary.cover_url_full` — display only; the matcher keeps
  hashing `cover_url`, because hashing a larger image moves every score.
  ComicVine picks largest-first and never falls back to a thumbnail tier;
  Metron's image is already full size. Zero API budget either way.
- `RateLimiterError` classifies as INVALID, like `CacheError`.
- `outcome_stats.record_gate_wait` splits the gate wait off the send, so
  `requests` counts responses received -- what the server's logs show.
- Tests move off `mokkari.session.requests.request`, which intercepts
  nothing under 4.8.0, onto a `requests` adapter mounted on the pooled
  session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ajslater
ajslater merged commit 50a7fa2 into develop Sep 21, 2026
2 checks passed
ajslater added a commit that referenced this pull request Sep 21, 2026
Merging #217 and #218 interleaved their two NEWS sections: both opened
`## v5.2.0` at the same spot, so git appended #217's Features bullets to
whichever list ended last -- Dependencies. Same entries, right headings.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ajslater added a commit that referenced this pull request Sep 21, 2026
…re (#220)

`make lint` has been exiting 2 since #218: vulture flags `stream`,
`verify`, `cert` and `proxies` on both fake `BaseAdapter.send` overrides.

The names cannot change. `requests.Session.send` calls
`adapter.send(request, **kwargs)` with those exact keywords, so renaming
them is a TypeError at runtime, and collapsing them into `**kwargs`
fails basedpyright's override check (positional parameter count
mismatch). So the signature stays and the body consumes them.

`del` rather than a `vulture_ignorelist.py` entry: the whitelist matches
by bare name across the whole tree, and `stream` / `verify` / `cert` /
`proxies` are ordinary enough words that listing them would blind vulture
to real dead code elsewhere. The existing `option_string` entry is safe
there precisely because nothing else would ever be called that.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ajslater
ajslater deleted the claude/cbx-520-mokkari-480 branch October 6, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant