DefStream-RabbitMQ 0.1.28__123-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.
@@ -0,0 +1,181 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Readable,
6
+ )
7
+ from .Serializer import (
8
+ Encoder,
9
+ Decoder,
10
+ )
11
+ from .Event import Event
12
+
13
+ from pika import (
14
+ BlockingConnection,
15
+ BasicProperties,
16
+ )
17
+
18
+ from time import time
19
+ from logging import getLogger
20
+ from uuid import uuid4
21
+
22
+ from typing import (
23
+ Dict,
24
+ Generator,
25
+ Optional,
26
+ )
27
+
28
+ __all__ = (
29
+ 'Stream',
30
+ )
31
+
32
+
33
+ class Stream(
34
+ Sendable,
35
+ Readable,
36
+ ):
37
+ """Stream of Event Broker for RabbitMQ"""
38
+ logger = getLogger('Broker.RabbitMQ.Stream')
39
+
40
+ def __init__(
41
+ self,
42
+ connection: BlockingConnection,
43
+ encode: Encoder,
44
+ decode: Decoder,
45
+ name: str,
46
+ ):
47
+ self.connection = connection
48
+ self.encode = encode
49
+ self.decode = decode
50
+ try:
51
+ self.channel = self.connection.channel()
52
+ except Exception as error:
53
+ self.logger.error('Stream channel creation failed: queue=%s error_type=%s', name, type(error).__name__)
54
+ raise
55
+ self.queue = name
56
+ self.event = None
57
+ return
58
+
59
+ def __del__(self):
60
+ channel = getattr(self, 'channel', None)
61
+ try:
62
+ if channel and channel.is_open:
63
+ channel.close()
64
+ self.logger.debug('Stream channel cleanup completed: queue=%s', self.queue)
65
+ except Exception as error:
66
+ self.logger.error('Stream channel cleanup failed: error_type=%s', type(error).__name__)
67
+ raise
68
+ return
69
+
70
+ def __str__(self): return self.queue
71
+
72
+ def send(
73
+ self,
74
+ body,
75
+ event: Optional[str] = None,
76
+ headers: Dict = {},
77
+ timestamp: Optional[int] = None,
78
+ id: Optional[str] = None,
79
+ ):
80
+ if not timestamp: timestamp = int(time())
81
+ if not id: id = uuid4().hex
82
+ properties = BasicProperties(
83
+ type=event if event else '',
84
+ headers=headers,
85
+ content_type=self.encode.format,
86
+ content_encoding=self.encode.charset,
87
+ timestamp=timestamp,
88
+ message_id=id,
89
+ )
90
+ self.logger.debug('Publishing Stream message: queue=%s message_id=%s publisher_confirm=False', self.queue, id)
91
+ try:
92
+ self.channel.basic_publish(
93
+ exchange='',
94
+ routing_key=self.queue,
95
+ properties=properties,
96
+ body=self.encode(body)
97
+ )
98
+ except Exception as error:
99
+ self.logger.error('Stream publication failed: queue=%s message_id=%s error_type=%s', self.queue, id, type(error).__name__)
100
+ raise
101
+ self.logger.debug('Stream publish API call completed: queue=%s message_id=%s', self.queue, id)
102
+ return id
103
+
104
+ def read(self, offset: int = 0, timeout: Optional[int] = None) -> Generator[Event, None, None]:
105
+ channel = self.channel
106
+ self.logger.debug('Starting Stream read: queue=%s offset=%s timeout_ms=%s qos=1', self.queue, offset, timeout)
107
+ try:
108
+ channel.basic_qos(0, 1, False)
109
+ except Exception as error:
110
+ self.logger.error('Failed to apply Stream QoS: queue=%s error_type=%s', self.queue, type(error).__name__)
111
+ raise
112
+
113
+ deadline = time() + (timeout / 1000) if timeout else None
114
+
115
+ failure_observed = False
116
+ try:
117
+ for method, properties, body in channel.consume(
118
+ queue=self.queue,
119
+ auto_ack=False,
120
+ inactivity_timeout=1,
121
+ arguments={'x-stream-offset': offset},
122
+ ):
123
+ if method is None:
124
+ if deadline and time() >= deadline:
125
+ break
126
+ continue
127
+ # pika delivers properties and body together with every non-empty method frame.
128
+ assert properties is not None and body is not None
129
+
130
+ try:
131
+ decoded = self.decode(body, properties.content_type, properties.content_encoding)
132
+ except Exception as error:
133
+ failure_observed = True
134
+ self.logger.error('Stream decoding failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
135
+ raise
136
+ self.logger.debug('Stream message received and decoded: queue=%s consumer_tag=%s delivery_tag=%s message_id=%s', self.queue, method.consumer_tag, method.delivery_tag, properties.message_id)
137
+ event = Event(
138
+ channel,
139
+ self.queue,
140
+ method.consumer_tag,
141
+ method.delivery_tag,
142
+ properties,
143
+ decoded,
144
+ )
145
+
146
+ try:
147
+ failure_observed = True
148
+ yield event
149
+ failure_observed = False
150
+ self.logger.debug('Stream ack requested: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
151
+ try:
152
+ channel.basic_ack(delivery_tag=method.delivery_tag)
153
+ except Exception as error:
154
+ failure_observed = True
155
+ self.logger.error('Stream ack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
156
+ raise
157
+ self.logger.debug('Stream ack API call completed: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
158
+ except Exception:
159
+ failure_observed = True
160
+ self.logger.warning('Attempting requeue after Stream processing exception: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
161
+ self.logger.debug('Stream nack requested: queue=%s delivery_tag=%s requeue=True', self.queue, method.delivery_tag)
162
+ try:
163
+ channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
164
+ except Exception as error:
165
+ self.logger.error('Stream nack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
166
+ raise
167
+ self.logger.debug('Stream nack API call completed: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
168
+ raise
169
+ except Exception as error:
170
+ if not failure_observed:
171
+ self.logger.error('Stream consumption loop failed: queue=%s error_type=%s', self.queue, type(error).__name__)
172
+ raise
173
+ finally:
174
+ self.logger.debug('Ending Stream read loop; requesting Consumer cancellation: queue=%s', self.queue)
175
+ try:
176
+ channel.cancel()
177
+ except Exception as error:
178
+ self.logger.error('Stream Consumer cancellation failed: queue=%s error_type=%s', self.queue, type(error).__name__)
179
+ raise
180
+ self.logger.debug('Stream Consumer cancelled: queue=%s', self.queue)
181
+ return
@@ -0,0 +1,90 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass
3
+ from datetime import timedelta
4
+ from typing import (
5
+ Any,
6
+ Optional,
7
+ Union,
8
+ )
9
+ from DefStream.Broker import validate_priority
10
+
11
+ __all__ = (
12
+ 'Subscription',
13
+ )
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Subscription:
18
+ queue: str
19
+ offset: Optional[int] = None
20
+ qos: Optional[int] = None
21
+ error: Optional[str] = None
22
+ max_priority: Optional[int] = None
23
+ priority: Optional[int] = None
24
+ timeout: Optional[Union[int, timedelta]] = None
25
+
26
+ def __post_init__(self) -> None:
27
+ if self.timeout is not None:
28
+ if isinstance(self.timeout, timedelta):
29
+ if self.timeout <= timedelta(0) or self.timeout.microseconds % 1000:
30
+ raise ValueError('Subscription timeout must be positive with whole-millisecond precision')
31
+ object.__setattr__(
32
+ self,
33
+ 'timeout',
34
+ self.timeout // timedelta(milliseconds=1),
35
+ )
36
+ elif isinstance(self.timeout, bool) or not isinstance(self.timeout, int):
37
+ raise TypeError('Subscription timeout must be integer milliseconds, a timedelta or None')
38
+ elif self.timeout <= 0:
39
+ raise ValueError('Subscription timeout must be positive')
40
+ if self.priority is not None and self.max_priority is None:
41
+ object.__setattr__(
42
+ self,
43
+ 'max_priority',
44
+ 255,
45
+ )
46
+ validate_priority(
47
+ self.max_priority,
48
+ 255,
49
+ minimum=1,
50
+ name='max_priority',
51
+ )
52
+ self.validate_priority(self.priority)
53
+ if not isinstance(self.queue, str) or not self.queue:
54
+ raise ValueError('Subscription queue must be a non-empty string')
55
+ if self.error is not None:
56
+ if not isinstance(
57
+ self.error,
58
+ str,
59
+ ) or not self.error:
60
+ raise ValueError('Subscription error must be a non-empty string or None')
61
+ if self.offset is not None and (isinstance(self.offset, bool) or not isinstance(self.offset, int)):
62
+ raise ValueError('Subscription offset must be an integer')
63
+ if self.qos is not None:
64
+ if isinstance(
65
+ self.qos,
66
+ bool,
67
+ ) or not isinstance(
68
+ self.qos,
69
+ int,
70
+ ) or not 0 <= self.qos <= 65535:
71
+ raise ValueError('Subscription qos must be an integer between 0 and 65535')
72
+ return
73
+
74
+ def validate_priority(
75
+ self,
76
+ priority: Optional[int],
77
+ ) -> None:
78
+ validate_priority(
79
+ priority,
80
+ 255 if self.max_priority is None else self.max_priority,
81
+ )
82
+ return
83
+
84
+ @classmethod
85
+ def parse(cls, parameters: Any) -> 'Subscription':
86
+ if isinstance(parameters, cls):
87
+ return parameters
88
+ if isinstance(parameters, Mapping):
89
+ return cls(**dict(parameters))
90
+ raise TypeError('RabbitMQ subscriptions require a Subscription or mapping')
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .Configuration import Configuration
4
+ from .Connection import (
5
+ Connection,
6
+ ExchangeType,
7
+ Parameters,
8
+ All,
9
+ Any,
10
+ Timer,
11
+ )
12
+ from .Exchange import Exchange
13
+ from .Queue import Queue
14
+ from .Stream import Stream
15
+ from .Consumer import Consumer
16
+ from .Subscription import Subscription
17
+ from .Event import (
18
+ Event,
19
+ EventHandler,
20
+ Requeue,
21
+ Reject,
22
+ )
23
+ from .Serializer import (
24
+ Encoder,
25
+ Decoder,
26
+ )
27
+
28
+ __all__ = (
29
+ 'Configuration',
30
+ 'Connection',
31
+ 'ExchangeType',
32
+ 'Parameters',
33
+ 'All',
34
+ 'Any',
35
+ 'Timer',
36
+ 'Exchange',
37
+ 'Queue',
38
+ 'Stream',
39
+ 'Consumer',
40
+ 'Subscription',
41
+ 'Requeue',
42
+ 'Reject',
43
+ 'Event',
44
+ 'EventHandler',
45
+ 'Encoder',
46
+ 'Decoder',
47
+ )
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: DefStream-RabbitMQ
3
+ Version: 0.1.28
4
+ Summary: Synchronous message processing with pluggable broker adapters
5
+ Home-page: https://github.com/yong5eon/DefStream
6
+ Author: Heo Yongseon
7
+ Project-URL: Source, https://github.com/yong5eon/DefStream
8
+ Project-URL: Issues, https://github.com/yong5eon/DefStream/issues
9
+ Project-URL: Releases, https://github.com/yong5eon/DefStream/releases
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: DefStream==0.1.28
20
+ Requires-Dist: pika>=1.3.2
21
+ Dynamic: author
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license-file
27
+ Dynamic: project-url
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # DefStream
33
+
34
+ Synchronous event/message processing with pluggable broker adapters
35
+
36
+ DefStream processes broker messages with ordinary Python task methods and can run them in thread or process pools. It requires Python 3.9 or later.
37
+
38
+ ## Installation
39
+
40
+ Install the client extra for the broker you use. A broker server must be available separately.
41
+
42
+ ```shell
43
+ python -m pip install "DefStream[RabbitMQ]"
44
+ ```
45
+
46
+ For Redis Streams, install `DefStream[Redis]`. To install both adapters and clients, use `DefStream[RabbitMQ,Redis]`. The plain `DefStream` installation includes neither broker implementation nor client. Each extra installs a separate, same-version adapter distribution, preserving existing import paths.
47
+
48
+ Before migrating an older integrated installation or historical standalone adapters, uninstall DefStream, DefStream-RabbitMQ and DefStream-Redis, then reinstall with the desired extras. Omitting an extra later does not remove an already installed adapter.
49
+
50
+ ## Quick start
51
+
52
+ The following worker listens to a local RabbitMQ server at `127.0.0.1:5672`. The example credentials are for local development only.
53
+
54
+ ```python
55
+ from signal import SIGINT, signal
56
+
57
+ from DefStream.Broker.Implements.RabbitMQ import Configuration, Connection, Reject, Subscription
58
+ from DefStream.Worker import Run, ThreadPool, Worker, WorkerSetup, declare
59
+
60
+
61
+ class Add(
62
+ Run,
63
+ parameters=Subscription(queue='example.add', error='example.add.error'),
64
+ on_error=Reject(),
65
+ ):
66
+ def run(self, a: int, b: int):
67
+ print(a + b)
68
+ return a + b
69
+
70
+
71
+ class Prepare(WorkerSetup):
72
+ def __call__(self, worker: Worker) -> None:
73
+ for task in worker.tasks:
74
+ declare(task=task, broker=worker.broker)
75
+
76
+
77
+ if __name__ == '__main__':
78
+ worker = Worker(
79
+ Connection(Configuration(host='127.0.0.1', port=5672, username='guest', password='guest')),
80
+ ThreadPool(4),
81
+ setups=Prepare(),
82
+ )
83
+ worker.add(Add)
84
+ signal(SIGINT, lambda *_: worker.stop())
85
+ worker.run()
86
+ ```
87
+
88
+ Run the script after starting RabbitMQ. The worker declares its task queue and waits for messages. Store production credentials outside source code.
89
+
90
+ ## License
91
+
92
+ See the `LICENSE` file included with the source distribution for the license terms.
@@ -0,0 +1,15 @@
1
+ DefStream/Broker/Implements/RabbitMQ/Configuration.py,sha256=sHysXc1nlJHbSjWD3BZxlXjKBAbz2AKDtr7ACVVVj0c,1094
2
+ DefStream/Broker/Implements/RabbitMQ/Connection.py,sha256=4c6IPO5udzs7hEWQPi5o9rFY-caV-j1WQcL71BkRN8o,15061
3
+ DefStream/Broker/Implements/RabbitMQ/Consumer.py,sha256=KUQ4cXydUxPZZ66FyzZFw9ZwjhKt0Dmxs3I_UlgzEVg,11297
4
+ DefStream/Broker/Implements/RabbitMQ/Event.py,sha256=r4r7XT_-2N6XSPz8hBiGNS5di6OYe37q3INYxJAYQCk,3362
5
+ DefStream/Broker/Implements/RabbitMQ/Exchange.py,sha256=EblUpf1SKUfmESREWNargJwGpoSwA5v0Th5MHcwW4Xg,8444
6
+ DefStream/Broker/Implements/RabbitMQ/Queue.py,sha256=XXypscxJNVrNuMAOgSI93lQkiW6EV5GWEcfYeiLH11w,9446
7
+ DefStream/Broker/Implements/RabbitMQ/Serializer.py,sha256=PbL_V89GHPjqh1zvOM-apExhD7Bgau2jinpVCsVUg00,1718
8
+ DefStream/Broker/Implements/RabbitMQ/Stream.py,sha256=vhfnIhpCe1gduT09VIfp6ZhpQtHyqz6bjWhlWAjNy9E,5698
9
+ DefStream/Broker/Implements/RabbitMQ/Subscription.py,sha256=DyjbZ0ZiYBuJDJxVTL_tzsvJluwvsCrcowYMRPcPnzA,2553
10
+ DefStream/Broker/Implements/RabbitMQ/__init__.py,sha256=gHDgzs28a-ZsTTZjBUSM7-yXPFJ4D0PZD7hOvvei7L8,665
11
+ defstream_rabbitmq-0.1.28.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
+ defstream_rabbitmq-0.1.28.dist-info/METADATA,sha256=vCIqMpo8J7774Mck0AUPoaD3e0HRo0VKXXfnEdPFqmI,3255
13
+ defstream_rabbitmq-0.1.28.dist-info/WHEEL,sha256=IaXmNPOa1UfbawqKJ84xaSRPGWFAZj4RsHm6AQpbyKc,102
14
+ defstream_rabbitmq-0.1.28.dist-info/top_level.txt,sha256=zsnisIu5wHL4b7VM8e9AggG9KTSdUU4morFqpb8YWFs,10
15
+ defstream_rabbitmq-0.1.28.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+ Build: 123
6
+