Skip to content

Synchronous components

To reuse blocking implementations, subclass the synchronous interfaces and wrap each object in its adapter. The result can be passed to either pipeline:

Sync interface Adapter Where the calls run
Extractor SyncExtractorAdapter fetch_full_text in a worker thread; search and parse_listing on the event loop, between pages
RelevanceFilter SyncRelevanceFilterAdapter worker thread
EntityExtractor SyncEntityExtractorAdapter worker thread
LLMClient SyncLLMClientAdapter worker thread
Exporter SyncExporterAdapter on the event loop, so exports never interleave
StateManager SyncStateManagerAdapter on the event loop, so updates are never lost
import json

from sci_etl_core import Exporter, SyncExporterAdapter


class JsonLinesExporter(Exporter):
    def export(self, data, destination):
        with open(destination, "a", encoding="utf-8") as handle:
            for row in data:
                handle.write(json.dumps(row) + "\n")


exporter = SyncExporterAdapter(JsonLinesExporter())

Pass the adapter wherever the pipeline expects the async interface, for example AsyncETLPipeline(..., exporter=exporter).

Calls that run on the event loop block it while they run: a slow exporter or state manager stalls every record in flight. Keep those calls fast, or implement the async interface instead.