add additional documentation for custom plugins

pull/266/head
Andrew Mirsky 2025-07-10 12:29:59 -04:00
rodzic 9c0b10ea2a
commit 33ac8b8dd7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
4 zmienionych plików z 121 dodań i 15 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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/#']
```

Wyświetl plik

@ -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

Wyświetl plik

@ -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__()