DocsDocumentation

Nitro Dispatch

Framework-agnostic plugin system with hooks and priorities.

A powerful, framework-agnostic plugin system for Python with advanced features like async/await support, hook priorities, timeouts, event namespacing, and plugin discovery.

Requirements

Python 3.9 or higher is required.

Installation

pip install nitro-dispatch

Features

Core Features

  • Simple API - Easy to learn with minimal boilerplate
  • Framework Agnostic - Works with any Python application
  • Hook System - Register callbacks for custom events with @hook decorator
  • Data Filtering - Each hook can modify and transform data
  • Error Isolation - Plugin errors don't crash your application
  • Dependency Management - Automatic dependency resolution
  • Zero Dependencies - No external dependencies required

Advanced Features

  • Async/Await Support - Native async hook execution
  • Hook Priorities - Control execution order (higher priority = runs first)
  • Timeout Protection - Prevent slow plugins from blocking
  • Event Namespacing - Organize events hierarchically (user.login, db.save)
  • Wildcard Events - Listen to multiple events (user.*, db.before_*)
  • Plugin Discovery - Auto-discover plugins from directories
  • Hot Reloading - Reload plugins without restarting
  • Stop Propagation - Halt an event chain from within hooks
  • Hook Tracing - Debug with detailed execution timing
  • Built-in Lifecycle Events - Hook into plugin lifecycle
  • Metadata Validation - Ensure plugin quality

Quick Start

from nitro_dispatch import PluginManager, PluginBase, hook

class WelcomePlugin(PluginBase):
    name = "welcome"

    @hook('user.login', priority=100)
    def greet_user(self, data):
        print(f"Welcome, {data['username']}!")
        data['greeted'] = True
        return data

manager = PluginManager()
manager.register(WelcomePlugin)
manager.load_all()

result = manager.trigger('user.login', {'username': 'Alice'})
# Output: Welcome, Alice!

Core Concepts

1. Plugins

Plugins inherit from PluginBase:

class MyPlugin(PluginBase):
    name = "my_plugin"              # Required: unique identifier
    version = "1.0.0"               # Plugin version
    description = "Does cool stuff" # Human-readable description
    author = "Your Name"            # Plugin author
    dependencies = []               # List of required plugin names

2. Hooks

Register callbacks for events using the @hook decorator:

class ValidationPlugin(PluginBase):
    name = "validator"

    @hook('before_save', priority=100, timeout=5.0)
    def validate(self, data):
        if not data.get('email'):
            raise ValueError("Email required")
        return data

Or register manually in on_load():

class LoggingPlugin(PluginBase):
    name = "logger"

    def on_load(self):
        self.register_hook('before_save', self.log_data, priority=50)

    def log_data(self, data):
        print(f"Saving: {data}")
        return data

3. Data Filtering

Hooks execute in priority order (highest first). Each hook receives data, modifies it, and returns it:

# Hook 1 (priority=100)
@hook('process_data', priority=100)
def add_timestamp(self, data):
    data['timestamp'] = datetime.now()
    return data

# Hook 2 (priority=50)
@hook('process_data', priority=50)
def add_id(self, data):
    data['id'] = generate_id()
    return data

# Data flows: original → add_timestamp → add_id → final result

Advanced Usage

Hook Priorities

Control execution order with priority values (higher = earlier):

class SecurityPlugin(PluginBase):
    @hook('user.login', priority=100)  # Runs first
    def security_check(self, data):
        return data

class LoggingPlugin(PluginBase):
    @hook('user.login', priority=10)  # Runs last
    def log_login(self, data):
        return data

Async/Await Support

Native support for async hooks:

class AsyncPlugin(PluginBase):
    @hook('data.fetch')
    async def fetch_from_api(self, data):
        result = await aiohttp.get('https://api.example.com')
        data['result'] = await result.json()
        return data

# Trigger async
result = await manager.trigger_async('data.fetch', {})

Hook Timeouts

Prevent slow plugins from blocking:

@hook('process_data', timeout=2.0)  # 2 second timeout
def slow_process(self, data):
    # If this takes > 2s, HookTimeoutError is raised
    time.sleep(5)  # This will timeout!
    return data

Event Namespacing with Wildcards

Organize events hierarchically and use wildcards:

class AuditPlugin(PluginBase):
    @hook('user.*')  # Matches user.login, user.logout, etc.
    def audit_user_events(self, data):
        log.info(f"User event: {data}")
        return data

    @hook('db.before_*')  # Matches db.before_save, db.before_delete
    def audit_db_operations(self, data):
        log.info(f"DB operation: {data}")
        return data

# Trigger events
manager.trigger('user.login', {})      # Caught by user.*
manager.trigger('db.before_save', {})  # Caught by db.before_*

Stop Propagation

Stop the hook chain from within a hook:

from nitro_dispatch import StopPropagation

class ValidationPlugin(PluginBase):
    @hook('process_data', priority=100)
    def validate(self, data):
        if not data.get('valid'):
            raise StopPropagation("Invalid data")
        return data

# Hooks with lower priority won't execute if validation fails

Plugin Discovery

Auto-discover and load plugins from directories:

manager = PluginManager()

# Discover plugins from directory
discovered = manager.discover_plugins(
    '~/.myapp/plugins',
    pattern='*_plugin.py',
    recursive=True
)

print(f"Discovered: {discovered}")
manager.load_all()

Hot Reloading

Reload plugins without restarting:

# Reload a specific plugin
manager.reload('my_plugin')

# The plugin will be unloaded, module reloaded, and loaded again

Enable/Disable Plugins

Toggle plugins at runtime:

# Disable a plugin (hooks won't execute)
manager.disable_plugin('optional_plugin')

# Re-enable it
manager.enable_plugin('optional_plugin')

Built-in Lifecycle Events

Hook into the plugin system's lifecycle:

def on_plugin_loaded(data):
    print(f"Plugin loaded: {data['plugin_name']}")
    return data

manager.register_hook(
    PluginManager.EVENT_PLUGIN_LOADED,
    on_plugin_loaded
)

# Built-in events:
# - nitro.plugin.registered
# - nitro.plugin.loaded
# - nitro.plugin.unloaded
# - nitro.plugin.error
# - nitro.app.startup
# - nitro.app.shutdown

Hook Tracing/Debugging

Enable detailed logging for debugging:

manager = PluginManager(log_level='DEBUG')
manager.enable_hook_tracing(True)

# Now all hook executions are logged with timing info
result = manager.trigger('user.login', {})

Error Handling Strategies

Configure how errors are handled:

# Log and continue (default)
manager.set_error_strategy('log_and_continue')

# Stop on first error
manager.set_error_strategy('fail_fast')

# Collect all errors
manager.set_error_strategy('collect_all')

Plugin Configuration

Pass configuration to plugins:

config = {
    'cache': {
        'max_size': 100,
        'ttl': 3600
    }
}
manager = PluginManager(config=config)

class CachePlugin(PluginBase):
    name = "cache"

    def on_load(self):
        max_size = self.get_config('max_size', 50)
        ttl = self.get_config('ttl', 1800)

API Reference

PluginManager

MethodDescription
__init__(config, log_level, validate_metadata)Initialize manager
register(plugin_class)Register a plugin class
unregister(plugin_name)Unregister and unload a plugin
load(plugin_name)Load a specific plugin
load_all()Load all registered plugins
unload(plugin_name)Unload a plugin
unload_all()Unload all plugins
reload(plugin_name)Hot reload a plugin
discover_plugins(directory, pattern, recursive)Auto-discover plugins
register_hook(event, callback, plugin, priority, timeout)Register a hook manually
unregister_hook(event, callback, plugin)Unregister a hook
trigger(event, data)Trigger event (sync)
trigger_async(event, data)Trigger event (async)
get_plugin(name)Get a loaded plugin by name
get_all_plugins()Get all loaded plugins
get_registered_plugins()Get names of registered plugins
get_loaded_plugins()Get names of loaded plugins
is_loaded(plugin_name)Check if a plugin is loaded
get_events()Get all registered event names
enable_plugin(name)Enable a plugin
disable_plugin(name)Disable a plugin
enable_hook_tracing(enabled)Enable debugging
set_error_strategy(strategy)Set error handling

PluginBase

Attribute/MethodDescription
namePlugin name (required)
versionPlugin version
descriptionPlugin description
authorPlugin author
dependenciesList of required plugins
enabledWhether the plugin is enabled
on_load()Called when plugin loads
on_unload()Called when plugin unloads
on_error(error)Called on hook errors
register_hook(event, callback, priority, timeout)Register a hook
unregister_hook(event, callback)Unregister a hook
trigger(event, data)Trigger an event from the plugin
get_config(key, default)Get configuration value

@hook Decorator

@hook(event_name, priority=50, timeout=None, async_hook=False)
ParameterDescription
event_nameEvent to listen for (supports wildcards)
priorityExecution priority (higher = earlier). Default: 50
timeoutMax execution time in seconds. Default: None
async_hookWhether hook is async (auto-detected)

Exceptions

All exceptions inherit from NitroPluginError:

ExceptionDescription
NitroPluginErrorBase exception for all Nitro Plugin errors
PluginLoadErrorRaised when a plugin fails to load
PluginRegistrationErrorRaised when plugin registration fails
PluginNotFoundErrorRaised when a requested plugin is not found
PluginDiscoveryErrorRaised when plugin discovery fails
DependencyErrorRaised when plugin dependencies cannot be resolved
HookErrorRaised when hook execution fails
HookTimeoutErrorRaised when a hook exceeds its timeout
ValidationErrorRaised when plugin metadata validation fails
StopPropagationRaised to stop hook propagation in the event chain
from nitro_dispatch import (
    NitroPluginError,
    PluginLoadError,
    HookTimeoutError,
    StopPropagation,
)

try:
    manager.load('my_plugin')
except PluginLoadError as e:
    print(f"Failed to load plugin: {e}")

Examples

Basic Usage

python examples/basic_usage.py

Advanced Features

python examples/advanced_usage.py
python examples/advanced_features.py

Plugin Discovery

python examples/discovery_example.py

Development

Setup

git clone https://github.com/nitrosh/nitro-dispatch.git
cd nitro-dispatch
pip install -e ".[dev]"

Run Tests

pytest
pytest --cov=nitro_dispatch

Format Code

black nitro_dispatch tests examples

Ecosystem

License

Please see LICENSE for licensing details.

API Reference

Auto-generated from the installed package's public API. Signatures and docstrings come directly from the source.

PluginManager

PluginManager#

PluginManager(config: Optional[Dict[str, Any]] = None, log_level: str = 'INFO', validate_metadata: bool = True) -> None

Central orchestrator for plugin lifecycle and event dispatch.

23 methods
disable_plugin(self, plugin_name: str) -> None

Disable a loaded plugin without unloading it.

discover_plugins(self, directory: Union[str, pathlib.Path], pattern: str = '*_plugin.py', recursive: bool = False) -> List[str]

Auto-register every plugin class found in a directory.

enable_hook_tracing(self, enabled: bool = True) -> None

Toggle per-hook timing logs for debugging.

enable_plugin(self, plugin_name: str) -> None

Enable a loaded plugin so its hooks execute again.

get_all_plugins(self) -> Dict[str, nitro_dispatch.core.plugin_base.PluginBase]

Return a shallow copy of the loaded-plugins map.

get_events(self) -> List[str]

Return every event name with at least one registered hook.

get_loaded_plugins(self) -> List[str]

Return names of every currently-loaded plugin.

get_plugin(self, plugin_name: str) -> Optional[nitro_dispatch.core.plugin_base.PluginBase]

Return a loaded plugin by name, or ``None`` if not loaded.

get_plugin_config(self, plugin_name: str, key: str, default: Any = None) -> Any

Read a config value for a specific plugin.

get_registered_plugins(self) -> List[str]

Return names of every registered plugin class.

is_loaded(self, plugin_name: str) -> bool

Report whether a plugin is currently loaded.

load(self, plugin_name: str) -> nitro_dispatch.core.plugin_base.PluginBase

Instantiate a registered plugin and attach its hooks.

load_all(self) -> List[str]

Load every registered plugin, respecting dependencies.

register(self, plugin_class: Type[nitro_dispatch.core.plugin_base.PluginBase], validate: bool = True) -> None

Register a plugin class so it can later be loaded.

register_hook(self, event_name: str, callback: Callable, plugin: Optional[nitro_dispatch.core.plugin_base.PluginBase] = None, priority: int = 50, timeout: Optional[float] = None) -> None

Register a callback for an event on the underlying registry.

reload(self, plugin_name: str) -> nitro_dispatch.core.plugin_base.PluginBase

Hot-reload a plugin, picking up source changes on disk.

set_error_strategy(self, strategy: str) -> None

Choose how hook exceptions are handled during dispatch.

trigger(self, event_name: str, data: Any = None) -> Any

Fire an event and run matching hooks synchronously.

trigger_async(self, event_name: str, data: Any = None) -> Any

Fire an event and run matching hooks asynchronously.

unload(self, plugin_name: str) -> None

Unload a single plugin and detach its hooks.

unload_all(self) -> None

Unload every currently-loaded plugin.

unregister(self, plugin_name: str) -> None

Remove a plugin's registration, unloading it first if needed.

unregister_hook(self, event_name: str, callback: Callable, plugin: Optional[nitro_dispatch.core.plugin_base.PluginBase] = None) -> None

Detach a previously registered hook from the registry.

PluginBase

PluginBase#

PluginBase() -> None

Base class all plugins must inherit from.

7 methods
get_config(self, key: str, default: Any = None) -> Any

Read a config value scoped to this plugin.

on_error(self, error: Exception) -> None

Handle exceptions raised by this plugin's hooks.

on_load(self) -> None

Run once when the plugin is loaded by the manager.

on_unload(self) -> None

Run once when the plugin is unloaded by the manager.

register_hook(self, event_name: str, callback: Callable, priority: int = 50, timeout: Optional[float] = None) -> None

Register a callback for an event.

trigger(self, event_name: str, data: Any = None) -> Any

Fire an event through this plugin's manager.

unregister_hook(self, event_name: str, callback: Callable) -> None

Detach a previously registered callback from an event.

HookRegistry

HookRegistry#

HookRegistry() -> None

Event bus storing hooks and dispatching them to listeners.

10 methods
clear_all(self) -> None

Remove every registered hook.

clear_event(self, event_name: str) -> None

Remove every hook registered under a single event name.

enable_hook_tracing(self, enabled: bool = True) -> None

Toggle per-hook timing logs for debugging.

get_all_events(self) -> List[str]

Return every registered event name.

get_hooks(self, event_name: str) -> List[Dict[str, Any]]

Return every hook that would run for an event, in priority order.

register(self, event_name: str, callback: Callable, plugin: Optional[Any] = None, priority: int = 50, timeout: Optional[float] = None) -> None

Register a callback to run when an event fires.

set_error_strategy(self, strategy: str) -> None

Choose how hook exceptions are handled during dispatch.

trigger(self, event_name: str, data: Any = None) -> Any

Fire an event and run matching hooks synchronously.

trigger_async(self, event_name: str, data: Any = None) -> Any

Fire an event asynchronously, running matching hooks.

unregister(self, event_name: str, callback: Callable, plugin: Optional[Any] = None) -> bool

Remove a previously registered callback from an event.

Decorators

hook#

hook(event_name: str, priority: int = 50, timeout: Optional[float] = None, async_hook: bool = False) -> Callable

Mark a plugin method as a hook for an event.

Exceptions

NitroPluginError#

Base class for every exception raised by Nitro Dispatch.

PluginLoadError#

Raised when a plugin cannot be loaded.

PluginRegistrationError#

Raised when a class cannot be registered as a plugin.

HookError#

Raised when a hook fails under the ``fail_fast`` error strategy.

PluginNotFoundError#

Raised when an operation targets a plugin that is not known.

DependencyError#

Raised when a plugin's declared dependency cannot be loaded.

StopPropagation#

Raised by a hook to halt the remaining hook chain for an event.

HookTimeoutError#

Raised when a hook exceeds its configured ``timeout``.

ValidationError#

Raised when a plugin's metadata fails validation at registration.

PluginDiscoveryError#

Raised when :meth:`PluginManager.discover_plugins` fails.