Files
changedetection.io/changedetectionio/processors/processor_registry.py
T
dgtlmoon 9dbe91e470 WIP
2025-03-17 19:17:18 +01:00

222 lines
8.1 KiB
Python

from loguru import logger
from changedetectionio.model import Watch
from .pluggy_interface import plugin_manager
from typing import Dict, Any, List, Tuple, Optional, TypeVar, Type
import functools
# Import and register internal plugins
from . import whois_plugin
from . import test_plugin
# Register plugins
plugin_manager.register(whois_plugin)
plugin_manager.register(test_plugin)
# Load any setuptools entrypoints
plugin_manager.load_setuptools_entrypoints("changedetectionio_processors")
# Type definitions for better type hinting
T = TypeVar('T')
ProcessorClass = TypeVar('ProcessorClass')
ProcessorForm = TypeVar('ProcessorForm')
ProcessorWatchModel = TypeVar('ProcessorWatchModel')
ProcessorInstance = TypeVar('ProcessorInstance')
# Cache for plugin name mapping to improve performance
_plugin_name_map: Dict[str, Any] = {}
def register_plugin(plugin_module):
"""Register a processor plugin"""
plugin_manager.register(plugin_module)
# Clear the plugin name map cache when a new plugin is registered
global _plugin_name_map
_plugin_name_map = {}
def _get_plugin_name_map() -> Dict[str, Any]:
"""Get a mapping of processor names to plugins
:return: Dictionary mapping processor names to plugins
"""
global _plugin_name_map
# Return cached map if available
if _plugin_name_map:
return _plugin_name_map
# Build the map
result = {}
# Get all plugins from the plugin manager
all_plugins = list(plugin_manager.get_plugins())
# First register known internal plugins by name for reliability
known_plugins = {
'whois': whois_plugin,
'test': test_plugin
}
for name, plugin in known_plugins.items():
if plugin in all_plugins:
result[name] = plugin
# Then process remaining plugins through the hook system
for plugin in all_plugins:
if plugin in known_plugins.values():
continue # Skip plugins we've already registered
try:
# Get the processor name from this plugin
name_results = plugin_manager.hook.get_processor_name(plugin=plugin)
if name_results:
plugin_name = name_results[0]
# Check for name collisions
if plugin_name in result:
logger.warning(f"Plugin name collision: '{plugin_name}' is already registered")
continue
result[plugin_name] = plugin
except Exception as e:
logger.error(f"Error getting processor name from plugin: {str(e)}")
# Cache the map
_plugin_name_map = result
return result
def _get_plugin_by_name(processor_name: str) -> Optional[Any]:
"""Get a plugin by its processor name
:param processor_name: Name of the processor
:return: Plugin object or None
"""
return _get_plugin_name_map().get(processor_name)
def _call_hook_for_plugin(plugin: Any, hook_name: str, default_value: T = None, **kwargs) -> Optional[T]:
"""Call a hook for a specific plugin and handle exceptions
:param plugin: The plugin to call the hook for
:param hook_name: Name of the hook to call
:param default_value: Default value to return if the hook call fails
:param kwargs: Additional arguments to pass to the hook
:return: Result of the hook call or default value
"""
if not plugin:
return default_value
try:
hook = getattr(plugin_manager.hook, hook_name)
results = hook(plugin=plugin, **kwargs)
if results:
return results[0]
except Exception as e:
logger.error(f"Error calling {hook_name} for plugin: {str(e)}")
return default_value
def get_all_processors() -> List[Tuple[str, str]]:
"""Get all processors
:return: List of tuples (processor_name, processor_description)
"""
processors = []
for processor_name, plugin in _get_plugin_name_map().items():
description = _call_hook_for_plugin(plugin, 'get_processor_description')
if description:
processors.append((processor_name, description))
return processors
def get_processor_class(processor_name: str) -> Optional[Type[ProcessorClass]]:
"""Get processor class by name
:param processor_name: Name of the processor
:return: Processor class or None
"""
plugin = _get_plugin_by_name(processor_name)
return _call_hook_for_plugin(plugin, 'get_processor_class')
def get_processor_form(processor_name: str) -> Optional[Type[ProcessorForm]]:
"""Get processor form by name
:param processor_name: Name of the processor
:return: Processor form class or None
"""
plugin = _get_plugin_by_name(processor_name)
return _call_hook_for_plugin(plugin, 'get_processor_form')
def get_processor_watch_model(processor_name: str) -> Type[ProcessorWatchModel]:
"""Get processor watch model by name
:param processor_name: Name of the processor
:return: Watch model class or default Watch model
"""
plugin = _get_plugin_by_name(processor_name)
return _call_hook_for_plugin(plugin, 'get_processor_watch_model', default_value=Watch.model)
def get_processor_site_check(processor_name: str, datastore: Any, watch_uuid: str) -> Optional[ProcessorInstance]:
"""Get a processor instance ready to perform site check
:param processor_name: Name of the processor
:param datastore: The application datastore
:param watch_uuid: The UUID of the watch to check
:return: A processor instance ready to perform site check, or None
"""
plugin = _get_plugin_by_name(processor_name)
if not plugin:
return None
# Try to get the perform_site_check implementation
try:
processor = _call_hook_for_plugin(
plugin,
'perform_site_check',
datastore=datastore,
watch_uuid=watch_uuid
)
if processor:
return processor
# If no perform_site_check hook implementation, try getting the class and instantiating it
processor_class = _call_hook_for_plugin(plugin, 'get_processor_class')
if processor_class:
return processor_class(datastore=datastore, watch_uuid=watch_uuid)
except Exception as e:
logger.error(f"Error getting processor site check for {processor_name}: {str(e)}")
return None
def get_display_link(url: str, processor_name: str) -> Optional[str]:
"""Get a custom display link for the given processor
:param url: The original URL from the watch
:param processor_name: Name of the processor
:return: A string with the custom display link or None to use the default
"""
plugin = _get_plugin_by_name(processor_name)
return _call_hook_for_plugin(
plugin,
'get_display_link',
url=url,
processor_name=processor_name
)
def get_plugin_processor_modules() -> List[Tuple[Any, str]]:
"""Get processor modules for all plugins that can be used with the find_processors function
This function adapts pluggy plugins to be compatible with the traditional find_processors system
:return: A list of (module, processor_name) tuples
"""
result = []
# Import base modules once to avoid repeated imports
from changedetectionio.processors.text_json_diff import processor as text_json_diff_processor
# For each plugin, map to a suitable module for find_processors
for processor_name, plugin in _get_plugin_name_map().items():
try:
processor_class = _call_hook_for_plugin(plugin, 'get_processor_class')
if processor_class:
# Check if this processor extends the text_json_diff processor
base_class_name = str(processor_class.__bases__[0].__name__)
if base_class_name == 'perform_site_check' or 'TextJsonDiffProcessor' in base_class_name:
result.append((text_json_diff_processor, processor_name))
except Exception as e:
logger.error(f"Error mapping processor module for {processor_name}: {str(e)}")
return result