From 892d38ba42da48c0a204a15be35bbb8eb3587474 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Sun, 9 Feb 2025 16:52:58 +0100 Subject: [PATCH] experiment with default plugins --- .../{conditions.py => conditions/__init__.py} | 15 +++++++++ .../conditions/default_plugin.py | 30 +++++++++++++++++ .../conditions/pluggy_interface.py | 32 +++++++++++++++++++ changedetectionio/forms.py | 6 +--- 4 files changed, 78 insertions(+), 5 deletions(-) rename changedetectionio/{conditions.py => conditions/__init__.py} (85%) create mode 100644 changedetectionio/conditions/default_plugin.py create mode 100644 changedetectionio/conditions/pluggy_interface.py diff --git a/changedetectionio/conditions.py b/changedetectionio/conditions/__init__.py similarity index 85% rename from changedetectionio/conditions.py rename to changedetectionio/conditions/__init__.py index a847aa4b..abb05933 100644 --- a/changedetectionio/conditions.py +++ b/changedetectionio/conditions/__init__.py @@ -1,5 +1,8 @@ from json_logic import jsonLogic from json_logic.builtins import BUILTINS +from .pluggy_interface import plugin_manager # Import the pluggy plugin manager +from . import default_plugin + import re # List of all supported JSON Logic operators @@ -78,7 +81,19 @@ CUSTOM_OPERATIONS = { "!contains_regex": not_contains_regex } +# ✅ Load plugins dynamically +for plugin in plugin_manager.get_plugins(): + new_ops = plugin.register_operators() + if isinstance(new_ops, dict): + CUSTOM_OPERATIONS.update(new_ops) + new_operator_choices = plugin.register_operator_choices() + if isinstance(new_operator_choices, list): + operator_choices.extend(new_operator_choices) + + new_field_choices = plugin.register_field_choices() + if isinstance(new_field_choices, list): + field_choices.extend(new_field_choices) def run(ruleset, data): """ diff --git a/changedetectionio/conditions/default_plugin.py b/changedetectionio/conditions/default_plugin.py new file mode 100644 index 00000000..2215c6b5 --- /dev/null +++ b/changedetectionio/conditions/default_plugin.py @@ -0,0 +1,30 @@ +import pluggy + +hookimpl = pluggy.HookimplMarker("conditions") + +@hookimpl +def register_operators(): + def starts_with(_, text, prefix): + return text.lower().startswith(prefix.lower()) + + def ends_with(_, text, suffix): + return text.lower().endswith(suffix.lower()) + + return { + "starts_with": starts_with, + "ends_with": ends_with + } + +@hookimpl +def register_operator_choices(): + return [ + ("starts_with", "Text Starts With"), + ("ends_with", "Text Ends With"), + ] + +@hookimpl +def register_field_choices(): + return [ + ("meta_description", "Meta Description"), + ("meta_keywords", "Meta Keywords"), + ] \ No newline at end of file diff --git a/changedetectionio/conditions/pluggy_interface.py b/changedetectionio/conditions/pluggy_interface.py new file mode 100644 index 00000000..16a4948e --- /dev/null +++ b/changedetectionio/conditions/pluggy_interface.py @@ -0,0 +1,32 @@ +import pluggy + +# Define `pluggy` hookspecs (Specifications for Plugins) +hookspec = pluggy.HookspecMarker("conditions") +hookimpl = pluggy.HookimplMarker("conditions") + + +class ConditionsSpec: + """Hook specifications for extending JSON Logic conditions.""" + + @hookspec + def register_operators(): + """Return a dictionary of new JSON Logic operators.""" + pass + + @hookspec + def register_operator_choices(): + """Return a list of new operator choices.""" + pass + + @hookspec + def register_field_choices(): + """Return a list of new field choices.""" + pass + + +# ✅ Set up `pluggy` Plugin Manager +plugin_manager = pluggy.PluginManager("conditions") +plugin_manager.add_hookspecs(ConditionsSpec) + +# Discover installed plugins +plugin_manager.load_setuptools_entrypoints("conditions") \ No newline at end of file diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index a8276e2b..d31cdd2b 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -5,9 +5,6 @@ from wtforms.widgets.core import TimeInput from changedetectionio.strtobool import strtobool from flask_wtf import FlaskForm -from wtforms import SelectField, StringField, SubmitField, FieldList, FormField -from wtforms.validators import DataRequired, URL -from flask_wtf.file import FileField from wtforms import ( BooleanField, @@ -310,7 +307,6 @@ class ValidateAppRiseServers(object): import apprise apobj = apprise.Apprise() # so that the custom endpoints are registered - from changedetectionio.apprise_plugin import apprise_custom_api_call_wrapper for server_url in field.data: url = server_url.strip() if url.startswith("#"): @@ -516,7 +512,7 @@ class quickWatchForm(Form): # Condition Rule Form (for each rule row) class ConditionForm(FlaskForm): - from .conditions import operator_choices, field_choices + from changedetectionio.conditions import operator_choices, field_choices operator = SelectField( "Operator",