-
OnlineSource — Contract every online provider must implement.
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
Functions
-
refresh_cache_unlink_once — Delete a response-cache file once per process for CacheMode.REFRESH.
-
resolve_cache_db_path — Resolve a source’s cache sqlite path.
source refresh_cache_unlink_once(cache_path: Path) โ None
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-Aftera second time in every worker at once. -
search_skip_reason : str | None — A
SKIP_*reason for the last emptysearch(), 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.