amgi-paho-mqtt 0.18.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.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.3
2
+ Name: amgi-paho-mqtt
3
+ Version: 0.18.0
4
+ Summary: Add your description here
5
+ Author: jack.burridge
6
+ Author-email: jack.burridge <jack.burridge@mail.com>
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: amgi-types==0.18.0
14
+ Requires-Dist: paho-mqtt>=2.1.0
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # amgi-paho-mqtt
19
+
20
+ :construction: This package is currently under development :construction:
21
+
22
+ AMGI will be here very soon
23
+
24
+ ## Installation
25
+
26
+ ```
27
+ pip install amgi-paho-mqtt==0.18.0
28
+ ```
29
+
30
+ ## Contact
31
+
32
+ For questions or suggestions, please contact [jack.burridge@mail.com](mailto:jack.burridge@mail.com).
33
+
34
+ ## License
35
+
36
+ Copyright 2025 AMGI
@@ -0,0 +1,19 @@
1
+ # amgi-paho-mqtt
2
+
3
+ :construction: This package is currently under development :construction:
4
+
5
+ AMGI will be here very soon
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ pip install amgi-paho-mqtt==0.18.0
11
+ ```
12
+
13
+ ## Contact
14
+
15
+ For questions or suggestions, please contact [jack.burridge@mail.com](mailto:jack.burridge@mail.com).
16
+
17
+ ## License
18
+
19
+ Copyright 2025 AMGI
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = [ "uv-build>=0.8.14,<0.9.0" ]
4
+
5
+ [project]
6
+ name = "amgi-paho-mqtt"
7
+ version = "0.18.0"
8
+ description = "Add your description here"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "jack.burridge", email = "jack.burridge@mail.com" },
12
+ ]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "amgi-types==0.18.0",
24
+ "paho-mqtt>=2.1.0",
25
+ ]
26
+
27
+ entry-points.amgi_server.amgi-paho-mqtt = "amgi_paho_mqtt:run"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "test-utils",
32
+ "testcontainers[mqtt]>=4.13.0",
33
+ ]
34
+
35
+ [tool.uv.sources]
36
+ amgi-types = { workspace = true }
37
+ test-utils = { workspace = true }
@@ -0,0 +1,183 @@
1
+ import asyncio
2
+ from asyncio import Event
3
+ from asyncio import Task
4
+ from socket import SO_SNDBUF
5
+ from socket import SOL_SOCKET
6
+ from typing import Any
7
+ from typing import Optional
8
+ from typing import TYPE_CHECKING
9
+
10
+ from amgi_common import Lifespan
11
+ from amgi_types import AMGIApplication
12
+ from amgi_types import AMGISendEvent
13
+ from amgi_types import MessageReceiveEvent
14
+ from amgi_types import MessageScope
15
+ from paho.mqtt.client import Client
16
+ from paho.mqtt.client import ConnectFlags
17
+ from paho.mqtt.client import DisconnectFlags
18
+ from paho.mqtt.client import MQTT_ERR_SUCCESS
19
+ from paho.mqtt.client import MQTTMessage
20
+ from paho.mqtt.enums import CallbackAPIVersion
21
+ from paho.mqtt.properties import Properties
22
+ from paho.mqtt.reasoncodes import ReasonCode
23
+
24
+ if TYPE_CHECKING:
25
+ from paho.mqtt.client import SocketLike
26
+
27
+
28
+ def run(
29
+ app: AMGIApplication,
30
+ topic: str,
31
+ host: str = "localhost",
32
+ port: int = 1883,
33
+ client_id: Optional[str] = None,
34
+ ) -> None:
35
+ asyncio.run(_run(app, topic, host, port, client_id))
36
+
37
+
38
+ async def _run(
39
+ app: AMGIApplication, topic: str, host: str, port: int, client_id: Optional[str]
40
+ ) -> None:
41
+ server = Server(app, topic, host, port, client_id)
42
+ await server.serve()
43
+
44
+
45
+ class _MessageReceive:
46
+ def __init__(self, message: MQTTMessage) -> None:
47
+ self._message = message
48
+
49
+ async def __call__(self) -> MessageReceiveEvent:
50
+ return {
51
+ "type": "message.receive",
52
+ "id": str(self._message.mid),
53
+ "headers": [],
54
+ "payload": self._message.payload,
55
+ }
56
+
57
+
58
+ class _MessageSend:
59
+ def __init__(self, client: Client) -> None:
60
+ self._client = client
61
+
62
+ async def __call__(self, message: AMGISendEvent) -> None:
63
+ if message["type"] == "message.send":
64
+ self._client.publish(message["address"], message["payload"])
65
+
66
+
67
+ class Server:
68
+ def __init__(
69
+ self,
70
+ app: AMGIApplication,
71
+ topic: str,
72
+ host: str,
73
+ port: int,
74
+ client_id: Optional[str],
75
+ ) -> None:
76
+ self._app = app
77
+ self._topic = topic
78
+ self._host = host
79
+ self._port = port
80
+ self._loop = asyncio.get_running_loop()
81
+
82
+ self._client = Client(CallbackAPIVersion.VERSION2, client_id=client_id)
83
+ self._client.on_connect = self._on_connect
84
+ self._client.on_message = self._on_message
85
+ self._client.on_disconnect = self._on_disconnect
86
+ self._client.on_socket_open = self._on_socket_open
87
+ self._client.on_socket_close = self._on_socket_close
88
+ self._client.on_socket_register_write = self._on_socket_register_write
89
+ self._client.on_socket_unregister_write = self._on_socket_unregister_write
90
+ self._client.on_subscribe = self._on_subscribe
91
+
92
+ self._subscribe_event = Event()
93
+ self._disconnected_event = Event()
94
+ self._stop_event = Event()
95
+ self._tasks = set[Task[None]]()
96
+
97
+ def _on_connect(
98
+ self,
99
+ client: Client,
100
+ userdata: Any,
101
+ connect_flags: ConnectFlags,
102
+ reason_code: ReasonCode,
103
+ properties: Optional[Properties],
104
+ ) -> None:
105
+ client.subscribe(self._topic)
106
+
107
+ def _on_message(self, client: Client, userdata: Any, message: MQTTMessage) -> None:
108
+ task = self._loop.create_task(self._handle_message(message))
109
+ self._tasks.add(task)
110
+ task.add_done_callback(self._tasks.discard)
111
+
112
+ async def _handle_message(self, message: MQTTMessage) -> None:
113
+ scope: MessageScope = {
114
+ "type": "message",
115
+ "amgi": {"version": "1.0", "spec_version": "1.0"},
116
+ "address": message.topic,
117
+ }
118
+ await self._app(scope, _MessageReceive(message), _MessageSend(self._client))
119
+
120
+ def _on_disconnect(
121
+ self,
122
+ client: Client,
123
+ userdata: Any,
124
+ disconnect_flags: DisconnectFlags,
125
+ reason_code: ReasonCode,
126
+ properties: Optional[Properties],
127
+ ) -> None:
128
+ self._disconnected_event.set()
129
+
130
+ def _on_socket_open(
131
+ self, client: Client, userdata: Any, socket: "SocketLike"
132
+ ) -> None:
133
+ self._loop.add_reader(socket, client.loop_read)
134
+ self._misc_task = self._loop.create_task(self._misc_loop())
135
+
136
+ def _on_socket_close(
137
+ self, client: Client, userdata: Any, socket: "SocketLike"
138
+ ) -> None:
139
+ self._loop.remove_reader(socket)
140
+ self._misc_task.cancel()
141
+
142
+ def _on_socket_register_write(
143
+ self, client: Client, userdata: Any, socket: "SocketLike"
144
+ ) -> None:
145
+ self._loop.add_writer(socket, client.loop_write)
146
+
147
+ def _on_socket_unregister_write(
148
+ self, client: Client, userdata: Any, socket: "SocketLike"
149
+ ) -> None:
150
+ self._loop.remove_writer(socket)
151
+
152
+ def _on_subscribe(
153
+ self,
154
+ client: Client,
155
+ userdata: Any,
156
+ mid: int,
157
+ reason_code_list: list[ReasonCode],
158
+ properties: Optional[Properties],
159
+ ) -> None:
160
+ self._subscribe_event.set()
161
+
162
+ async def _misc_loop(self) -> None:
163
+ while self._client.loop_misc() == MQTT_ERR_SUCCESS:
164
+ try:
165
+ await asyncio.sleep(1)
166
+ except asyncio.CancelledError:
167
+ break
168
+
169
+ async def serve(self) -> None:
170
+ self._client.connect(self._host, self._port, 60)
171
+ self._client.socket().setsockopt(SOL_SOCKET, SO_SNDBUF, 2048)
172
+
173
+ await self._subscribe_event.wait()
174
+
175
+ async with Lifespan(self._app) as state:
176
+ await self._stop_event.wait()
177
+ self._client.unsubscribe(self._topic)
178
+ await asyncio.gather(*self._tasks)
179
+ self._client.disconnect()
180
+ await self._disconnected_event.wait()
181
+
182
+ def stop(self) -> None:
183
+ self._stop_event.set()
File without changes