Skip to main content

Plugin System

IterableData provides a comprehensive plugin system that enables third-party developers to extend the library with custom formats, codecs, database drivers, validation rules, and engines without modifying the core library.

Overview

The plugin system uses Python's entry point mechanism (via setuptools/pip) for automatic plugin discovery. Plugins are discovered lazily when needed and integrated seamlessly with existing IterableData functionality.

Key Features

  • Automatic Discovery: Plugins are discovered via entry points - no manual registration needed
  • Backward Compatible: Built-in formats/codecs take precedence over plugins
  • Error Isolation: Plugin errors don't crash the core library
  • Metadata Support: Plugins can provide version, author, and custom metadata
  • Programmatic Registration: Alternative to entry points for dynamic registration

Plugin Types

The plugin system supports five types of plugins:

  1. Format Plugins - Custom data formats
  2. Codec Plugins - Custom compression codecs
  3. Database Driver Plugins - Custom database drivers
  4. Validation Rule Plugins - Custom validation rules
  5. Engine Plugins - Custom processing engines

Creating a Format Plugin

Step 1: Create Your Format Class

# myplugin/format.py
from iterable.base import BaseFileIterable
from iterable.types import Row

class CustomFormatIterable(BaseFileIterable):
"""Custom format implementation."""

@staticmethod
def id() -> str:
return "customformat"

@staticmethod
def fileexts() -> list[str]:
return [".custom", ".cf"]

def read(self, skip_empty: bool = True) -> Row:
"""Read a row from the custom format."""
# Your implementation here
...

def read_bulk(self, num: int = 1000) -> list[Row]:
"""Read multiple rows."""
# Your implementation here
...

Step 2: Register via Entry Point

In your pyproject.toml:

[project.entry-points."iterabledata.formats"]
customformat = "myplugin.format:CustomFormatIterable"

Or in setup.py:

setup(
name="iterabledata-plugin-customformat",
entry_points={
"iterabledata.formats": [
"customformat = myplugin.format:CustomFormatIterable",
],
},
)

Step 3: Install and Use

pip install -e .

Now your format can be used just like built-in formats:

from iterable import open_iterable

with open_iterable("data.custom") as source:
for row in source:
print(row)

Creating a Codec Plugin

# myplugin/codec.py
from iterable.codecs.base import BaseCodec

class CustomCodec(BaseCodec):
"""Custom compression codec."""

@staticmethod
def fileexts() -> list[str]:
return [".custom", ".cc"]

def open(self) -> None:
"""Open the codec."""
# Your implementation here
...

def read(self, size: int = -1) -> bytes:
"""Read compressed data."""
# Your implementation here
...

Register in pyproject.toml:

[project.entry-points."iterabledata.codecs"]
customcodec = "myplugin.codec:CustomCodec"

Creating a Database Driver Plugin

# myplugin/db.py
from iterable.db.base import DBDriver
from collections.abc import Iterator
from iterable.types import Row

class CustomDBDriver(DBDriver):
"""Custom database driver."""

def connect(self) -> None:
"""Establish database connection."""
# Your implementation here
...

def iterate(self) -> Iterator[Row]:
"""Return iterator of rows."""
# Your implementation here
...

Register in pyproject.toml:

[project.entry-points."iterabledata.database_drivers"]
customdb = "myplugin.db:CustomDBDriver"

Use with open_iterable():

with open_iterable(
"customdb://connection_string",
engine="customdb",
iterableargs={"query": "SELECT * FROM table"}
) as source:
for row in source:
print(row)

Creating a Validation Rule Plugin

# myplugin/validation.py
def validate_custom_field(value: Any) -> tuple[bool, str | None]:
"""Validate a custom field."""
if not value:
return False, "Field is required"
if len(str(value)) < 5:
return False, "Field must be at least 5 characters"
return True, None

Register in pyproject.toml:

[project.entry-points."iterabledata.validation_rules"]
custom.field = "myplugin.validation:validate_custom_field"

Use with validation hooks:

from iterable.helpers.validation import rules_validator

rules = {"field_name": ["custom.field"]}
hook = rules_validator(rules)

with open_iterable("data.csv", iterableargs={"validation_hook": hook}) as source:
for row in source:
process(row)

Programmatic Registration

Instead of entry points, you can register plugins programmatically:

from iterable.plugins import register_format

register_format(
"customformat",
"myplugin.format",
"CustomFormatIterable",
metadata={"version": "1.0.0", "author": "Your Name"}
)

Plugin Discovery

Plugins are discovered automatically when IterableData is first used. Discovery happens lazily and is cached for performance.

Manual Discovery

You can manually trigger plugin discovery:

from iterable.plugins import discover_plugins

discover_plugins()

Checking Registered Plugins

from iterable.plugins import get_plugin_registry

registry = get_plugin_registry()

# List all registered formats (including plugins)
formats = registry.list_formats()
print(f"Registered formats: {formats}")

# Get plugin metadata
metadata = registry.get_metadata("format:customformat")
print(f"Plugin metadata: {metadata}")

Priority System

Built-in formats and codecs always take precedence over plugins. If a plugin registers a format with the same ID as a built-in format, the built-in format will be used.

# This will NOT override the built-in CSV format
register_format("csv", "myplugin.format", "MyCSVFormat")

# Built-in CSV is still used
from iterable.helpers.detect import _get_format_registry
registry = _get_format_registry()
assert registry["csv"] == ("iterable.datatypes.csv", "CSVIterable")

Error Handling

Plugin errors are isolated and don't crash the core library:

  • Import Errors: If a plugin's dependencies are missing, the plugin is skipped
  • Registration Errors: Invalid entry points are logged but don't stop discovery
  • Runtime Errors: Plugin errors during use are handled according to error policies

Plugin Metadata

Plugins can provide metadata for version tracking and documentation:

register_format(
"customformat",
"myplugin.format",
"CustomFormatIterable",
metadata={
"version": "1.0.0",
"author": "Your Name",
"description": "Custom format plugin",
"homepage": "https://example.com/plugin"
}
)

Metadata is accessible via:

registry = get_plugin_registry()
metadata = registry.get_metadata("format:customformat")

Testing Plugins

When testing plugins, you may want to reset the discovery state:

from iterable.plugins import reset_discovery, discover_plugins

# Reset discovery state
reset_discovery()

# Re-discover plugins
discover_plugins()

Complete Example

Here's a complete example of a format plugin package:

Project Structure

iterabledata-plugin-customformat/
├── pyproject.toml
├── src/
│ └── customformat/
│ ├── __init__.py
│ └── format.py
└── README.md

pyproject.toml

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "iterabledata-plugin-customformat"
version = "1.0.0"
description = "Custom format plugin for IterableData"
requires-python = ">=3.10"
dependencies = ["iterabledata"]

[project.entry-points."iterabledata.formats"]
customformat = "customformat.format:CustomFormatIterable"

src/customformat/format.py

from iterable.base import BaseFileIterable
from iterable.types import Row

class CustomFormatIterable(BaseFileIterable):
@staticmethod
def id() -> str:
return "customformat"

@staticmethod
def fileexts() -> list[str]:
return [".custom", ".cf"]

def read(self, skip_empty: bool = True) -> Row:
# Implementation
...

Installation and Usage

pip install -e .
from iterable import open_iterable

with open_iterable("data.custom") as source:
for row in source:
print(row)

Reference plugin package

A minimal third-party package that adds one format end to end:

iterabledata-plugin-acme/
pyproject.toml
src/iterabledata_plugin_acme/
__init__.py
acme.py # BaseFileIterable subclass
tests/test_acme.py
README.md

pyproject.toml:

[project]
name = "iterabledata-plugin-acme"
version = "0.1.0"
dependencies = ["iterabledata>=1.0"]

[project.entry-points."iterabledata.formats"]
acme = "iterabledata_plugin_acme.acme:AcmeIterable"

Implement AcmeIterable like any in-tree format (id(), fileexts(), read(), raise WriteNotSupportedError if read-only). Then:

pip install -e ./iterabledata-plugin-acme
from iterable import open_iterable

with open_iterable("sample.acme") as source:
for row in source:
print(row)

Built-in format ids always win over plugins with the same id. For an in-tree checklist (registry, detection, docs site), see Adding formats.

Best Practices

  1. Follow Naming Conventions: Use descriptive plugin IDs (e.g., customformat not cf)
  2. Provide Metadata: Include version, author, and description in metadata
  3. Handle Errors Gracefully: Implement proper error handling in your plugin
  4. Test Thoroughly: Test your plugin with various data sources
  5. Document Usage: Provide clear documentation and examples
  6. Version Compatibility: Test with different IterableData versions