pax_global_header00006660000000000000000000000064143436234460014523gustar00rootroot0000000000000052 comment=eb13ec8a61e31982dae6b43dcdf570979b5318b5 asyncio-mqtt-0.16.1/000077500000000000000000000000001434362344600142405ustar00rootroot00000000000000asyncio-mqtt-0.16.1/.github/000077500000000000000000000000001434362344600156005ustar00rootroot00000000000000asyncio-mqtt-0.16.1/.github/codecov.yml000066400000000000000000000002751434362344600177510ustar00rootroot00000000000000coverage: 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/000077500000000000000000000000001434362344600176355ustar00rootroot00000000000000asyncio-mqtt-0.16.1/.github/workflows/publish.yml000066400000000000000000000010721434362344600220260ustar00rootroot00000000000000name: 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.yml000066400000000000000000000023641434362344600213440ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000001551434362344600162310ustar00rootroot00000000000000__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.yaml000066400000000000000000000023641434362344600205260ustar00rootroot00000000000000repos: - 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.md000066400000000000000000000370551434362344600160630ustar00rootroot00000000000000# 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.md000066400000000000000000000037711434362344600165010ustar00rootroot00000000000000# 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/LICENSE000066400000000000000000000026701434362344600152520ustar00rootroot00000000000000Copyright 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.md000066400000000000000000000363151434362344600155270ustar00rootroot00000000000000

Idiomatic asyncio MQTT Client 🙌

License: BSD-3-Clause PyPI version Supported Python versions PyPI downloads Coverage Coverage pre-commit.ci status Typing: strict Code Style: Black

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 📋 License: BSD-3-Clause 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 🎯 PyPI 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.
![GitHub stars](https://img.shields.io/github/stars/eclipse/paho.mqtt.python) ![license](https://img.shields.io/github/license/eclipse/paho.mqtt.python) - [gmqtt](https://github.com/wialon/gmqtt) — Own protocol implementation. Asynchronous.
![GitHub stars](https://img.shields.io/github/stars/wialon/gmqtt) ![license](https://img.shields.io/github/license/wialon/gmqtt) - [fastapi-mqtt](https://github.com/sabuhish/fastapi-mqtt) — Asynchronous wrapper around gmqtt. Simplifies integration in your FastAPI application.
![GitHub stars](https://img.shields.io/github/stars/sabuhish/fastapi-mqtt) ![license](https://img.shields.io/github/license/sabuhish/fastapi-mqtt) - [amqtt](https://github.com/Yakifo/amqtt) — Own protocol implementation. Asynchronous. Includes a broker.
![GitHub stars](https://img.shields.io/github/stars/Yakifo/amqtt) ![license](https://img.shields.io/github/license/Yakifo/amqtt) - [mqttools](https://github.com/eerimoq/mqttools) — Own protocol implementation. Asynchronous.
![GitHub stars](https://img.shields.io/github/stars/eerimoq/mqttools) ![license](https://img.shields.io/github/license/eerimoq/mqttools) - [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.
![GitHub stars](https://img.shields.io/github/stars/bkanuka/trio-paho-mqtt) ![license](https://img.shields.io/github/license/bkanuka/trio-paho-mqtt) asyncio-mqtt-0.16.1/asyncio_mqtt/000077500000000000000000000000001434362344600167525ustar00rootroot00000000000000asyncio-mqtt-0.16.1/asyncio_mqtt/__init__.py000066400000000000000000000011061434362344600210610ustar00rootroot00000000000000# 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.py000066400000000000000000000776101434362344600206150ustar00rootroot00000000000000# 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.py000066400000000000000000000032351434362344600204600ustar00rootroot00000000000000# 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.typed000066400000000000000000000000001434362344600204370ustar00rootroot00000000000000asyncio-mqtt-0.16.1/asyncio_mqtt/types.py000066400000000000000000000003651434362344600204740ustar00rootroot00000000000000import 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/000077500000000000000000000000001434362344600160565ustar00rootroot00000000000000asyncio-mqtt-0.16.1/examples/EXAMPLES.md000066400000000000000000000052271434362344600176240ustar00rootroot00000000000000# Examples ![license](https://img.shields.io/github/license/sbtinstruments/asyncio-mqtt) ![semver](https://img.shields.io/github/v/tag/sbtinstruments/asyncio-mqtt?sort=semver) ## 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 ![license](https://img.shields.io/github/license/sbtinstruments/asyncio-mqtt) 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.toml000066400000000000000000000056671434362344600171720ustar00rootroot00000000000000[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/000077500000000000000000000000001434362344600154025ustar00rootroot00000000000000asyncio-mqtt-0.16.1/tests/__init__.py000066400000000000000000000000001434362344600175010ustar00rootroot00000000000000asyncio-mqtt-0.16.1/tests/conftest.py000066400000000000000000000005511434362344600176020ustar00rootroot00000000000000from __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.crt000066400000000000000000000026541434362344600207550ustar00rootroot00000000000000-----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.py000066400000000000000000000323341434362344600202760ustar00rootroot00000000000000from __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.py000066400000000000000000000041761434362344600201540ustar00rootroot00000000000000import 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)}"