comicbox.online_session

source module comicbox.online_session

OnlineSession β€” Codex-facing API for batch online metadata tagging.

Wraps the existing Comicbox.run_online_lookup() per-file flow with a Codex-friendly surface:

  • Per-source credential validation up front (fail-fast).
  • Direct :class:MatchMode API (no string alias layer; ASK needs a PromptHandler or defer_prompts=True, since the session has no built-in prompt resolver of its own).
  • One programmatic PromptHandler per session that the matcher calls whenever it would otherwise hit the CLI questionary prompt.
  • Event stream via on_event= for UI feedback.
  • Cancellation token: callers stop the batch between files.

The internal matcher / selector / rate-limit machinery is untouched; this module is a faΓ§ade.

Classes

source class OnlineCredentials()

Credentials for the online sources Codex may enable.

source class OnlinePrompt()

Codex-facing handoff for an ambiguous match.

The PromptHandler receives one of these and returns a PromptResponse.

source class PromptResponse()

Codex-side decision for one prompt.

action mirrors :data:SelectorAction; payload is the index for choose, the "<source>:<id>" string for manual, the new :class:MatchMode value (e.g. "auto") for set_policy, the new :class:Prompts value (e.g. "never") for set_prompts, or None otherwise.

source class PromptHandler()

Bases : Protocol

Codex’s prompt-resolution callback.

All handlers must implement :meth:request β€” the single-prompt form used by every code path that ships today. Handlers MAY additionally implement :meth:request_many to opt into the future batched-prompt delivery path (plan Β§3.4 / step 8): when the engine grows the per-session prompt queue, handlers exposing request_many get the full pending window in one call; handlers without it continue to be invoked one prompt at a time. v1 delivers prompts via request only β€” see tasks/prepare-for-codex-writing/03-batched-prompt-handler.md for the deferred implementation plan.

Methods

  • request — Resolve one prompt; return the chosen action/payload.

source method PromptHandler.request(prompt: OnlinePrompt) β†’ PromptResponse

Resolve one prompt; return the chosen action/payload.

source class BatchedPromptHandler()

Bases : Protocol

A PromptHandler that also supports batched prompt delivery.

Codex’s defer-mode UI naturally yields a window of prompts the user will resolve together. When the engine gains the per-session prompt queue (step 8), it will prefer request_many over request for handlers structurally matching this Protocol.

Methods

  • request — Resolve one prompt; return the chosen action/payload.

  • request_many — Resolve a window of prompts in one call.

source method BatchedPromptHandler.request(prompt: OnlinePrompt) β†’ PromptResponse

Resolve one prompt; return the chosen action/payload.

source method BatchedPromptHandler.request_many(prompts: Sequence[OnlinePrompt]) β†’ Sequence[PromptResponse]

Resolve a window of prompts in one call.

Return one response per prompt, in the same order. The engine will not interleave request and request_many calls within a single drain window.

source class OnlineResult()

Per-file outcome of an online tagging call.

tags is the file’s full merged metadata, present even when the lookup applied nothing new (skip, no-match, deferred prompt) β€” it is NOT evidence of a match. matched is: it’s True only when an online source actually contributed metadata, so writers should gate on it to avoid re-writing a file with its own existing tags.

source class DeferredPrompt()

A prompt the session skipped under defer_prompts mode.

Captures everything Codex needs to render the prompt later in a review-tagging UI: the file it came from, source, candidates, match context, and the fingerprint used to key the dedup cache. Codex feeds the user’s resolution back via :meth:OnlineSession.preload_resolution.

source class OnlineSession(*, sources: Iterable[str] = (‘metron’, ‘comicvine’), ids: Mapping[str, int] | None = None, credentials: OnlineCredentials | None = None, match: MatchMode = MatchMode.AUTO, prompts: Prompts = Prompts.ASK, prompt_handler: PromptHandler | None = None, on_event: EventHandler | None = None, rematch: bool = False, first_wins: bool = True, defer_prompts: bool = False, series_batching: bool = True, config: ComicboxSettings | None = None, client_name: str | None = None)

Codex-friendly faΓ§ade over Comicbox’s online lookup.

Construction validates per-source credentials and pre-computes the ComicboxSettings layer that each per-file Comicbox instance will see. Mutable state β€” the lookup policy (match / prompts) and the cancel token β€” lives on the instance and may be updated from any thread. The policy lives in an OnlineSessionState shared with every box the session spawns, so a change made at a prompt needs no mirroring back: the box and the session read the same owner.

ids pins an issue id per source for a single-comic session: a pinned source fetches that id directly while the unpinned sources search, so one run can mix id retrieval and search and merge both.

config supplies the settings the session layers its tagging preferences over. An embedder that already holds configured ComicboxSettings β€” naming its own cache directory or effort, neither of which an OnlineSession keyword covers β€” passes them here instead of exporting environment variables for the settings loader to find. Omitted, the session reads config files and the environment itself.

Validate inputs, build per-session state. See class docstring.

Attributes

  • match : MatchMode — Current session match mode (read-only; mutate via set_match()).

  • prompts : Prompts — Current session prompt policy.

  • cancelled : bool — Whether cancel() has been called.

  • abort_reason : str | None — Why the session stopped itself, or None if nobody’s lookup aborted.

  • defer_prompts : bool — Whether the session is currently in defer-prompts mode.

Methods

  • set_match — Change the session match mode for subsequent file lookups.

  • set_prompts — Change the session prompt policy for subsequent file lookups.

  • cancel — Stop accepting new files. In-flight lookup runs to completion.

  • close — Release the pooled connections and cache handles both sources hold.

  • set_defer_prompts — Toggle defer-prompts mode for subsequent file lookups.

  • deferred_prompts — Snapshot the queued deferred prompts. Codex’s review-UI input.

  • clear_deferred_prompts — Drop the deferred-prompt queue without resolving them.

  • preload_resolution — Seed the dedup cache so a re-run auto-resolves the fingerprint.

  • preload_series_resolution — Seed the series cache from outside the session.

  • series_cache_snapshot — Snapshot the current series cache. Useful for persistence.

  • clear_series_cache — Drop every series resolution from the cache.

  • rate_limit_status — Snapshot of each enabled source’s current rate-limit budget.

  • tag — Tag one file synchronously.

  • tag_many — Tag many files sequentially. Stops accepting new ones on cancel.

source property OnlineSession.match: MatchMode

Current session match mode (read-only; mutate via set_match()).

set_match accepts MatchMode.ASK only while a prompt handler or deferred prompts can resolve the prompts it raises. A selector answering set_policy: "ask" sets it without that check, so this may report ASK where set_match would refuse.

source property OnlineSession.prompts: Prompts

Current session prompt policy.

source method OnlineSession.set_match(match: MatchMode) β†’ None

Change the session match mode for subsequent file lookups.

MatchMode.ASK prompts for every match, so it is accepted only while the session has a prompt handler or defers its prompts.

source method OnlineSession.set_prompts(prompts: Prompts) β†’ None

Change the session prompt policy for subsequent file lookups.

source method OnlineSession.cancel() β†’ None

Stop accepting new files. In-flight lookup runs to completion.

source method OnlineSession.close() β†’ None

Release the pooled connections and cache handles both sources hold.

Optional, and not a cancel: the session stays usable afterwards, at the cost of reopening what it needs on its next request. The upstream clients are process-wide per credential set, so closing while another OnlineSession in the same process is mid-run costs that run the same reopen. Acceptable, and the alternative – leaving the sockets and sqlite handles to be finalized – is what emits ResourceWarning.

Comic Vine is the one sharp edge: its rate-limit buckets are closed with the session, so a lookup already holding one can fail outright rather than merely reconnect. Close between files, not under one.

source property OnlineSession.cancelled: bool

Whether cancel() has been called.

source property OnlineSession.abort_reason: str | None

Why the session stopped itself, or None if nobody’s lookup aborted.

None after a plain :meth:cancel β€” that is the caller’s own doing and needs no explanation. Set when a lookup raised OnlineLookupAbortedError, most usefully when the day’s API quota ran out, which stops a run the caller never asked to stop.

source property OnlineSession.defer_prompts: bool

Whether the session is currently in defer-prompts mode.

source method OnlineSession.set_defer_prompts(*, defer: bool) β†’ None

Toggle defer-prompts mode for subsequent file lookups.

Turning deferral off leaves the match mode alone, even when it is MatchMode.ASK and no prompt handler is set. The ASK guard in the constructor and set_match is an entry check, not an invariant this toggle re-enforces: a session left in ASK without a resolver prompts for every match, and each lookup reports the missing resolver at prompt time, as for any session without one.

source method OnlineSession.deferred_prompts() β†’ tuple[DeferredPrompt, …]

Snapshot the queued deferred prompts. Codex’s review-UI input.

source method OnlineSession.clear_deferred_prompts() β†’ None

Drop the deferred-prompt queue without resolving them.

source method OnlineSession.preload_resolution(fingerprint: str, *, action: Literal[‘choose’, ‘skip’, ‘manual’], payload: int | str | None = None, chosen_volume_id: int | None = None) β†’ None

Seed the dedup cache so a re-run auto-resolves the fingerprint.

Codex’s defer-mode flow: drain deferred_prompts() after the batch, present them in a review UI, call preload_resolution() for each one the user resolves, then re-tag the affected files (with defer_prompts toggled off if desired). The cache hit fires the same way it does for in-batch dedup. A non-empty cache is enough to install the bridged selector β€” but with no handler and defer_prompts off, a cache MISS (the re-search produced a different candidate set than the fingerprint was minted from) raises rather than falling back to the interactive CLI prompt.

source method OnlineSession.preload_series_resolution(*, source: str, series_fingerprint: str, volume_id: int) β†’ None

Seed the series cache from outside the session.

Codex’s use case: persist resolved series across runs in its own DB and replay them at session-start to skip the cold-path search entirely on subsequent imports. Same first-writer-wins semantic as in-batch population β€” if the key is already cached, this is a no-op.

source method OnlineSession.series_cache_snapshot() β†’ dict[tuple[str, str], int]

Snapshot the current series cache. Useful for persistence.

source method OnlineSession.clear_series_cache() β†’ None

Drop every series resolution from the cache.

source method OnlineSession.rate_limit_status() β†’ dict[str, dict[str, Any]]

Snapshot of each enabled source’s current rate-limit budget.

Every entry maps a window name to a JSON-safe limit / remaining / reset_epoch triple, so one renderer handles both sources; only the window names differ, because the two services meter differently.

Metron carries the live per-account state mokkari>=4 tracks from X-RateLimit-* response headers::

{"burst": {"limit": 20, "remaining": 19, "reset_epoch": ...},
 "sustained": {"limit": 5000, "remaining": 4987, "reset_epoch": ...}}

The sustained (daily) limit varies by Metron OpenCollective donor tier, so it is only discoverable from these headers.

Comic Vine meters per resource, so it reports one window per endpoint pool it has actually used::

{"issues": {"limit": 200, "remaining": 197, "reset_epoch": ...},
 "search": {"limit": 200, "remaining": 199, "reset_epoch": ...}}

Its numbers are read from the rate-limit bucket file rather than from a live client, so they survive across runs and are available before this session issues a request. reset_epoch is when the window’s oldest request ages out and frees the next slot.

A source reports {} until it has data: for Metron that means no request has gone out yet in this process (no shared session, or no header seen); for Comic Vine, that no bucket file exists yet or it could not be read.

source method OnlineSession.tag(path: Path) β†’ OnlineResult

Tag one file synchronously.

source method OnlineSession.tag_many(paths: Iterable[Path]) β†’ Iterator[OnlineResult]

Tag many files sequentially. Stops accepting new ones on cancel.

When series_batching is on (the default), paths are reordered upfront by a lightweight filename-derived fingerprint so issues of the same series cluster together. The first issue of each cluster triggers the cold-path search; the remaining issues hit the per-session series cache and skip the search entirely. Order within a cluster is the original input order; the cluster order itself is deterministic (sorted by fingerprint) so re-runs produce identical cache-key sequences.

Counting each cluster up front also lets the source that resolves a series decide to list the whole thing once rather than once per comic (OnlineSource.prefetch_volume) β€” worth the most here, where the run is sequential and every saved request is saved wall clock too.