kopia lustrzana https://github.com/Yakifo/amqtt
Merge branch '0.11.3-rc.1' into config_dataclasses
commit
c8e97bd167
|
@ -0,0 +1,7 @@
|
|||
docs/**
|
||||
docs_test/**
|
||||
docs_web/**
|
||||
tests/**
|
||||
htmlcov/**
|
||||
cache/**
|
||||
dist/**
|
|
@ -4,6 +4,9 @@ __pycache__
|
|||
node_modules
|
||||
.vite
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.patch
|
||||
|
||||
#------- Environment Files -------
|
||||
.python-version
|
||||
|
|
2
Makefile
2
Makefile
|
@ -1,7 +1,7 @@
|
|||
# Image name and tag
|
||||
IMAGE_NAME := amqtt
|
||||
IMAGE_TAG := latest
|
||||
VERSION_TAG := 0.11.1
|
||||
VERSION_TAG := 0.11.3-rc.1
|
||||
REGISTRY := amqtt/$(IMAGE_NAME)
|
||||
|
||||
# Platforms to build for
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
"""INIT."""
|
||||
|
||||
__version__ = "0.11.1"
|
||||
__version__ = "0.11.3-rc.1"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Module for plugins requiring additional dependencies."""
|
|
@ -1,5 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## 0.11.2
|
||||
|
||||
- config-file based plugin loading [PR #240](https://github.com/Yakifo/amqtt/pull/240)
|
||||
- dockerfile build update to support psutils [PR #239](https://github.com/Yakifo/amqtt/pull/239)
|
||||
- pass client session info to event callbacks [PR #241](https://github.com/Yakifo/amqtt/pull/241)
|
||||
- Require at least one auth [PR #244](https://github.com/Yakifo/amqtt/pull/244)
|
||||
- improvements in retaining messages [PR #248](https://github.com/Yakifo/amqtt/pull/248)
|
||||
- updating docker compose with resource limits [PR #253](https://github.com/Yakifo/amqtt/pull/253)
|
||||
- improve static type checking for plugin's `Config` class [PR #249](https://github.com/Yakifo/amqtt/pull/249)
|
||||
- broker shouldn't allow clients to publish to '$' topics [PR #254](https://github.com/Yakifo/amqtt/pull/254)
|
||||
- publishing to a topic with `*` is allowed, while `#` and `+` are not [PR #251](https://github.com/Yakifo/amqtt/pull/251)
|
||||
- updated samples; plugin config consistency (yaml and python dict) [PR #252](https://github.com/Yakifo/amqtt/pull/252)
|
||||
- add cpu, mem and broker version to dashboard [PR #257](https://github.com/Yakifo/amqtt/pull/257)
|
||||
- [Issue 246](https://github.com/Yakifo/amqtt/issues/246) don't retain QoS 1 or 2 messages if client connects with clean session true
|
||||
- [Issue 175](https://github.com/Yakifo/amqtt/issues/175) plugin examples
|
||||
- [Issue 81](https://github.com/Yakifo/amqtt/issues/81) Abstract factory for plugins
|
||||
- [Issue 74](https://github.com/Yakifo/amqtt/issues/74) 模拟500个客户端并发,连接broker。
|
||||
- [Issue 60](https://github.com/Yakifo/amqtt/issues/60) amqtt server not relaying traffic
|
||||
- [Issue 31](https://github.com/Yakifo/amqtt/issues/31) Plugin config in yaml file not under - plugins entry
|
||||
- [Issue 27](https://github.com/Yakifo/amqtt/issues/27) don't retain messages from anonymous clients
|
||||
- [Issue 250](https://github.com/Yakifo/amqtt/issues/250) client doesn't prevent publishing to wildcard topics
|
||||
- [Issue 245](https://github.com/Yakifo/amqtt/issues/245) prevent clients from publishing to `$` topics
|
||||
- [Issue 196](https://github.com/Yakifo/amqtt/issues/196) proposal: enhancement to broker plugin configuration
|
||||
- [Issue 187](https://github.com/Yakifo/amqtt/issues/187) anonymous login allowed even if plugin isn't enabled
|
||||
- [Issue 123](https://github.com/Yakifo/amqtt/issues/123) Messages sent to mqtt can be consumed in time, but they occupy more and more memory
|
||||
|
||||
## 0.11.1
|
||||
|
||||
- [PR #226](https://github.com/Yakifo/amqtt/pull/226) Consolidate super classes for plugins
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Contributed Plugins
|
||||
|
||||
Plugins that are not part of the core functionality of the aMQTT broker or client, often requiring additional dependencies.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -108,9 +108,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
|
||||
|
@ -119,7 +119,7 @@ listeners:
|
|||
type: ws
|
||||
ssl: on
|
||||
certfile: /some/certfile
|
||||
keyfile: /some/key
|
||||
keyfile: /some/keyfile
|
||||
timeout-disconnect-delay: 2
|
||||
plugins:
|
||||
- amqtt.plugins.authentication.AnonymousAuthPlugin:
|
||||
|
@ -129,7 +129,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/#']
|
||||
```
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "amqttio",
|
||||
"private": true,
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React from "react";
|
||||
|
||||
export type DataPoint = {
|
||||
timestamp: string; // ISO format
|
||||
time: string // ISO format
|
||||
timestamp: number; // epoch milliseconds
|
||||
value: number;
|
||||
};
|
||||
|
||||
|
|
|
@ -95,7 +95,8 @@ export default function MainGrid() {
|
|||
if(payload.topic in topic_map) {
|
||||
const { update } = topic_map[payload.topic];
|
||||
const newPoint: DataPoint = {
|
||||
timestamp: new Date().toISOString(),
|
||||
time: new Date().toISOString(),
|
||||
timestamp: Date.now(),
|
||||
value: d
|
||||
};
|
||||
update(current => [...current, newPoint])
|
||||
|
@ -228,10 +229,10 @@ export default function MainGrid() {
|
|||
<strong>up for</strong> {serverUptime}
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, md: 6}}>
|
||||
<SessionsChart title={'Sent Messages'} label={''} data={sent} isConnected={isConnected}/>
|
||||
<SessionsChart title={'Sent Messages'} label={''} data={sent} isConnected={isConnected} isPerSecond/>
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, md: 6}}>
|
||||
<SessionsChart title={'Received Messages'} label={''} data={received} isConnected={isConnected}/>
|
||||
<SessionsChart title={'Received Messages'} label={''} data={received} isConnected={isConnected} isPerSecond/>
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, md: 6}}>
|
||||
<SessionsChart title={'Bytes Out'} label={'Bytes'} data={bytesOut} isConnected={isConnected}/>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import CountUp from 'react-countup';
|
||||
import type { DataPoint } from '../../assets/helpers.jsx';
|
||||
import {CircularProgress} from "@mui/material";
|
||||
import {useRef} from "react";
|
||||
|
||||
const currentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
|
@ -117,6 +118,23 @@
|
|||
|
||||
export default function SessionsChart(props: any) {
|
||||
|
||||
const lastCalc = useRef<number>(0);
|
||||
|
||||
const calc_per_second = (curValue: DataPoint, lastValue: DataPoint) => {
|
||||
if(!props.isPerSecond) { return ''; }
|
||||
|
||||
if(!curValue || !lastValue) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if(curValue.timestamp - lastValue.timestamp > 0) {
|
||||
const per_second = (curValue.value - lastValue.value) / ((curValue.timestamp - lastValue.timestamp) / 1000);
|
||||
lastCalc.current = Math.trunc(per_second * 10) / 10;
|
||||
}
|
||||
|
||||
return `${lastCalc.current} / sec`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="outlined" sx={{ width: '100%' }}>
|
||||
<CardContent>
|
||||
|
@ -143,6 +161,9 @@
|
|||
|
||||
/>} {props.label}
|
||||
</Typography>
|
||||
<p>
|
||||
{ calc_per_second(props.data[props.data.length-1], props.data[props.data.length-2]) }
|
||||
</p>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{ props.data.length < 2 ? <NoDataDisplay isConnected={props.isConnected}/> :
|
||||
|
|
|
@ -40,6 +40,7 @@ nav:
|
|||
- Plugins:
|
||||
- Packaged: packaged_plugins.md
|
||||
- Custom: custom_plugins.md
|
||||
- Contributed: contrib_plugins.md
|
||||
- Configuration:
|
||||
- Broker: references/broker_config.md
|
||||
- Client: references/client_config.md
|
||||
|
@ -139,7 +140,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
|
||||
|
|
|
@ -20,7 +20,7 @@ classifiers = [
|
|||
"Programming Language :: Python :: 3.13"
|
||||
]
|
||||
|
||||
version = "0.11.1"
|
||||
version = "0.11.3-rc.1"
|
||||
requires-python = ">=3.10.0"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
|
|
@ -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__()
|
2
uv.lock
2
uv.lock
|
@ -9,7 +9,7 @@ resolution-markers = [
|
|||
|
||||
[[package]]
|
||||
name = "amqtt"
|
||||
version = "0.11.1"
|
||||
version = "0.11.3rc1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "dacite" },
|
||||
|
|
Ładowanie…
Reference in New Issue