messagex-client 0.1.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.
- messagex/__init__.py +32 -0
- messagex/client.py +531 -0
- messagex/errors.py +25 -0
- messagex/events.py +58 -0
- messagex/frame.py +57 -0
- messagex/py.typed +0 -0
- messagex/types.py +82 -0
- messagex_client-0.1.0.dist-info/METADATA +664 -0
- messagex_client-0.1.0.dist-info/RECORD +12 -0
- messagex_client-0.1.0.dist-info/WHEEL +5 -0
- messagex_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- messagex_client-0.1.0.dist-info/top_level.txt +1 -0
messagex/frame.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import struct
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
|
|
7
|
+
HEADER_SIZE = 4
|
|
8
|
+
DEFAULT_MAX_FRAME_SIZE = 1024 * 1024
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encode_frame(message: Any, max_frame_size: int = DEFAULT_MAX_FRAME_SIZE) -> bytes:
|
|
12
|
+
"""Encode ``message`` as JSON with a 4-byte big-endian length prefix."""
|
|
13
|
+
try:
|
|
14
|
+
text = json.dumps(message, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
|
|
15
|
+
except (TypeError, ValueError) as exc:
|
|
16
|
+
raise TypeError("MessageX protocol message must be JSON serializable") from exc
|
|
17
|
+
|
|
18
|
+
body = text.encode("utf-8")
|
|
19
|
+
if len(body) == 0 or len(body) > max_frame_size:
|
|
20
|
+
raise ValueError(f"MessageX frame size must be between 1 and {max_frame_size} bytes")
|
|
21
|
+
return struct.pack(">I", len(body)) + body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FrameDecoder:
|
|
25
|
+
"""Incremental decoder that turns a byte stream into frame bodies."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, max_frame_size: int = DEFAULT_MAX_FRAME_SIZE) -> None:
|
|
28
|
+
self._max_frame_size = max_frame_size
|
|
29
|
+
self._buffer = bytearray()
|
|
30
|
+
|
|
31
|
+
def push(self, chunk: bytes) -> List[bytes]:
|
|
32
|
+
if not chunk:
|
|
33
|
+
return []
|
|
34
|
+
self._buffer.extend(chunk)
|
|
35
|
+
frames: List[bytes] = []
|
|
36
|
+
|
|
37
|
+
while len(self._buffer) >= HEADER_SIZE:
|
|
38
|
+
(length,) = struct.unpack_from(">I", self._buffer, 0)
|
|
39
|
+
if length < 1 or length > self._max_frame_size:
|
|
40
|
+
self.reset()
|
|
41
|
+
raise ValueError(f"Invalid MessageX frame length: {length}")
|
|
42
|
+
|
|
43
|
+
frame_length = HEADER_SIZE + length
|
|
44
|
+
if len(self._buffer) < frame_length:
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
frames.append(bytes(self._buffer[HEADER_SIZE:frame_length]))
|
|
48
|
+
del self._buffer[:frame_length]
|
|
49
|
+
|
|
50
|
+
return frames
|
|
51
|
+
|
|
52
|
+
def reset(self) -> None:
|
|
53
|
+
self._buffer = bytearray()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def buffered_bytes(self) -> int:
|
|
57
|
+
return len(self._buffer)
|
messagex/py.typed
ADDED
|
File without changes
|
messagex/types.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Awaitable, Callable, Dict, Optional, TypedDict, Union
|
|
5
|
+
|
|
6
|
+
try: # Python 3.8+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
except ImportError: # pragma: no cover
|
|
9
|
+
from typing_extensions import Literal # type: ignore
|
|
10
|
+
|
|
11
|
+
ExchangeType = Literal["direct", "fanout", "topic", "headers"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DeliveredMessage:
|
|
16
|
+
"""A message delivered to a consumer. Settle it with :meth:`ack` or :meth:`nack`."""
|
|
17
|
+
|
|
18
|
+
consumer_tag: str
|
|
19
|
+
queue_name: str
|
|
20
|
+
message_id: str
|
|
21
|
+
delivery_tag: str
|
|
22
|
+
body: Any
|
|
23
|
+
headers: Dict[str, Any]
|
|
24
|
+
routing_key: str
|
|
25
|
+
delivery_count: int
|
|
26
|
+
created_at: str
|
|
27
|
+
_ack: Optional[Callable[[], Awaitable[None]]] = field(default=None, repr=False, compare=False)
|
|
28
|
+
_nack: Optional[Callable[[bool], Awaitable[None]]] = field(default=None, repr=False, compare=False)
|
|
29
|
+
|
|
30
|
+
async def ack(self) -> None:
|
|
31
|
+
"""Acknowledge successful processing."""
|
|
32
|
+
assert self._ack is not None
|
|
33
|
+
await self._ack()
|
|
34
|
+
|
|
35
|
+
async def nack(self, requeue: bool = True) -> None:
|
|
36
|
+
"""Reject the delivery, optionally requeueing it."""
|
|
37
|
+
assert self._nack is not None
|
|
38
|
+
await self._nack(requeue)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
MessageHandler = Callable[[DeliveredMessage], Union[None, Awaitable[None]]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConsumerSubscription:
|
|
46
|
+
consumer_tag: str
|
|
47
|
+
queue_name: str
|
|
48
|
+
_cancel: Callable[[], Awaitable[None]] = field(repr=False, compare=False)
|
|
49
|
+
|
|
50
|
+
async def cancel(self) -> None:
|
|
51
|
+
await self._cancel()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class PublishResult:
|
|
56
|
+
exchange_name: str
|
|
57
|
+
routed_queues: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class QueueInfo(TypedDict, total=False):
|
|
61
|
+
id: str
|
|
62
|
+
name: str
|
|
63
|
+
durable: bool
|
|
64
|
+
exclusive: bool
|
|
65
|
+
autoDelete: bool
|
|
66
|
+
maxDeliveryCount: Optional[int]
|
|
67
|
+
deadLetterQueue: Optional[str]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExchangeInfo(TypedDict, total=False):
|
|
71
|
+
id: str
|
|
72
|
+
name: str
|
|
73
|
+
type: str
|
|
74
|
+
durable: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BindingInfo(TypedDict, total=False):
|
|
78
|
+
id: str
|
|
79
|
+
exchangeName: str
|
|
80
|
+
queueName: str
|
|
81
|
+
routingKey: str
|
|
82
|
+
arguments: Dict[str, Any]
|