diff --git a/app/__init__.py b/app/__init__.py index b60ed56..5e575ee 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,6 +33,7 @@ def create_app(): from .errors import bp as errors_bp app.register_blueprint(errors_bp) + # Plugin auto-loader # Plugin auto-loader plugin_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins')) for plugin in os.listdir(plugin_path): diff --git a/app/events.py b/app/events.py new file mode 100644 index 0000000..a4ae535 --- /dev/null +++ b/app/events.py @@ -0,0 +1,6 @@ +# Event name definitions + +ON_USER_CREATED = 'on_user_created' +ON_SUBMISSION_SAVED = 'on_submission_saved' +ON_PLANT_TRANSFERRED = 'on_plant_transferred' +ON_GROWLOG_UPDATED = 'on_growlog_updated' diff --git a/app/hooks.py b/app/hooks.py new file mode 100644 index 0000000..114dc41 --- /dev/null +++ b/app/hooks.py @@ -0,0 +1,32 @@ +from typing import Callable, Dict, List +import threading + +class EventDispatcher: + """Central event dispatcher for registering and firing events.""" + _listeners: Dict[str, List[Callable]] = {} + _lock = threading.Lock() + + @classmethod + def register(cls, event_name: str, func: Callable): + """Register a listener for a specific event.""" + with cls._lock: + cls._listeners.setdefault(event_name, []).append(func) + + @classmethod + def dispatch(cls, event_name: str, *args, **kwargs): + """Dispatch an event to all registered listeners.""" + with cls._lock: + listeners = list(cls._listeners.get(event_name, [])) + for listener in listeners: + try: + listener(*args, **kwargs) + except Exception as e: + # Optionally log the exception + pass + +def listen_event(event_name: str): + """Decorator to register a function as an event listener.""" + def decorator(func: Callable): + EventDispatcher.register(event_name, func) + return func + return decorator diff --git a/files.zip b/files.zip new file mode 100644 index 0000000..78950b0 Binary files /dev/null and b/files.zip differ diff --git a/new.zip b/new.zip deleted file mode 100644 index b3f1f81..0000000 Binary files a/new.zip and /dev/null differ diff --git a/plugins/admin/__init__.py b/plugins/admin/__init__.py new file mode 100644 index 0000000..864d480 --- /dev/null +++ b/plugins/admin/__init__.py @@ -0,0 +1,6 @@ +import click +from flask import Flask + +def register_cli(app: Flask): + # No CLI commands yet for admin plugin + pass diff --git a/plugins/admin/plugin.json b/plugins/admin/plugin.json new file mode 100644 index 0000000..2aa8b50 --- /dev/null +++ b/plugins/admin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "admin", + "version": "1.0.0", + "description": "Administration dashboard and plugin manager", + "entry_point": null +} \ No newline at end of file diff --git a/plugins/auth/__init__.py b/plugins/auth/__init__.py new file mode 100644 index 0000000..c87f661 --- /dev/null +++ b/plugins/auth/__init__.py @@ -0,0 +1,6 @@ +import click +from flask import Flask + +def register_cli(app: Flask): + # No CLI commands yet for auth plugin + pass diff --git a/plugins/auth/plugin.json b/plugins/auth/plugin.json new file mode 100644 index 0000000..cd79a46 --- /dev/null +++ b/plugins/auth/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "auth", + "version": "1.0.0", + "description": "User authentication and authorization plugin", + "entry_point": null +} \ No newline at end of file diff --git a/plugins/cli/plugin.json b/plugins/cli/plugin.json new file mode 100644 index 0000000..7a92909 --- /dev/null +++ b/plugins/cli/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "cli", + "version": "1.0.0", + "description": "Command-line interface plugin", + "entry_point": null +} \ No newline at end of file diff --git a/plugins/plant/__init__.py b/plugins/plant/__init__.py index 587a922..df24533 100644 --- a/plugins/plant/__init__.py +++ b/plugins/plant/__init__.py @@ -1 +1,6 @@ -# plant plugin init +import click +from flask import Flask + +def register_cli(app: Flask): + # No CLI commands yet for plant plugin + pass diff --git a/plugins/plant/plugin.json b/plugins/plant/plugin.json new file mode 100644 index 0000000..c859ad4 --- /dev/null +++ b/plugins/plant/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "plant", + "version": "1.0.0", + "description": "Plant profile management plugin", + "entry_point": null +} \ No newline at end of file diff --git a/tests/test_app_init.py b/tests/test_app_init.py new file mode 100644 index 0000000..8475da9 --- /dev/null +++ b/tests/test_app_init.py @@ -0,0 +1,10 @@ + +import pytest +from app import create_app + +def test_app_loads_plugins(): + app = create_app({'TESTING': True}) + # Assuming app.plugins is a dict of loaded plugin modules + assert hasattr(app, 'plugins'), "App object missing 'plugins' attribute" + for plugin in ["auth", "admin", "plant", "cli"]: + assert plugin in app.plugins, f"Plugin '{plugin}' not loaded into app.plugins" diff --git a/tests/test_event_dispatcher.py b/tests/test_event_dispatcher.py new file mode 100644 index 0000000..a77fd69 --- /dev/null +++ b/tests/test_event_dispatcher.py @@ -0,0 +1,22 @@ +import pytest +from app.hooks import EventDispatcher, listen_event + +def test_register_and_dispatch(): + results = [] + def listener1(a, b=None): + results.append(('l1', a, b)) + def listener2(a, b=None): + results.append(('l2', a, b)) + EventDispatcher.register('test_event', listener1) + EventDispatcher.register('test_event', listener2) + EventDispatcher.dispatch('test_event', 1, b=2) + assert ('l1', 1, 2) in results + assert ('l2', 1, 2) in results + +def test_listen_event_decorator(): + results = [] + @listen_event('decorated_event') + def handler(x): + results.append(x) + EventDispatcher.dispatch('decorated_event', 'hello') + assert results == ['hello'] diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..07b5ded --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,7 @@ +import app.events as events + +def test_event_names_exist(): + assert hasattr(events, 'ON_USER_CREATED') + assert hasattr(events, 'ON_SUBMISSION_SAVED') + assert hasattr(events, 'ON_PLANT_TRANSFERRED') + assert hasattr(events, 'ON_GROWLOG_UPDATED') diff --git a/tests/test_plugin_metadata.py b/tests/test_plugin_metadata.py new file mode 100644 index 0000000..1bfa266 --- /dev/null +++ b/tests/test_plugin_metadata.py @@ -0,0 +1,25 @@ + +import os +import json +import pytest +import importlib + +# Directory containing plugins +PLUGINS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'plugins')) + +@pytest.mark.parametrize("plugin", ["auth", "admin", "plant", "cli"]) +def test_plugin_metadata_and_init(plugin, tmp_path, monkeypatch): + # Construct plugin path + plugin_path = os.path.join(PLUGINS_DIR, plugin) + # Test plugin.json exists and contains required keys + meta_path = os.path.join(plugin_path, 'plugin.json') + assert os.path.isfile(meta_path), f"plugin.json missing for {plugin}" + meta = json.loads(open(meta_path).read()) + for key in ("name", "version", "description"): + assert key in meta, f"{{key}} missing in {plugin}/plugin.json" + # Test __init__.py can be imported and has register_cli + module_name = f"plugins.{plugin}" + spec = importlib.util.spec_from_file_location(module_name, os.path.join(plugin_path, '__init__.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert hasattr(module, "register_cli"), f"register_cli missing in {plugin}/__init__.py"