From 33ac8b8dd772e2df3bbf8c3413605fbb15e2a80f Mon Sep 17 00:00:00 2001 From: Andrew Mirsky Date: Thu, 10 Jul 2025 12:29:59 -0400 Subject: [PATCH] add additional documentation for custom plugins --- docs/custom_plugins.md | 41 +++++++++++---- docs/references/broker_config.md | 8 +-- mkdocs.rtd.yml | 2 +- samples/broker_custom_plugin.py | 85 ++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 samples/broker_custom_plugin.py diff --git a/docs/custom_plugins.md b/docs/custom_plugins.md index 346babb..ebad484 100644 --- a/docs/custom_plugins.md +++ b/docs/custom_plugins.md @@ -1,14 +1,15 @@ -from dataclasses import dataclass - # Custom Plugins With the aMQTT plugins framework, one can add additional functionality to the client or broker without -having to rewrite any of the core logic. +having to rewrite any of the core logic. Plugins can receive broker or client events [events](custom_plugins.md#events), +used for [client authentication](custom_plugins.md#authentication-plugins) and controlling [topic access](custom_plugins.md#topic-filter-plugins). + +## Overview To create a custom plugin, subclass from `BasePlugin` (client or broker) or `BaseAuthPlugin` (broker only) or `BaseTopicPlugin` (broker only). Each custom plugin may define settings specific to itself by creating -a nested (or inner) `dataclass` named `Config` which declares each option and a default value (if applicable). A -plugin's configuration dataclass will be type-checked and made available from within the `self.context` instance variable. +a nested (ie. inner) `dataclass` named `Config` which declares each option and a default value (if applicable). A +plugin's configuration dataclass will be type-checked and made available from within the `self.config` instance variable. ```python from dataclasses import dataclass, field @@ -24,27 +25,44 @@ class TwoClassName(BasePlugin[BaseContext]): """This is a plugin with configuration options.""" def __init__(self, context: BaseContext): super().__init__(context) - my_option_one: str = self.context.config.option1 + self.my_option_one: str = self.config.option1 + + async def on_broker_pre_start(self) -> None: + print(f"On broker pre-start, my option1 is: {self.my_option_one}") @dataclass class Config: option1: int option3: str = field(default="my_default_value") - ``` This plugin class then should be added to the configuration file of the broker or client (or to the `config` -dictionary passed to the `Broker` or `MQTTClient`). +dictionary passed to the `Broker` or `MQTTClient`), such as `myBroker.yaml`: ```yaml -... -... +--- +listeners: + default: + type: tcp + bind: 0.0.0.0:1883 plugins: module.submodule.file.OneClassName: module.submodule.file.TwoClassName: option1: 123 ``` +and then run via `amqtt -c myBroker.yaml`. + +??? note "Example: custom plugin within broker script" + The example `samples/broker_custom_plugin.py` demonstrates how to load a custom plugin + by passing a config dictionary when instantiating a `Broker`. While this example is functional, + `samples` is an invalid python module (it does not have a `__init__.py`); it is recommended + that custom plugins are placed in a python module. + + ```python + --8<-- "samples/broker_custom_plugin.py" + ``` + ??? warning "Deprecated: activating plugins using `EntryPoints`" With the aMQTT plugins framework, one can add additional functionality to the client or broker without having to rewrite any of the core logic. To define a custom list of plugins to be loaded, add this section @@ -60,8 +78,11 @@ plugins: ::: amqtt.plugins.base.BasePlugin + + ## Events + All plugins are notified of events if the `BasePlugin` subclass implements one or more of these methods: ### Client and Broker diff --git a/docs/references/broker_config.md b/docs/references/broker_config.md index 47cd900..7655528 100644 --- a/docs/references/broker_config.md +++ b/docs/references/broker_config.md @@ -116,9 +116,9 @@ listeners: ssl: on cafile: /some/cafile capath: /some/folder - capath: certificate data + capath: 'certificate data' certfile: /some/certfile - keyfile: /some/key + keyfile: /some/keyfile my-ws-1: bind: 0.0.0.0:8080 type: ws @@ -127,7 +127,7 @@ listeners: type: ws ssl: on certfile: /some/certfile - keyfile: /some/key + keyfile: /some/keyfile timeout-disconnect-delay: 2 plugins: - amqtt.plugins.authentication.AnonymousAuthPlugin: @@ -137,7 +137,7 @@ plugins: - amqtt.plugins.topic_checking.TopicAccessControlListPlugin: acl: username1: ['repositories/+/master', 'calendar/#', 'data/memes'] - username2: [ 'calendar/2025/#', 'data/memes'] + username2: ['calendar/2025/#', 'data/memes'] anonymous: ['calendar/2025/#'] ``` diff --git a/mkdocs.rtd.yml b/mkdocs.rtd.yml index 7f248d9..e9b608f 100644 --- a/mkdocs.rtd.yml +++ b/mkdocs.rtd.yml @@ -135,7 +135,7 @@ plugins: ignore_init_summary: true docstring_section_style: list filters: ["!^_"] - heading_level: 1 + heading_level: 2 inherited_members: true merge_init_into_class: true parameter_headings: true diff --git a/samples/broker_custom_plugin.py b/samples/broker_custom_plugin.py new file mode 100644 index 0000000..82850a8 --- /dev/null +++ b/samples/broker_custom_plugin.py @@ -0,0 +1,85 @@ +import asyncio +import logging +import os +from dataclasses import dataclass +from pathlib import Path + +from amqtt.broker import Broker +from amqtt.plugins.base import BasePlugin +from amqtt.session import Session + +""" +This sample shows how to run a broker without stacktraces on keyboard interrupt +""" + +logger = logging.getLogger(__name__) + + +class RemoteInfoPlugin(BasePlugin): + + async def on_broker_client_connected(self, *, client_id:str, client_session:Session) -> None: + display_port_str = f"on port '{client_session.remote_port}'" if self.config.display_port else '' + + logger.info(f"client '{client_id}' connected from" + f" '{client_session.remote_address}' {display_port_str}") + + @dataclass + class Config: + display_port: bool = False + +config = { + "listeners": { + "default": { + "type": "tcp", + "bind": "0.0.0.0:1883", + }, + "ws-mqtt": { + "bind": "127.0.0.1:8080", + "type": "ws", + "max_connections": 10, + }, + }, + "plugins": { + 'amqtt.plugins.authentication.AnonymousAuthPlugin': { 'allow_anonymous': True}, + 'samples.broker_custom_plugin.RemoteInfoPlugin': { 'display_port': True }, + } +} + +async def main_loop(): + broker = Broker(config) + try: + await broker.start() + while True: + await asyncio.sleep(1) + except asyncio.CancelledError: + await broker.shutdown() + +async def main(): + t = asyncio.create_task(main_loop()) + try: + await t + except asyncio.CancelledError: + pass + +def __main__(): + + formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" + logging.basicConfig(level=logging.INFO, format=formatter) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + task = loop.create_task(main()) + + try: + loop.run_until_complete(task) + except KeyboardInterrupt: + logger.info("KeyboardInterrupt received. Stopping server...") + task.cancel() + loop.run_until_complete(task) # Ensure task finishes cleanup + finally: + logger.info("Server stopped.") + loop.close() + +if __name__ == "__main__": + __main__()