mip-client-python 1.0.0__py3-none-any.whl

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.
mip_client/__init__.py ADDED
@@ -0,0 +1,578 @@
1
+ """
2
+ MIP Client - Python client for the MIP (MSIP) protocol.
3
+ Handles connections, events, errors, and auto-reconnection.
4
+ """
5
+
6
+ import asyncio
7
+ import struct
8
+ import time
9
+ from dataclasses import dataclass
10
+ from enum import IntEnum, IntFlag
11
+ from typing import Callable, Optional, Any
12
+ import logging
13
+
14
+ __version__ = "1.0.0"
15
+ __all__ = [
16
+ "MIPClient",
17
+ "FrameType",
18
+ "Flags",
19
+ "FrameHeader",
20
+ "MIPMessage",
21
+ "MIPError",
22
+ "MIPClientOptions",
23
+ "create_client",
24
+ "get_frame_type_name",
25
+ "MAGIC",
26
+ "VERSION",
27
+ "HEADER_SIZE",
28
+ ]
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ============================================================================
33
+ # Constants
34
+ # ============================================================================
35
+
36
+ MAGIC = 0x4D534950 # "MSIP"
37
+ VERSION = 1
38
+ HEADER_SIZE = 24
39
+ MSG_KIND_EVENT = 0x0001
40
+
41
+
42
+ class FrameType(IntEnum):
43
+ """Frame types for the MIP protocol"""
44
+ HELLO = 0x0001
45
+ SUBSCRIBE = 0x0002
46
+ UNSUBSCRIBE = 0x0003
47
+ PUBLISH = 0x0004
48
+ EVENT = 0x0005
49
+ ACK = 0x0006
50
+ ERROR = 0x0007
51
+ PING = 0x0008
52
+ PONG = 0x0009
53
+ CLOSE = 0x000A
54
+
55
+
56
+ class Flags(IntFlag):
57
+ """Flags for frame options"""
58
+ NONE = 0b0000_0000
59
+ ACK_REQUIRED = 0b0000_0001
60
+ COMPRESSED = 0b0000_0010
61
+ URGENT = 0b0000_0100
62
+
63
+
64
+ # ============================================================================
65
+ # Types & Interfaces
66
+ # ============================================================================
67
+
68
+ @dataclass
69
+ class MIPClientOptions:
70
+ """Configuration options for the MIP client"""
71
+ host: str = "127.0.0.1"
72
+ port: int = 9000
73
+ auto_reconnect: bool = True
74
+ reconnect_delay: float = 3.0
75
+ max_reconnect_attempts: int = 10
76
+ ping_interval: float = 0.0
77
+
78
+
79
+ @dataclass
80
+ class FrameHeader:
81
+ """Parsed frame header"""
82
+ magic: int
83
+ version: int
84
+ flags: int
85
+ frame_type: FrameType
86
+ msg_kind: int
87
+ payload_length: int
88
+ msg_id: int
89
+
90
+
91
+ @dataclass
92
+ class MIPMessage:
93
+ """Received message event"""
94
+ header: FrameHeader
95
+ topic: str
96
+ message: str
97
+
98
+
99
+ @dataclass
100
+ class MIPError(Exception):
101
+ """Error details"""
102
+ message: str
103
+ code: Optional[int] = None
104
+ raw: Optional[bytes] = None
105
+
106
+ def __str__(self) -> str:
107
+ return self.message
108
+
109
+
110
+ # ============================================================================
111
+ # Event Callback Types
112
+ # ============================================================================
113
+
114
+ OnConnect = Callable[[], None]
115
+ OnDisconnect = Callable[[], None]
116
+ OnReconnecting = Callable[[int], None]
117
+ OnMessage = Callable[[MIPMessage], None]
118
+ OnEvent = Callable[[MIPMessage], None]
119
+ OnAck = Callable[[int], None]
120
+ OnPong = Callable[[], None]
121
+ OnError = Callable[[MIPError], None]
122
+ OnFrame = Callable[[FrameHeader, bytes], None]
123
+
124
+
125
+ # ============================================================================
126
+ # MIP Client Class
127
+ # ============================================================================
128
+
129
+ class MIPClient:
130
+ """Async MIP protocol client with auto-reconnection support"""
131
+
132
+ def __init__(self, options: Optional[MIPClientOptions] = None, **kwargs: Any):
133
+ """
134
+ Initialize MIP client.
135
+
136
+ Args:
137
+ options: MIPClientOptions instance
138
+ **kwargs: Alternative way to pass options (host, port, auto_reconnect, etc...)
139
+ """
140
+ if options is None:
141
+ options = MIPClientOptions(**kwargs)
142
+
143
+ self._options = options
144
+ self._reader: Optional[asyncio.StreamReader] = None
145
+ self._writer: Optional[asyncio.StreamWriter] = None
146
+ self._buffer: bytes = b""
147
+ self._connected: bool = False
148
+ self._reconnect_attempts: int = 0
149
+ self._reconnect_task: Optional[asyncio.Task] = None
150
+ self._ping_task: Optional[asyncio.Task] = None
151
+ self._read_task: Optional[asyncio.Task] = None
152
+ self._close_task: Optional[asyncio.Task] = None
153
+ self._msg_id_counter: int = 0
154
+ self._running: bool = False
155
+
156
+ # Event callbacks
157
+ self._on_connect: list[OnConnect] = []
158
+ self._on_disconnect: list[OnDisconnect] = []
159
+ self._on_reconnecting: list[OnReconnecting] = []
160
+ self._on_message: list[OnMessage] = []
161
+ self._on_event: list[OnEvent] = []
162
+ self._on_ack: list[OnAck] = []
163
+ self._on_pong: list[OnPong] = []
164
+ self._on_error: list[OnError] = []
165
+ self._on_frame: list[OnFrame] = []
166
+
167
+ # --------------------------------------------------------------------------
168
+ # Properties
169
+ # --------------------------------------------------------------------------
170
+
171
+ @property
172
+ def is_connected(self) -> bool:
173
+ """Check if the client is connected"""
174
+ return self._connected
175
+
176
+ @property
177
+ def options(self) -> MIPClientOptions:
178
+ """Get client options"""
179
+ return self._options
180
+
181
+ # --------------------------------------------------------------------------
182
+ # Event Registration
183
+ # --------------------------------------------------------------------------
184
+
185
+ def on_connect(self, callback: OnConnect) -> "MIPClient":
186
+ """Register connect event callback"""
187
+ self._on_connect.append(callback)
188
+ return self
189
+
190
+ def on_disconnect(self, callback: OnDisconnect) -> "MIPClient":
191
+ """Register disconnect event callback"""
192
+ self._on_disconnect.append(callback)
193
+ return self
194
+
195
+ def on_reconnecting(self, callback: OnReconnecting) -> "MIPClient":
196
+ """Register reconnecting event callback"""
197
+ self._on_reconnecting.append(callback)
198
+ return self
199
+
200
+ def on_message(self, callback: OnMessage) -> "MIPClient":
201
+ """Register message event callback"""
202
+ self._on_message.append(callback)
203
+ return self
204
+
205
+ def on_event(self, callback: OnEvent) -> "MIPClient":
206
+ """Register event callback"""
207
+ self._on_event.append(callback)
208
+ return self
209
+
210
+ def on_ack(self, callback: OnAck) -> "MIPClient":
211
+ """Register ACK event callback"""
212
+ self._on_ack.append(callback)
213
+ return self
214
+
215
+ def on_pong(self, callback: OnPong) -> "MIPClient":
216
+ """Register pong event callback"""
217
+ self._on_pong.append(callback)
218
+ return self
219
+
220
+ def on_error(self, callback: OnError) -> "MIPClient":
221
+ """Register error event callback"""
222
+ self._on_error.append(callback)
223
+ return self
224
+
225
+ def on_frame(self, callback: OnFrame) -> "MIPClient":
226
+ """Register raw frame event callback"""
227
+ self._on_frame.append(callback)
228
+ return self
229
+
230
+ # --------------------------------------------------------------------------
231
+ # Public API
232
+ # --------------------------------------------------------------------------
233
+
234
+ async def connect(self) -> None:
235
+ """Connect to the MIP server"""
236
+ if self._connected:
237
+ return
238
+
239
+ self._running = True
240
+
241
+ try:
242
+ self._reader, self._writer = await asyncio.open_connection(
243
+ self._options.host, self._options.port
244
+ )
245
+ self._connected = True
246
+ self._reconnect_attempts = 0
247
+ self._buffer = b""
248
+
249
+ # Start background tasks
250
+ self._read_task = asyncio.create_task(self._read_loop())
251
+ self._setup_ping_interval()
252
+
253
+ # Emit connect event
254
+ for callback in self._on_connect:
255
+ callback()
256
+
257
+ except Exception as e:
258
+ self._emit_error(MIPError(message=str(e)))
259
+ raise
260
+
261
+ async def disconnect(self) -> None:
262
+ """Disconnect from the server"""
263
+ self._options.auto_reconnect = False
264
+ self._running = False
265
+ await self._cleanup()
266
+
267
+ if self._writer:
268
+ await self._send_close()
269
+ self._writer.close()
270
+ try:
271
+ await self._writer.wait_closed()
272
+ except Exception:
273
+ pass
274
+ self._writer = None
275
+ self._reader = None
276
+
277
+ self._connected = False
278
+
279
+ def subscribe(self, topic: str, require_ack: bool = True) -> int:
280
+ """Subscribe to a topic"""
281
+ topic_bytes = topic.encode("utf-8")
282
+ flags = Flags.ACK_REQUIRED if require_ack else Flags.NONE
283
+ return self._send_frame(FrameType.SUBSCRIBE, topic_bytes, flags)
284
+
285
+ def unsubscribe(self, topic: str, require_ack: bool = True) -> int:
286
+ """Unsubscribe from a topic"""
287
+ topic_bytes = topic.encode("utf-8")
288
+ flags = Flags.ACK_REQUIRED if require_ack else Flags.NONE
289
+ return self._send_frame(FrameType.UNSUBSCRIBE, topic_bytes, flags)
290
+
291
+ def publish(
292
+ self,
293
+ topic: str,
294
+ message: "str | bytes",
295
+ flags: Flags = Flags.NONE
296
+ ) -> int:
297
+ """Publish a message to a topic"""
298
+ topic_bytes = topic.encode("utf-8")
299
+ if isinstance(message, bytes):
300
+ message_bytes = message
301
+ else:
302
+ message_bytes = str(message).encode("utf-8")
303
+
304
+ # Build payload: [topic_length (2 bytes)] [topic] [message]
305
+ payload = struct.pack(">H", len(topic_bytes)) + topic_bytes + message_bytes
306
+ return self._send_frame(FrameType.PUBLISH, payload, flags)
307
+
308
+ def ping(self) -> int:
309
+ """Send a ping to the server"""
310
+ return self._send_frame(FrameType.PING, b"")
311
+
312
+ def send_raw_frame(
313
+ self,
314
+ frame_type: FrameType,
315
+ payload: bytes,
316
+ flags: Flags = Flags.NONE
317
+ ) -> int:
318
+ """Send raw frame (advanced usage)"""
319
+ return self._send_frame(frame_type, payload, flags)
320
+
321
+ # --------------------------------------------------------------------------
322
+ # Private Methods
323
+ # --------------------------------------------------------------------------
324
+
325
+ def _generate_msg_id(self) -> int:
326
+ """Generate unique message ID"""
327
+ self._msg_id_counter += 1
328
+ return int(time.time() * 1000000) + self._msg_id_counter
329
+
330
+ def _build_header(
331
+ self,
332
+ frame_type: FrameType,
333
+ payload_length: int,
334
+ flags: int = 0,
335
+ msg_id: Optional[int] = None
336
+ ) -> bytes:
337
+ """Build frame header"""
338
+ if msg_id is None:
339
+ msg_id = self._generate_msg_id()
340
+
341
+ # Header format: magic(4) + version(1) + flags(1) + frame_type(2) +
342
+ # msg_kind(2) + reserved(2) + payload_length(4) + msg_id(8)
343
+ return struct.pack(
344
+ ">IBBHHHI Q",
345
+ MAGIC,
346
+ VERSION,
347
+ flags,
348
+ frame_type,
349
+ MSG_KIND_EVENT,
350
+ 0, # reserved
351
+ payload_length,
352
+ msg_id
353
+ )
354
+
355
+ def _send_frame(
356
+ self,
357
+ frame_type: FrameType,
358
+ payload: bytes,
359
+ flags: int = 0
360
+ ) -> int:
361
+ """Send a frame to the server"""
362
+ if not self._writer or not self._connected:
363
+ raise MIPError(message="Client is not connected")
364
+
365
+ msg_id = self._generate_msg_id()
366
+ header = self._build_header(frame_type, len(payload), flags, msg_id)
367
+ self._writer.write(header + payload)
368
+ return msg_id
369
+
370
+ async def _send_close(self) -> None:
371
+ """Send close frame"""
372
+ if self._writer and self._connected:
373
+ try:
374
+ header = self._build_header(FrameType.CLOSE, 0)
375
+ self._writer.write(header)
376
+ await self._writer.drain()
377
+ except Exception:
378
+ pass
379
+
380
+ async def _read_loop(self) -> None:
381
+ """Main read loop for incoming data"""
382
+ try:
383
+ while self._running and self._reader:
384
+ data = await self._reader.read(4096)
385
+ if not data:
386
+ break
387
+ self._handle_data(data)
388
+ except asyncio.CancelledError:
389
+ raise
390
+ except Exception as e:
391
+ if self._connected:
392
+ self._emit_error(MIPError(message=str(e)))
393
+ await self._handle_close()
394
+
395
+ def _handle_data(self, data: bytes) -> None:
396
+ """Handle incoming data"""
397
+ self._buffer += data
398
+
399
+ while len(self._buffer) >= HEADER_SIZE:
400
+ magic = struct.unpack(">I", self._buffer[:4])[0]
401
+
402
+ if magic != MAGIC:
403
+ self._emit_error(MIPError(message="Invalid magic number", code=magic))
404
+ if self._writer:
405
+ self._writer.close()
406
+ return
407
+
408
+ payload_length = struct.unpack(">I", self._buffer[12:16])[0]
409
+
410
+ if len(self._buffer) < HEADER_SIZE + payload_length:
411
+ return # Wait for more data
412
+
413
+ header = self._parse_header(self._buffer[:HEADER_SIZE])
414
+ payload = self._buffer[HEADER_SIZE:HEADER_SIZE + payload_length]
415
+
416
+ self._process_frame(header, payload)
417
+
418
+ self._buffer = self._buffer[HEADER_SIZE + payload_length:]
419
+
420
+ def _parse_header(self, buffer: bytes) -> FrameHeader:
421
+ """Parse frame header from bytes"""
422
+ magic, version, flags, frame_type, msg_kind, _, payload_length, msg_id = struct.unpack(
423
+ ">IBBHHHI Q", buffer
424
+ )
425
+ return FrameHeader(
426
+ magic=magic,
427
+ version=version,
428
+ flags=flags,
429
+ frame_type=FrameType(frame_type),
430
+ msg_kind=msg_kind,
431
+ payload_length=payload_length,
432
+ msg_id=msg_id
433
+ )
434
+
435
+ def _process_frame(self, header: FrameHeader, payload: bytes) -> None:
436
+ """Process received frame"""
437
+ # Emit raw frame event
438
+ for callback in self._on_frame:
439
+ callback(header, payload)
440
+
441
+ if header.frame_type in (FrameType.EVENT, FrameType.PUBLISH):
442
+ msg = self._parse_message(header, payload)
443
+ if msg:
444
+ if header.frame_type == FrameType.EVENT:
445
+ for callback in self._on_event:
446
+ callback(msg)
447
+ for callback in self._on_message:
448
+ callback(msg)
449
+
450
+ elif header.frame_type == FrameType.ACK:
451
+ for callback in self._on_ack:
452
+ callback(header.msg_id)
453
+
454
+ elif header.frame_type == FrameType.PONG:
455
+ for callback in self._on_pong:
456
+ callback()
457
+
458
+ elif header.frame_type == FrameType.ERROR:
459
+ error_msg = payload.decode("utf-8")
460
+ self._emit_error(MIPError(message=error_msg, raw=payload))
461
+
462
+ elif header.frame_type == FrameType.CLOSE:
463
+ self._close_task = asyncio.create_task(self._handle_close())
464
+
465
+ def _parse_message(self, header: FrameHeader, payload: bytes) -> Optional[MIPMessage]:
466
+ """Parse message from payload"""
467
+ if len(payload) < 2:
468
+ return None
469
+
470
+ topic_length = struct.unpack(">H", payload[:2])[0]
471
+
472
+ if len(payload) < 2 + topic_length:
473
+ return None
474
+
475
+ topic = payload[2:2 + topic_length].decode("utf-8")
476
+ message = payload[2 + topic_length:].decode("utf-8")
477
+
478
+ return MIPMessage(header=header, topic=topic, message=message)
479
+
480
+ async def _handle_close(self) -> None:
481
+ """Handle connection close"""
482
+ was_connected = self._connected
483
+ self._connected = False
484
+ await self._cleanup()
485
+
486
+ if was_connected:
487
+ for callback in self._on_disconnect:
488
+ callback()
489
+
490
+ if self._options.auto_reconnect and self._running:
491
+ self._schedule_reconnect()
492
+
493
+ def _schedule_reconnect(self) -> None:
494
+ """Schedule reconnection attempt"""
495
+ if self._reconnect_task and not self._reconnect_task.done():
496
+ return
497
+
498
+ max_attempts = self._options.max_reconnect_attempts
499
+ if max_attempts > 0 and self._reconnect_attempts >= max_attempts:
500
+ self._emit_error(MIPError(
501
+ message=f"Max reconnection attempts ({max_attempts}) reached"
502
+ ))
503
+ return
504
+
505
+ self._reconnect_attempts += 1
506
+ for callback in self._on_reconnecting:
507
+ callback(self._reconnect_attempts)
508
+
509
+ async def reconnect() -> None:
510
+ await asyncio.sleep(self._options.reconnect_delay)
511
+ try:
512
+ await self.connect()
513
+ except Exception:
514
+ if self._options.auto_reconnect and self._running:
515
+ self._schedule_reconnect()
516
+
517
+ self._reconnect_task = asyncio.create_task(reconnect())
518
+
519
+ def _setup_ping_interval(self) -> None:
520
+ """Setup automatic ping interval"""
521
+ if self._options.ping_interval > 0:
522
+ async def ping_loop():
523
+ while self._connected and self._running:
524
+ await asyncio.sleep(self._options.ping_interval)
525
+ if self._connected:
526
+ self.ping()
527
+
528
+ self._ping_task = asyncio.create_task(ping_loop())
529
+
530
+ async def _cleanup(self) -> None:
531
+ """Cleanup background tasks"""
532
+ if self._ping_task:
533
+ self._ping_task.cancel()
534
+ try:
535
+ await self._ping_task
536
+ except asyncio.CancelledError:
537
+ raise
538
+ self._ping_task = None
539
+
540
+ if self._read_task:
541
+ self._read_task.cancel()
542
+ try:
543
+ await self._read_task
544
+ except asyncio.CancelledError:
545
+ raise
546
+ self._read_task = None
547
+
548
+ if self._reconnect_task:
549
+ self._reconnect_task.cancel()
550
+ try:
551
+ await self._reconnect_task
552
+ except asyncio.CancelledError:
553
+ raise
554
+ self._reconnect_task = None
555
+
556
+ def _emit_error(self, error: MIPError) -> None:
557
+ """Emit error to callbacks"""
558
+ for callback in self._on_error:
559
+ callback(error)
560
+
561
+
562
+ # ============================================================================
563
+ # Utility Functions
564
+ # ============================================================================
565
+
566
+ def get_frame_type_name(frame_type: FrameType) -> str:
567
+ """Get the name of a frame type"""
568
+ return frame_type.name
569
+
570
+
571
+ def create_client(
572
+ host: str = "127.0.0.1",
573
+ port: int = 9000,
574
+ **kwargs: Any
575
+ ) -> MIPClient:
576
+ """Create a client with default options"""
577
+ options = MIPClientOptions(host=host, port=port, **kwargs)
578
+ return MIPClient(options)
mip_client/py.typed ADDED
File without changes
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: mip-client-python
3
+ Version: 1.0.0
4
+ Summary: Python client for the MIP (MSIP) protocol - handles connections, events, errors, and auto-reconnection
5
+ Project-URL: Homepage, https://github.com/DoctorPok42/MIP-Clients
6
+ Project-URL: Repository, https://github.com/DoctorPok42/MIP-Clients/tree/main/mip-client-python
7
+ Project-URL: Documentation, https://github.com/DoctorPok42/MIP-Clients/tree/main/mip-client-python#readme
8
+ Author-email: DoctorPok42 <pokdoctor@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: async,client,mip,msip,networking,protocol
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Networking
24
+ Requires-Python: >=3.8
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # MIP-Client-python
31
+
32
+ Python async client for the MIP (MSIP) protocol - handles connections, events, errors, and auto-reconnection.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install mip-client-python
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Basic Connection
43
+
44
+ ```python
45
+ import asyncio
46
+ from mip_client import MIPClient
47
+
48
+ async def main():
49
+ client = MIPClient(host="127.0.0.1", port=9000)
50
+
51
+ # Register event callbacks
52
+ client.on_connect(lambda: print("Connected to server"))
53
+ client.on_disconnect(lambda: print("Disconnected"))
54
+ client.on_error(lambda err: print(f"Error: {err.message}"))
55
+ client.on_message(lambda msg: print(f"[{msg.topic}] {msg.message}"))
56
+
57
+ # Connect
58
+ await client.connect()
59
+
60
+ # Keep running
61
+ await asyncio.sleep(60)
62
+ await client.disconnect()
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ### Subscribe / Publish
68
+
69
+ ```python
70
+ # Subscribe to a topic
71
+ client.subscribe("my-topic")
72
+
73
+ # Listen for messages
74
+ def handle_message(msg):
75
+ print(f"Topic: {msg.topic}")
76
+ print(f"Message: {msg.message}")
77
+
78
+ client.on_message(handle_message)
79
+
80
+ # Publish a message
81
+ client.publish("my-topic", "Hello World!")
82
+ ```
83
+
84
+ ### Advanced Options
85
+
86
+ ```python
87
+ from mip_client import MIPClient, Flags
88
+
89
+ client = MIPClient(
90
+ host="127.0.0.1",
91
+ port=9000,
92
+ auto_reconnect=True, # Auto-reconnect (default: True)
93
+ reconnect_delay=3.0, # Delay between reconnections in seconds (default: 3.0)
94
+ max_reconnect_attempts=10, # Max attempts (0 = infinite)
95
+ ping_interval=5.0, # Ping interval in seconds (0 = disabled)
96
+ )
97
+
98
+ # Publish with flags
99
+ client.publish("urgent-topic", "Important!", Flags.URGENT | Flags.ACK_REQUIRED)
100
+
101
+ # Listen for ACKs
102
+ client.on_ack(lambda msg_id: print(f"ACK received for: {msg_id}"))
103
+
104
+ # Manual ping
105
+ client.ping()
106
+ client.on_pong(lambda: print("Pong received!"))
107
+ ```
108
+
109
+ ### Auto-Reconnection
110
+
111
+ ```python
112
+ client.on_reconnecting(lambda attempt: print(f"Reconnection attempt #{attempt}..."))
113
+
114
+ def on_connect():
115
+ # Re-subscribe after reconnection
116
+ client.subscribe("my-topic")
117
+
118
+ client.on_connect(on_connect)
119
+ ```
120
+
121
+ ### Using create_client helper
122
+
123
+ ```python
124
+ from mip_client import create_client
125
+
126
+ client = create_client("127.0.0.1", 9000, auto_reconnect=True)
127
+ ```
128
+
129
+ ## API Reference
130
+
131
+ ### MIPClient
132
+
133
+ - `connect()` - Connect to the server (async)
134
+ - `disconnect()` - Disconnect from the server (async)
135
+ - `subscribe(topic, require_ack=True)` - Subscribe to a topic
136
+ - `unsubscribe(topic, require_ack=True)` - Unsubscribe from a topic
137
+ - `publish(topic, message, flags=Flags.NONE)` - Publish a message
138
+ - `ping()` - Send a ping to the server
139
+
140
+ ### Events
141
+
142
+ - `on_connect(callback)` - Called when connected
143
+ - `on_disconnect(callback)` - Called when disconnected
144
+ - `on_reconnecting(callback)` - Called when reconnecting (receives attempt number)
145
+ - `on_message(callback)` - Called when a message is received
146
+ - `on_event(callback)` - Called when an event is received
147
+ - `on_ack(callback)` - Called when an ACK is received
148
+ - `on_pong(callback)` - Called when a pong is received
149
+ - `on_error(callback)` - Called when an error occurs
150
+
151
+ ### Flags
152
+
153
+ - `Flags.NONE` - No flags
154
+ - `Flags.ACK_REQUIRED` - Request acknowledgment
155
+ - `Flags.COMPRESSED` - Compressed payload
156
+ - `Flags.URGENT` - Urgent message
@@ -0,0 +1,6 @@
1
+ mip_client/__init__.py,sha256=a_ET2iUamLWQ4WyJ2GdPoeTh58GCdpcXZWaHhTMnGM4,18776
2
+ mip_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ mip_client_python-1.0.0.dist-info/METADATA,sha256=go1fTZUUiqEoBSlFIIuCyd5sSpr9m3Xj188IodNBRBQ,4540
4
+ mip_client_python-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ mip_client_python-1.0.0.dist-info/licenses/LICENSE,sha256=tK6bDr9ftFIdQ6J3OnDj8-kLxbIgpig6wp4RpyEc9lA,1083
6
+ mip_client_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rémi
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.