Skip to content

Plugin System Overview

The scraping framework uses a decorator-based plugin system to keep the pipeline extensible without modifying core code.

Architecture

graph TD
    Registry["core/registry.py<br/>(decorator registry)"]
    Fetcher["BaseFetcherPlugin"]
    Parser["BaseParserPlugin"]
    Storage["BaseStoragePlugin"]

    subgraph Fetchers
        CB[cloakbrowser_fetcher]
        WR[wreq_fetcher]
    end
    subgraph Parsers
        GP[generic_parser]
        JP[jsonld_parser]
        AP[apollo_json_parser]
        G2[g2_listing_parser]
        HP[hermes_listing_parser]
        IP[idealista_detail]
        TP[trustpilot_nextdata_parser]
        JPP[jsonpath_parser]
    end
    subgraph Storage
        S3[garage_s3_storage]
        PG[postgres_storage]
        JS[json_storage]
        CS[csv_storage]
        MS[markdown_storage]
    end

    Fetcher --> Registry
    Parser --> Registry
    Storage --> Registry

    Registry --> CB & WR
    Registry --> GP & JP & AP & G2 & HP & IP & TP & JPP
    Registry --> S3 & PG & JS & CS & MS

Plugin Registration

Plugins register themselves via decorators:

from core.registry import register_fetcher, register_parser, register_storage

@register_fetcher
class MyFetcher(BaseFetcherPlugin):
    plugin_id = "my_fetcher"

    async def fetch(self, url: str, context: dict) -> dict:
        ...

The registry is populated by registry._load_builtins() which is called once at worker startup. Profiles reference plugins by their plugin_id:

# profile.yaml
stages:
  listing:
    plugin: my_fetcher     # ← plugin_id
    parser: generic         # ← parser plugin_id

Base Classes

All plugins inherit from abstract base classes in plugins/base.py:

BaseFetcherPlugin

class BaseFetcherPlugin(ABC):
    plugin_id: str

    @abstractmethod
    async def fetch(self, url: str, context: dict) -> dict:
        """Fetch a URL and return raw response data.

        Returns:
            {"url": str, "status": int, "html": str, "headers": dict}
        """

BaseParserPlugin

class BaseParserPlugin(ABC):
    plugin_id: str

    @classmethod
    def from_stage_config(cls, selectors, stage_config):
        """Build parser from stage configuration."""

    @abstractmethod
    def parse(self, raw: dict) -> list[dict]:
        """Parse raw HTML into structured items.

        Args:
            raw: {"url": str, "status": int, "html": str, "headers": dict, "plugin_id": str}
        Returns:
            list of structured item dicts
        """

BaseStoragePlugin

class BaseStoragePlugin(ABC):
    plugin_id: str

    @abstractmethod
    async def store(self, items: list[dict]) -> None:
        """Persist a batch of structured items."""

[!TIP] Use Claude to enrich this section with detailed documentation for each plugin's internal logic, edge cases, and configuration options.