Plugin API
Plugins are the site-specific half of Ladon. A plugin bundles a Source
(discovers top-level refs), one or more Expanders (fan out through the
URL tree), and a Sink (fetches each leaf and returns a record). All
protocols are structural (PEP 544) — no inheritance from Ladon is required.
Ladon ships two parallel protocol hierarchies: sync and async.
Running a plugin
Use run_plugin() for the normal whole-plugin path: it calls
plugin.source.discover(client) once, runs every discovered root in source
order, and returns a PluginRunResult. The aggregate exposes total leaf
counts and errors while retaining a RunResult per root in results.
RunConfig.leaf_limit applies separately to every discovered root. If a
later root raises a globally fatal error, earlier roots may already have run
their on_leaf callback; make that callback idempotent when retrying a whole
plugin run.
run_crawl(top_ref, ...) remains available when the caller intentionally
owns root discovery or needs to process one known root. The async equivalents
are async_run_plugin() and async_run_crawl(); async whole-plugin runs keep
root processing ordered, while each root's leaf work uses async_concurrency.
Sync protocols
typing.Protocol definitions for Ladon crawl plugins.
Adapters implement these protocols by structural subtyping — no inheritance from this module is required. This keeps third-party plugins decoupled from Ladon internals.
All adapters receive an object satisfying SyncHttpClientProtocol: either
the native HttpClient or curl-cffi CurlHttpClient implementation. They
must not construct their own HTTP sessions or import requests directly.
The three-layer pipeline is:
Source → [Expander, ...] → Sink
Source[RefT] produces top-level refs. Each
Expander[RefT, RecordT, ChildRawT] takes a ref and returns an
Expansion (record + child refs). Sink[RefT, RecordT] takes a leaf
ref and returns a final record. CrawlPlugin[TopRefT, LeafRefT,
LeafRecordT] bundles all three.
Source
Bases: Protocol[SourceRefT_co]
Discover top-level refs from an external source.
Source code in src/ladon/plugins/protocol.py
Expander
Bases: Protocol[ExpanderRefT_contra, ExpanderRecordT_co, ExpanderChildRawT_co]
Expand one ref into a record plus child refs.
Source code in src/ladon/plugins/protocol.py
expand(ref, client)
Fetch ref, return its record and the child refs to process next.
Raises:
| Type | Description |
|---|---|
ExpansionNotReadyError
|
ref is not yet ready to be expanded. |
PartialExpansionError
|
child list is incomplete. |
ChildListUnavailableError
|
child list could not be retrieved. |
Source code in src/ladon/plugins/protocol.py
Sink
Bases: Protocol[SinkRefT_contra, SinkRecordT_co]
Consume a leaf ref and return its final record.
Source code in src/ladon/plugins/protocol.py
consume(ref, client)
Fetch and parse one leaf ref, returning a complete record.
Context for the leaf (e.g. parent data) flows through
ref.raw — no parent-record parameter is needed here.
Raises:
| Type | Description |
|---|---|
LeafUnavailableError
|
ref could not be fetched or parsed. |
Source code in src/ladon/plugins/protocol.py
CrawlPlugin
Bases: Protocol[PluginTopRefT_co, PluginLeafRefT_contra, PluginLeafRecordT_co]
Bundle of all adapters for one crawl domain.
name is a short identifier used in log lines and error messages
(e.g. "acme_shop", "widgetco"). source produces
top-level refs. expanders is an ordered list of expansion steps
(one per tree level above the leaves). sink consumes the leaf
refs produced by the last expander.
The heterogeneous chain interior deliberately uses Any because a
single homogeneous sequence cannot express per-stage types; see ADR-015.
CLI convention
When loaded via ladon run --plugin module:Class, the CLI
instantiates the plugin as plugin_cls(client=client). Adapters
intended for CLI use must accept client as a keyword argument
in __init__. This constraint is not enforced by the Protocol
check — it is a CLI convention only and not part of this protocol.
Source code in src/ladon/plugins/protocol.py
Async protocols
The async protocols mirror the sync ones exactly but use async def
methods and accept AsyncHttpClientProtocol instead of
SyncHttpClientProtocol.
typing.Protocol definitions for async Ladon crawl plugins.
Async adapters implement these protocols by structural subtyping — no inheritance from this module is required.
All async adapters receive an object satisfying AsyncHttpClientProtocol:
either the native AsyncHttpClient or curl-cffi AsyncCurlHttpClient
implementation. They must not construct their own HTTP sessions or import
httpx directly.
The three-layer pipeline is:
AsyncSource → [AsyncExpander, ...] → AsyncSink
AsyncSource[RefT] discovers top-level refs. Each
AsyncExpander[RefT, RecordT, ChildRawT] awaits a ref and returns an
Expansion. AsyncSink[RefT, RecordT] awaits a leaf ref and returns a
final record. AsyncCrawlPlugin[TopRefT, LeafRefT, LeafRecordT] bundles
all three.
AsyncSource
Bases: Protocol[AsyncSourceRefT_co]
Discover top-level refs from an external source, asynchronously.
Source code in src/ladon/plugins/async_protocol.py
AsyncExpander
Bases: Protocol[AsyncExpanderRefT_contra, AsyncExpanderRecordT_co, AsyncExpanderChildRawT_co]
Expand one ref into a record plus child refs, asynchronously.
Source code in src/ladon/plugins/async_protocol.py
expand(ref, client)
async
Fetch ref, return its record and the child refs to process next.
Raises:
| Type | Description |
|---|---|
ExpansionNotReadyError
|
ref is not yet ready to be expanded. |
PartialExpansionError
|
child list is incomplete. |
ChildListUnavailableError
|
child list could not be retrieved. |
Source code in src/ladon/plugins/async_protocol.py
AsyncSink
Bases: Protocol[AsyncSinkRefT_contra, AsyncSinkRecordT_co]
Consume a leaf ref and return its final record, asynchronously.
Source code in src/ladon/plugins/async_protocol.py
consume(ref, client)
async
Fetch and parse one leaf ref, returning a complete record.
Context for the leaf flows through ref.raw — no parent-record
parameter is needed here.
Raises:
| Type | Description |
|---|---|
LeafUnavailableError
|
ref could not be fetched or parsed. |
Source code in src/ladon/plugins/async_protocol.py
AsyncCrawlPlugin
Bases: Protocol[AsyncPluginTopRefT_co, AsyncPluginLeafRefT_contra, AsyncPluginLeafRecordT_co]
Bundle of all async adapters for one crawl domain.
name is a short identifier used in log lines and error messages.
source discovers top-level refs. expanders is an ordered list
of async expansion steps. sink consumes the leaf refs produced by
the last expander.
The heterogeneous chain interior deliberately uses Any because a
single homogeneous sequence cannot express per-stage types; see ADR-015.
Source code in src/ladon/plugins/async_protocol.py
Data models
Immutable data models for Ladon plugin adapters.
All models are frozen dataclasses. Adapters produce them; the runner
consumes them. The raw field on Ref carries house-specific data
that does not fit the shared schema.
Ref[RawT] preserves adapter-specific raw context. Expansion[RecordT,
ChildRawT] carries the current record and typed child refs to the next stage.
Ref
dataclass
Bases: Generic[RawT]
Generic reference to any crawlable resource.
url is the canonical URL of the resource. raw carries any
house-specific data discovered alongside the URL (e.g. an ID or code
needed by the expander). Omitting raw selects the default
Mapping[str, object] specialization and supplies an empty dict.
Source code in src/ladon/plugins/models.py
Expansion
dataclass
Bases: Generic[RecordT_co, ChildRawT_co]
Result of an Expander.expand() call.
Carries the record for the expanded node plus the child refs to be processed next (either expanded further or consumed by a Sink).
Source code in src/ladon/plugins/models.py
Errors
Error taxonomy for Ladon house plugins.
Each exception maps to a specific runner behaviour. Expansion signals remain typed because they describe tree completeness; non-fatal Phase-3 exceptions are recorded independently so one bad leaf does not discard the rest of a run.
PluginError
ExpansionNotReadyError
Bases: PluginError
The ref is not yet ready to be expanded (e.g. content not live).
The runner should skip this ref without writing to DB or disk. Do not retry during the same run; the ref will be discovered again on the next scheduled run.
Source code in src/ladon/plugins/errors.py
PartialExpansionError
Bases: PluginError
The child list was fetched but is incomplete (e.g. a paginated response returned fewer items than the declared total).
Runner behaviour: non-fatal for non-first expanders — the affected
branch is isolated and recorded in RunResult.errors. Propagates
unchanged from the first expander. Always fatal from a Sink because
Phase 3 has no branch to isolate.
Raise this instead of ChildListUnavailableError when the HTTP
response was valid but the payload signals an incomplete result.
Raise ChildListUnavailableError when the response could not be
parsed or the request itself failed.
Source code in src/ladon/plugins/errors.py
ChildListUnavailableError
Bases: PluginError
The child list could not be retrieved.
Fatal for this ref's run. Raised when the network request succeeded but the response cannot be parsed into a usable child list. Always fatal from a Sink because Phase 3 has no branch to isolate.
Source code in src/ladon/plugins/errors.py
LeafUnavailableError
Bases: PluginError
A single leaf ref could not be fetched or parsed.
Non-fatal. The runner logs the failure, increments leaves_failed, and continues to the next leaf.
AssetDownloadError
Bases: PluginError
An asset download failed.
Not recovered from by the runner — propagates as a fatal error that aborts the run. Plugins requiring non-fatal handling must catch this exception internally before returning from the Sink or Expander.