comicbox.formats.base.online.sources.base

source module comicbox.formats.base.online.sources.base

OnlineSource abstract base class.

Each concrete subclass wraps one upstream API client (mokkari, simyan, โ€ฆ) behind a uniform contract the lookup mixin can drive.

Classes

  • OnlineSource — Contract every online provider must implement.

Functions

Delete a response-cache file once per process for CacheMode.REFRESH.

REFRESH means “discard stale state at run start”, not “disable caching”. Clients are rebuilt during a run (and sources per file), so an unguarded unlink wiped the response cache between calls โ€” e.g. between search() and get() of the same file โ€” defeating the +1-API-call-per-unique-volume amortization get() relies on and re-spending API budget on every file of a --cache refresh batch.

source resolve_cache_db_path(cache_dir: Path | None, name: str, suffix: str = ‘cache’, *, create: bool = True) โ†’ Path

Resolve a source’s cache sqlite path.

Honours an explicit online.cache_dir; otherwise uses the platformdirs user cache path for comicbox. create=False skips making the parent directory, for read-only callers (a budget readout) that must not have side effects.

source class OnlineSource(credentials: OnlineSourceCredentials, settings: OnlineSettings)

Bases : ABC

Contract every online provider must implement.

Subclasses set the three ClassVar identifiers below and implement is_configured, get, and (in M3 onward) search.

Store refs needed for client construction.

Attributes

  • paces_rate_limit : ClassVar[bool] — same Retry-After a second time in every worker at once.

  • search_skip_reason : str | None — A SKIP_* reason for the last empty search(), or None.

Methods

  • reset_search_skip_reason — Clear the reason, which a source does at the top of its own search.

  • is_configured — Return True when the source has every credential it needs.

  • get — Fetch a full issue record by upstream id, return as a plain dict.

  • search — Build search criteria from a comic profile and return candidates.

  • probe — Verify the credentials with this source’s cheapest request.

  • classify_retry_exception — Classify one exception from this source’s client library.

  • lookup_issue — Direct volume-scoped issue lookup; bypasses the fuzzy search path.

  • prefetch_volume — Optionally pull a whole volume’s issue list in one go.

  • cache_db_path — Resolve the cache sqlite path for this source, creating its parent.

source property OnlineSource.search_skip_reason: str | None

A SKIP_* reason for the last empty search(), or None.

Set by a source that declined to search at all, so the lookup reports the file as skipped-with-a-reason rather than as a source that looked and found nothing. The two are not interchangeable: a comic nobody looked at should be tried again, and one that genuinely missed should not.

source method OnlineSource.reset_search_skip_reason() โ†’ None

Clear the reason, which a source does at the top of its own search.

The reason’s whole lifetime belongs to the source that sets it. The lookup only reads it, and reads it structurally โ€” a source is whatever implements the search protocol, not necessarily a subclass of this, and most never decline to search at all.

source method OnlineSource.is_configured() โ†’ bool

Return True when the source has every credential it needs.

source method OnlineSource.get(issue_id: int) โ†’ dict[str, Any]

Fetch a full issue record by upstream id, return as a plain dict.

The shape is whatever the upstream library returns post-model_dump (or equivalent). Comicbox’s per-source transform converts it to the internal schema.

source method OnlineSource.search(profile: ComicProfile) โ†’ list[Candidate]

Build search criteria from a comic profile and return candidates.

Each candidate carries display fields, the upstream id, and any precomputed cover hash. The matcher scores them; the lookup mixin decides AUTO_WRITE / PROMPT / SKIP / NO_MATCH.

source method OnlineSource.probe() โ†’ Mapping[str, Mapping[str, Any]] | None

Verify the credentials with this source’s cheapest request.

One request, on a private client with no response cache, so neither a shared session nor a cached answer can stand in for the server’s, and never retried: comicbox doctor reports the first answer. Exceptions propagate for classify_retry_exception to name.

Returns the rate-limit windows the request left, as {window: {"limit", "remaining", "reset_epoch"}}, or None when the source keeps none.

source staticmethod OnlineSource.classify_retry_exception(exc: BaseException) โ†’ RetryCategory | None

Classify one exception from this source’s client library.

Consulted by the with_retry decorator (retry._classify) off the bound instance โ€” the same seam as retry_sleep/on_rate_limit. Each subclass owns its client library’s taxonomy: isinstance against the real exception classes, imported inside the method to honor the package-wide lazy-import convention. Return None for exceptions this library did not raise; retry.py then falls back to its conservative default (programmer/config errors raise, the rest retry on the generic schedule).

source method OnlineSource.lookup_issue(volume_id: int, issue_number: str | None) โ†’ Candidate | None

Direct volume-scoped issue lookup; bypasses the fuzzy search path.

Series-first batching (plan ยง3.10) caches a resolved volume_id per series fingerprint once per session. Subsequent comics in the same series skip the expensive search call and ask the source “what’s issue N in volume V” directly โ€” same rate-limit budget, cheaper per-call and unambiguous on healthy data sources.

Return one Candidate when the lookup finds a matching issue, or None when it doesn’t. None signals the caller to fall back to the normal search path (the cache entry stays unchanged so the first-writer-wins discipline holds).

Sources that haven’t implemented this raise NotImplementedError; callers detect that via try/except and fall back to search.

Failure semantics live here so each source only implements the actual API call (_lookup_issue_in_volume): source-side errors are logged and degrade to None (fall back to search), abort signals propagate, NotImplementedError propagates.

source method OnlineSource.prefetch_volume(volume_id: int, cluster_size: int) โ†’ None

Optionally pull a whole volume’s issue list in one go.

Deliberately concrete and empty, not abstract: a source that cannot list a volume cheaply has nothing to implement, and making this abstract would force every source to write the same no-op.

Called once, by the worker that resolved volume_id, when a batch holds cluster_size comics from that series. A source that can list a volume in fewer requests than the per-comic lookups it would replace should do so and serve the rest of the cluster from memory; one that cannot should do nothing, which is the default.

Best-effort by contract: a failure here must degrade to the per-comic path, never fail the comic. Implementations own that.

source method OnlineSource.cache_db_path(suffix: str = ‘cache’) โ†’ Path

Resolve the cache sqlite path for this source, creating its parent.

See resolve_cache_db_path, which read-only callers share.