Merge branch 'rc' into test_paho

pull/190/head
Andrew Mirsky 2025-06-01 11:12:46 -04:00
commit b30a52386f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
23 zmienionych plików z 221 dodań i 283 usunięć

Wyświetl plik

@ -10,75 +10,6 @@ ci:
- pylint
repos:
# Codespell for spelling corrections
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
args:
- --ignore-words-list=ihs,ro,fo,assertIn,astroid,formated
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types:
- csv
- json
# General pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
exclude: tests/_test_files/certs/
- id: check-merge-conflict
- id: check-added-large-files
- id: check-case-conflict
# - id: no-commit-to-branch
# args: [--branch, main]
- id: check-executables-have-shebangs
- id: trailing-whitespace
name: Trim Trailing Whitespace
description: This hook trims trailing whitespace.
entry: trailing-whitespace-fixer
language: python
types: [text]
args: [--markdown-linebreak-ext=md]
- id: check-toml
- id: check-json
- id: check-yaml
args: [--allow-multiple-documents]
- id: mixed-line-ending
# Prettier for code formatting
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
additional_dependencies:
- prettier@3.2.5
- prettier-plugin-sort-json@3.1.0
exclude_types:
- python
# Secret detection
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args:
- --exclude-files=tests/*
- --exclude-files=samples/client_subscribe_acl.py
- --exclude-files=docs/quickstart.rst
- repo: https://github.com/gitleaks/gitleaks
rev: v8.26.0
hooks:
- id: gitleaks
# YAML Linting
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
hooks:
- id: yamllint
# Python-specific hooks ######################################################
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.10
@ -89,12 +20,6 @@ repos:
- --unsafe-fixes
- --line-length=130
- --exit-non-zero-on-fix
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
hooks:
- id: pyupgrade
args: [--py313-plus]
# Local hooks for mypy and pylint
- repo: local

Wyświetl plik

@ -1,2 +0,0 @@
---
jsonRecursiveSort: true

Wyświetl plik

@ -1,70 +0,0 @@
---
extends: default
yaml-files:
- "*.yaml"
- "*.yml"
- ".yamllint"
ignore-from-file: .gitignore
rules:
braces:
level: error
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
level: error
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
level: error
max-spaces-before: 0
max-spaces-after: 1
commas:
level: error
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
level: error
require-starting-space: true
min-spaces-from-content: 1
comments-indentation: false
document-end:
level: error
present: false
document-start:
level: warning
present: true
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
hyphens:
level: error
max-spaces-after: 1
indentation:
level: error
spaces: 2
indent-sequences: consistent
check-multi-line-strings: false
key-duplicates:
level: error
line-length: disable
new-line-at-end-of-file:
level: error
new-lines:
level: error
type: unix
trailing-spaces:
level: error
truthy: disable
octal-values:
forbid-implicit-octal: true
forbid-explicit-octal: true

Wyświetl plik

@ -31,7 +31,7 @@ from amqtt.utils import format_client_message, gen_client_id, read_yaml_config
from .plugins.manager import BaseContext, PluginManager
_CONFIG_LISTENER: TypeAlias = dict[str, int | bool | dict[str, Any]]
_BROADCAST: TypeAlias = dict[str, Session | str | bytes | int | None]
_BROADCAST: TypeAlias = dict[str, Session | str | bytes | bytearray | int | None]
_defaults = read_yaml_config(Path(__file__).parent / "scripts/default_broker.yaml")
@ -60,7 +60,7 @@ class Action(Enum):
class RetainedApplicationMessage(ApplicationMessage):
__slots__ = ("data", "qos", "source_session", "topic")
def __init__(self, source_session: Session | None, topic: str, data: bytes, qos: int | None = None) -> None:
def __init__(self, source_session: Session | None, topic: str, data: bytes | bytearray, qos: int | None = None) -> None:
super().__init__(None, topic, qos, data, retain=True)
self.source_session = source_session
self.topic = topic
@ -934,7 +934,7 @@ class Broker:
self,
session: Session | None,
topic: str | None,
data: bytes | None,
data: bytes | bytearray | None,
force_qos: int | None = None,
) -> None:
broadcast: _BROADCAST = {"session": session, "topic": topic, "data": data}

Wyświetl plik

@ -5,7 +5,7 @@ from amqtt.adapters import ReaderAdapter
from amqtt.errors import NoDataError
def bytes_to_hex_str(data: bytes) -> str:
def bytes_to_hex_str(data: bytes | bytearray) -> str:
"""Convert a sequence of bytes into its displayable hex representation, ie: 0x??????.
:param data: byte sequence
@ -105,7 +105,7 @@ def encode_string(string: str) -> bytes:
return int_to_bytes(data_length, 2) + data
def encode_data_with_length(data: bytes) -> bytes:
def encode_data_with_length(data: bytes | bytearray) -> bytes:
"""Encode data with its length as prefix.
:param data: data to encode

Wyświetl plik

@ -28,7 +28,7 @@ class ConnackVariableHeader(MQTTVariableHeader):
return_code = bytes_to_int(data[1])
return cls(session_parent, return_code)
def to_bytes(self) -> bytearray:
def to_bytes(self) -> bytes | bytearray:
out = bytearray(2)
# Connect acknowledge flags
out[0] = 1 if self.session_parent else 0

Wyświetl plik

@ -69,7 +69,7 @@ class ConnectVariableHeader(MQTTVariableHeader):
return cls(flags, keep_alive, protocol_name, protocol_level)
def to_bytes(self) -> bytearray:
def to_bytes(self) -> bytes | bytearray:
out = bytearray()
# Protocol name
@ -222,7 +222,7 @@ class ConnectPayload(MQTTPayload[ConnectVariableHeader]):
self,
fixed_header: MQTTFixedHeader | None = None,
variable_header: ConnectVariableHeader | None = None,
) -> bytes:
) -> bytes | bytearray:
out = bytearray()
# Client identifier
if self.client_id is not None:

Wyświetl plik

@ -82,6 +82,8 @@ class MQTTFixedHeader:
async def decode_remaining_length() -> int:
"""Decode the remaining length from the stream."""
multiplier: int
value: int
multiplier, value = 1, 0
buffer = bytearray()
while True:
@ -123,7 +125,7 @@ class MQTTVariableHeader(ABC):
await writer.drain()
@abstractmethod
def to_bytes(self) -> bytes:
def to_bytes(self) -> bytes | bytearray:
"""Serialize the variable header to bytes."""
@property
@ -173,7 +175,7 @@ class MQTTPayload(Generic[_VH], ABC):
await writer.drain()
@abstractmethod
def to_bytes(self, fixed_header: MQTTFixedHeader | None = None, variable_header: _VH | None = None) -> bytes:
def to_bytes(self, fixed_header: MQTTFixedHeader | None = None, variable_header: _VH | None = None) -> bytes | bytearray:
pass
@classmethod

Wyświetl plik

@ -198,7 +198,7 @@ class ProtocolHandler:
async def mqtt_publish(
self,
topic: str,
data: bytes,
data: bytes | bytearray ,
qos: int | None,
retain: bool,
ack_timeout: int | None = None,

Wyświetl plik

@ -22,7 +22,7 @@ class PublishVariableHeader(MQTTVariableHeader):
"""Return a string representation of the PublishVariableHeader object."""
return f"{type(self).__name__}(topic={self.topic_name}, packet_id={self.packet_id})"
def to_bytes(self) -> bytearray:
def to_bytes(self) -> bytes | bytearray:
out = bytearray()
out.extend(encode_string(self.topic_name))
if self.packet_id is not None:
@ -69,7 +69,7 @@ class PublishPayload(MQTTPayload[MQTTVariableHeader]):
buffer = await reader.read(data_length - length_read)
data.extend(buffer)
length_read = len(data)
return cls(data)
return cls(bytes(data))
def __repr__(self) -> str:
"""Return a string representation of the PublishPayload object."""
@ -109,7 +109,7 @@ class PublishPacket(MQTTPacket[PublishVariableHeader, PublishPayload, MQTTFixedH
packet = cls(variable_header=v_header, payload=payload)
packet.dup_flag = dup_flag
packet.retain_flag = retain
packet.qos = qos
packet.qos = qos or 0
return packet
def set_flags(self, dup_flag: bool = False, qos: int = 0, retain_flag: bool = False) -> None:

Wyświetl plik

@ -1,31 +1,30 @@
from pathlib import Path
from typing import Any
from passlib.apps import custom_app_context as pwd_context
from amqtt.broker import BrokerContext
from amqtt.plugins.base import BasePlugin
from amqtt.session import Session
_PARTS_EXPECTED_LENGTH = 2 # Expected number of parts in a valid line
class BaseAuthPlugin:
class BaseAuthPlugin(BasePlugin):
"""Base class for authentication plugins."""
def __init__(self, context: BrokerContext) -> None:
self.context = context
self.auth_config = self.context.config.get("auth", None) if self.context.config else None
super().__init__(context)
self.auth_config: dict[str, Any] | None = self._get_config_section("auth")
if not self.auth_config:
self.context.logger.warning("'auth' section not found in context configuration")
async def authenticate(self, *args: None, **kwargs: Session) -> bool | None:
async def authenticate(self, *, session: Session) -> bool | None:
"""Logic for session authentication.
Args:
*args: positional arguments (not used)
**kwargs: payload from broker
```
session: amqtt.session.Session
```
session: amqtt.session.Session
Returns:
- `True` if user is authentication succeed, `False` if user authentication fails
@ -42,8 +41,8 @@ class BaseAuthPlugin:
class AnonymousAuthPlugin(BaseAuthPlugin):
"""Authentication plugin allowing anonymous access."""
async def authenticate(self, *args: None, **kwargs: Session) -> bool:
authenticated = await super().authenticate(*args, **kwargs)
async def authenticate(self, *, session: Session) -> bool:
authenticated = await super().authenticate(session=session)
if authenticated:
# Default to allowing anonymous
allow_anonymous = self.auth_config.get("allow-anonymous", True) if isinstance(self.auth_config, dict) else True
@ -51,7 +50,6 @@ class AnonymousAuthPlugin(BaseAuthPlugin):
self.context.logger.debug("Authentication success: config allows anonymous")
return True
session: Session | None = kwargs.get("session")
if session and session.username:
self.context.logger.debug(f"Authentication success: session has username '{session.username}'")
return True
@ -95,11 +93,10 @@ class FileAuthPlugin(BaseAuthPlugin):
except Exception:
self.context.logger.exception(f"Unexpected error reading password file '{password_file}'")
async def authenticate(self, *args: None, **kwargs: Session) -> bool | None:
async def authenticate(self, *, session: Session) -> bool | None:
"""Authenticate users based on the file-stored user database."""
authenticated = await super().authenticate(*args, **kwargs)
authenticated = await super().authenticate(session=session)
if authenticated:
session = kwargs.get("session")
if not session:
self.context.logger.debug("Authentication failure: no session provided")
return False

Wyświetl plik

@ -0,0 +1,19 @@
from typing import Any
from amqtt.broker import BrokerContext
class BasePlugin:
"""The base from which all plugins should inherit."""
def __init__(self, context: BrokerContext) -> None:
self.context = context
def _get_config_section(self, name: str) -> dict[str, Any] | None:
if not self.context.config or not self.context.config.get(name, None):
return None
section_config: int | dict[str, Any] | None = self.context.config.get(name, None)
# mypy has difficulty excluding int from `config`'s type, unless isinstance` is its own check
if isinstance(section_config, int):
return None
return section_config

Wyświetl plik

@ -3,18 +3,15 @@ from functools import partial
import logging
from typing import TYPE_CHECKING, Any
from amqtt.plugins.manager import BaseContext
from amqtt.plugins.base import BasePlugin
if TYPE_CHECKING:
from amqtt.session import Session
class EventLoggerPlugin:
class EventLoggerPlugin(BasePlugin):
"""A plugin to log events dynamically based on method names."""
def __init__(self, context: BaseContext) -> None:
self.context = context
async def log_event(self, *args: Any, **kwargs: Any) -> None:
"""Log the occurrence of an event."""
event_name = kwargs["event_name"].replace("old", "")
@ -28,12 +25,9 @@ class EventLoggerPlugin:
raise AttributeError(msg)
class PacketLoggerPlugin:
class PacketLoggerPlugin(BasePlugin):
"""A plugin to log MQTT packets sent and received."""
def __init__(self, context: BaseContext) -> None:
self.context = context
async def on_mqtt_packet_received(self, *args: Any, **kwargs: Any) -> None:
"""Log an MQTT packet when it is received."""
packet = kwargs.get("packet")

Wyświetl plik

@ -2,6 +2,8 @@ import asyncio
from collections import deque # pylint: disable=C0412
from typing import SupportsIndex, SupportsInt # pylint: disable=C0412
from amqtt.plugins.base import BasePlugin
try:
from collections.abc import Buffer
except ImportError:
@ -40,9 +42,9 @@ STAT_CLIENTS_CONNECTED = "clients_connected"
STAT_CLIENTS_DISCONNECTED = "clients_disconnected"
class BrokerSysPlugin:
class BrokerSysPlugin(BasePlugin):
def __init__(self, context: BrokerContext) -> None:
self.context = context
super().__init__(context)
# Broker statistics initialization
self._stats: dict[str, int] = {}
self._sys_handle: asyncio.Handle | None = None

Wyświetl plik

@ -1,30 +1,29 @@
from typing import Any
from amqtt.broker import Action
from amqtt.plugins.manager import BaseContext
from amqtt.broker import Action, BrokerContext
from amqtt.plugins.base import BasePlugin
from amqtt.session import Session
class BaseTopicPlugin:
class BaseTopicPlugin(BasePlugin):
"""Base class for topic plugins."""
def __init__(self, context: BaseContext) -> None:
self.context = context
self.topic_config: dict[str, Any] | None = self.context.config.get("topic-check", None) if self.context.config else None
def __init__(self, context: BrokerContext) -> None:
super().__init__(context)
self.topic_config: dict[str, Any] | None = self._get_config_section("topic-check")
if self.topic_config is None:
self.context.logger.warning("'topic-check' section not found in context configuration")
async def topic_filtering(self, *args: Any, **kwargs: Any) -> bool:
async def topic_filtering(
self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None
) -> bool:
"""Logic for filtering out topics.
Args:
*args: positional arguments (not used)
**kwargs: payload from broker
```
session: amqtt.session.Session
topic: str
action: amqtt.broker.Action
```
session: amqtt.session.Session
topic: str
action: amqtt.broker.Action
Returns:
bool: `True` if topic is allowed, `False` otherwise
@ -32,21 +31,21 @@ class BaseTopicPlugin:
"""
if not self.topic_config:
# auth config section not found
self.context.logger.warning("'auth' section not found in context configuration")
self.context.logger.warning("'topic-check' section not found in context configuration")
return False
return True
class TopicTabooPlugin(BaseTopicPlugin):
def __init__(self, context: BaseContext) -> None:
def __init__(self, context: BrokerContext) -> None:
super().__init__(context)
self._taboo: list[str] = ["prohibited", "top-secret", "data/classified"]
async def topic_filtering(self, *args: Any, **kwargs: Any) -> bool:
filter_result = await super().topic_filtering(*args, **kwargs)
async def topic_filtering(
self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None
) -> bool:
filter_result = await super().topic_filtering(session=session, topic=topic, action=action)
if filter_result:
session = kwargs.get("session")
topic = kwargs.get("topic")
if session and session.username == "admin":
return True
return not (topic and topic in self._taboo)
@ -54,6 +53,7 @@ class TopicTabooPlugin(BaseTopicPlugin):
class TopicAccessControlListPlugin(BaseTopicPlugin):
@staticmethod
def topic_ac(topic_requested: str, topic_allowed: str) -> bool:
req_split = topic_requested.split("/")
@ -74,22 +74,22 @@ class TopicAccessControlListPlugin(BaseTopicPlugin):
break
return ret
async def topic_filtering(self, *args: Any, **kwargs: Any) -> bool:
filter_result = await super().topic_filtering(*args, **kwargs)
async def topic_filtering(
self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None
) -> bool:
filter_result = await super().topic_filtering(session=session, topic=topic, action=action)
if not filter_result:
return False
# hbmqtt and older amqtt do not support publish filtering
action = kwargs.get("action")
if action == Action.PUBLISH and self.topic_config is not None and "publish-acl" not in self.topic_config:
# maintain backward compatibility, assume permitted
return True
req_topic = kwargs.get("topic")
req_topic = topic
if not req_topic:
return False
session = kwargs.get("session")
username = session.username if session else None
if username is None:
username = "anonymous"
@ -100,7 +100,7 @@ class TopicAccessControlListPlugin(BaseTopicPlugin):
elif self.topic_config is not None and action == Action.SUBSCRIBE:
acl = self.topic_config.get("acl", {})
allowed_topics = acl.get(username, None)
allowed_topics = acl.get(username, [])
if not allowed_topics:
return False

Wyświetl plik

@ -31,7 +31,7 @@ class ApplicationMessage:
"topic",
)
def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes, retain: bool) -> None:
def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes | bytearray, retain: bool) -> None:
self.packet_id: int | None = packet_id
""" Publish message packet identifier
<http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718025>_
@ -80,7 +80,7 @@ class ApplicationMessage:
:param dup: force dup flag
:return: :class:`amqtt.mqtt.publish.PublishPacket` built from ApplicationMessage instance attributes
"""
return PublishPacket.build(self.topic, self.data, self.packet_id, dup, self.qos, self.retain)
return PublishPacket.build(self.topic, bytes(self.data), self.packet_id, dup, self.qos, self.retain)
def __eq__(self, other: object) -> bool:
"""Compare two ApplicationMessage instances based on their packet_id.
@ -109,7 +109,7 @@ class OutgoingApplicationMessage(ApplicationMessage):
__slots__ = ("direction",)
def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes, retain: bool) -> None:
def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes | bytearray, retain: bool) -> None:
super().__init__(packet_id, topic, qos, data, retain)
self.direction: int = OUTGOING

Wyświetl plik

@ -1,7 +1,30 @@
# Custom Plugins
Every plugin listed in the `project.entry-points` is loaded and notified of events
by defining any of the following methods:
With the aMQTT Broker plugins framework, one can add additional functionality to the broker without
having to subclass or rewrite any of the core broker logic. To define a custom list of plugins to be loaded,
add this section to your `pyproject.toml`"
```toml
[project.entry-points."mypackage.mymodule.plugins"]
plugin_alias = "module.submodule.file:ClassName"
```
and specify the namespace when instantiating the broker:
```python
from amqtt.broker import Broker
broker = Broker(plugin_namespace='mypackage.mymodule.plugins')
```
Each plugin has access to the full configuration file through the provided `BaseContext` and can define
its own variables to configure its behavior.
::: amqtt.plugins.base.BasePlugin
Plugins that are defined in the`project.entry-points` are loaded and notified of events by when the subclass
implements one or more of these methods:
- `on_mqtt_packet_sent`
- `on_mqtt_packet_received`
@ -18,7 +41,7 @@ by defining any of the following methods:
## Authentication Plugins
Of the plugins listed in `project.entry-points`, plugins can be used to validate client sessions
Of the plugins listed in `project.entry-points`, one or more can be used to validate client sessions
by specifying their alias in `auth` > `plugins` section of the config:
```yaml
@ -27,22 +50,23 @@ auth:
- plugin_alias_name
```
These plugins should sub-class from `BaseAuthPlugin` and implement the `authenticate` method.
These plugins should subclass from `BaseAuthPlugin` and implement the `authenticate` method.
::: amqtt.plugins.authentication.BaseAuthPlugin
## Topic Filter Plugins
Of the plugins listed in `project.entry-points`, plugins can be used to validate client sessions
Of the plugins listed in `project.entry-points`, one or more can be used to determine topic access
by specifying their alias in `topic-check` > `plugins` section of the config:
```yaml
topic-check:
enable: True
plugins:
- plugin_alias_name
```
These plugins should sub-class from `BaseTopicPlugin` and implement the `topic_filtering` method.
These plugins should subclass from `BaseTopicPlugin` and implement the `topic_filtering` method.
::: amqtt.plugins.topic_checking.BaseTopicPlugin

Wyświetl plik

@ -1,12 +1,10 @@
# Existing Plugins
With the aMQTT Broker plugins framework, one can add additional functionality without
having to rewrite core logic. The list of plugins that get loaded are specified in `pyproject.toml`;
each plugin can then check the configuration to determine how to behave (including disabling).
having to rewrite core logic. Plugins loaded by default are specified in `pyproject.toml`:
```toml
[project.entry-points."amqtt.broker.plugins"]
plugin_alias = "module.submodule.file:ClassName"
```yaml
--8<-- "pyproject.toml:included"
```
## auth_anonymous (Auth Plugin)
@ -14,7 +12,7 @@ plugin_alias = "module.submodule.file:ClassName"
`amqtt.plugins.authentication:AnonymousAuthPlugin`
**Config Options**
**Configuration**
```yaml
auth:
@ -34,7 +32,7 @@ auth:
clients are authorized by providing username and password, compared against file
**Config Options**
**Configuration**
```yaml
@ -64,7 +62,6 @@ print(sha512_crypt.hash(passwd))
`amqtt.plugins.topic_checking:TopicTabooPlugin`
Prevents using topics named: `prohibited`, `top-secret`, and `data/classified`
**Configuration**
@ -82,6 +79,19 @@ topic-check:
**Configuration**
- `acl` *(list)*: determines subscription access; if `publish-acl` is not specified, determine both publish and subscription access.
The list should be a key-value pair, where:
`<username>:[<topic1>, <topic2>, ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`).
- `publish-acl` *(list)*: determines publish access. This parameter defines the list of access control rules; each item is a key-value pair, where:
`<username>:[<topic1>, <topic2>, ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`).
!!! info "Reserved usernames"
- The username `admin` is allowed access to all topics.
- The username `anonymous` will control allowed topics, if using the `auth_anonymous` plugin.
```yaml
topic-check:
enabled: true
@ -95,20 +105,17 @@ topic-check:
- .
```
## Plugin: $SYS
`amqtt.plugins.sys.broker:BrokerSysPlugin`
Publishes, on a periodic basis, statistics about the broker
**Config Options**
**Configuration**
- `sys_interval` - int, seconds between updates
### Supported Topics
**Supported Topics**
- `$SYS/broker/load/bytes/received` - payload: `data`, int
- `$SYS/broker/load/bytes/sent` - payload: `data`, int

Wyświetl plik

@ -33,9 +33,30 @@ Client disconnect timeout without a keep-alive
Configuration for authentication behaviour:
- `plugins` *(list[string])*: defines the list of plugins which are activated as authentication plugins. Note the plugins must be defined in the `amqtt.broker.plugins` [entry point](https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata).
- `allow-anonymous` *(bool)*: used by the internal `amqtt.plugins.authentication.AnonymousAuthPlugin` plugin. This parameter enables (`on`) or disable anonymous connection, i.e. connection without username.
- `password-file` *(string)*: used by the internal `amqtt.plugins.authentication.FileAuthPlugin` plugin. Path to file which includes `username:password` pair, one per line. The password should be encoded using sha-512 with `mkpasswd -m sha-512` or:
- `plugins` *(list[string])*: defines the list of plugins which are activated as authentication plugins.
!!! note "Entry points"
Plugins used here must first be defined in the `amqtt.broker.plugins` [entry point](https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata).
!!! danger "Legacy behavior"
if `plugins` is omitted from the `auth` section, all plugins listed in the `amqtt.broker.plugins` entrypoint will be enabled
for authentication, *including allowing anonymous login.*
`plugins: []` will deny connections from all clients.
- `allow-anonymous` *(bool)*: `True` will allow anonymous connections.
*Used by the internal `amqtt.plugins.authentication.AnonymousAuthPlugin` plugin*
!!! danger "Username only connections"
`False` does not disable the `auth_anonymous` plugin; connections will still be allowed as long as a username is provided.
If security is required, do not include `auth_anonymous` in the `plugins` list.
- `password-file` *(string)*: Path to file which includes `username:password` pair, one per line. The password should be encoded using sha-512 with `mkpasswd -m sha-512` or:
```python
import sys
from getpass import getpass
@ -44,6 +65,8 @@ Configuration for authentication behaviour:
passwd = input() if not sys.stdin.isatty() else getpass()
print(sha512_crypt.hash(passwd))
```
*Used by the internal `amqtt.plugins.authentication.FileAuthPlugin` plugin.*
### `topic-check` *(mapping)*
@ -51,12 +74,23 @@ Configuration for access control policies for publishing and subscribing to topi
- `enabled` *(bool)*: Enable access control policies (`true`). `false` will allow clients to publish and subscribe to any topic.
- `plugins` *(list[string])*: defines the list of plugins which are activated as access control plugins. Note the plugins must be defined in the `amqtt.broker.plugins` [entry point](https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins).
- `acl` *(list)*: used by the internal `amqtt.plugins.topic_acl.TopicAclPlugin` plugin to determine subscription access. This parameter defines the list of access control rules; each item is a key-value pair, where:
- `acl` *(list)*: plugin to determine subscription access; if `publish-acl` is not specified, determine both publish and subscription access.
The list should be a key-value pair, where:
`<username>:[<topic1>, <topic2>, ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`).
use `anonymous` username for the list of allowed topics if using the `auth_anonymous` plugin.
- `publish-acl` *(list)*: used by the internal `amqtt.plugins.topic_acl.TopicAclPlugin` plugin to determine publish access. This parameter defines the list of access control rules; each item is a key-value pair, where:
*used by the `amqtt.plugins.topic_acl.TopicAclPlugin`*
- `publish-acl` *(list)*: plugin to determine publish access. This parameter defines the list of access control rules; each item is a key-value pair, where:
`<username>:[<topic1>, <topic2>, ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`).
use `anonymous` username for the list of allowed topics if using the `auth_anonymous` plugin.
!!! info "Reserved usernames"
- The username `admin` is allowed access to all topic.
- The username `anonymous` will control allowed topics if using the `auth_anonymous` plugin.
*used by the `amqtt.plugins.topic_acl.TopicAclPlugin`*

Wyświetl plik

@ -88,7 +88,6 @@ theme:
extra_css:
- assets/extra.css
#extra_javascript:
#- assets/extra.js
@ -117,7 +116,6 @@ markdown_extensions:
- toc:
permalink: "¤"
plugins:
- search
- autorefs

Wyświetl plik

@ -35,7 +35,7 @@ dependencies = [
dev = [
"hatch>=1.14.1",
"hypothesis>=6.130.8",
"mypy<1.15.0",
"mypy>=1.15.0",
"paho-mqtt>=2.1.0",
"poethepoet>=0.34.0",
"pre-commit>=4.2.0", # https://pypi.org/project/pre-commit
@ -97,6 +97,7 @@ test_plugin = "tests.plugins.test_manager:EmptyTestPlugin"
event_plugin = "tests.plugins.test_manager:EventTestPlugin"
packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin"
# --8<-- [start:included]
[project.entry-points."amqtt.broker.plugins"]
event_logger_plugin = "amqtt.plugins.logging_amqtt:EventLoggerPlugin"
packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin"
@ -105,6 +106,8 @@ auth_file = "amqtt.plugins.authentication:FileAuthPlugin"
topic_taboo = "amqtt.plugins.topic_checking:TopicTabooPlugin"
topic_acl = "amqtt.plugins.topic_checking:TopicAccessControlListPlugin"
broker_sys = "amqtt.plugins.sys.broker:BrokerSysPlugin"
# --8<-- [end:included]
[project.entry-points."amqtt.client.plugins"]
packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin"

Wyświetl plik

@ -2,7 +2,7 @@ import logging
import pytest
from amqtt.broker import Action
from amqtt.broker import Action, BrokerContext, Broker
from amqtt.plugins.manager import BaseContext
from amqtt.plugins.topic_checking import BaseTopicPlugin, TopicAccessControlListPlugin, TopicTabooPlugin
from amqtt.session import Session
@ -29,7 +29,7 @@ async def test_base_no_config(logdog):
assert log_records[0].message == "'topic-check' section not found in context configuration"
assert log_records[1].levelno == logging.WARNING
assert log_records[1].message == "'auth' section not found in context configuration"
assert log_records[1].message == "'topic-check' section not found in context configuration"
assert pile.is_empty()
@ -37,7 +37,8 @@ async def test_base_no_config(logdog):
async def test_base_empty_config(logdog):
"""Check BaseTopicPlugin returns false if topic-check is empty."""
with logdog() as pile:
context = BaseContext()
broker = Broker()
context = BrokerContext(broker)
context.logger = logging.getLogger("testlog")
context.config = {"topic-check": {}}
@ -47,9 +48,12 @@ async def test_base_empty_config(logdog):
# Should have printed just one warning
log_records = list(pile.drain(name="testlog"))
assert len(log_records) == 1
assert len(log_records) == 2
assert log_records[0].levelno == logging.WARNING
assert log_records[0].message == "'auth' section not found in context configuration"
assert log_records[0].message == "'topic-check' section not found in context configuration"
assert log_records[1].levelno == logging.WARNING
assert log_records[1].message == "'topic-check' section not found in context configuration"
@pytest.mark.asyncio
@ -106,7 +110,7 @@ async def test_taboo_empty_config(logdog):
assert log_records[0].levelno == logging.WARNING
assert log_records[0].message == "'topic-check' section not found in context configuration"
assert log_records[1].levelno == logging.WARNING
assert log_records[1].message == "'auth' section not found in context configuration"
assert log_records[1].message == "'topic-check' section not found in context configuration"
@pytest.mark.asyncio
@ -264,7 +268,7 @@ async def test_taclp_empty_config(logdog):
log_records = list(pile.drain(name="testlog"))
assert len(log_records) == 2
assert log_records[0].message == "'topic-check' section not found in context configuration"
assert log_records[1].message == "'auth' section not found in context configuration"
assert log_records[1].message == "'topic-check' section not found in context configuration"
@pytest.mark.asyncio

57
uv.lock
Wyświetl plik

@ -76,7 +76,7 @@ provides-extras = ["ci"]
dev = [
{ name = "hatch", specifier = ">=1.14.1" },
{ name = "hypothesis", specifier = ">=6.130.8" },
{ name = "mypy", specifier = "<1.15.0" },
{ name = "mypy", specifier = ">=1.15.0" },
{ name = "paho-mqtt", specifier = ">=2.1.0" },
{ name = "poethepoet", specifier = ">=0.34.0" },
{ name = "pre-commit", specifier = ">=4.2.0" },
@ -1193,40 +1193,41 @@ wheels = [
[[package]]
name = "mypy"
version = "1.14.1"
version = "1.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" },
{ url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" },
{ url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" },
{ url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" },
{ url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" },
{ url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" },
{ url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" },
{ url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" },
{ url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" },
{ url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" },
{ url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" },
{ url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" },
{ url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" },
{ url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" },
{ url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" },
{ url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" },
{ url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" },
{ url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" },
{ url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" },
{ url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" },
{ url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" },
{ url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" },
{ url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" },
{ url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" },
{ url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" },
{ url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" },
{ url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" },
{ url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" },
{ url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" },
{ url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" },
{ url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" },
{ url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" },
{ url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" },
{ url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" },
{ url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" },
{ url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" },
{ url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" },
{ url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" },
{ url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" },
{ url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" },
{ url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" },
{ url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" },
{ url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" },
{ url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" },
{ url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" },
]
[[package]]