pax_global_header 0000666 0000000 0000000 00000000064 14343623446 0014523 g ustar 00root root 0000000 0000000 52 comment=eb13ec8a61e31982dae6b43dcdf570979b5318b5
asyncio-mqtt-0.16.1/ 0000775 0000000 0000000 00000000000 14343623446 0014240 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/.github/ 0000775 0000000 0000000 00000000000 14343623446 0015600 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/.github/codecov.yml 0000664 0000000 0000000 00000000275 14343623446 0017751 0 ustar 00root root 0000000 0000000 coverage:
precision: 1
# PRs can result in small changes of e.g. 0.04 percent; don't let these
# prevent a passing check
status:
project:
default:
threshold: 0.2%
asyncio-mqtt-0.16.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14343623446 0017635 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/.github/workflows/publish.yml 0000664 0000000 0000000 00000001072 14343623446 0022026 0 ustar 00root root 0000000 0000000 name: Publish package to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install dependencies
run: pip install build
- name: Create package
run: python -m build -s -w .
- name: Upload package
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
asyncio-mqtt-0.16.1/.github/workflows/test.yml 0000664 0000000 0000000 00000002364 14343623446 0021344 0 ustar 00root root 0000000 0000000 name: test suite
on:
push:
branches: [master]
paths:
- "**.py"
pull_request:
paths:
- "**.py"
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest]
python-version:
["3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.8, pypy-3.9]
include:
- os: macos-latest
python-version: "3.7"
- os: macos-latest
python-version: "3.11"
- os: windows-latest
python-version: "3.7"
- os: windows-latest
python-version: "3.11"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: pip-test-${{ matrix.python-version }}-${{ matrix.os }}
- name: Install dependencies
run: pip install -e .[tests]
- name: Test with pytest
run: pytest --cov=asyncio_mqtt --cov=tests --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
asyncio-mqtt-0.16.1/.gitignore 0000664 0000000 0000000 00000000155 14343623446 0016231 0 ustar 00root root 0000000 0000000 __pycache__
build
dist
*.egg-info
local_test.py
.venv
.idea/
.coverage
coverage.xml
asyncio_mqtt/_version.py
asyncio-mqtt-0.16.1/.pre-commit-config.yaml 0000664 0000000 0000000 00000002364 14343623446 0020526 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.158
hooks:
- id: ruff
args:
- --fix
- --target-version
- py37
- repo: https://github.com/psf/black
rev: 22.10.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
hooks:
- id: mypy
additional_dependencies:
- anyio
- pytest
- types-paho-mqtt == 1.6.0.1
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.4
hooks:
- id: prettier
- repo: https://github.com/markdownlint/markdownlint
rev: v0.12.0
hooks:
- id: markdownlint
args:
- "--rules"
- "~MD002,~MD007,~MD013,~MD024,~MD026,~MD029,~MD033,~MD036"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
hooks:
- id: python-check-blanket-type-ignore
- id: python-check-blanket-noqa
asyncio-mqtt-0.16.1/CHANGELOG.md 0000664 0000000 0000000 00000037055 14343623446 0016063 0 ustar 00root root 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.16.1]
### Fixed
- Add properties in Message, the last release skipped this commit. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse)
## [0.16.0]
### Added
- Add properties in Message. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#166](https://github.com/sbtinstruments/asyncio-mqtt/pull/166)
### Changed
- Update Ruff and fix new lint errors. Contributed by [pre-commit-ci](https://github.com/apps/pre-commit-ci) and [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#161](https://github.com/sbtinstruments/asyncio-mqtt/pull/161)
### Fixed
- Fix typo in description metadata. Contributed by [(@pi-slh)](https://github.com/pi-slh) in [#162](https://github.com/sbtinstruments/asyncio-mqtt/pull/162)
## [0.15.0]
### Added
- Allow multiple message generators at the same time. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#147](https://github.com/sbtinstruments/asyncio-mqtt/pull/147)
### Changed
- Simplify message filtering. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#147](https://github.com/sbtinstruments/asyncio-mqtt/pull/147)
- Switch from `flake8` to `ruff` for linting. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#155](https://github.com/sbtinstruments/asyncio-mqtt/pull/155)
## [0.14.0]
### Added
- Add [pre-commit](https://pre-commit.com/) for continuous integration. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#144](https://github.com/sbtinstruments/asyncio-mqtt/pull/144)
- Add tests and coverage for continuous integration. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#145](https://github.com/sbtinstruments/asyncio-mqtt/pull/145) and [#149](https://github.com/sbtinstruments/asyncio-mqtt/pull/149)
- Add continuous deployment to PyPI. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#151](https://github.com/sbtinstruments/asyncio-mqtt/pull/151)
### Changed
- Reorder `README.md` sections in order of importance. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#140](https://github.com/sbtinstruments/asyncio-mqtt/pull/140)
- Change from `setup.py` to `pyproject.toml`. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#151](https://github.com/sbtinstruments/asyncio-mqtt/pull/151)
### Deprecated
- Deprecate `master` branch in favor of `main` branch.
### Removed
- Drop Python 3.6 support. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#146](https://github.com/sbtinstruments/asyncio-mqtt/pull/146)
### Fixed
- Fix lifespan example in `README.md`. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) and [Felix Böhm (@empicano)](https://github.com/empicano) in [#135](https://github.com/sbtinstruments/asyncio-mqtt/pull/135)
## [0.13.0]
### Added
- Add `proxy` and `tls_params` examples to `README.md`. Contributed by [Muhammad Sohaib Arshid (@Sohaib90)](https://github.com/Sohaib90) in [#128](https://github.com/sbtinstruments/asyncio-mqtt/pull/128)
- Add `proxy` option. Contributed by [Muhammad Sohaib Arshid (@Sohaib90)](https://github.com/Sohaib90) in [#127](https://github.com/sbtinstruments/asyncio-mqtt/pull/127)
- Add `tls_params` option. Contributed by [Muhammad Sohaib Arshid (@Sohaib90)](https://github.com/Sohaib90) in [#126](https://github.com/sbtinstruments/asyncio-mqtt/pull/126)
- Add WebSocket connection options. Contributed by [Dustin C. Hatch (@AdmiralNemo)](https://github.com/AdmiralNemo) in [#115](https://github.com/sbtinstruments/asyncio-mqtt/pull/115)
- Add LICENSE to tarballs produced during build. Contributed by [Stewart Haines (@stewarthaines)](https://github.com/stewarthaines) in [#107](https://github.com/sbtinstruments/asyncio-mqtt/pull/107)
### Changed
- Rework type hints of the entire project to make it compliant with mypy in strict mode. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#133](https://github.com/sbtinstruments/asyncio-mqtt/pull/133)
- Rework related projects in `README.md`. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#132](https://github.com/sbtinstruments/asyncio-mqtt/pull/132)
- Increase LoC count from 600 to 700 in `README.md`.
### Fixed
- Fix autocomplete for `_outgoing_call` decorator. Contributed by [Jonathan Plasse (@JonathanPlasse)](https://github.com/JonathanPlasse) in [#134](https://github.com/sbtinstruments/asyncio-mqtt/pull/134)
- Fix missing ProtocolVersion import in `README.md` and format the entire `README.md`. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#130](https://github.com/sbtinstruments/asyncio-mqtt/pull/130)
- Fix 'asyncio_mqtt' has no attribute 'TLSParameters' error. Contributed by [Felix Böhm (@empicano)](https://github.com/empicano) in [#129](https://github.com/sbtinstruments/asyncio-mqtt/pull/129)
- Fix formatting in `README.md`. Contributed by [Marcelo Trylesinski (@Kludex)](https://github.com/Kludex) in [#128](https://github.com/sbtinstruments/asyncio-mqtt/pull/128)
- Fix race conditions that may cause `InvalidStateError`. Contributed by [Andreas Hangauer (@aha79)](https://github.com/aha79) in [#123](https://github.com/sbtinstruments/asyncio-mqtt/pull/123)
- Fix thread safety in socket close callback. Contributed by [Dustin C. Hatch (@AdmiralNemo)](https://github.com/AdmiralNemo) in [#114](https://github.com/sbtinstruments/asyncio-mqtt/pull/114)
## [0.12.1] - 2022-01-19
### Fixed
- Fix `TypeError` with the new `_outgoing_call` decorator.
## [0.12.0] - 2022-01-19
### Added
- Add backpressure mechanism to limit the number of concurrent outgoing calls. Contributed by [Aaron Bach (@bachya)](https://github.com/bachya) in [#101](https://github.com/sbtinstruments/asyncio-mqtt/pull/101)
## [0.11.1] - 2022-01-10
### Fixed
- Fix race condition in the "advanced example" in `README.md`. Contributed by [Steve Palmer (@steverpalmer)](https://github.com/steverpalmer) in [#93](https://github.com/sbtinstruments/asyncio-mqtt/pull/93)
- Make `password` keyword argument optional (as it always should have been). Contributed by [@Shikoruma](https://github.com/Shikoruma) in [#89](https://github.com/sbtinstruments/asyncio-mqtt/pull/89)
- Fix false postive type error from Pyright regarding `asynccontextmanager`. Contributed by [Shawn Wilsher (@sdwilsh)](https://github.com/sdwilsh) in [#87](https://github.com/sbtinstruments/asyncio-mqtt/pull/87)
- Fix type hints (mostly related to async_generator). Contributed by [@laundmo](https://github.com/laundmo) in [#86](https://github.com/sbtinstruments/asyncio-mqtt/pull/86)
## [0.11.0] - 2021-11-04
### Added
- Add link to PyPI package with a badge in `README.md`. Contributed by [Paul Barker (@pbrkr)](https://github.com/pbrkr) in [#72](https://github.com/sbtinstruments/asyncio-mqtt/pull/72)
### Changed
- Convert reason codes into English error messages. Contributed by [@CptSpaceToaster](https://github.cm/CptSpaceToaster) in [#74](https://github.com/sbtinstruments/asyncio-mqtt/pull/74)
### Fixed
- Fix event loop block caused by paho's reconnection feature. Contributed by [Martin Hjelmare (@MartinHjelmare)](https://github.cm/MartinHjelmare) in [#85](https://github.com/sbtinstruments/asyncio-mqtt/pull/85)
## [0.10.0] - 2021-07-13
### Added
- Add new parameter `socket_options` to `Client.__init__`. Contributed by [@xydan83](https://github.com/xydan83) in [#71](https://github.com/sbtinstruments/asyncio-mqtt/pull/71)
- Add new parameter `message_retry_set` to `Client.__init__`. Contributed by [@xydan83](https://github.com/xydan83) in [#69](https://github.com/sbtinstruments/asyncio-mqtt/pull/69)
- Export `ProtocolVersion` from the main module. Contributed by [André (@tropxy)](https://github.com/tropxy) in [#65](https://github.com/sbtinstruments/asyncio-mqtt/pull/65) (1/2)
- Add documentation about publishers, client arguments, etc. Contributed by [André (@tropxy)](https://github.com/tropxy) in [#65](https://github.com/sbtinstruments/asyncio-mqtt/pull/65) (2/2)
### Fixed
- Fix race condition that caused `InvalidStateError` in `force_disconnect()`. Contributed by [@functionpointer](https://github.com/functionpointer) in [#67](https://github.com/sbtinstruments/asyncio-mqtt/pull/67)
## [0.9.1] - 2021-05-13
### Fixed
- Fix handling of MQTTv5 reason codes. Contributed by [Jakob Schlyter (@jschlyter)](https://github.com/jschlyter) in [#59](https://github.com/sbtinstruments/asyncio-mqtt/pull/59)
- Account for `-1` socket handles in the close callback. Contributed by [@wrobell](https://github.com/wrobell) in [#60](https://github.com/sbtinstruments/asyncio-mqtt/pull/60)
## [0.9.0] - 2021-05-03
### Added
- Add type hints. Contributed by [Ellis Percival (@flyte)](https://github.com/flyte) in [#37](https://github.com/sbtinstruments/asyncio-mqtt/pull/37)
- Add the `keepalive`, `bind_address`, `bind_port`, `clean_start`, `properties` arguments. Contributed by [Marcin Jaworski (@yawor)](https://github.com/yawor) in [#56](https://github.com/sbtinstruments/asyncio-mqtt/pull/56)
### Fixed
- Fix Python 3.6 compatibility. Contributed by [(@fipwmaqzufheoxq92ebc)](https://github.com/fipwmaqzufheoxq92ebc) in [#57](https://github.com/sbtinstruments/asyncio-mqtt/pull/57).
Note that asyncio-mqtt officially targets Python 3.7. Compatibility with 3.6 is community-driven.
- Fix "Broken pipe" error. Contributed by [Gilbert (@gilbertsmink)](https://github.com/gilbertsmink) in [#55](https://github.com/sbtinstruments/asyncio-mqtt/pull/55)
- Fix socket check when you select the WebSocket transport. Contributed by [Robert Chmielowiec (@chmielowiec)](https://github.com/chmielowiec) in [#54](https://github.com/sbtinstruments/asyncio-mqtt/pull/54)
- Fix `TypeError` on invalid username and password combination
- Check that \_misc_task is not None before trying to cancel it. Contributed by [Ellis Percival (@flyte)](https://github.com/flyte) in [#41](https://github.com/sbtinstruments/asyncio-mqtt/pull/41)
- Fix exception in `on_socket_open`: Non-thread-safe operation invoked on an event loop other than the current one. Contributed by [Ellis Percival (@flyte)](https://github.com/flyte) in [#40](https://github.com/sbtinstruments/asyncio-mqtt/pull/40)
## [0.8.1] - 2021-02-23
### Fixed
- Fix `AttributeError` when you use WebSockets.
Contributed by [Robert Chmielowiec (@chmielowiec)](https://github.com/chmielowiec) in [#36](https://github.com/sbtinstruments/asyncio-mqtt/pull/36)
- Fix `asyncio.InvalidStateError` in the `_on_connect` callback.
Contributed by [Maxim Shmalovsky (@vitalalerter)](https://github.com/vitalalerter) in [#31](https://github.com/sbtinstruments/asyncio-mqtt/pull/31)
- Fix "Future exception was never retrieved" on disconnect.
Contributed by [Martin Hjelmare (@martinhjelmare)](https://github.com/martinhjelmare) in [#25](https://github.com/sbtinstruments/asyncio-mqtt/pull/25)
- Fix `connect` so it no longer blocks the event loop.
Contributed by [Øystein Haug Olsen (@oholsen)](https://github.com/oholsen) in [#23](https://github.com/sbtinstruments/asyncio-mqtt/pull/23)
## [0.8.0] - 2020-11-09
### Added
- Add `transport` argument to `Client`
Contributed by [@opengs](https://github.com/opengs) in [#21](https://github.com/sbtinstruments/asyncio-mqtt/pull/21)
- Add `clean_session` argument to `Client`
Contributed by [@nadyka](https://github.com/madnadyka) in [#17](https://github.com/sbtinstruments/asyncio-mqtt/pull/17)
## [0.7.0] - 2020-08-04
I've tested the library for production use at SBT Instruments.
This uncovered a bunch of bugs and missing features that I've adressed
in this release. We are approaching a 1.0.0 release. Let me know
if you want something changed before that via the issue tracker on GitHub.
### Added
- Add support for MQTTv5.
- Add `will` keyword argument to `Client`.
- Add `MqttConnectError` with specific error messages for connection failures.
- Add `Client.id` property that returns the client ID (or `None` if
the client ID was not specified during construction).
### Fixed
- Fix unhandled exception error.
- Fix "Task was destroyed but it is pending" error.
- Fix compatibility with `asyncqt`'s event loop.
- Fix race condition in `Client.connect` that raised an `AttributeError`.
- Fix "[asyncio] Future exception was never retrieved" debug message.
- Fix support for python 3.6.
Contributed by [Derrick Lyndon Pallas (@pallas)](https://github.com/pallas) in [#12](https://github.com/sbtinstruments/asyncio-mqtt/pull/12)
## [0.6.0] - 2020-06-26
### Changed
- No longer logs exception in `Client.__aexit__`. It's perfectly valid
to exit due to, e.g., `asyncio.CancelledError` so let's not treat it as an
error.
## [0.5.0] - 2020-06-08
### Added
- Add support for python 3.6.
Contributed by [Derrick Lyndon Pallas (@pallas)](https://github.com/pallas) in [#7](https://github.com/sbtinstruments/asyncio-mqtt/pull/7) (1/2).
- Add `client_id` and `tls_context` keyword arguments to the `Client` constructor.
Contributed by [Derrick Lyndon Pallas (@pallas)](https://github.com/pallas) in [#7](https://github.com/sbtinstruments/asyncio-mqtt/pull/7) (2/2).
- Add `timeout` keyword argument to both `Client.connect` and `Client.disconnect`. Default value of `10` seconds (like the other functions).
### Changed
- Propagate disconnection errors to subscribers. This enables user code to detect if a disconnect occurs. E.g., due to network errors.
## [0.4.0] - 2020-05-06
### Changed
- **BREAKING CHANGE:** Forward the [MQTTMessage](https://github.com/eclipse/paho.mqtt.python/blob/1eec03edf39128e461e6729694cf5d7c1959e5e4/src/paho/mqtt/client.py#L355)
class from paho-mqtt instead of just the `payload`. This applies to both
`Client.filtered_messages` and `Client.unfiltered_messages`.
This way, user code not only gets the message `payload` but also the `topic`, `qos` level, `retain` flag, etc.
Contributed by [Matthew Bradbury (@MBradbury)](https://github.com/MBradbury) in [#3](https://github.com/sbtinstruments/asyncio-mqtt/pull/3).
## [0.3.0] - 2020-04-13
### Added
- Add `username` and `password` keyword arguments to the `Client` constructor.
Contributed by [@gluap](https://github.com/gluap) in [#1](https://github.com/sbtinstruments/asyncio-mqtt/pull/1).
### Fixed
- Fix log message context for `Client.filtered_messages`.
## [0.2.1] - 2020-04-07
### Fixed
- Fix regression with the `Client._wait_for` helper method introduced in the latest
release.
## [0.2.0] - 2020-04-07
### Changed
- **BREAKING CHANGE:** Replace all uses of `asyncio.TimeoutError` with `MqttError`.
Calls to `Client.subscribe`/`unsubscribe`/`publish` will no longer raise `asyncio.TimeoutError.`
The new behaviour makes it easier to reason about, which exceptions the library
throws:
- Wrong input parameters? Raise `ValueError`.
- Network or protocol failures? `MqttError`.
- Broken library state? `RuntimeError`.
## [0.1.3] - 2020-04-07
### Fixed
- Fix how keyword arguments are forwarded in `Client.publish` and `Client.subscribe`.
## [0.1.2] - 2020-04-06
### Fixed
- Remove log call that was erroneously put in while debugging the latest release.
## [0.1.1] - 2020-04-06
### Fixed
- Add missing parameters to `Client.publish`.
- Fix error in `Client.publish` when paho-mqtt publishes immediately.
## [0.1.0] - 2020-04-06
Initial release.
asyncio-mqtt-0.16.1/CONTRIBUTING.md 0000664 0000000 0000000 00000003771 14343623446 0016501 0 ustar 00root root 0000000 0000000 # Contributing
## Setting up an environment
Clone the `asyncio-mqtt`.
Inside the repository, create a virtual environment.
```bash
python3 -m venv .venv
```
Activate the virtual environment.
```bash
source ./env/bin/activate
```
Upgrade `pip`.
```bash
pip install --upgrade pip
```
Install the development dependencies.
```bash
pip install -e .[tests,lint,format]
```
Install [pre-commit](https://pre-commit.com/) so that your code is formatted and checked when you are doing a commit.
```bash
pip install pre-commit
pre-commit install
```
### Visual Studio Code
If you are using VSCode, here are the settings to activate on save,
- `black` to format.
- `mypy` to lint.
- Install [charliermarsh.ruff](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) extension to lint, sort imports, and auto-fix lint errors (`ruff` is a fast equivalent to `flake8`)
```json
{
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"python.formatting.provider": "black",
"python.linting.mypyEnabled": true
}
```
## Testing
To test the code use [pytest](https://docs.pytest.org/en/7.1.x/).
```bash
pytest
```
To do the full coverage of `asyncio-mqtt`, run the following command.
```bash
pytest --cov=src --cov=tests --cov-report=html
```
To view the coverage open `htmlcov/index.html`.
## Committing
After doing `git commit`, `pre-commit` will check the committed code.
The check can be passed, skipped or failed.
If the check failed, it is possible it auto-fixed the code, so you will only need to stage and commit again for it to pass.
If it did not auto-fixed the code, you will need to do it manually.
`pre-commit` will only check the code that is staged, the unstaged code will be stashed during the checks.
## Making a Pull Request
The branch to contribute is `master`.
You should create a draft pull request if you still need to work on it.
You should update `CHANGELOG.md` to reflect the change done in your pull request.
asyncio-mqtt-0.16.1/LICENSE 0000664 0000000 0000000 00000002670 14343623446 0015252 0 ustar 00root root 0000000 0000000 Copyright 2020 (c) SBT Instruments
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
asyncio-mqtt-0.16.1/README.md 0000664 0000000 0000000 00000036315 14343623446 0015527 0 ustar 00root root 0000000 0000000
Idiomatic asyncio MQTT Client 🙌
Write code like this:
**Subscriber**
```python
async with Client("test.mosquitto.org") as client:
async with client.messages() as messages:
await client.subscribe("humidity/#")
async for message in messages:
print(message.payload)
```
**Publisher**
```python
async with Client("test.mosquitto.org") as client:
await client.publish("humidity/outside", payload=0.38)
```
_asyncio-mqtt_ combines the stability of the time-proven [paho-mqtt](https://github.com/eclipse/paho.mqtt.python) library with a modern, asyncio-based interface.
- No more callbacks! 👍
- No more return codes (welcome to the `MqttError`)
- Graceful disconnection (forget about `on_unsubscribe`, `on_disconnect`, etc.)
- Compatible with `async` code
- Fully type-hinted
- Did we mention no more callbacks?
The whole thing is less than [700 lines of code](asyncio-mqtt/client.py).
## Contents 🔍
- [Installation](#installation-)
- [Note for Windows users](#note-for-windows-users)
- [Advanced usage](#advanced-usage-)
- [Configuring the client](#configuring-the-client)
- [Filtering messages](#filtering-messages)
- [Sharing the connection](#sharing-the-connection)
- [Side by side with web frameworks](#side-by-side-with-web-frameworks)
- [Why can't I `connect`/`disconnect` manually?](#why-cant-i-connectdisconnect-manually)
- [Listening without blocking](#listening-without-blocking)
- [Reconnecting](#reconnecting)
- [TLS](#tls)
- [Proxying](#proxying)
- [License](#license-)
- [Versioning](#versioning-)
- [Changelog](#changelog-)
- [Related projects](#related-projects-)
## Installation 📚
_asyncio-mqtt_ can be installed via `pip install asyncio-mqtt`. It requires Python 3.7+ to run. The only dependency is [paho-mqtt](https://github.com/eclipse/paho.mqtt.python).
If you can't wait for the latest version and want to install directly from GitHub, use:
`pip install git+https://github.com/sbtinstruments/asyncio-mqtt`
### Note for Windows users
Since Python 3.8, the default asyncio event loop is the `ProactorEventLoop`. Said loop [doesn't support the `add_reader` method](https://docs.python.org/3/library/asyncio-platforms.html#windows) that is required by _asyncio-mqtt_. Please switch to an event loop that supports the `add_reader` method such as the built-in `SelectorEventLoop`:
```python
# Change to the "Selector" event loop
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# Run your async application as usual
asyncio.run(main())
```
## Advanced usage ⚡
Let's make the example from before more interesting:
### Configuring the client
You can configure quite a few things when initializing the client. These are all the possible parameters together with their default values. See [paho-mqtt's documentation](https://github.com/eclipse/paho.mqtt.python) for more information about the individual parameters.
```python
import asyncio_mqtt as aiomqtt
import paho.mqtt as mqtt
aiomqtt.Client(
hostname="test.mosquitto.org", # The only non-optional parameter
port=1883,
username=None,
password=None,
logger=None,
client_id=None,
tls_context=None,
tls_params=None,
proxy=None,
protocol=None,
will=None,
clean_session=None,
transport="tcp",
keepalive=60,
bind_address="",
bind_port=0,
clean_start=mqtt.client.MQTT_CLEAN_START_FIRST_ONLY,
properties=None,
message_retry_set=20,
socket_options=(),
max_concurrent_outgoing_calls=None,
websocket_path=None,
websocket_headers=None,
)
```
### Filtering messages
Imagine you're measuring temperature and humidity on the outside and inside, and our topics look like this: `temperature/outside`. You want to receive all types of measurements but handle them differently. _asyncio-mqtt_ provides `Topic.matches()` to make this easy:
```python
import asyncio
import asyncio_mqtt as aiomqtt
async def main():
async with aiomqtt.Client("test.mosquitto.org") as client:
async with client.messages() as messages:
await client.subscribe("#")
async for message in messages:
if message.topic.matches("humidity/outside"):
print(f"[humidity/outside] {message.payload}")
if message.topic.matches("+/inside"):
print(f"[+/inside] {message.payload}")
if message.topic.matches("temperature/#"):
print(f"[temperature/#] {message.payload}")
asyncio.run(main())
```
Note that in our example, messages to `temperature/inside` are handled twice!
### Sharing the connection
In many cases, you'll want to send and receive messages in different locations in your code. You could create a new client each time, but
1. this is not very performant, and
2. you'll use a lot more network bandwidth.
You can share the connection by passing the `Client` instance to all functions that need it:
```python
import asyncio
import asyncio_mqtt as aiomqtt
async def publish_humidity(client):
await client.publish("humidity/outside", payload=0.38)
async def publish_temperature(client):
await client.publish("temperature/outside", payload=28.3)
async def main():
async with aiomqtt.Client("test.mosquitto.org") as client:
await publish_humidity(client)
await publish_temperature(client)
asyncio.run(main())
```
#### Side by side with web frameworks
Most web frameworks take control over the "main" function, which makes it difficult to figure out where to create and connect the `Client` and how to share this connection.
Some frameworks like [Starlette](https://github.com/encode/starlette) directly support lifespan context managers, with which you can safely set up a global client instance that you can then pass to functions that need it, just like before:
```python
import asyncio
import asyncio_mqtt as aiomqtt
import starlette.applications
import contextlib
client = None
@contextlib.asynccontextmanager
async def lifespan(app):
global client
async with aiomqtt.Client("test.mosquitto.org") as c:
client = c
yield
app = starlette.applications.Starlette(
routes=[],
lifespan=lifespan,
)
```
FastAPI (which is built upon Starlette) doesn't expose that API yet, but there are [multiple](https://github.com/tiangolo/fastapi/pull/5503) [open PRs](https://github.com/tiangolo/fastapi/pull/2944) to add it. In the meantime, you can work around it via FastAPI's dependency injection.
#### Why can't I `connect`/`disconnect` manually?
Managing a connection by calling `connect` and `disconnect` directly is a bit tricky. For example, when you're disconnecting the client, you'd have to make sure that there's no other task that still relies on the connection. There are many similar situations where something can easily go wrong.
Context managers take care of all connection and disconnection logic for you, in a way that makes it very difficult to shoot yourself in the foot. They are a lot easier and less error-prone to use than `connect`/`disconnect`.
Supporting both context managers and manual `connect`/`disconnect` would add a lot of complexity to _asyncio-mqtt_. To keep maintainer burden manageable, we focus only on the preferred option: context managers.
### Listening without blocking
If you run the basic example for subscribing and listening for messages, you'll notice that the program doesn't finish until you stop it. Waiting for messages blocks the execution of everything that comes afterwards. If you want to run other code after starting your listener (e.g. handling HTTP requests in a web framework) you don't want this.
To solve this, you can use asyncio's `create_task` without `await`ing the created task. The concept is similar to starting a new thread without `join`ing it in a multithreaded application.
```python
import asyncio
import asyncio_mqtt as aiomqtt
async def listen():
async with aiomqtt.Client("test.mosquitto.org") as client:
async with client.messages() as messages:
await client.subscribe("humidity/#")
async for message in messages:
print(message.payload)
async def main():
# Wait for messages in (unawaited) asyncio task
loop = asyncio.get_event_loop()
task = loop.create_task(listen())
# This will still run!
print("Magic!")
# If you don't await the task here the program will simply finish.
# However, if you're using an async web framework you usually don't have to await
# the task, as the framework runs in an endless loop.
await task
asyncio.run(main())
```
### Reconnecting
You can reconnect when the connection to the broker is lost by wrapping your code in a `try/except`-block and listening for `MqttError`s.
```python
import asyncio
import asyncio_mqtt as aiomqtt
async def main():
reconnect_interval = 5 # In seconds
while True:
try:
async with aiomqtt.Client("test.mosquitto.org") as client:
async with client.messages() as messages:
await client.subscribe("humidity/#")
async for message in messages:
print(message.payload.decode())
except aiomqtt.MqttError as error:
print(f'Error "{error}". Reconnecting in {reconnect_interval} seconds.')
await asyncio.sleep(reconnect_interval)
asyncio.run(main())
```
### TLS
You can configure TLS via the `TLSParameters` class. The parameters are directly passed through to paho-mqtt's `tls_set` function. See [paho-mqtt's documentation](https://github.com/eclipse/paho.mqtt.python) for more information about the individual parameters.
```python
import asyncio
import asyncio_mqtt as aiomqtt
import ssl
tls_params = aiomqtt.TLSParameters(
ca_certs=None,
certfile=None,
keyfile=None,
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None,
)
async def main():
async with aiomqtt.Client("test.mosquitto.org", tls_params=tls_params) as client:
await client.publish("humidity/outside", payload=0.38)
asyncio.run(main())
```
### Proxying
You can configure proxying via the `ProxySettings` class. The parameters are directly passed through to paho-mqtt's `proxy_set` functionality. Both SOCKS and HTTP proxies are supported. Note that proxying is an extra feature (even in paho-mqtt) that requires the `PySocks` dependency. See [paho-mqtt's documentation](https://github.com/eclipse/paho.mqtt.python) for more information about the individual parameters.
```python
import asyncio
import asyncio_mqtt as aiomqtt
import socks
proxy_params = aiomqtt.ProxySettings(
proxy_type=socks.HTTP,
proxy_addr="www.example.com",
proxy_rdns=True,
proxy_username=None,
proxy_password=None,
)
async def main():
async with aiomqtt.Client("test.mosquitto.org", proxy=proxy_params) as client:
await client.publish("humidity/outside", payload=0.38)
asyncio.run(main())
```
## License 📋
Note that the underlying paho-mqtt library is dual-licensed. One of the licenses is the so-called [Eclipse Distribution License v1.0](https://www.eclipse.org/org/documents/edl-v10.php). It is almost word-for-word identical to the [BSD 3-clause License](https://opensource.org/licenses/BSD-3-Clause). The only differences are:
- One use of "COPYRIGHT OWNER" (EDL) instead of "COPYRIGHT HOLDER" (BSD)
- One use of "Eclipse Foundation, Inc." (EDL) instead of "copyright holder" (BSD)
## Versioning 🎯
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Expect API changes until we reach version `1.0.0`. After `1.0.0`, breaking changes will only occur in major release (e.g., `2.0.0`, `3.0.0`, etc.).
## Changelog 🚧
Please refer to the [CHANGELOG](https://github.com/sbtinstruments/asyncio-mqtt/blob/master/CHANGELOG.md) document. It adheres to the principles of [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Related projects 🌟
Is _asyncio-mqtt_ not what you are looking for? Try another client:
- [paho-mqtt](https://github.com/eclipse/paho.mqtt.python) — Own protocol implementation. Synchronous.
 
- [gmqtt](https://github.com/wialon/gmqtt) — Own protocol implementation. Asynchronous.
 
- [fastapi-mqtt](https://github.com/sabuhish/fastapi-mqtt) — Asynchronous wrapper around gmqtt. Simplifies integration in your FastAPI application.
 
- [amqtt](https://github.com/Yakifo/amqtt) — Own protocol implementation. Asynchronous. Includes a broker.
 
- [mqttools](https://github.com/eerimoq/mqttools) — Own protocol implementation. Asynchronous.
 
- [trio-paho-mqtt](https://github.com/bkanuka/trio-paho-mqtt) — Asynchronous wrapper around paho-mqtt (similar to _asyncio-mqtt_). Based on trio instead of asyncio.
 
asyncio-mqtt-0.16.1/asyncio_mqtt/ 0000775 0000000 0000000 00000000000 14343623446 0016752 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/asyncio_mqtt/__init__.py 0000664 0000000 0000000 00000001106 14343623446 0021061 0 ustar 00root root 0000000 0000000 # SPDX-License-Identifier: BSD-3-Clause
from ._version import __version__, __version_tuple__
from .client import (
Client,
Message,
ProtocolVersion,
ProxySettings,
TLSParameters,
Topic,
TopicLike,
Wildcard,
WildcardLike,
Will,
)
from .error import MqttCodeError, MqttError
__all__ = [
"__version__",
"__version_tuple__",
"Client",
"Message",
"ProtocolVersion",
"ProxySettings",
"TLSParameters",
"Topic",
"TopicLike",
"Wildcard",
"WildcardLike",
"Will",
"MqttCodeError",
"MqttError",
]
asyncio-mqtt-0.16.1/asyncio_mqtt/client.py 0000664 0000000 0000000 00000077610 14343623446 0020615 0 ustar 00root root 0000000 0000000 # SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import asyncio
import functools
import logging
import socket
import ssl
import sys
from contextlib import contextmanager
from dataclasses import dataclass
from enum import IntEnum
from types import TracebackType
from typing import (
Any,
AsyncGenerator,
Awaitable,
Callable,
Coroutine,
Generator,
Iterable,
Iterator,
cast,
)
if sys.version_info >= (3, 10):
from typing import Concatenate, ParamSpec, TypeAlias
else:
from typing_extensions import Concatenate, ParamSpec, TypeAlias
from contextlib import asynccontextmanager
import paho.mqtt.client as mqtt
from paho.mqtt.properties import Properties
from .error import MqttCodeError, MqttConnectError, MqttError
from .types import PayloadType, T
MQTT_LOGGER = logging.getLogger("mqtt")
MQTT_LOGGER.setLevel(logging.WARNING)
_PahoSocket: TypeAlias = "socket.socket | ssl.SSLSocket | mqtt.WebsocketWrapper | Any"
WebSocketHeaders: TypeAlias = (
"dict[str, str] | Callable[[dict[str, str]], dict[str, str]]"
)
class ProtocolVersion(IntEnum):
"""A mapping of paho-mqtt protocol versions to an Enum for use in type hints."""
V31 = mqtt.MQTTv31
V311 = mqtt.MQTTv311
V5 = mqtt.MQTTv5
@dataclass(frozen=True)
class Will:
topic: str
payload: PayloadType | None = None
qos: int = 0
retain: bool = False
properties: mqtt.Properties | None = None
# TLS set parameter class
@dataclass(frozen=True)
class TLSParameters:
ca_certs: str | None = None
certfile: str | None = None
keyfile: str | None = None
cert_reqs: ssl.VerifyMode | None = None
tls_version: Any | None = None
ciphers: str | None = None
keyfile_password: str | None = None
# Proxy parameters class
class ProxySettings:
def __init__(
self,
*,
proxy_type: int,
proxy_addr: str,
proxy_rdns: bool | None = True,
proxy_username: str | None = None,
proxy_password: str | None = None,
):
self.proxy_args = {
"proxy_type": proxy_type,
"proxy_addr": proxy_addr,
"proxy_rdns": proxy_rdns,
"proxy_username": proxy_username,
"proxy_password": proxy_password,
}
# See the overloads of `socket.setsockopt` for details.
SocketOption: TypeAlias = "tuple[int, int, int | bytes] | tuple[int, int, None, int]"
SubscribeTopic: TypeAlias = "str | tuple[str, mqtt.SubscribeOptions] | list[tuple[str, mqtt.SubscribeOptions]] | list[tuple[str, int]]"
P = ParamSpec("P")
# TODO: Simplify the logic that surrounds `self._outgoing_calls_sem` with
# `nullcontext` when we support Python 3.10 (`nullcontext` becomes async-aware in
# 3.10). See: https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext
def _outgoing_call(
method: Callable[Concatenate[Client, P], Coroutine[Any, Any, T]]
) -> Callable[Concatenate[Client, P], Coroutine[Any, Any, T]]:
@functools.wraps(method)
async def decorated(self: Client, *args: P.args, **kwargs: P.kwargs) -> T:
if not self._outgoing_calls_sem:
return await method(self, *args, **kwargs)
async with self._outgoing_calls_sem:
return await method(self, *args, **kwargs)
return decorated
@dataclass(frozen=True)
class Wildcard:
"""A topic, optionally with wildcards (+ and #). Can only be subscribed to."""
value: str
def __str__(self) -> str:
return self.value
def __post_init__(self) -> None:
"""Validate the wildcard."""
if not isinstance(self.value, str):
raise TypeError("wildcard must be a string")
if (
len(self.value) == 0
or len(self.value) > 65535
or "#/" in self.value
or any(
"+" in level or "#" in level
for level in self.value.split("/")
if len(level) > 1
)
):
raise ValueError(f"Invalid wildcard: {self.value}")
WildcardLike: TypeAlias = "str | Wildcard"
@dataclass(frozen=True)
class Topic(Wildcard):
"""A topic that can be published and subscribed to."""
def __post_init__(self) -> None:
"""Validate the topic."""
if not isinstance(self.value, str):
raise TypeError("topic must be a string")
if (
len(self.value) == 0
or len(self.value) > 65535
or "+" in self.value
or "#" in self.value
):
raise ValueError(f"Invalid topic: {self.value}")
def matches(self, wildcard: WildcardLike) -> bool:
"""Check if the topic is matched by a given wildcard."""
if not isinstance(wildcard, Wildcard):
wildcard = Wildcard(wildcard)
# Split topics into levels to compare them one by one
topic_levels = self.value.split("/")
wildcard_levels = str(wildcard).split("/")
if wildcard_levels[0] == "$share":
# Shared subscriptions use the topic structure: $share//
wildcard_levels = wildcard_levels[2:]
def recurse(x: list[str], y: list[str]) -> bool:
if not x:
if not y:
return True
return False
if not y:
return False
if y[0] == "#":
return True
if x[0] == y[0] or y[0] == "+":
return recurse(x[1:], y[1:])
return False
return recurse(topic_levels, wildcard_levels)
TopicLike: TypeAlias = "str | Topic"
class Message:
"""Custom message class that allows us to use our own Topic class."""
def __init__(
self,
topic: TopicLike,
payload: PayloadType,
qos: int,
retain: bool,
mid: int,
properties: Properties | None,
):
self.topic = Topic(topic) if not isinstance(topic, Topic) else topic
self.payload = payload
self.qos = qos
self.retain = retain
self.mid = mid
self.properties = properties
@classmethod
def _from_paho_message(cls, message: mqtt.MQTTMessage) -> Message:
return cls(
topic=message.topic,
payload=message.payload,
qos=message.qos,
retain=message.retain,
mid=message.mid,
properties=message.properties if hasattr(message, "properties") else None,
)
class Client:
def __init__( # noqa: C901
self,
hostname: str,
port: int = 1883,
*,
username: str | None = None,
password: str | None = None,
logger: logging.Logger | None = None,
client_id: str | None = None,
tls_context: ssl.SSLContext | None = None,
tls_params: TLSParameters | None = None,
proxy: ProxySettings | None = None,
protocol: ProtocolVersion | None = None,
will: Will | None = None,
clean_session: bool | None = None,
transport: str = "tcp",
keepalive: int = 60,
bind_address: str = "",
bind_port: int = 0,
clean_start: int = mqtt.MQTT_CLEAN_START_FIRST_ONLY,
properties: Properties | None = None,
message_retry_set: int = 20,
socket_options: Iterable[SocketOption] | None = None,
max_concurrent_outgoing_calls: int | None = None,
websocket_path: str | None = None,
websocket_headers: WebSocketHeaders | None = None,
):
self._hostname = hostname
self._port = port
self._keepalive = keepalive
self._bind_address = bind_address
self._bind_port = bind_port
self._clean_start = clean_start
self._properties = properties
self._loop = asyncio.get_event_loop()
self._connected: asyncio.Future[int | mqtt.ReasonCodes] = asyncio.Future()
self._disconnected: asyncio.Future[
int | mqtt.ReasonCodes | None
] = asyncio.Future()
# Pending subscribe, unsubscribe, and publish calls
self._pending_subscribes: dict[
int, asyncio.Future[tuple[int] | list[mqtt.ReasonCodes]]
] = {}
self._pending_unsubscribes: dict[int, asyncio.Event] = {}
self._pending_publishes: dict[int, asyncio.Event] = {}
self._pending_calls_threshold: int = 10
self._misc_task: asyncio.Task[None] | None = None
# List of all callbacks to call when a message is received
self._on_message_callbacks: list[Callable[[Message], None]] = []
self._unfiltered_messages_callback: Callable[
[mqtt.Client, Any, mqtt.MQTTMessage], None
] | None = None
self._outgoing_calls_sem: asyncio.Semaphore | None
if max_concurrent_outgoing_calls is not None:
self._outgoing_calls_sem = asyncio.Semaphore(max_concurrent_outgoing_calls)
else:
self._outgoing_calls_sem = None
if protocol is None:
protocol = ProtocolVersion.V311
self._client: mqtt.Client = mqtt.Client(
client_id=client_id,
protocol=protocol,
clean_session=clean_session,
transport=transport,
reconnect_on_failure=False,
)
self._client.on_connect = self._on_connect
self._client.on_disconnect = self._on_disconnect
self._client.on_subscribe = self._on_subscribe
self._client.on_unsubscribe = self._on_unsubscribe
self._client.on_message = self._on_message
self._client.on_publish = self._on_publish
# Callbacks for custom event loop
self._client.on_socket_open = self._on_socket_open
self._client.on_socket_close = self._on_socket_close
self._client.on_socket_register_write = self._on_socket_register_write
self._client.on_socket_unregister_write = self._on_socket_unregister_write
if logger is None:
logger = MQTT_LOGGER
self._client.enable_logger(logger)
if username is not None:
self._client.username_pw_set(username=username, password=password)
if tls_context is not None:
self._client.tls_set_context(tls_context)
if tls_params is not None:
self._client.tls_set(
ca_certs=tls_params.ca_certs,
certfile=tls_params.certfile,
keyfile=tls_params.keyfile,
cert_reqs=tls_params.cert_reqs,
tls_version=tls_params.tls_version,
ciphers=tls_params.ciphers,
keyfile_password=tls_params.keyfile_password,
)
if proxy is not None:
self._client.proxy_set(**proxy.proxy_args)
if websocket_path is not None:
self._client.ws_set_options(path=websocket_path, headers=websocket_headers)
if will is not None:
self._client.will_set(
will.topic, will.payload, will.qos, will.retain, will.properties
)
self._client.message_retry_set(message_retry_set)
if socket_options is None:
socket_options = ()
self._socket_options = tuple(socket_options)
@property
def id( # noqa: A003 # TODO: When doing BREAKING CHANGES rename to avoid shadowing builtin id
self,
) -> str:
"""Return the client ID.
Note that paho-mqtt stores the client ID as `bytes` internally.
We assume that the client ID is a UTF8-encoded string and decode
it first.
"""
return cast(bytes, self._client._client_id).decode() # type: ignore[attr-defined]
@property
def _pending_calls(self) -> Generator[int, None, None]:
"""Yield all message IDs with pending calls."""
yield from self._pending_subscribes.keys()
yield from self._pending_unsubscribes.keys()
yield from self._pending_publishes.keys()
async def connect(self, *, timeout: int = 10) -> None:
try:
loop = asyncio.get_running_loop()
# [3] Run connect() within an executor thread, since it blocks on socket
# connection for up to `keepalive` seconds: https://git.io/Jt5Yc
await loop.run_in_executor(
None,
self._client.connect,
self._hostname,
self._port,
self._keepalive,
self._bind_address,
self._bind_port,
self._clean_start,
self._properties,
)
client_socket = self._client.socket()
_set_client_socket_defaults(client_socket, self._socket_options)
# paho.mqtt.Client.connect may raise one of several exceptions.
# We convert all of them to the common MqttError for user convenience.
# See: https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1770
except (OSError, mqtt.WebsocketConnectionError) as error:
raise MqttError(str(error)) from None
await self._wait_for(self._connected, timeout=timeout)
async def disconnect(self, *, timeout: int = 10) -> None:
rc = self._client.disconnect()
# Early out on error
if rc != mqtt.MQTT_ERR_SUCCESS:
raise MqttCodeError(rc, "Could not disconnect")
# Wait for acknowledgement
await self._wait_for(self._disconnected, timeout=timeout)
async def force_disconnect(self) -> None:
if not self._disconnected.done():
self._disconnected.set_result(None)
@_outgoing_call
async def subscribe(
self,
topic: SubscribeTopic,
qos: int = 0,
options: mqtt.SubscribeOptions | None = None,
properties: Properties | None = None,
*args: Any,
timeout: int = 10,
**kwargs: Any,
) -> tuple[int] | list[mqtt.ReasonCodes]:
result, mid = self._client.subscribe(
topic, qos, options, properties, *args, **kwargs
)
# Early out on error
if result != mqtt.MQTT_ERR_SUCCESS:
raise MqttCodeError(result, "Could not subscribe to topic")
# Create future for when the on_subscribe callback is called
callback_result: asyncio.Future[
tuple[int] | list[mqtt.ReasonCodes]
] = asyncio.Future()
with self._pending_call(mid, callback_result, self._pending_subscribes):
# Wait for callback_result
return await self._wait_for(callback_result, timeout=timeout)
@_outgoing_call
async def unsubscribe(
self,
topic: str | list[str],
properties: Properties | None = None,
*args: Any,
timeout: int = 10,
**kwargs: Any,
) -> None:
result, mid = self._client.unsubscribe(topic, properties, *args, **kwargs)
# Early out on error
if result != mqtt.MQTT_ERR_SUCCESS:
raise MqttCodeError(result, "Could not unsubscribe from topic")
# Create event for when the on_unsubscribe callback is called
confirmation = asyncio.Event()
with self._pending_call(mid, confirmation, self._pending_unsubscribes):
# Wait for confirmation
await self._wait_for(confirmation.wait(), timeout=timeout)
@_outgoing_call
async def publish(
self,
topic: str,
payload: PayloadType = None,
qos: int = 0,
retain: bool = False,
properties: Properties | None = None,
*args: Any,
timeout: int = 10,
**kwargs: Any,
) -> None:
info = self._client.publish(
topic, payload, qos, retain, properties, *args, **kwargs
) # [2]
# Early out on error
if info.rc != mqtt.MQTT_ERR_SUCCESS:
raise MqttCodeError(info.rc, "Could not publish message")
# Early out on immediate success
if info.is_published():
return
# Create event for when the on_publish callback is called
confirmation = asyncio.Event()
with self._pending_call(info.mid, confirmation, self._pending_publishes):
# Wait for confirmation
await self._wait_for(confirmation.wait(), timeout=timeout)
@asynccontextmanager
async def filtered_messages(
self, topic_filter: str, *, queue_maxsize: int = 0
) -> AsyncGenerator[AsyncGenerator[mqtt.MQTTMessage, None], None]:
"""Return async generator of messages that match the given filter."""
MQTT_LOGGER.warning(
"filtered_messages() is deprecated and will be removed in a future version."
" Use messages() together with Topic.matches() instead."
)
callback, generator = self._deprecated_callback_and_generator(
log_context=f'topic_filter="{topic_filter}"', queue_maxsize=queue_maxsize
)
try:
self._client.message_callback_add(topic_filter, callback)
# Back to the caller (run whatever is inside the with statement)
yield generator
finally:
# We are exiting the with statement. Remove the topic filter.
self._client.message_callback_remove(topic_filter)
@asynccontextmanager
async def unfiltered_messages(
self, *, queue_maxsize: int = 0
) -> AsyncGenerator[AsyncGenerator[mqtt.MQTTMessage, None], None]:
"""Return async generator of all messages that are not caught in filters."""
MQTT_LOGGER.warning(
"unfiltered_messages() is deprecated and will be removed in a future"
" version. Use messages() instead."
)
# Early out
if self._unfiltered_messages_callback is not None:
raise RuntimeError(
"Only a single unfiltered_messages generator can be used at a time."
)
callback, generator = self._deprecated_callback_and_generator(
log_context="unfiltered", queue_maxsize=queue_maxsize
)
try:
self._unfiltered_messages_callback = callback
# Back to the caller (run whatever is inside the with statement)
yield generator
finally:
# We are exiting the with statement. Unset the callback.
self._unfiltered_messages_callback = None
@asynccontextmanager
async def messages(
self, *, queue_maxsize: int = 0
) -> AsyncGenerator[AsyncGenerator[Message, None], None]:
"""Return async generator of incoming messages.
Use queue_maxsize to restrict the queue size. If the queue is full,
incoming messages will be discarded (and a warning is logged).
If queue_maxsize is less than or equal to zero, the queue size is infinite.
"""
callback, generator = self._callback_and_generator(queue_maxsize=queue_maxsize)
try:
# Add to the list of callbacks to call when a message is received
self._on_message_callbacks.append(callback)
# Back to the caller (run whatever is inside the with statement)
yield generator
finally:
# We are exiting the with statement. Remove the callback from the list.
self._on_message_callbacks.remove(callback)
def _deprecated_callback_and_generator(
self, *, log_context: str, queue_maxsize: int = 0
) -> tuple[
Callable[[mqtt.Client, Any, mqtt.MQTTMessage], None],
AsyncGenerator[mqtt.MQTTMessage, None],
]:
# Queue to hold the incoming messages
messages: asyncio.Queue[mqtt.MQTTMessage] = asyncio.Queue(maxsize=queue_maxsize)
# Callback for the underlying API
def _put_in_queue(
client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage
) -> None:
try:
messages.put_nowait(message)
except asyncio.QueueFull:
MQTT_LOGGER.warning(
f"[{log_context}] Message queue is full. Discarding message."
)
# The generator that we give to the caller
async def _message_generator() -> AsyncGenerator[mqtt.MQTTMessage, None]:
# Forward all messages from the queue
while True:
# Wait until we either:
# 1. Receive a message
# 2. Disconnect from the broker
get: asyncio.Task[mqtt.MQTTMessage] = self._loop.create_task(
messages.get()
)
try:
done, _ = await asyncio.wait(
(get, self._disconnected), return_when=asyncio.FIRST_COMPLETED
)
except asyncio.CancelledError:
# If the asyncio.wait is cancelled, we must make sure
# to also cancel the underlying tasks.
get.cancel()
raise
if get in done:
# We received a message. Return the result.
yield get.result()
else:
# We got disconnected from the broker. Cancel the "get" task.
get.cancel()
# Stop the generator with the following exception
raise MqttError("Disconnected during message iteration")
return _put_in_queue, _message_generator()
def _callback_and_generator(
self, *, queue_maxsize: int = 0
) -> tuple[Callable[[Message], None], AsyncGenerator[Message, None]]:
# Queue to hold the incoming messages
messages: asyncio.Queue[Message] = asyncio.Queue(maxsize=queue_maxsize)
def _callback(message: Message) -> None:
"""Put the new message in the queue."""
try:
messages.put_nowait(message)
except asyncio.QueueFull:
MQTT_LOGGER.warning("Message queue is full. Discarding message.")
async def _generator() -> AsyncGenerator[Message, None]:
"""Forward all messages from the message queue."""
while True:
# Wait until we either:
# 1. Receive a message
# 2. Disconnect from the broker
get: asyncio.Task[Message] = self._loop.create_task(messages.get())
try:
done, _ = await asyncio.wait(
(get, self._disconnected), return_when=asyncio.FIRST_COMPLETED
)
except asyncio.CancelledError:
# If the asyncio.wait is cancelled, we must make sure
# to also cancel the underlying tasks.
get.cancel()
raise
if get in done:
# We received a message. Return the result.
yield get.result()
else:
# We got disconnected from the broker. Cancel the "get" task.
get.cancel()
# Stop the generator with the following exception
raise MqttError("Disconnected during message iteration")
return _callback, _generator()
async def _wait_for(
self, fut: Awaitable[T], timeout: float | None, **kwargs: Any
) -> T:
try:
return await asyncio.wait_for(fut, timeout=timeout, **kwargs)
except asyncio.TimeoutError:
raise MqttError("Operation timed out") from None
@contextmanager
def _pending_call(
self, mid: int, value: T, pending_dict: dict[int, T]
) -> Iterator[None]:
if mid in self._pending_calls:
raise RuntimeError(
f'There already exists a pending call for message ID "{mid}"'
)
pending_dict[mid] = value # [1]
try:
# Log a warning if there is a concerning number of pending calls
pending = len(list(self._pending_calls))
if pending > self._pending_calls_threshold:
MQTT_LOGGER.warning(f"There are {pending} pending publish calls.")
# Back to the caller (run whatever is inside the with statement)
yield
finally:
# The normal procedure is:
# * We add the item at [1]
# * A callback will remove the item
#
# However, if the callback doesn't get called (e.g., due to a
# network error) we still need to remove the item from the dict.
try:
del pending_dict[mid]
except KeyError:
pass
def _on_connect(
self,
client: mqtt.Client,
userdata: Any,
flags: dict[str, int],
rc: int | mqtt.ReasonCodes,
properties: mqtt.Properties | None = None,
) -> None:
# Return early if already connected. Sometimes, paho-mqtt calls _on_connect
# multiple times. Maybe because we receive multiple CONNACK messages
# from the server. In any case, we return early so that we don't set
# self._connected twice (as it raises an asyncio.InvalidStateError).
if self._connected.done():
return
if rc == mqtt.CONNACK_ACCEPTED:
self._connected.set_result(rc)
else:
self._connected.set_exception(MqttConnectError(rc))
def _on_disconnect(
self,
client: mqtt.Client,
userdata: Any,
rc: int | mqtt.ReasonCodes | None,
properties: mqtt.Properties | None = None,
) -> None:
# Return early if the disconnect is already acknowledged.
# Sometimes (e.g., due to timeouts), paho-mqtt calls _on_disconnect
# twice. We return early to avoid setting self._disconnected twice
# (as it raises an asyncio.InvalidStateError).
if self._disconnected.done():
return
# Return early if we are not connected yet. This avoids calling
# `_disconnected.set_exception` with an exception that will never
# be retrieved (since `__aexit__` won't get called if `__aenter__`
# fails). In turn, this avoids asyncio debug messages like the
# following:
#
# "[asyncio] Future exception was never retrieved"
#
# See also: https://docs.python.org/3/library/asyncio-dev.html#detect-never-retrieved-exceptions
if not self._connected.done() or self._connected.exception() is not None:
return
if rc == mqtt.MQTT_ERR_SUCCESS:
self._disconnected.set_result(rc)
else:
self._disconnected.set_exception(MqttCodeError(rc, "Unexpected disconnect"))
def _on_subscribe(
self,
client: mqtt.Client,
userdata: Any,
mid: int,
granted_qos: tuple[int] | list[mqtt.ReasonCodes],
properties: mqtt.Properties | None = None,
) -> None:
try:
fut = self._pending_subscribes.pop(mid)
if not fut.done():
fut.set_result(granted_qos)
except KeyError:
MQTT_LOGGER.error(f'Unexpected message ID "{mid}" in on_subscribe callback')
def _on_unsubscribe(
self,
client: mqtt.Client,
userdata: Any,
mid: int,
properties: mqtt.Properties | None = None,
reason_codes: list[mqtt.ReasonCodes] | mqtt.ReasonCodes | None = None,
) -> None:
try:
self._pending_unsubscribes.pop(mid).set()
except KeyError:
MQTT_LOGGER.error(
f'Unexpected message ID "{mid}" in on_unsubscribe callback'
)
def _on_message(
self, client: mqtt.Client, userdata: Any, message: mqtt.MQTTMessage
) -> None:
# Call the deprecated unfiltered_messages callback
if self._unfiltered_messages_callback is not None:
self._unfiltered_messages_callback(client, userdata, message)
# Convert the paho.mqtt message into our own Message type
m = Message._from_paho_message(message)
for callback in self._on_message_callbacks:
callback(m)
def _on_publish(self, client: mqtt.Client, userdata: Any, mid: int) -> None:
try:
self._pending_publishes.pop(mid).set()
except KeyError:
# Do nothing since [2] may call on_publish before it even returns.
# That is, the message may already be published before we even get a
# chance to set up the 'pending_call' logic.
pass
def _on_socket_open(
self, client: mqtt.Client, userdata: Any, sock: _PahoSocket
) -> None:
def callback() -> None:
# client.loop_read() may raise an exception, such as BadPipe. It's
# usually a sign that the underlaying connection broke, therefore we
# disconnect straight away
try:
client.loop_read()
except Exception as exc:
if not self._disconnected.done():
self._disconnected.set_exception(exc)
self._loop.add_reader(sock.fileno(), callback)
# paho-mqtt calls this function from the executor thread on which we've called
# `self._client.connect()` (see [3]), so we create a callback function to
# schedule `_misc_loop()` and run it on the loop thread-safely.
def create_task_callback() -> None:
self._misc_task = self._loop.create_task(self._misc_loop())
self._loop.call_soon_threadsafe(create_task_callback)
def _on_socket_close(
self, client: mqtt.Client, userdata: Any, sock: _PahoSocket
) -> None:
fileno = sock.fileno()
if fileno > -1:
self._loop.remove_reader(fileno)
if self._misc_task is not None and not self._misc_task.done():
self._loop.call_soon_threadsafe(self._misc_task.cancel)
def _on_socket_register_write(
self, client: mqtt.Client, userdata: Any, sock: _PahoSocket
) -> None:
def callback() -> None:
# client.loop_write() may raise an exception, such as BadPipe. It's
# usually a sign that the underlaying connection broke, therefore we
# disconnect straight away
try:
client.loop_write()
except Exception as exc:
if not self._disconnected.done():
self._disconnected.set_exception(exc)
self._loop.add_writer(sock, callback)
def _on_socket_unregister_write(
self, client: mqtt.Client, userdata: Any, sock: _PahoSocket
) -> None:
self._loop.remove_writer(sock)
async def _misc_loop(self) -> None:
while self._client.loop_misc() == mqtt.MQTT_ERR_SUCCESS:
await asyncio.sleep(1)
async def __aenter__(self) -> "Client":
"""Connect to the broker."""
await self.connect()
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
"""Disconnect from the broker."""
# Early out if already disconnected...
if self._disconnected.done():
disc_exc = self._disconnected.exception()
if disc_exc is not None:
# ...by raising the error that caused the disconnect
raise disc_exc
# ...by returning since the disconnect was intentional
return
# Try to gracefully disconnect from the broker
try:
await self.disconnect()
except MqttError as error:
# We tried to be graceful. Now there is no mercy.
MQTT_LOGGER.warning(
f'Could not gracefully disconnect due to "{error}". Forcing'
" disconnection."
)
await self.force_disconnect()
def _set_client_socket_defaults(
client_socket: _PahoSocket | None, socket_options: Iterable[SocketOption]
) -> None:
# Note that socket may be None if, e.g., the username and
# password combination didn't work. In this case, we return early.
if client_socket is None:
return
# Furthermore, paho sometimes gives us a socket wrapper instead of
# the raw socket. E.g., for WebSocket-based connections.
if not isinstance(client_socket, socket.socket):
return
# At this point, we know that we got an actual socket. We change
# some of the default options.
for socket_option in socket_options:
client_socket.setsockopt(*socket_option)
asyncio-mqtt-0.16.1/asyncio_mqtt/error.py 0000664 0000000 0000000 00000003235 14343623446 0020460 0 ustar 00root root 0000000 0000000 # SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
from typing import Any
import paho.mqtt.client as mqtt
class MqttError(Exception):
"""Base exception for all asyncio-mqtt exceptions."""
pass
class MqttCodeError(MqttError):
def __init__(self, rc: int | mqtt.ReasonCodes | None, *args: Any):
super().__init__(*args)
self.rc = rc
def __str__(self) -> str:
if isinstance(self.rc, mqtt.ReasonCodes):
return f"[code:{self.rc.value}] {str(self.rc)}"
if isinstance(self.rc, int):
return f"[code:{self.rc}] {mqtt.error_string(self.rc)}"
return f"[code:{self.rc}] {super().__str__()}"
class MqttConnectError(MqttCodeError):
def __init__(self, rc: int | mqtt.ReasonCodes):
if isinstance(rc, mqtt.ReasonCodes):
return super().__init__(rc)
msg = "Connection refused"
try:
msg += f": {_CONNECT_RC_STRINGS[rc]}"
except KeyError:
pass
super().__init__(rc, msg)
return None
_CONNECT_RC_STRINGS: dict[int, str] = {
# Reference: https://github.com/eclipse/paho.mqtt.python/blob/v1.5.0/src/paho/mqtt/client.py#L1898
# 0: Connection successful
# 1: Connection refused - incorrect protocol version
1: "Incorrect protocol version",
# 2: Connection refused - invalid client identifier
2: "Invalid client identifier",
# 3: Connection refused - server unavailable
3: "Server unavailable",
# 4: Connection refused - bad username or password
4: "Bad username or password",
# 5: Connection refused - not authorised
5: "Not authorised"
# 6-255: Currently unused.
}
asyncio-mqtt-0.16.1/asyncio_mqtt/py.typed 0000664 0000000 0000000 00000000000 14343623446 0020437 0 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/asyncio_mqtt/types.py 0000664 0000000 0000000 00000000365 14343623446 0020474 0 ustar 00root root 0000000 0000000 import sys
from typing import TypeVar
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
T = TypeVar("T")
PayloadType: TypeAlias = "str | bytes | bytearray | int | float | None"
asyncio-mqtt-0.16.1/examples/ 0000775 0000000 0000000 00000000000 14343623446 0016056 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/examples/EXAMPLES.md 0000664 0000000 0000000 00000005227 14343623446 0017624 0 ustar 00root root 0000000 0000000 # Examples


## Sending a JSON payload
### Subscriber
The following example describes a subscriber expecting to receive a JSON
payload, which, also, connects to the client using Basic Auth and specifying
the MQTT protocol to be used. Please beware that some MQTT brokers requires you
to specify the protocol to be used. For more arguments that the Client may
accept, please check [client.py](https://github.com/sbtinstruments/asyncio-mqtt/blob/f4736adf0d3c5b87a39ea27afd025ed58c7bb54c/asyncio_mqtt/client.py#L70)
Please observe that the content which is decoded is not the message received but
the content (payload) which is of the type [MQTTMessage](https://github.com/eclipse/paho.mqtt.python/blob/c339cea2652a957d47de68eafb2a76736c1514e6/src/paho/mqtt/client.py#L355)
```python
import json
from asyncio_mqtt import Client, ProtocolVersion
async with Client(
"test.mosquitto.org",
username="username",
password="password",
protocol=ProtocolVersion.V31
) as client:
async with client.filtered_messages("floors/+/humidity") as messages:
# subscribe is done afterwards so that we just start receiving messages
# from this point on
await client.subscribe("floors/#")
async for message in messages:
print(message.topic)
print(json.loads(message.payload))
```
### Publisher
The publisher, besides specifying the Protocol and using also Basic Auth to
acess the broker, it also specifies the [QoS](https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/) desired
```python
import json
from asyncio_mqtt import Client, ProtocolVersion
async with Client(
"test.mosquitto.org",
username="username",
password="password",
protocol=ProtocolVersion.V31
) as client:
message = {"state": 3}
await client.publish("floors/bed_room/humidity",
payload=json.dumps(message), qos=2, retain=False)
```
## License

Note that the underlying paho-mqtt library is dual-licensed. One of the licenses is the so-called [Eclipse Distribution License v1.0](https://www.eclipse.org/org/documents/edl-v10.php). It is almost word-for-word identical to the [BSD 3-clause License](https://opensource.org/licenses/BSD-3-Clause). The only differences are:
- One use of "COPYRIGHT OWNER" (EDL) instead of "COPYRIGHT HOLDER" (BSD)
- One use of "Eclipse Foundation, Inc." (EDL) instead of "copyright holder" (BSD)
asyncio-mqtt-0.16.1/pyproject.toml 0000664 0000000 0000000 00000005667 14343623446 0017172 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools >= 65", "setuptools-scm >= 7"]
build-backend = "setuptools.build_meta"
[project]
name = "asyncio_mqtt"
description = "Idiomatic asyncio wrapper around paho-mqtt"
readme = "README.md"
authors = [{ name = "Frederik Aalund", email = "fpa@sbtinstruments.com" }]
maintainers = [
{ name = "Jonathan Plasse", email = "jonathan.plasse@live.fr" },
{ name = "Felix Böhm", email = "felix@felixboehm.dev" },
]
keywords = ["mqtt", "async", "asyncio", "paho-mqtt", "wrapper"]
license = { text = "BSD 3-Clause License" }
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
requires-python = ">= 3.7"
dependencies = [
"paho-mqtt>=1.6.0",
"typing_extensions>=4.4.0; python_version<'3.10'",
]
dynamic = ["version"]
[project.urls]
"Source code" = "https://github.com/sbtinstruments/asyncio-mqtt"
"Issue tracker" = "https://github.com/sbtinstruments/asyncio-mqtt/issues"
[project.optional-dependencies]
lint = ["mypy>=0.991", "ruff>=0.0.158", "types-paho-mqtt>=1.6.0.1"]
format = ["black>=22.10.0"]
tests = ["pytest>=7.2.0", "pytest-cov>=4.0.0", "anyio>=3.6.2"]
[tool.setuptools]
packages = ["asyncio_mqtt"]
[tool.setuptools_scm]
write_to = "asyncio_mqtt/_version.py"
[tool.ruff]
select = [
"A", # builtins
"B", # bugbear
"C4", # comprehensions
"C90", # mccabe
"D", # docstring
"E", # style errors
"F", # flakes
"I", # import sorting
"N", # naming
"PGH", # pygrep-hooks
"PLC", # pylint convention
"PLE", # pylint error
"PLR", # pylint refactor
"PLW", # pylint warning
"Q", # quotes
"RET", # return
"RUF", # ruff
"S", # bandit
"T10", # debugger
"T20", # print
"UP", # upgrade
"W", # style warnings
"YTT", # sys.version
]
ignore = [
"D10", # Missing docstring
"D203", # PEP8 docstring convention
"D212",
"D213",
"D214",
"D215",
"D404",
"D405",
"D406",
"D407",
"D408",
"D409",
"D410",
"D411",
"D413",
"D415",
"D416",
"D417",
"E501", # Line too long
"S101", # Use of `assert` detected
]
[tool.mypy]
strict = true
show_error_codes = true
no_strict_concatenate = true # TODO: remove when dropping python 3.7
[tool.pytest.ini_options]
filterwarnings = [
"error",
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning",
]
[tool.coverage.run]
branch = true
asyncio-mqtt-0.16.1/tests/ 0000775 0000000 0000000 00000000000 14343623446 0015402 5 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/tests/__init__.py 0000664 0000000 0000000 00000000000 14343623446 0017501 0 ustar 00root root 0000000 0000000 asyncio-mqtt-0.16.1/tests/conftest.py 0000664 0000000 0000000 00000000551 14343623446 0017602 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from typing import Any
import pytest
@pytest.fixture
def anyio_backend() -> tuple[str, dict[str, Any]]:
if sys.platform == "win32":
from asyncio.windows_events import WindowsSelectorEventLoopPolicy
return ("asyncio", {"policy": WindowsSelectorEventLoopPolicy()})
return ("asyncio", {})
asyncio-mqtt-0.16.1/tests/mosquitto.org.crt 0000664 0000000 0000000 00000002654 14343623446 0020755 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE-----
MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL
BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG
A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU
BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv
by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE
BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES
MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp
dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg
UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW
Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA
s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH
3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo
E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT
MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV
6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC
6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf
+pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK
sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839
LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE
m/XriWr/Cq4h/JfB7NTsezVslgkBaoU=
-----END CERTIFICATE-----
asyncio-mqtt-0.16.1/tests/test_client.py 0000664 0000000 0000000 00000032334 14343623446 0020276 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import logging
import ssl
import sys
from pathlib import Path
import anyio
import anyio.abc
import paho.mqtt.client as mqtt
import pytest
from asyncio_mqtt import Client, ProtocolVersion, TLSParameters, Topic, Wildcard, Will
from asyncio_mqtt.types import PayloadType
pytestmark = pytest.mark.anyio
HOSTNAME = "test.mosquitto.org"
OS_PY_VERSION = sys.platform + "_" + ".".join(map(str, sys.version_info[:2]))
TOPIC_HEADER = OS_PY_VERSION + "/tests/asyncio_mqtt/"
async def test_topic_validation() -> None:
"""Test that Topic raises Exceptions for invalid topics."""
with pytest.raises(TypeError):
Topic(True) # type: ignore[arg-type]
with pytest.raises(TypeError):
Topic(1.0) # type: ignore[arg-type]
with pytest.raises(TypeError):
Topic(None) # type: ignore[arg-type]
with pytest.raises(TypeError):
Topic([]) # type: ignore[arg-type]
with pytest.raises(ValueError):
Topic("a/b/#")
with pytest.raises(ValueError):
Topic("a/+/c")
with pytest.raises(ValueError):
Topic("#")
with pytest.raises(ValueError):
Topic("")
with pytest.raises(ValueError):
Topic("a" * 65536)
async def test_wildcard_validation() -> None:
"""Test that Wildcard raises Exceptions for invalid wildcards."""
with pytest.raises(TypeError):
Wildcard(True) # type: ignore[arg-type]
with pytest.raises(TypeError):
Wildcard(1.0) # type: ignore[arg-type]
with pytest.raises(TypeError):
Wildcard(None) # type: ignore[arg-type]
with pytest.raises(TypeError):
Wildcard([]) # type: ignore[arg-type]
with pytest.raises(ValueError):
Wildcard("a/#/c")
with pytest.raises(ValueError):
Wildcard("a/b+/c")
with pytest.raises(ValueError):
Wildcard("a/b/#c")
with pytest.raises(ValueError):
Wildcard("")
with pytest.raises(ValueError):
Wildcard("a" * 65536)
async def test_topic_matches() -> None:
"""Test that Topic.matches() does and doesn't match some test wildcards."""
topic = Topic("a/b/c")
assert topic.matches("a/b/c")
assert topic.matches("a/+/c")
assert topic.matches("+/+/+")
assert topic.matches("+/#")
assert topic.matches("#")
assert topic.matches("$share/group/a/b/c")
assert topic.matches("$share/group/a/b/+")
assert not topic.matches("abc")
assert not topic.matches("a/b")
assert not topic.matches("a/b/c/d")
assert not topic.matches("a/b/z")
assert not topic.matches("$share/a/b/c")
assert not topic.matches("$test/group/a/b/c")
async def test_multiple_messages_generators() -> None:
"""Test that multiple Client.messages() generators can be used at the same time."""
topic = TOPIC_HEADER + "multiple_messages_generators"
async def handler(tg: anyio.abc.TaskGroup) -> None:
async with client.messages() as messages:
async for message in messages:
assert str(message.topic) == topic
tg.cancel_scope.cancel()
async with Client(HOSTNAME) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic)
tg.start_soon(handler, tg)
tg.start_soon(handler, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic)
async def test_client_filtered_messages() -> None:
topic_header = TOPIC_HEADER + "filtered_messages/"
good_topic = topic_header + "good"
bad_topic = topic_header + "bad"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.filtered_messages(good_topic) as messages:
async for message in messages:
assert message.topic == good_topic
tg.cancel_scope.cancel()
async with Client(HOSTNAME) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic_header + "#")
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(bad_topic, 2)
await client.publish(good_topic, 2)
async def test_client_unfiltered_messages() -> None:
topic_header = TOPIC_HEADER + "unfiltered_messages/"
topic_filtered = topic_header + "filtered"
topic_unfiltered = topic_header + "unfiltered"
async def handle_unfiltered_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.unfiltered_messages() as messages:
async for message in messages:
assert message.topic == topic_unfiltered
tg.cancel_scope.cancel()
async def handle_filtered_messages() -> None:
async with client.filtered_messages(topic_filtered) as messages:
async for message in messages:
assert message.topic == topic_filtered
async with Client(HOSTNAME) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic_header + "#")
tg.start_soon(handle_filtered_messages)
tg.start_soon(handle_unfiltered_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic_filtered, 2)
await client.publish(topic_unfiltered, 2)
async def test_client_unsubscribe() -> None:
topic_header = TOPIC_HEADER + "unsubscribe/"
topic1 = topic_header + "1"
topic2 = topic_header + "2"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.unfiltered_messages() as messages:
is_first_message = True
async for message in messages:
if is_first_message:
assert message.topic == topic1
is_first_message = False
else:
assert message.topic == topic2
tg.cancel_scope.cancel()
async with Client(HOSTNAME) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic1)
await client.subscribe(topic2)
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic1, 2)
await client.unsubscribe(topic1)
await client.publish(topic1, 2)
await client.publish(topic2, 2)
@pytest.mark.parametrize(
"protocol, length",
((ProtocolVersion.V31, 22), (ProtocolVersion.V311, 0), (ProtocolVersion.V5, 0)),
)
async def test_client_id(protocol: ProtocolVersion, length: int) -> None:
client = Client(HOSTNAME, protocol=protocol)
assert len(client.id) == length
async def test_client_will() -> None:
topic = TOPIC_HEADER + "will"
event = anyio.Event()
async def launch_client() -> None:
with anyio.CancelScope(shield=True) as cs:
async with Client(HOSTNAME) as client:
await client.subscribe(topic)
event.set()
async with client.filtered_messages(topic) as messages:
async for message in messages:
assert message.topic == topic
cs.cancel()
async with anyio.create_task_group() as tg:
tg.start_soon(launch_client)
await event.wait()
async with Client(HOSTNAME, will=Will(topic)) as client:
client._client._sock_close() # type: ignore[attr-defined]
async def test_client_tls_context() -> None:
topic = TOPIC_HEADER + "tls_context"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.filtered_messages(topic) as messages:
async for message in messages:
assert message.topic == topic
tg.cancel_scope.cancel()
async with Client(
HOSTNAME,
8883,
tls_context=ssl.SSLContext(protocol=ssl.PROTOCOL_TLS),
) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic)
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic)
async def test_client_tls_params() -> None:
topic = TOPIC_HEADER + "tls_params"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.filtered_messages(topic) as messages:
async for message in messages:
assert message.topic == topic
tg.cancel_scope.cancel()
async with Client(
HOSTNAME,
8883,
tls_params=TLSParameters(
ca_certs=str(Path.cwd() / "tests" / "mosquitto.org.crt")
),
) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic)
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic)
async def test_client_username_password() -> None:
topic = TOPIC_HEADER + "username_password"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.filtered_messages(topic) as messages:
async for message in messages:
assert message.topic == topic
tg.cancel_scope.cancel()
async with Client(
HOSTNAME, username="asyncio-mqtt", password="012" # noqa: S106
) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic)
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic)
async def test_client_logger() -> None:
logger = logging.getLogger("asyncio-mqtt")
async with Client(HOSTNAME, logger=logger) as client:
assert logger is client._client._logger # type: ignore[attr-defined]
async def test_client_max_concurrent_outgoing_calls(
monkeypatch: pytest.MonkeyPatch,
) -> None:
topic = TOPIC_HEADER + "max_concurrent_outgoing_calls"
class MockPahoClient(mqtt.Client):
def subscribe(
self,
topic: str
| tuple[str, mqtt.SubscribeOptions]
| list[tuple[str, mqtt.SubscribeOptions]]
| list[tuple[str, int]],
qos: int = 0,
options: mqtt.SubscribeOptions | None = None,
properties: mqtt.Properties | None = None,
) -> tuple[int, int]:
assert client._outgoing_calls_sem is not None
assert client._outgoing_calls_sem.locked()
return super().subscribe(topic, qos, options, properties)
def unsubscribe(
self, topic: str | list[str], properties: mqtt.Properties | None = None
) -> tuple[int, int]:
assert client._outgoing_calls_sem is not None
assert client._outgoing_calls_sem.locked()
return super().unsubscribe(topic, properties)
def publish(
self,
topic: str,
payload: PayloadType | None = None,
qos: int = 0,
retain: bool = False,
properties: mqtt.Properties | None = None,
) -> mqtt.MQTTMessageInfo:
assert client._outgoing_calls_sem is not None
assert client._outgoing_calls_sem.locked()
return super().publish(topic, payload, qos, retain, properties)
monkeypatch.setattr(mqtt, "Client", MockPahoClient)
async with Client(HOSTNAME, max_concurrent_outgoing_calls=1) as client:
await client.subscribe(topic)
await client.unsubscribe(topic)
await client.publish(topic)
async def test_client_websockets() -> None:
topic = TOPIC_HEADER + "websockets"
async def handle_messages(tg: anyio.abc.TaskGroup) -> None:
async with client.filtered_messages(topic) as messages:
async for message in messages:
assert message.topic == topic
tg.cancel_scope.cancel()
async with Client(
HOSTNAME,
8080,
transport="websockets",
websocket_path="/",
websocket_headers={"foo": "bar"},
) as client:
async with anyio.create_task_group() as tg:
await client.subscribe(topic)
tg.start_soon(handle_messages, tg)
await anyio.wait_all_tasks_blocked()
await client.publish(topic)
async def test_client_pending_calls_threshold(caplog: pytest.LogCaptureFixture) -> None:
topic = TOPIC_HEADER + "pending_calls_threshold"
async with Client(HOSTNAME) as client:
nb_publish = client._pending_calls_threshold + 1
async with anyio.create_task_group() as tg:
for _ in range(nb_publish):
tg.start_soon(client.publish, topic)
assert caplog.record_tuples == [
(
"mqtt",
logging.WARNING,
f"There are {nb_publish} pending publish calls.",
)
]
async def test_client_no_pending_calls_warnings_with_max_concurrent_outgoing_calls(
caplog: pytest.LogCaptureFixture,
) -> None:
topic = (
TOPIC_HEADER + "no_pending_calls_warnings_with_max_concurrent_outgoing_calls"
)
async with Client(HOSTNAME, max_concurrent_outgoing_calls=1) as client:
nb_publish = client._pending_calls_threshold + 1
async with anyio.create_task_group() as tg:
for _ in range(nb_publish):
tg.start_soon(client.publish, topic)
assert caplog.record_tuples == []
asyncio-mqtt-0.16.1/tests/test_error.py 0000664 0000000 0000000 00000004176 14343623446 0020154 0 ustar 00root root 0000000 0000000 import paho.mqtt.client as mqtt
import pytest
from paho.mqtt.packettypes import PacketTypes
from asyncio_mqtt.error import _CONNECT_RC_STRINGS, MqttCodeError, MqttConnectError
@pytest.mark.parametrize(
"rc",
(
mqtt.MQTT_ERR_SUCCESS,
mqtt.MQTT_ERR_NOMEM,
mqtt.MQTT_ERR_PROTOCOL,
mqtt.MQTT_ERR_INVAL,
mqtt.MQTT_ERR_NO_CONN,
mqtt.MQTT_ERR_CONN_REFUSED,
mqtt.MQTT_ERR_NOT_FOUND,
mqtt.MQTT_ERR_CONN_LOST,
mqtt.MQTT_ERR_TLS,
mqtt.MQTT_ERR_PAYLOAD_SIZE,
mqtt.MQTT_ERR_NOT_SUPPORTED,
mqtt.MQTT_ERR_AUTH,
mqtt.MQTT_ERR_ACL_DENIED,
mqtt.MQTT_ERR_UNKNOWN,
mqtt.MQTT_ERR_ERRNO,
mqtt.MQTT_ERR_QUEUE_SIZE,
mqtt.MQTT_ERR_KEEPALIVE,
-1,
),
)
def test_mqtt_code_error_int(rc: int) -> None:
assert str(MqttCodeError(rc)) == f"[code:{rc}] {mqtt.error_string(rc)}"
@pytest.mark.parametrize(
"packet_type, a_name",
(
(PacketTypes.CONNACK, "Success"),
(PacketTypes.PUBACK, "Success"),
(PacketTypes.SUBACK, "Granted QoS 1"),
),
)
def test_mqtt_code_error_reason_codes(packet_type: int, a_name: str) -> None:
rc = mqtt.ReasonCodes(packet_type, a_name)
assert str(MqttCodeError(rc)) == f"[code:{rc.value}] {str(rc)}"
def test_mqtt_code_error_none() -> None:
assert str(MqttCodeError(None)) == "[code:None] "
@pytest.mark.parametrize("rc, message", list(_CONNECT_RC_STRINGS.items()) + [(0, "")])
def test_mqtt_connect_error_int(rc: int, message: str) -> None:
error = MqttConnectError(rc)
arg = "Connection refused"
if rc in _CONNECT_RC_STRINGS:
arg += f": {message}"
assert error.args[0] == arg
assert str(error) == f"[code:{rc}] {mqtt.error_string(rc)}"
@pytest.mark.parametrize(
"packet_type, a_name",
(
(PacketTypes.CONNACK, "Success"),
(PacketTypes.PUBACK, "Success"),
(PacketTypes.SUBACK, "Granted QoS 1"),
),
)
def test_mqtt_connect_error_reason_codes(packet_type: int, a_name: str) -> None:
rc = mqtt.ReasonCodes(packet_type, a_name)
assert str(MqttConnectError(rc)) == f"[code:{rc.value}] {str(rc)}"