Phase 9: Plugin Architecture¶
Truthound's plugin architecture is designed for extensibility and maintainability. External packages can extend validators, reporters, datasources, and more.
Table of Contents¶
Overview¶
Key components of the plugin architecture:
- PluginManager: Plugin lifecycle management (discovery, load, activate, unload)
- PluginRegistry: Plugin registration and lookup
- HookManager: Event-based extension system
- PluginDiscovery: Automatic plugin discovery (Entry points, directory scanning)
Quick Start¶
Using Plugins¶
from truthound.plugins import PluginManager, get_plugin_manager
# Use global manager
manager = get_plugin_manager()
# Discover plugins
manager.discover_plugins()
# Load specific plugin
manager.load_plugin("my-validator-plugin")
# Load all plugins
manager.load_all()
# Check active plugins
for plugin in manager.get_active_plugins():
print(f"{plugin.name} v{plugin.version}")
Managing Plugins via CLI¶
# List discovered plugins
truthound plugins list
# Plugin details
truthound plugins info my-plugin
# Load plugin
truthound plugins load my-plugin
# Unload plugin
truthound plugins unload my-plugin
# Enable/disable plugin
truthound plugins enable my-plugin
truthound plugins disable my-plugin
# Create new plugin template
truthound plugins create my-new-plugin --type validator
CLI Command Behavior¶
| Command | Description | Prerequisites |
|---|---|---|
list |
Shows all discovered plugins | None |
info |
Displays plugin metadata | None |
load |
Loads and optionally activates a plugin | Plugin must be discovered |
unload |
Unloads a loaded plugin | Plugin must be loaded |
enable |
Enables a plugin (loads if necessary) | Plugin must be discovered |
disable |
Disables a plugin (loads if necessary) | Plugin must be discovered |
Important Notes:
- The
unloadcommand only works on plugins that are currently loaded. Attempting to unload a plugin that has not been loaded will result in an error: - The
enableanddisablecommands will automatically load the plugin if it is not already loaded. - Each CLI invocation creates a new plugin manager instance, so plugin state is not persisted between commands unless explicitly saved.
Plugin Types¶
1. ValidatorPlugin¶
Adds custom validation rules.
from truthound.plugins import ValidatorPlugin, PluginInfo, PluginType
from truthound.validators.base import Validator, ValidationIssue
from truthound.types import Severity
import polars as pl
class MyValidator(Validator):
name = "my_validator"
category = "custom"
def validate(self, lf: pl.LazyFrame) -> list[ValidationIssue]:
issues = []
# Implement validation logic
return issues
class MyValidatorPlugin(ValidatorPlugin):
def _get_plugin_name(self) -> str:
return "my-validator-plugin"
def _get_plugin_version(self) -> str:
return "1.0.0"
def _get_description(self) -> str:
return "Custom validators for my use case"
def get_validators(self) -> list[type]:
return [MyValidator]
2. ReporterPlugin¶
Adds new output formats.
from truthound.plugins import ReporterPlugin
from truthound.reporters.base import ValidationReporter, ReporterConfig
from truthound.core import ValidationResult
class XMLReporter(ValidationReporter[ReporterConfig]):
name = "xml"
file_extension = ".xml"
def render(self, data: ValidationResult) -> str:
# XML rendering logic
return "<report>...</report>"
class XMLReporterPlugin(ReporterPlugin):
def _get_plugin_name(self) -> str:
return "xml-reporter"
def get_reporters(self) -> dict[str, type]:
return {"xml": XMLReporter}
3. HookPlugin¶
Registers hooks that respond to events.
from truthound.plugins import HookPlugin, HookType
from typing import Any, Callable
class NotifierPlugin(HookPlugin):
def _get_plugin_name(self) -> str:
return "notifier"
def get_hooks(self) -> dict[str, Callable]:
return {
HookType.AFTER_VALIDATION.value: self._on_validation_complete,
HookType.ON_ERROR.value: self._on_error,
}
def _on_validation_complete(self, datasource, result, issues, **kwargs):
if issues:
print(f"Found {len(issues)} issues!")
def _on_error(self, error, context, **kwargs):
print(f"Error occurred: {error}")
4. DataSourcePlugin¶
Adds new data source types.
from truthound.plugins import DataSourcePlugin
from truthound.datasources.base import BaseDataSource
class MongoDataSource(BaseDataSource):
source_type = "mongodb"
# Implementation...
class MongoPlugin(DataSourcePlugin):
def _get_plugin_name(self) -> str:
return "mongodb-source"
def get_datasource_types(self) -> dict[str, type]:
return {"mongodb": MongoDataSource}
Creating Plugins¶
Directory Structure¶
truthound-plugin-myfeature/
├── myfeature/
│ ├── __init__.py
│ └── plugin.py
├── pyproject.toml
└── README.md
pyproject.toml Configuration¶
[project]
name = "truthound-plugin-myfeature"
version = "0.1.0"
dependencies = ["truthound>=0.1.0"]
[project.entry-points."truthound.plugins"]
myfeature = "myfeature:MyFeaturePlugin"
Creating Templates via CLI¶
# Create validator plugin + auto install (recommended)
truthound new plugin my_validator --type validator --install
# Create reporter plugin + auto install
truthound new plugin my_reporter --type reporter --install
# Create hook plugin + auto install
truthound new plugin my_notifier --type hook --install
# Create without install (manual installation required)
truthound new plugin my_validator --type validator
cd truthound-plugin-my_validator && pip install -e .
Tip: Using the
--install(-i) flag automatically runspip install -e .after plugin creation, making it immediately available for use.
Hook System¶
Available Hook Types¶
| Hook | Description | Handler Signature |
|---|---|---|
before_validation |
Before validation starts | (datasource, validators, **kwargs) |
after_validation |
After validation completes | (datasource, result, issues, **kwargs) |
on_issue_found |
When issue is found | (issue, validator, **kwargs) |
before_profile |
Before profiling starts | (datasource, config, **kwargs) |
after_profile |
After profiling completes | (datasource, profile, **kwargs) |
on_report_generate |
When report is generated | (report, format, **kwargs) |
on_error |
When error occurs | (error, context, **kwargs) |
on_plugin_load |
When plugin is loaded | (plugin, manager) |
on_plugin_unload |
When plugin is unloaded | (plugin, manager) |
Using Decorators¶
from truthound.plugins import before_validation, after_validation, on_error
@before_validation(priority=50) # Lower priority executes first
def log_start(datasource, validators, **kwargs):
print(f"Validating {datasource} with {len(validators)} validators")
@after_validation()
def log_complete(datasource, result, issues, **kwargs):
print(f"Found {len(issues)} issues")
@on_error()
def handle_error(error, context, **kwargs):
print(f"Error: {error}")
Using HookManager Directly¶
from truthound.plugins import HookManager, HookType
hooks = HookManager()
# Register hook
hooks.register(
HookType.BEFORE_VALIDATION,
my_handler,
priority=100,
source="my-plugin"
)
# Trigger hook
results = hooks.trigger(
HookType.BEFORE_VALIDATION,
datasource=source,
validators=["null", "range"]
)
# Disable hooks from specific source
hooks.disable(source="my-plugin")
CLI Commands¶
Plugin Creation¶
# Create plugin + auto install (recommended)
truthound new plugin my_validator --type validator --install
# Using short options
truthound new plugin my_validator -t validator -i
# Create with all options
truthound new plugin enterprise \
--type full \
--author "Your Name" \
--description "Enterprise validators" \
--install \
--output ./my-plugins/
Plugin Management¶
# List plugins (with details)
truthound plugins list --verbose
# JSON output
truthound plugins list --json
# Filter by type
truthound plugins list --type validator
# Filter by state
truthound plugins list --state active
# Plugin info
truthound plugins info my-plugin --json
# Load plugin
truthound plugins load my-plugin --activate
# Unload plugin
truthound plugins unload my-plugin
# Enable/disable plugin
truthound plugins enable my-plugin
truthound plugins disable my-plugin
Advanced Usage¶
Plugin Configuration¶
from truthound.plugins import PluginManager, PluginConfig
manager = PluginManager()
# Per-plugin configuration
config = PluginConfig(
enabled=True,
priority=50, # Load order (lower = earlier)
settings={
"api_key": "...",
"timeout": 30,
},
auto_load=True,
)
manager.set_plugin_config("my-plugin", config)
manager.load_plugin("my-plugin")
Dependency Management¶
from truthound.plugins import Plugin, PluginInfo, PluginType
class DependentPlugin(Plugin):
@property
def info(self) -> PluginInfo:
return PluginInfo(
name="dependent-plugin",
version="1.0.0",
plugin_type=PluginType.CUSTOM,
dependencies=("base-plugin",), # Depends on other plugin
python_dependencies=("requests", "jinja2"), # Python package dependencies
)
Version Compatibility¶
PluginInfo(
name="my-plugin",
version="1.0.0",
plugin_type=PluginType.VALIDATOR,
min_truthound_version="0.5.0",
max_truthound_version="2.0.0",
)
Context Manager Usage¶
from truthound.plugins import PluginManager
with PluginManager() as manager:
manager.discover_plugins()
manager.load_all()
# Perform operations...
# All plugins automatically unloaded
Loading Plugins from Directory¶
from truthound.plugins import PluginManager
from pathlib import Path
manager = PluginManager()
manager.add_plugin_directory(Path("./my-plugins"))
manager.discover_plugins()
Example Plugins¶
Truthound includes reference example plugins:
from truthound.plugins.examples import (
CustomValidatorPlugin, # Custom business rule validator
SlackNotifierPlugin, # Slack notification hook
XMLReporterPlugin, # XML reporter
)
See the truthound/plugins/examples/ directory for detailed implementations.
API Reference¶
Core Classes¶
Plugin[ConfigT]: Plugin base classPluginConfig: Plugin configurationPluginInfo: Plugin metadataPluginType: Plugin type enumPluginState: Plugin state enum
Specialized Base Classes¶
ValidatorPlugin: Validator plugin base classReporterPlugin: Reporter plugin base classDataSourcePlugin: DataSource plugin base classHookPlugin: Hook plugin base class
Management¶
PluginManager: Plugin lifecycle managementPluginRegistry: Plugin registration/lookupPluginDiscovery: Plugin discoveryHookManager: Hook registration/execution
Exceptions¶
PluginError: Base plugin errorPluginLoadError: Load failurePluginNotFoundError: Plugin not foundPluginDependencyError: Dependency not satisfiedPluginCompatibilityError: Version incompatibility
Enterprise Features¶
Advanced plugin features are included for enterprise environments:
Enterprise Plugin Manager¶
from truthound.plugins import create_enterprise_manager
# Create enterprise manager with security level
manager = create_enterprise_manager(
security_level="enterprise", # "development", "standard", "enterprise", "strict"
require_signature=True, # Require plugin signature
enable_hot_reload=True, # Enable hot reload
)
# Load plugin
plugin = await manager.load("my-plugin")
# Execute in sandbox
result = await manager.execute_in_sandbox("my-plugin", my_function, arg1, arg2)
Security Sandbox¶
Execute plugins in an isolated environment to enhance system security:
from truthound.plugins import (
SandboxFactory,
IsolationLevel,
SecurityPolicyPresets,
)
# Create sandbox by isolation level
sandbox = SandboxFactory().create(IsolationLevel.PROCESS)
# Use security policy preset
policy = SecurityPolicyPresets.ENTERPRISE.to_policy()
Code Signing¶
Verify plugin integrity and origin:
from pathlib import Path
from truthound.plugins import (
SigningServiceImpl,
SignatureAlgorithm,
TrustStoreImpl,
TrustLevel,
create_verification_chain,
)
# Sign plugin
service = SigningServiceImpl(
algorithm=SignatureAlgorithm.HMAC_SHA256,
signer_id="my-org",
)
signature = service.sign(
plugin_path=Path("my_plugin/"),
private_key=b"secret_key",
)
# Configure trust store
trust_store = TrustStoreImpl()
trust_store.set_signer_trust("my-org", TrustLevel.TRUSTED)
# Verify signature
chain = create_verification_chain(trust_store=trust_store)
result = chain.verify(plugin_path, signature, context={})
Hot Reload¶
Reload plugins without restarting the application:
from truthound.plugins import HotReloadManager, ReloadStrategy, LifecycleManager
lifecycle = LifecycleManager()
reload_manager = HotReloadManager(
lifecycle,
default_strategy=ReloadStrategy.GRACEFUL,
)
# Start watching plugin
await reload_manager.watch(
plugin_id="my-plugin",
plugin_path=Path("plugins/my-plugin/"),
auto_reload=True,
)
# Manual reload
result = await reload_manager.reload("my-plugin")
Version Constraints¶
Supports semantic version constraints:
from truthound.plugins import parse_constraint
# Various version constraint expressions
constraint = parse_constraint("^1.2.3") # >=1.2.3 && <2.0.0
constraint = parse_constraint("~1.2.3") # >=1.2.3 && <1.3.0
constraint = parse_constraint(">=1.0.0,<2.0.0") # Range specification
# Check version compatibility
is_compatible = constraint.is_satisfied_by("1.5.0")
Dependency Graph¶
Automatic plugin dependency management:
from truthound.plugins import DependencyGraph, DependencyType
graph = DependencyGraph()
graph.add_node("plugin-c", "1.0.0")
graph.add_node("plugin-b", "1.0.0",
dependencies={"plugin-c": DependencyType.REQUIRED})
graph.add_node("plugin-a", "1.0.0",
dependencies={"plugin-b": DependencyType.REQUIRED})
# Determine load order
load_order = graph.get_load_order()
# -> ['plugin-c', 'plugin-b', 'plugin-a']
# Detect circular dependencies
cycles = graph.detect_cycles()
For detailed Enterprise features, refer to .claude/docs/phase-09-plugins.md.