connectmq 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- connectmq-1.0.0/LICENSE +21 -0
- connectmq-1.0.0/PKG-INFO +182 -0
- connectmq-1.0.0/README.md +160 -0
- connectmq-1.0.0/pyproject.toml +35 -0
- connectmq-1.0.0/src/connectmq/__init__.py +43 -0
- connectmq-1.0.0/src/connectmq/__main__.py +5 -0
- connectmq-1.0.0/src/connectmq/cli.py +97 -0
- connectmq-1.0.0/src/connectmq/client.py +352 -0
- connectmq-1.0.0/src/connectmq/py.typed +0 -0
connectmq-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ConnectMQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
connectmq-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: connectmq
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Publish and subscribe on the free public MQTT broker broker.connectmq.com in one line. Preconfigured paho-mqtt client and CLI for TCP, TLS, WebSocket and secure WebSocket.
|
|
5
|
+
Project-URL: Homepage, https://connectmq.com
|
|
6
|
+
Project-URL: Documentation, https://connectmq.com/blog/free-public-mqtt-broker
|
|
7
|
+
Author: ConnectMQ Team
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: connectmq,free-mqtt-broker,iot,mqtt,mqtt-broker,mqtt-client,paho-mqtt,public-mqtt-broker,pubsub
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Communications
|
|
16
|
+
Classifier: Topic :: Internet
|
|
17
|
+
Classifier: Topic :: System :: Networking
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: paho-mqtt<3,>=2.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# connectmq
|
|
24
|
+
|
|
25
|
+
Publish and subscribe on the free public MQTT broker **broker.connectmq.com** in one line, with no account needed.
|
|
26
|
+
The package is a preconfigured [paho-mqtt](https://pypi.org/project/paho-mqtt/) client and CLI for TCP, TLS, WebSocket and secure WebSocket.
|
|
27
|
+
It also works with private brokers created on [connectmq.com](https://connectmq.com).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install connectmq
|
|
31
|
+
connectmq test
|
|
32
|
+
# OK mqtts://broker.connectmq.com:8883 connect 220 ms, round-trip 46 ms
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.9 or later.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import connectmq
|
|
41
|
+
|
|
42
|
+
prefix = connectmq.topic_prefix() # e.g. "connectmq/demo/7f3a91"
|
|
43
|
+
|
|
44
|
+
# Publish (dicts and lists are sent as JSON)
|
|
45
|
+
connectmq.publish(f"{prefix}/temperature", {"value": 21.5}, retain=True)
|
|
46
|
+
|
|
47
|
+
# Subscribe: iterate over incoming messages
|
|
48
|
+
for msg in connectmq.subscribe(f"{prefix}/#", count=1, timeout=10):
|
|
49
|
+
print(msg.topic, msg.json())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The public area is shared, so use a random prefix to keep your topics apart from other testers.
|
|
53
|
+
|
|
54
|
+
## Reuse one connection
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import connectmq
|
|
58
|
+
|
|
59
|
+
with connectmq.Client(transport="tls") as client:
|
|
60
|
+
# Callback style: returns a function that unsubscribes
|
|
61
|
+
stop = client.subscribe("connectmq/demo/7f3a91/#", lambda m: print(m.topic, m.payload))
|
|
62
|
+
|
|
63
|
+
for i in range(10):
|
|
64
|
+
client.publish("connectmq/demo/7f3a91/counter", i, qos=1)
|
|
65
|
+
|
|
66
|
+
reply = client.once("connectmq/demo/7f3a91/reply", timeout=5) # raises TimeoutError
|
|
67
|
+
stop()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
| Function | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `publish(topic, payload, qos=0, retain=False, **options)` | Publish one message with a short-lived connection |
|
|
75
|
+
| `subscribe(topics, qos=0, count=0, timeout=None, **options)` | Iterator over messages; stops after `count` messages or `timeout` seconds of silence |
|
|
76
|
+
| `once(topic, timeout=30, **options)` | Next message on a topic filter; raises `TimeoutError` |
|
|
77
|
+
| `connect(**options)` / `Client(**options)` | Connected client; use as a context manager |
|
|
78
|
+
| `test(transport="tls", **options)` | Connects, publishes, receives; returns `TestResult(ok, url, connect_ms, round_trip_ms, error)` |
|
|
79
|
+
| `broker_info()` | Endpoints and limits of the public broker |
|
|
80
|
+
| `topic_prefix(root="connectmq/demo")` | Random topic prefix |
|
|
81
|
+
| `broker_url(transport, host, port)` | URL for `tcp`, `tls`, `ws` or `wss` |
|
|
82
|
+
| `topic_matches(filter, topic)` | Wildcard matching for `+`, `#` and `$share/...` |
|
|
83
|
+
|
|
84
|
+
`Client` methods: `publish()`, `subscribe(topics, callback, qos)`, `messages(topics, qos, count, timeout)`, `once()` and `close()`.
|
|
85
|
+
The underlying paho client is available as `client.mqtt`.
|
|
86
|
+
|
|
87
|
+
Callbacks are registered before the broker acknowledges a subscription, so retained messages are never missed.
|
|
88
|
+
|
|
89
|
+
`Message` has `topic`, `payload` (UTF-8 string), `raw` (bytes), `qos`, `retain`, `received` (datetime) and `json()`.
|
|
90
|
+
|
|
91
|
+
### Options
|
|
92
|
+
|
|
93
|
+
| Option | Default | Notes |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `transport` | `"tls"` | `tcp` 1883, `tls` 8883, `ws` 8083, `wss` 8084 (`/mqtt`) |
|
|
96
|
+
| `host` | `"broker.connectmq.com"` | Any MQTT broker |
|
|
97
|
+
| `port` | Standard port | |
|
|
98
|
+
| `client_id` | `py-` plus a random suffix | Must be unique and non-empty |
|
|
99
|
+
| `username`, `password` | `None` | Not needed on the public broker |
|
|
100
|
+
| `protocol` | `5` | `5` for MQTT 5.0, `4` for MQTT 3.1.1 |
|
|
101
|
+
| `keepalive`, `timeout` | `60`, `10.0` | Seconds |
|
|
102
|
+
|
|
103
|
+
## Examples
|
|
104
|
+
|
|
105
|
+
**Send sensor readings**
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import random, time, connectmq
|
|
109
|
+
|
|
110
|
+
with connectmq.Client() as client:
|
|
111
|
+
while True:
|
|
112
|
+
client.publish("connectmq/demo/7f3a91/sensor", {"temperature": round(random.uniform(20, 25), 1)})
|
|
113
|
+
time.sleep(5)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Log messages to CSV**
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import csv, connectmq
|
|
120
|
+
|
|
121
|
+
with open("mqtt-log.csv", "a", newline="") as f:
|
|
122
|
+
writer = csv.writer(f)
|
|
123
|
+
for msg in connectmq.subscribe("connectmq/demo/7f3a91/#"):
|
|
124
|
+
writer.writerow([msg.received.isoformat(), msg.topic, msg.payload])
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Private ConnectMQ broker**
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import os, connectmq
|
|
131
|
+
|
|
132
|
+
client = connectmq.connect(
|
|
133
|
+
host="YOUR_BROKER_HOST",
|
|
134
|
+
transport="tls",
|
|
135
|
+
username=os.environ["MQTT_USERNAME"],
|
|
136
|
+
password=os.environ["MQTT_PASSWORD"],
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## CLI
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
connectmq test # connectivity and round-trip
|
|
144
|
+
connectmq info # endpoints and limits
|
|
145
|
+
connectmq prefix # random topic prefix
|
|
146
|
+
connectmq sub "connectmq/demo/7f3a91/#"
|
|
147
|
+
connectmq pub connectmq/demo/7f3a91/temp '{"value":21.5}' -q 1 -r
|
|
148
|
+
echo hello | connectmq pub connectmq/demo/7f3a91/msg
|
|
149
|
+
python -m connectmq test # if the script folder is not on PATH
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| Option | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `-t, --transport` | `tcp`, `tls` (default), `ws`, `wss` |
|
|
155
|
+
| `-H, --host`, `-P, --port` | Broker host and port |
|
|
156
|
+
| `-q, --qos` | 0, 1 or 2 |
|
|
157
|
+
| `-r, --retain` | Retained message (`pub`) |
|
|
158
|
+
| `-n, --count` | Exit after n messages (`sub`) |
|
|
159
|
+
| `-j, --json` | JSON line output (`sub`) |
|
|
160
|
+
| `-u, --username`, `-p, --password` | Credentials for private brokers |
|
|
161
|
+
|
|
162
|
+
## Public broker
|
|
163
|
+
|
|
164
|
+
| Protocol | URL |
|
|
165
|
+
|---|---|
|
|
166
|
+
| MQTT (TCP) | `mqtt://broker.connectmq.com:1883` |
|
|
167
|
+
| MQTT over TLS | `mqtts://broker.connectmq.com:8883` |
|
|
168
|
+
| WebSocket | `ws://broker.connectmq.com:8083/mqtt` |
|
|
169
|
+
| Secure WebSocket | `wss://broker.connectmq.com:8084/mqtt` |
|
|
170
|
+
|
|
171
|
+
Access is anonymous, but the client ID must not be empty. Limits: 64 KB per message, 10 messages per second and 10 subscriptions per client, topics up to 8 levels deep, and sessions expire after at most 30 minutes.
|
|
172
|
+
|
|
173
|
+
> **Everything on the public broker is public.** Any client can subscribe to `#`.
|
|
174
|
+
> Use the broker only for testing, and always under a random topic prefix.
|
|
175
|
+
> For production, create a private broker with its own credentials at [connectmq.com](https://connectmq.com).
|
|
176
|
+
|
|
177
|
+
More: [Free Public MQTT Broker for Testing](https://connectmq.com/blog/free-public-mqtt-broker).
|
|
178
|
+
Also available for Node.js (`@connectmq/mqtt`), .NET (`ConnectMQ.Mqtt`), Node-RED (`node-red-contrib-connectmq`) and PowerShell (`MQTTBroker`).
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# connectmq
|
|
2
|
+
|
|
3
|
+
Publish and subscribe on the free public MQTT broker **broker.connectmq.com** in one line, with no account needed.
|
|
4
|
+
The package is a preconfigured [paho-mqtt](https://pypi.org/project/paho-mqtt/) client and CLI for TCP, TLS, WebSocket and secure WebSocket.
|
|
5
|
+
It also works with private brokers created on [connectmq.com](https://connectmq.com).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install connectmq
|
|
9
|
+
connectmq test
|
|
10
|
+
# OK mqtts://broker.connectmq.com:8883 connect 220 ms, round-trip 46 ms
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.9 or later.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import connectmq
|
|
19
|
+
|
|
20
|
+
prefix = connectmq.topic_prefix() # e.g. "connectmq/demo/7f3a91"
|
|
21
|
+
|
|
22
|
+
# Publish (dicts and lists are sent as JSON)
|
|
23
|
+
connectmq.publish(f"{prefix}/temperature", {"value": 21.5}, retain=True)
|
|
24
|
+
|
|
25
|
+
# Subscribe: iterate over incoming messages
|
|
26
|
+
for msg in connectmq.subscribe(f"{prefix}/#", count=1, timeout=10):
|
|
27
|
+
print(msg.topic, msg.json())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The public area is shared, so use a random prefix to keep your topics apart from other testers.
|
|
31
|
+
|
|
32
|
+
## Reuse one connection
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import connectmq
|
|
36
|
+
|
|
37
|
+
with connectmq.Client(transport="tls") as client:
|
|
38
|
+
# Callback style: returns a function that unsubscribes
|
|
39
|
+
stop = client.subscribe("connectmq/demo/7f3a91/#", lambda m: print(m.topic, m.payload))
|
|
40
|
+
|
|
41
|
+
for i in range(10):
|
|
42
|
+
client.publish("connectmq/demo/7f3a91/counter", i, qos=1)
|
|
43
|
+
|
|
44
|
+
reply = client.once("connectmq/demo/7f3a91/reply", timeout=5) # raises TimeoutError
|
|
45
|
+
stop()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
| Function | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `publish(topic, payload, qos=0, retain=False, **options)` | Publish one message with a short-lived connection |
|
|
53
|
+
| `subscribe(topics, qos=0, count=0, timeout=None, **options)` | Iterator over messages; stops after `count` messages or `timeout` seconds of silence |
|
|
54
|
+
| `once(topic, timeout=30, **options)` | Next message on a topic filter; raises `TimeoutError` |
|
|
55
|
+
| `connect(**options)` / `Client(**options)` | Connected client; use as a context manager |
|
|
56
|
+
| `test(transport="tls", **options)` | Connects, publishes, receives; returns `TestResult(ok, url, connect_ms, round_trip_ms, error)` |
|
|
57
|
+
| `broker_info()` | Endpoints and limits of the public broker |
|
|
58
|
+
| `topic_prefix(root="connectmq/demo")` | Random topic prefix |
|
|
59
|
+
| `broker_url(transport, host, port)` | URL for `tcp`, `tls`, `ws` or `wss` |
|
|
60
|
+
| `topic_matches(filter, topic)` | Wildcard matching for `+`, `#` and `$share/...` |
|
|
61
|
+
|
|
62
|
+
`Client` methods: `publish()`, `subscribe(topics, callback, qos)`, `messages(topics, qos, count, timeout)`, `once()` and `close()`.
|
|
63
|
+
The underlying paho client is available as `client.mqtt`.
|
|
64
|
+
|
|
65
|
+
Callbacks are registered before the broker acknowledges a subscription, so retained messages are never missed.
|
|
66
|
+
|
|
67
|
+
`Message` has `topic`, `payload` (UTF-8 string), `raw` (bytes), `qos`, `retain`, `received` (datetime) and `json()`.
|
|
68
|
+
|
|
69
|
+
### Options
|
|
70
|
+
|
|
71
|
+
| Option | Default | Notes |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `transport` | `"tls"` | `tcp` 1883, `tls` 8883, `ws` 8083, `wss` 8084 (`/mqtt`) |
|
|
74
|
+
| `host` | `"broker.connectmq.com"` | Any MQTT broker |
|
|
75
|
+
| `port` | Standard port | |
|
|
76
|
+
| `client_id` | `py-` plus a random suffix | Must be unique and non-empty |
|
|
77
|
+
| `username`, `password` | `None` | Not needed on the public broker |
|
|
78
|
+
| `protocol` | `5` | `5` for MQTT 5.0, `4` for MQTT 3.1.1 |
|
|
79
|
+
| `keepalive`, `timeout` | `60`, `10.0` | Seconds |
|
|
80
|
+
|
|
81
|
+
## Examples
|
|
82
|
+
|
|
83
|
+
**Send sensor readings**
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import random, time, connectmq
|
|
87
|
+
|
|
88
|
+
with connectmq.Client() as client:
|
|
89
|
+
while True:
|
|
90
|
+
client.publish("connectmq/demo/7f3a91/sensor", {"temperature": round(random.uniform(20, 25), 1)})
|
|
91
|
+
time.sleep(5)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Log messages to CSV**
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import csv, connectmq
|
|
98
|
+
|
|
99
|
+
with open("mqtt-log.csv", "a", newline="") as f:
|
|
100
|
+
writer = csv.writer(f)
|
|
101
|
+
for msg in connectmq.subscribe("connectmq/demo/7f3a91/#"):
|
|
102
|
+
writer.writerow([msg.received.isoformat(), msg.topic, msg.payload])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Private ConnectMQ broker**
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import os, connectmq
|
|
109
|
+
|
|
110
|
+
client = connectmq.connect(
|
|
111
|
+
host="YOUR_BROKER_HOST",
|
|
112
|
+
transport="tls",
|
|
113
|
+
username=os.environ["MQTT_USERNAME"],
|
|
114
|
+
password=os.environ["MQTT_PASSWORD"],
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## CLI
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
connectmq test # connectivity and round-trip
|
|
122
|
+
connectmq info # endpoints and limits
|
|
123
|
+
connectmq prefix # random topic prefix
|
|
124
|
+
connectmq sub "connectmq/demo/7f3a91/#"
|
|
125
|
+
connectmq pub connectmq/demo/7f3a91/temp '{"value":21.5}' -q 1 -r
|
|
126
|
+
echo hello | connectmq pub connectmq/demo/7f3a91/msg
|
|
127
|
+
python -m connectmq test # if the script folder is not on PATH
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Option | Description |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `-t, --transport` | `tcp`, `tls` (default), `ws`, `wss` |
|
|
133
|
+
| `-H, --host`, `-P, --port` | Broker host and port |
|
|
134
|
+
| `-q, --qos` | 0, 1 or 2 |
|
|
135
|
+
| `-r, --retain` | Retained message (`pub`) |
|
|
136
|
+
| `-n, --count` | Exit after n messages (`sub`) |
|
|
137
|
+
| `-j, --json` | JSON line output (`sub`) |
|
|
138
|
+
| `-u, --username`, `-p, --password` | Credentials for private brokers |
|
|
139
|
+
|
|
140
|
+
## Public broker
|
|
141
|
+
|
|
142
|
+
| Protocol | URL |
|
|
143
|
+
|---|---|
|
|
144
|
+
| MQTT (TCP) | `mqtt://broker.connectmq.com:1883` |
|
|
145
|
+
| MQTT over TLS | `mqtts://broker.connectmq.com:8883` |
|
|
146
|
+
| WebSocket | `ws://broker.connectmq.com:8083/mqtt` |
|
|
147
|
+
| Secure WebSocket | `wss://broker.connectmq.com:8084/mqtt` |
|
|
148
|
+
|
|
149
|
+
Access is anonymous, but the client ID must not be empty. Limits: 64 KB per message, 10 messages per second and 10 subscriptions per client, topics up to 8 levels deep, and sessions expire after at most 30 minutes.
|
|
150
|
+
|
|
151
|
+
> **Everything on the public broker is public.** Any client can subscribe to `#`.
|
|
152
|
+
> Use the broker only for testing, and always under a random topic prefix.
|
|
153
|
+
> For production, create a private broker with its own credentials at [connectmq.com](https://connectmq.com).
|
|
154
|
+
|
|
155
|
+
More: [Free Public MQTT Broker for Testing](https://connectmq.com/blog/free-public-mqtt-broker).
|
|
156
|
+
Also available for Node.js (`@connectmq/mqtt`), .NET (`ConnectMQ.Mqtt`), Node-RED (`node-red-contrib-connectmq`) and PowerShell (`MQTTBroker`).
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "connectmq"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Publish and subscribe on the free public MQTT broker broker.connectmq.com in one line. Preconfigured paho-mqtt client and CLI for TCP, TLS, WebSocket and secure WebSocket."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "ConnectMQ Team" }]
|
|
14
|
+
keywords = ["mqtt", "mqtt-broker", "public-mqtt-broker", "free-mqtt-broker", "mqtt-client", "iot", "pubsub", "paho-mqtt", "connectmq"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Communications",
|
|
21
|
+
"Topic :: Internet",
|
|
22
|
+
"Topic :: System :: Networking",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["paho-mqtt>=2.0,<3"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://connectmq.com"
|
|
29
|
+
Documentation = "https://connectmq.com/blog/free-public-mqtt-broker"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
connectmq = "connectmq.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/connectmq"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Publish and subscribe on the free public MQTT broker broker.connectmq.com.
|
|
2
|
+
|
|
3
|
+
>>> import connectmq
|
|
4
|
+
>>> connectmq.publish("connectmq/demo/7f3a91/temp", {"value": 21.5})
|
|
5
|
+
>>> for msg in connectmq.subscribe("connectmq/demo/7f3a91/#", count=1):
|
|
6
|
+
... print(msg.topic, msg.payload)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .client import (
|
|
10
|
+
BROKER_HOST,
|
|
11
|
+
ENDPOINTS,
|
|
12
|
+
Client,
|
|
13
|
+
Message,
|
|
14
|
+
TestResult,
|
|
15
|
+
broker_info,
|
|
16
|
+
broker_url,
|
|
17
|
+
connect,
|
|
18
|
+
once,
|
|
19
|
+
publish,
|
|
20
|
+
subscribe,
|
|
21
|
+
test,
|
|
22
|
+
topic_matches,
|
|
23
|
+
topic_prefix,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "1.0.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BROKER_HOST",
|
|
30
|
+
"ENDPOINTS",
|
|
31
|
+
"Client",
|
|
32
|
+
"Message",
|
|
33
|
+
"TestResult",
|
|
34
|
+
"broker_info",
|
|
35
|
+
"broker_url",
|
|
36
|
+
"connect",
|
|
37
|
+
"once",
|
|
38
|
+
"publish",
|
|
39
|
+
"subscribe",
|
|
40
|
+
"test",
|
|
41
|
+
"topic_matches",
|
|
42
|
+
"topic_prefix",
|
|
43
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Command-line interface: ``connectmq test | info | prefix | sub | pub``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from . import Client, broker_info, test, topic_prefix
|
|
11
|
+
|
|
12
|
+
EPILOG = """examples:
|
|
13
|
+
connectmq test
|
|
14
|
+
connectmq sub "connectmq/demo/7f3a91/#"
|
|
15
|
+
connectmq pub connectmq/demo/7f3a91/temp '{"value":21.5}' -q 1
|
|
16
|
+
|
|
17
|
+
Public area: anyone can read your messages. Private brokers: https://connectmq.com"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parser() -> argparse.ArgumentParser:
|
|
21
|
+
connection = argparse.ArgumentParser(add_help=False)
|
|
22
|
+
connection.add_argument("-t", "--transport", choices=["tcp", "tls", "ws", "wss"], default="tls", help="default: tls")
|
|
23
|
+
connection.add_argument("-H", "--host", default="broker.connectmq.com", help="default: broker.connectmq.com")
|
|
24
|
+
connection.add_argument("-P", "--port", type=int, help="override the transport's standard port")
|
|
25
|
+
connection.add_argument("-u", "--username", help="for private brokers")
|
|
26
|
+
connection.add_argument("-p", "--password", help="for private brokers")
|
|
27
|
+
connection.add_argument("-q", "--qos", type=int, choices=[0, 1, 2], default=0)
|
|
28
|
+
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="connectmq",
|
|
31
|
+
description="MQTT client for the free public broker broker.connectmq.com",
|
|
32
|
+
epilog=EPILOG,
|
|
33
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
34
|
+
)
|
|
35
|
+
commands = parser.add_subparsers(dest="command")
|
|
36
|
+
commands.add_parser("test", parents=[connection], help="check connectivity and message round-trip (default)")
|
|
37
|
+
commands.add_parser("info", help="show public broker endpoints and limits")
|
|
38
|
+
commands.add_parser("prefix", help="print a random topic prefix")
|
|
39
|
+
sub = commands.add_parser("sub", parents=[connection], help="subscribe and print messages")
|
|
40
|
+
sub.add_argument("topics", nargs="+")
|
|
41
|
+
sub.add_argument("-n", "--count", type=int, default=0, help="exit after n messages")
|
|
42
|
+
sub.add_argument("-j", "--json", action="store_true", help="print JSON lines")
|
|
43
|
+
pub = commands.add_parser("pub", parents=[connection], help="publish a message (stdin if omitted)")
|
|
44
|
+
pub.add_argument("topic")
|
|
45
|
+
pub.add_argument("message", nargs="?")
|
|
46
|
+
pub.add_argument("-r", "--retain", action="store_true")
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
51
|
+
args = _parser().parse_args(argv)
|
|
52
|
+
command = args.command or "test"
|
|
53
|
+
if command == "info":
|
|
54
|
+
print(json.dumps(broker_info(), indent=2))
|
|
55
|
+
return 0
|
|
56
|
+
if command == "prefix":
|
|
57
|
+
print(topic_prefix())
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
options = {
|
|
61
|
+
"transport": getattr(args, "transport", "tls"),
|
|
62
|
+
"host": getattr(args, "host", "broker.connectmq.com"),
|
|
63
|
+
"port": getattr(args, "port", None),
|
|
64
|
+
"username": getattr(args, "username", None),
|
|
65
|
+
"password": getattr(args, "password", None),
|
|
66
|
+
}
|
|
67
|
+
try:
|
|
68
|
+
if command == "test":
|
|
69
|
+
r = test(**options)
|
|
70
|
+
if r.ok:
|
|
71
|
+
print(f"OK {r.url} connect {r.connect_ms:.0f} ms, round-trip {r.round_trip_ms:.0f} ms")
|
|
72
|
+
return 0
|
|
73
|
+
print(f"FAIL {r.url} {r.error}")
|
|
74
|
+
return 1
|
|
75
|
+
with Client(**options) as client:
|
|
76
|
+
if command == "pub":
|
|
77
|
+
message = args.message if args.message is not None else sys.stdin.read().rstrip("\r\n")
|
|
78
|
+
client.publish(args.topic, message, qos=args.qos, retain=args.retain)
|
|
79
|
+
print(f"Published to {args.topic}", file=sys.stderr)
|
|
80
|
+
else:
|
|
81
|
+
print(f"Subscribed to {', '.join(args.topics)}. Press Ctrl+C to stop.", file=sys.stderr)
|
|
82
|
+
for m in client.messages(args.topics, qos=args.qos, count=args.count):
|
|
83
|
+
if args.json:
|
|
84
|
+
record = {"topic": m.topic, "payload": m.payload, "qos": m.qos, "retain": m.retain, "received": m.received.isoformat()}
|
|
85
|
+
print(json.dumps(record), flush=True)
|
|
86
|
+
else:
|
|
87
|
+
print(f"{m.received.astimezone():%H:%M:%S} {m.topic} {m.payload}", flush=True)
|
|
88
|
+
return 0
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
return 0
|
|
91
|
+
except Exception as err: # noqa: BLE001 - CLI reports errors as text
|
|
92
|
+
print(f"Error: {err}", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
sys.exit(main())
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""MQTT client preconfigured for the ConnectMQ public broker (broker.connectmq.com), built on paho-mqtt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import paho.mqtt.client as mqtt
|
|
15
|
+
|
|
16
|
+
BROKER_HOST = "broker.connectmq.com"
|
|
17
|
+
|
|
18
|
+
ENDPOINTS: Dict[str, Tuple[str, int, Optional[str]]] = {
|
|
19
|
+
"tcp": ("mqtt", 1883, None),
|
|
20
|
+
"tls": ("mqtts", 8883, None),
|
|
21
|
+
"ws": ("ws", 8083, "/mqtt"),
|
|
22
|
+
"wss": ("wss", 8084, "/mqtt"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Payload = Union[str, bytes, bytearray, int, float, dict, list, None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Message:
|
|
30
|
+
"""A received MQTT message."""
|
|
31
|
+
|
|
32
|
+
topic: str
|
|
33
|
+
raw: bytes
|
|
34
|
+
qos: int
|
|
35
|
+
retain: bool
|
|
36
|
+
received: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def payload(self) -> str:
|
|
40
|
+
"""Payload decoded as UTF-8."""
|
|
41
|
+
return self.raw.decode("utf-8", errors="replace")
|
|
42
|
+
|
|
43
|
+
def json(self) -> Any:
|
|
44
|
+
"""Payload parsed as JSON."""
|
|
45
|
+
return json.loads(self.raw)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class TestResult:
|
|
50
|
+
"""Result of :func:`test`."""
|
|
51
|
+
|
|
52
|
+
url: str
|
|
53
|
+
ok: bool
|
|
54
|
+
connect_ms: Optional[float]
|
|
55
|
+
round_trip_ms: Optional[float]
|
|
56
|
+
error: Optional[str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def broker_url(transport: str = "tls", host: str = BROKER_HOST, port: Optional[int] = None) -> str:
|
|
60
|
+
"""URL of the broker for a transport: ``tcp``, ``tls``, ``ws`` or ``wss``."""
|
|
61
|
+
scheme, default_port, path = _endpoint(transport)
|
|
62
|
+
return f"{scheme}://{host}:{port or default_port}{path or ''}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def broker_info() -> Dict[str, Any]:
|
|
66
|
+
"""Endpoints and limits of the public broker."""
|
|
67
|
+
return {
|
|
68
|
+
"host": BROKER_HOST,
|
|
69
|
+
"urls": {name: broker_url(name) for name in ENDPOINTS},
|
|
70
|
+
"authentication": "Anonymous (non-empty client ID required)",
|
|
71
|
+
"limits": {
|
|
72
|
+
"max_message_size": "64 KB",
|
|
73
|
+
"publish_rate": "10 messages/second per client",
|
|
74
|
+
"subscriptions": 10,
|
|
75
|
+
"max_topic_depth": 8,
|
|
76
|
+
"qos": [0, 1, 2],
|
|
77
|
+
"session_expiry": "30 minutes",
|
|
78
|
+
},
|
|
79
|
+
"note": "Public area: anyone can read your messages. Get a private broker at https://connectmq.com",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def topic_prefix(root: str = "connectmq/demo") -> str:
|
|
84
|
+
"""Random topic prefix such as ``connectmq/demo/7f3a91``, to avoid collisions on the shared public area."""
|
|
85
|
+
return f"{root}/{uuid.uuid4().hex[:6]}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def topic_matches(topic_filter: str, topic: str) -> bool:
|
|
89
|
+
"""MQTT topic filter matching with ``+``, ``#`` and ``$share/group/`` prefixes."""
|
|
90
|
+
if topic_filter.startswith("$share/"):
|
|
91
|
+
topic_filter = topic_filter.split("/", 2)[2] if topic_filter.count("/") >= 2 else ""
|
|
92
|
+
return mqtt.topic_matches_sub(topic_filter, topic)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _endpoint(transport: str) -> Tuple[str, int, Optional[str]]:
|
|
96
|
+
try:
|
|
97
|
+
return ENDPOINTS[transport]
|
|
98
|
+
except KeyError:
|
|
99
|
+
raise ValueError(f"Unknown transport {transport!r}. Use tcp, tls, ws or wss.") from None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _encode(payload: Payload) -> Union[str, bytes, bytearray, None]:
|
|
103
|
+
if payload is None or isinstance(payload, (str, bytes, bytearray)):
|
|
104
|
+
return payload
|
|
105
|
+
if isinstance(payload, (dict, list)):
|
|
106
|
+
return json.dumps(payload)
|
|
107
|
+
return str(payload)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Client:
|
|
111
|
+
"""Connected MQTT client. Use it as a context manager, or call :meth:`close` when done.
|
|
112
|
+
|
|
113
|
+
>>> with Client() as c:
|
|
114
|
+
... c.publish("connectmq/demo/7f3a91/hello", "Hi")
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
transport: str = "tls",
|
|
120
|
+
host: str = BROKER_HOST,
|
|
121
|
+
port: Optional[int] = None,
|
|
122
|
+
client_id: Optional[str] = None,
|
|
123
|
+
username: Optional[str] = None,
|
|
124
|
+
password: Optional[str] = None,
|
|
125
|
+
protocol: int = 5,
|
|
126
|
+
keepalive: int = 60,
|
|
127
|
+
timeout: float = 10.0,
|
|
128
|
+
) -> None:
|
|
129
|
+
scheme, default_port, path = _endpoint(transport)
|
|
130
|
+
self.url = broker_url(transport, host, port)
|
|
131
|
+
self.client_id = client_id or f"py-{uuid.uuid4().hex[:8]}"
|
|
132
|
+
self.timeout = timeout
|
|
133
|
+
self._handlers: Dict[int, Tuple[List[str], Callable[[Message], None]]] = {}
|
|
134
|
+
self._next_handler = 0
|
|
135
|
+
self._lock = threading.Lock()
|
|
136
|
+
self._acks: Dict[int, "queue.Queue[Any]"] = {}
|
|
137
|
+
self._connected = threading.Event()
|
|
138
|
+
self._connect_result: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
version = mqtt.MQTTv5 if protocol == 5 else mqtt.MQTTv311
|
|
141
|
+
self.mqtt = mqtt.Client(
|
|
142
|
+
mqtt.CallbackAPIVersion.VERSION2,
|
|
143
|
+
client_id=self.client_id,
|
|
144
|
+
protocol=version,
|
|
145
|
+
transport="websockets" if path else "tcp",
|
|
146
|
+
**({} if version == mqtt.MQTTv5 else {"clean_session": True}),
|
|
147
|
+
)
|
|
148
|
+
if path:
|
|
149
|
+
self.mqtt.ws_set_options(path=path)
|
|
150
|
+
if scheme in ("mqtts", "wss"):
|
|
151
|
+
self.mqtt.tls_set()
|
|
152
|
+
if username is not None:
|
|
153
|
+
self.mqtt.username_pw_set(username, password)
|
|
154
|
+
self.mqtt.on_connect = self._on_connect
|
|
155
|
+
self.mqtt.on_message = self._on_message
|
|
156
|
+
self.mqtt.on_subscribe = self._on_ack
|
|
157
|
+
self.mqtt.on_unsubscribe = self._on_ack
|
|
158
|
+
|
|
159
|
+
self.mqtt.connect_timeout = timeout
|
|
160
|
+
try:
|
|
161
|
+
self.mqtt.connect(host, port or default_port, keepalive)
|
|
162
|
+
except OSError as err:
|
|
163
|
+
raise ConnectionError(f"Could not connect to {self.url}: {err}") from err
|
|
164
|
+
self.mqtt.loop_start()
|
|
165
|
+
if not self._connected.wait(timeout):
|
|
166
|
+
self._stop()
|
|
167
|
+
raise TimeoutError(f"Timed out connecting to {self.url}.")
|
|
168
|
+
if self._connect_result:
|
|
169
|
+
self._stop()
|
|
170
|
+
raise ConnectionError(f"Broker refused the connection: {self._connect_result}")
|
|
171
|
+
|
|
172
|
+
# paho callbacks -------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _on_connect(self, client, userdata, flags, reason_code, properties) -> None:
|
|
175
|
+
if reason_code.is_failure:
|
|
176
|
+
self._connect_result = str(reason_code)
|
|
177
|
+
self._connected.set()
|
|
178
|
+
|
|
179
|
+
def _on_ack(self, client, userdata, mid, reason_codes, properties) -> None:
|
|
180
|
+
with self._lock:
|
|
181
|
+
waiter = self._acks.get(mid)
|
|
182
|
+
if waiter is not None:
|
|
183
|
+
waiter.put(reason_codes)
|
|
184
|
+
|
|
185
|
+
def _on_message(self, client, userdata, msg) -> None:
|
|
186
|
+
message = Message(msg.topic, bytes(msg.payload), msg.qos, bool(msg.retain))
|
|
187
|
+
with self._lock:
|
|
188
|
+
handlers = list(self._handlers.values())
|
|
189
|
+
for filters, handler in handlers:
|
|
190
|
+
if any(topic_matches(f, msg.topic) for f in filters):
|
|
191
|
+
handler(message)
|
|
192
|
+
|
|
193
|
+
# public API -----------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_connected(self) -> bool:
|
|
197
|
+
return self.mqtt.is_connected()
|
|
198
|
+
|
|
199
|
+
def publish(self, topic: str, payload: Payload = None, qos: int = 0, retain: bool = False) -> None:
|
|
200
|
+
"""Publish a message. ``dict`` and ``list`` payloads are sent as JSON.
|
|
201
|
+
Publish ``""`` with ``retain=True`` to clear a retained message."""
|
|
202
|
+
info = self.mqtt.publish(topic, _encode(payload), qos=qos, retain=retain)
|
|
203
|
+
if info.rc != mqtt.MQTT_ERR_SUCCESS:
|
|
204
|
+
raise ConnectionError(f"Publish to {topic!r} failed: {mqtt.error_string(info.rc)}")
|
|
205
|
+
if qos > 0:
|
|
206
|
+
info.wait_for_publish(self.timeout)
|
|
207
|
+
if not info.is_published():
|
|
208
|
+
raise TimeoutError(f"Publish to {topic!r} was not acknowledged within {self.timeout} s.")
|
|
209
|
+
|
|
210
|
+
def subscribe(
|
|
211
|
+
self, topics: Union[str, Iterable[str]], callback: Callable[[Message], None], qos: int = 0
|
|
212
|
+
) -> Callable[[], None]:
|
|
213
|
+
"""Call ``callback(message)`` for every message on the topic filter(s).
|
|
214
|
+
Returns a function that unsubscribes. The callback is registered before the broker
|
|
215
|
+
acknowledges the subscription, so retained messages are not missed."""
|
|
216
|
+
filters = [topics] if isinstance(topics, str) else list(topics)
|
|
217
|
+
if not filters:
|
|
218
|
+
raise ValueError("At least one topic filter is required.")
|
|
219
|
+
with self._lock:
|
|
220
|
+
handler_id = self._next_handler
|
|
221
|
+
self._next_handler += 1
|
|
222
|
+
self._handlers[handler_id] = (filters, callback)
|
|
223
|
+
try:
|
|
224
|
+
codes = self._request(lambda: self.mqtt.subscribe([(f, qos) for f in filters]))
|
|
225
|
+
rejected = [f for f, c in zip(filters, codes) if getattr(c, "is_failure", False) or c == 128]
|
|
226
|
+
if rejected:
|
|
227
|
+
raise ValueError(f"Subscription rejected: {', '.join(rejected)}")
|
|
228
|
+
except Exception:
|
|
229
|
+
with self._lock:
|
|
230
|
+
self._handlers.pop(handler_id, None)
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
def unsubscribe() -> None:
|
|
234
|
+
with self._lock:
|
|
235
|
+
if self._handlers.pop(handler_id, None) is None:
|
|
236
|
+
return
|
|
237
|
+
still_used = {f for fs, _ in self._handlers.values() for f in fs}
|
|
238
|
+
unused = [f for f in filters if f not in still_used]
|
|
239
|
+
if unused and self.is_connected:
|
|
240
|
+
self._request(lambda: self.mqtt.unsubscribe(unused))
|
|
241
|
+
|
|
242
|
+
return unsubscribe
|
|
243
|
+
|
|
244
|
+
def messages(
|
|
245
|
+
self, topics: Union[str, Iterable[str]], qos: int = 0, count: int = 0, timeout: Optional[float] = None
|
|
246
|
+
) -> Iterator[Message]:
|
|
247
|
+
"""Iterate over messages on the topic filter(s). Stops after ``count`` messages or
|
|
248
|
+
``timeout`` seconds without a message (both optional)."""
|
|
249
|
+
inbox: "queue.Queue[Message]" = queue.Queue()
|
|
250
|
+
unsubscribe = self.subscribe(topics, inbox.put, qos)
|
|
251
|
+
try:
|
|
252
|
+
received = 0
|
|
253
|
+
while not count or received < count:
|
|
254
|
+
try:
|
|
255
|
+
yield inbox.get(timeout=timeout)
|
|
256
|
+
except queue.Empty:
|
|
257
|
+
return
|
|
258
|
+
received += 1
|
|
259
|
+
finally:
|
|
260
|
+
unsubscribe()
|
|
261
|
+
|
|
262
|
+
def once(self, topic: str, timeout: float = 30.0, qos: int = 0) -> Message:
|
|
263
|
+
"""Wait for the next message on a topic filter. Raises ``TimeoutError``."""
|
|
264
|
+
for message in self.messages(topic, qos=qos, count=1, timeout=timeout):
|
|
265
|
+
return message
|
|
266
|
+
raise TimeoutError(f"No message on {topic!r} within {timeout} s.")
|
|
267
|
+
|
|
268
|
+
def close(self) -> None:
|
|
269
|
+
"""Disconnect and stop the network thread."""
|
|
270
|
+
self._stop()
|
|
271
|
+
|
|
272
|
+
def _request(self, send: Callable[[], Tuple[int, Optional[int]]]) -> Any:
|
|
273
|
+
waiter: "queue.Queue[Any]" = queue.Queue()
|
|
274
|
+
with self._lock:
|
|
275
|
+
rc, mid = send()
|
|
276
|
+
if rc != mqtt.MQTT_ERR_SUCCESS or mid is None:
|
|
277
|
+
raise ConnectionError(f"Request failed: {mqtt.error_string(rc)}")
|
|
278
|
+
self._acks[mid] = waiter
|
|
279
|
+
try:
|
|
280
|
+
return waiter.get(timeout=self.timeout)
|
|
281
|
+
except queue.Empty:
|
|
282
|
+
raise TimeoutError("The broker did not acknowledge the request.") from None
|
|
283
|
+
finally:
|
|
284
|
+
with self._lock:
|
|
285
|
+
self._acks.pop(mid, None)
|
|
286
|
+
|
|
287
|
+
def _stop(self) -> None:
|
|
288
|
+
try:
|
|
289
|
+
if self.mqtt.is_connected():
|
|
290
|
+
self.mqtt.disconnect()
|
|
291
|
+
finally:
|
|
292
|
+
self.mqtt.loop_stop()
|
|
293
|
+
|
|
294
|
+
def __enter__(self) -> "Client":
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def __exit__(self, *exc: object) -> None:
|
|
298
|
+
self.close()
|
|
299
|
+
|
|
300
|
+
def __repr__(self) -> str:
|
|
301
|
+
return f"<connectmq.Client {self.url} client_id={self.client_id!r} connected={self.is_connected}>"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def connect(**options: Any) -> Client:
|
|
305
|
+
"""Connect to broker.connectmq.com (or another broker) and return a :class:`Client`.
|
|
306
|
+
Options: transport, host, port, client_id, username, password, protocol, keepalive, timeout."""
|
|
307
|
+
return Client(**options)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def publish(topic: str, payload: Payload = None, qos: int = 0, retain: bool = False, **options: Any) -> None:
|
|
311
|
+
"""Publish one message using a short-lived connection."""
|
|
312
|
+
with Client(**options) as client:
|
|
313
|
+
client.publish(topic, payload, qos=qos, retain=retain)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def subscribe(
|
|
317
|
+
topics: Union[str, Iterable[str]], qos: int = 0, count: int = 0, timeout: Optional[float] = None, **options: Any
|
|
318
|
+
) -> Iterator[Message]:
|
|
319
|
+
"""Iterate over messages using its own connection, which closes when iteration ends.
|
|
320
|
+
|
|
321
|
+
>>> for msg in subscribe("connectmq/demo/7f3a91/#", count=3):
|
|
322
|
+
... print(msg.topic, msg.payload)
|
|
323
|
+
"""
|
|
324
|
+
with Client(**options) as client:
|
|
325
|
+
yield from client.messages(topics, qos=qos, count=count, timeout=timeout)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def once(topic: str, timeout: float = 30.0, qos: int = 0, **options: Any) -> Message:
|
|
329
|
+
"""Wait for the next message on a topic filter using a short-lived connection."""
|
|
330
|
+
with Client(**options) as client:
|
|
331
|
+
return client.once(topic, timeout=timeout, qos=qos)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test(transport: str = "tls", **options: Any) -> TestResult:
|
|
335
|
+
"""Connect, subscribe, publish and receive a test message. Never raises; check ``ok``."""
|
|
336
|
+
url = broker_url(transport, options.get("host", BROKER_HOST), options.get("port"))
|
|
337
|
+
started = time.perf_counter()
|
|
338
|
+
connect_ms = None
|
|
339
|
+
try:
|
|
340
|
+
with Client(transport=transport, **options) as client:
|
|
341
|
+
connect_ms = (time.perf_counter() - started) * 1000
|
|
342
|
+
topic = f"{topic_prefix()}/ping"
|
|
343
|
+
token = uuid.uuid4().hex
|
|
344
|
+
received = threading.Event()
|
|
345
|
+
client.subscribe(topic, lambda m: m.payload == token and received.set())
|
|
346
|
+
sent = time.perf_counter()
|
|
347
|
+
client.publish(topic, token)
|
|
348
|
+
if not received.wait(10):
|
|
349
|
+
raise TimeoutError("Round-trip timed out.")
|
|
350
|
+
return TestResult(url, True, connect_ms, (time.perf_counter() - sent) * 1000, None)
|
|
351
|
+
except Exception as err: # noqa: BLE001 - reported in the result
|
|
352
|
+
return TestResult(url, False, connect_ms, None, str(err))
|
|
File without changes
|