DefStream 0.1.17__64-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.
- DefStream/Broker/Configuration.py +18 -0
- DefStream/Broker/Connection.py +141 -0
- DefStream/Broker/Consumer.py +59 -0
- DefStream/Broker/Errors.py +14 -0
- DefStream/Broker/Event.py +35 -0
- DefStream/Broker/Implements/RabbitMQ/Configuration.py +48 -0
- DefStream/Broker/Implements/RabbitMQ/Connection.py +542 -0
- DefStream/Broker/Implements/RabbitMQ/Consumer.py +359 -0
- DefStream/Broker/Implements/RabbitMQ/Event.py +127 -0
- DefStream/Broker/Implements/RabbitMQ/Exchange.py +278 -0
- DefStream/Broker/Implements/RabbitMQ/Policies.py +9 -0
- DefStream/Broker/Implements/RabbitMQ/Queue.py +319 -0
- DefStream/Broker/Implements/RabbitMQ/Serializer.py +61 -0
- DefStream/Broker/Implements/RabbitMQ/Stream.py +179 -0
- DefStream/Broker/Implements/RabbitMQ/Subscription.py +74 -0
- DefStream/Broker/Implements/RabbitMQ/__init__.py +47 -0
- DefStream/Broker/Implements/Redis/Configuration.py +59 -0
- DefStream/Broker/Implements/Redis/Connection.py +265 -0
- DefStream/Broker/Implements/Redis/Consumer.py +364 -0
- DefStream/Broker/Implements/Redis/Event.py +191 -0
- DefStream/Broker/Implements/Redis/Queue.py +284 -0
- DefStream/Broker/Implements/Redis/Serializer.py +48 -0
- DefStream/Broker/Implements/Redis/Subscription.py +46 -0
- DefStream/Broker/Implements/Redis/__init__.py +29 -0
- DefStream/Broker/Implements/__init__.py +1 -0
- DefStream/Broker/Policies.py +25 -0
- DefStream/Broker/Priority.py +26 -0
- DefStream/Broker/Protocol.py +45 -0
- DefStream/Broker/__init__.py +59 -0
- DefStream/Descriptor/DataObject.py +409 -0
- DefStream/Descriptor/Types.py +248 -0
- DefStream/Descriptor/Value.py +188 -0
- DefStream/Descriptor/__init__.py +172 -0
- DefStream/Inspector/Extension.py +69 -0
- DefStream/Inspector/Extensions/Console.html +373 -0
- DefStream/Inspector/Extensions/Console.py +415 -0
- DefStream/Inspector/Extensions/Metrics.py +316 -0
- DefStream/Inspector/Extensions/Ping.py +107 -0
- DefStream/Inspector/Extensions/Specifications.py +176 -0
- DefStream/Inspector/Extensions/Status.html +206 -0
- DefStream/Inspector/Extensions/Status.py +124 -0
- DefStream/Inspector/Extensions/__init__.py +13 -0
- DefStream/Inspector/Inspector.py +347 -0
- DefStream/Inspector/Response.py +14 -0
- DefStream/Inspector/__init__.py +9 -0
- DefStream/Validator/Pattern.py +36 -0
- DefStream/Validator/Patterns/Array.py +30 -0
- DefStream/Validator/Patterns/Common.py +92 -0
- DefStream/Validator/Patterns/Compare.py +180 -0
- DefStream/Validator/Patterns/Condition.py +60 -0
- DefStream/Validator/Patterns/DataObject.py +85 -0
- DefStream/Validator/Patterns/DateTime.py +111 -0
- DefStream/Validator/Patterns/Number.py +43 -0
- DefStream/Validator/Patterns/Object.py +150 -0
- DefStream/Validator/Patterns/Size.py +110 -0
- DefStream/Validator/Patterns/String.py +85 -0
- DefStream/Validator/Patterns/Type.py +350 -0
- DefStream/Validator/Patterns/__init__.py +137 -0
- DefStream/Validator/Validate.py +29 -0
- DefStream/Validator/Validator.py +30 -0
- DefStream/Validator/__init__.py +11 -0
- DefStream/Worker/Executor.py +371 -0
- DefStream/Worker/Message.py +180 -0
- DefStream/Worker/Pool.py +245 -0
- DefStream/Worker/Run.py +357 -0
- DefStream/Worker/Specification.py +87 -0
- DefStream/Worker/Startup.py +174 -0
- DefStream/Worker/Worker.py +421 -0
- DefStream/Worker/__init__.py +47 -0
- DefStream/__init__.py +0 -0
- defstream-0.1.17.dist-info/METADATA +1174 -0
- defstream-0.1.17.dist-info/RECORD +75 -0
- defstream-0.1.17.dist-info/WHEEL +6 -0
- defstream-0.1.17.dist-info/licenses/LICENSE +674 -0
- defstream-0.1.17.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
Iterable,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = (
|
|
28
|
+
'Queue'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Stream(
|
|
33
|
+
Sendable,
|
|
34
|
+
Readable,
|
|
35
|
+
):
|
|
36
|
+
"""Stream of Event Broker for RabbitMQ"""
|
|
37
|
+
logger = getLogger('DefStream.Broker.Implements.RabbitMQ.Stream')
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
connection: BlockingConnection,
|
|
42
|
+
encode: Encoder,
|
|
43
|
+
decode: Decoder,
|
|
44
|
+
name: str = None,
|
|
45
|
+
):
|
|
46
|
+
self.connection = connection
|
|
47
|
+
self.encode = encode
|
|
48
|
+
self.decode = decode
|
|
49
|
+
try:
|
|
50
|
+
self.channel = self.connection.channel()
|
|
51
|
+
except Exception as error:
|
|
52
|
+
self.logger.error('Stream channel creation failed: queue=%s error_type=%s', name, type(error).__name__)
|
|
53
|
+
raise
|
|
54
|
+
self.channel.auto_decode = False
|
|
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: str = None,
|
|
76
|
+
headers: Dict = {},
|
|
77
|
+
timestamp: int = None,
|
|
78
|
+
id: 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: int = None) -> Iterable[Event]:
|
|
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
|
+
|
|
128
|
+
try:
|
|
129
|
+
decoded = self.decode(body, properties.content_type, properties.content_encoding)
|
|
130
|
+
except Exception as error:
|
|
131
|
+
failure_observed = True
|
|
132
|
+
self.logger.error('Stream decoding failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
|
|
133
|
+
raise
|
|
134
|
+
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)
|
|
135
|
+
event = Event(
|
|
136
|
+
channel,
|
|
137
|
+
self.queue,
|
|
138
|
+
method.consumer_tag,
|
|
139
|
+
method.delivery_tag,
|
|
140
|
+
properties,
|
|
141
|
+
decoded,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
failure_observed = True
|
|
146
|
+
yield event
|
|
147
|
+
failure_observed = False
|
|
148
|
+
self.logger.debug('Stream ack requested: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
|
|
149
|
+
try:
|
|
150
|
+
channel.basic_ack(delivery_tag=method.delivery_tag)
|
|
151
|
+
except Exception as error:
|
|
152
|
+
failure_observed = True
|
|
153
|
+
self.logger.error('Stream ack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
|
|
154
|
+
raise
|
|
155
|
+
self.logger.debug('Stream ack API call completed: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
|
|
156
|
+
except Exception:
|
|
157
|
+
failure_observed = True
|
|
158
|
+
self.logger.warning('Attempting requeue after Stream processing exception: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
|
|
159
|
+
self.logger.debug('Stream nack requested: queue=%s delivery_tag=%s requeue=True', self.queue, method.delivery_tag)
|
|
160
|
+
try:
|
|
161
|
+
channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
162
|
+
except Exception as error:
|
|
163
|
+
self.logger.error('Stream nack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
|
|
164
|
+
raise
|
|
165
|
+
self.logger.debug('Stream nack API call completed: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
|
|
166
|
+
raise
|
|
167
|
+
except Exception as error:
|
|
168
|
+
if not failure_observed:
|
|
169
|
+
self.logger.error('Stream consumption loop failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
170
|
+
raise
|
|
171
|
+
finally:
|
|
172
|
+
self.logger.debug('Ending Stream read loop; requesting Consumer cancellation: queue=%s', self.queue)
|
|
173
|
+
try:
|
|
174
|
+
channel.cancel()
|
|
175
|
+
except Exception as error:
|
|
176
|
+
self.logger.error('Stream Consumer cancellation failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
177
|
+
raise
|
|
178
|
+
self.logger.debug('Stream Consumer cancelled: queue=%s', self.queue)
|
|
179
|
+
return
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Optional,
|
|
6
|
+
)
|
|
7
|
+
from DefStream.Broker import validate_priority
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
'Subscription',
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Subscription:
|
|
16
|
+
queue: str
|
|
17
|
+
offset: Optional[int] = None
|
|
18
|
+
qos: Optional[int] = None
|
|
19
|
+
error: Optional[str] = None
|
|
20
|
+
max_priority: Optional[int] = None
|
|
21
|
+
priority: Optional[int] = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
if self.priority is not None and self.max_priority is None:
|
|
25
|
+
object.__setattr__(
|
|
26
|
+
self,
|
|
27
|
+
'max_priority',
|
|
28
|
+
255,
|
|
29
|
+
)
|
|
30
|
+
validate_priority(
|
|
31
|
+
self.max_priority,
|
|
32
|
+
255,
|
|
33
|
+
minimum=1,
|
|
34
|
+
name='max_priority',
|
|
35
|
+
)
|
|
36
|
+
self.validate_priority(self.priority)
|
|
37
|
+
if not isinstance(self.queue, str) or not self.queue:
|
|
38
|
+
raise ValueError('Subscription queue must be a non-empty string')
|
|
39
|
+
if self.error is not None:
|
|
40
|
+
if not isinstance(
|
|
41
|
+
self.error,
|
|
42
|
+
str,
|
|
43
|
+
) or not self.error:
|
|
44
|
+
raise ValueError('Subscription error must be a non-empty string or None')
|
|
45
|
+
if self.offset is not None and (isinstance(self.offset, bool) or not isinstance(self.offset, int)):
|
|
46
|
+
raise ValueError('Subscription offset must be an integer')
|
|
47
|
+
if self.qos is not None:
|
|
48
|
+
if isinstance(
|
|
49
|
+
self.qos,
|
|
50
|
+
bool,
|
|
51
|
+
) or not isinstance(
|
|
52
|
+
self.qos,
|
|
53
|
+
int,
|
|
54
|
+
) or not 0 <= self.qos <= 65535:
|
|
55
|
+
raise ValueError('Subscription qos must be an integer between 0 and 65535')
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
def validate_priority(
|
|
59
|
+
self,
|
|
60
|
+
priority: Optional[int],
|
|
61
|
+
) -> None:
|
|
62
|
+
validate_priority(
|
|
63
|
+
priority,
|
|
64
|
+
255 if self.max_priority is None else self.max_priority,
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def parse(cls, parameters: Any) -> 'Subscription':
|
|
70
|
+
if isinstance(parameters, cls):
|
|
71
|
+
return parameters
|
|
72
|
+
if isinstance(parameters, Mapping):
|
|
73
|
+
return cls(**dict(parameters))
|
|
74
|
+
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
|
+
)
|
|
11
|
+
from .Exchange import Exchange
|
|
12
|
+
from .Queue import Queue
|
|
13
|
+
from .Stream import Stream
|
|
14
|
+
from .Consumer import Consumer
|
|
15
|
+
from .Subscription import Subscription
|
|
16
|
+
from .Policies import (
|
|
17
|
+
Requeue,
|
|
18
|
+
Reject,
|
|
19
|
+
)
|
|
20
|
+
from .Event import (
|
|
21
|
+
Event,
|
|
22
|
+
EventHandler,
|
|
23
|
+
)
|
|
24
|
+
from .Serializer import (
|
|
25
|
+
Encoder,
|
|
26
|
+
Decoder,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
'Configuration',
|
|
31
|
+
'Connection',
|
|
32
|
+
'ExchangeType',
|
|
33
|
+
'Parameters',
|
|
34
|
+
'All',
|
|
35
|
+
'Any',
|
|
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,59 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from DefStream.Broker import Configuration as BaseConfiguration
|
|
5
|
+
from .Serializer import (
|
|
6
|
+
Decoder,
|
|
7
|
+
Encoder,
|
|
8
|
+
JavaScriptObjectNotationDecoder,
|
|
9
|
+
JavaScriptObjectNotationEncoder,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'Configuration',
|
|
14
|
+
'ConnectionType',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionType(Enum):
|
|
19
|
+
Pool = 'pool'
|
|
20
|
+
Cluster = 'cluster'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Configuration(BaseConfiguration):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
host: str = '127.0.0.1',
|
|
27
|
+
port: int = 6379,
|
|
28
|
+
username: Optional[str] = None,
|
|
29
|
+
password: Optional[str] = None,
|
|
30
|
+
secure: bool = False,
|
|
31
|
+
type: ConnectionType = ConnectionType.Pool,
|
|
32
|
+
max: int = 10,
|
|
33
|
+
database: int = 0,
|
|
34
|
+
timeout: float = 5,
|
|
35
|
+
encode: Optional[Encoder] = None,
|
|
36
|
+
decode: Optional[Decoder] = None,
|
|
37
|
+
):
|
|
38
|
+
if not isinstance(
|
|
39
|
+
type,
|
|
40
|
+
ConnectionType,
|
|
41
|
+
):
|
|
42
|
+
raise ValueError('Redis connection type must be a ConnectionType')
|
|
43
|
+
if max < 1:
|
|
44
|
+
raise ValueError('Redis max connections must be positive')
|
|
45
|
+
if database < 0 or (type == ConnectionType.Cluster and database != 0):
|
|
46
|
+
raise ValueError('Redis database must be non-negative; Cluster requires database 0')
|
|
47
|
+
if timeout <= 0:
|
|
48
|
+
raise ValueError('Redis connection timeout must be positive')
|
|
49
|
+
self.host = host
|
|
50
|
+
self.port = port
|
|
51
|
+
self.username = username
|
|
52
|
+
self.password = password
|
|
53
|
+
self.secure = secure
|
|
54
|
+
self.type = type
|
|
55
|
+
self.max = max
|
|
56
|
+
self.database = database
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
self.encode = encode if encode is not None else JavaScriptObjectNotationEncoder()
|
|
59
|
+
self.decode = decode if decode is not None else JavaScriptObjectNotationDecoder()
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Optional,
|
|
5
|
+
Union,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from redis import Redis
|
|
9
|
+
from redis.cluster import RedisCluster
|
|
10
|
+
|
|
11
|
+
from DefStream.Broker import (
|
|
12
|
+
Connection as BaseConnection,
|
|
13
|
+
GetQueue,
|
|
14
|
+
GetStream,
|
|
15
|
+
GetConsumer,
|
|
16
|
+
Declarable,
|
|
17
|
+
Emittable,
|
|
18
|
+
EventHandler,
|
|
19
|
+
)
|
|
20
|
+
from .Configuration import (
|
|
21
|
+
Configuration,
|
|
22
|
+
ConnectionType,
|
|
23
|
+
)
|
|
24
|
+
from .Consumer import Consumer
|
|
25
|
+
from .Queue import Queue
|
|
26
|
+
from .Subscription import Subscription
|
|
27
|
+
|
|
28
|
+
__all__ = (
|
|
29
|
+
'Connection',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Connection(
|
|
34
|
+
BaseConnection,
|
|
35
|
+
GetQueue,
|
|
36
|
+
GetStream,
|
|
37
|
+
GetConsumer,
|
|
38
|
+
Declarable,
|
|
39
|
+
Emittable,
|
|
40
|
+
):
|
|
41
|
+
logger = getLogger('DefStream.Broker.Implements.Redis.Connection')
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
conf: Configuration,
|
|
46
|
+
):
|
|
47
|
+
self.conf = conf
|
|
48
|
+
self.connection: Optional[Union[Redis, RedisCluster]] = None
|
|
49
|
+
|
|
50
|
+
def connect(self) -> None:
|
|
51
|
+
if self.connection is not None:
|
|
52
|
+
self.logger.debug('Reusing existing Redis connection')
|
|
53
|
+
return
|
|
54
|
+
connection_type = self.conf.type.value
|
|
55
|
+
self.logger.info(
|
|
56
|
+
'Connecting to Redis: type=%s',
|
|
57
|
+
connection_type,
|
|
58
|
+
)
|
|
59
|
+
self.logger.debug(
|
|
60
|
+
'Redis connection settings: host=%s port=%s database=%s TLS=%s max=%s timeout=%ss',
|
|
61
|
+
self.conf.host,
|
|
62
|
+
self.conf.port,
|
|
63
|
+
self.conf.database,
|
|
64
|
+
self.conf.secure,
|
|
65
|
+
self.conf.max,
|
|
66
|
+
self.conf.timeout,
|
|
67
|
+
)
|
|
68
|
+
parameters = {
|
|
69
|
+
'host': self.conf.host,
|
|
70
|
+
'port': self.conf.port,
|
|
71
|
+
'username': self.conf.username,
|
|
72
|
+
'password': self.conf.password,
|
|
73
|
+
'ssl': self.conf.secure,
|
|
74
|
+
'max_connections': self.conf.max,
|
|
75
|
+
'socket_connect_timeout': self.conf.timeout,
|
|
76
|
+
'socket_timeout': self.conf.timeout,
|
|
77
|
+
'decode_responses': False,
|
|
78
|
+
}
|
|
79
|
+
connection = None
|
|
80
|
+
try:
|
|
81
|
+
if self.conf.type == ConnectionType.Cluster:
|
|
82
|
+
connection = RedisCluster(**parameters)
|
|
83
|
+
else:
|
|
84
|
+
connection = Redis(
|
|
85
|
+
db=self.conf.database,
|
|
86
|
+
**parameters,
|
|
87
|
+
)
|
|
88
|
+
connection.ping()
|
|
89
|
+
except Exception as error:
|
|
90
|
+
self.logger.error(
|
|
91
|
+
'Redis connection or PING failed: type=%s error_type=%s',
|
|
92
|
+
connection_type,
|
|
93
|
+
type(error).__name__,
|
|
94
|
+
)
|
|
95
|
+
if connection is not None:
|
|
96
|
+
connection.close()
|
|
97
|
+
raise
|
|
98
|
+
self.connection = connection
|
|
99
|
+
self.logger.info(
|
|
100
|
+
'Redis connection verified with PING: type=%s',
|
|
101
|
+
connection_type,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def connected(self) -> Union[Redis, RedisCluster]:
|
|
105
|
+
if self.connection is None:
|
|
106
|
+
raise RuntimeError('Connect the Redis broker before use')
|
|
107
|
+
return self.connection
|
|
108
|
+
|
|
109
|
+
def declare(
|
|
110
|
+
self,
|
|
111
|
+
parameters: Any,
|
|
112
|
+
) -> None:
|
|
113
|
+
subscription = Subscription.parse(parameters)
|
|
114
|
+
self.logger.debug(
|
|
115
|
+
'Delegating stream declaration: queue=%s group=%s',
|
|
116
|
+
subscription.queue,
|
|
117
|
+
subscription.group,
|
|
118
|
+
)
|
|
119
|
+
self.create_queue(
|
|
120
|
+
subscription.queue,
|
|
121
|
+
group=subscription.group,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def emit(
|
|
125
|
+
self,
|
|
126
|
+
body: Any,
|
|
127
|
+
parameters: Any,
|
|
128
|
+
) -> str:
|
|
129
|
+
subscription = Subscription.parse(parameters)
|
|
130
|
+
self.logger.debug(
|
|
131
|
+
'Delegating message publication: queue=%s group=%s',
|
|
132
|
+
subscription.queue,
|
|
133
|
+
subscription.group,
|
|
134
|
+
)
|
|
135
|
+
return self.queue(
|
|
136
|
+
subscription.queue,
|
|
137
|
+
group=subscription.group,
|
|
138
|
+
).send(body)
|
|
139
|
+
|
|
140
|
+
def queue(
|
|
141
|
+
self,
|
|
142
|
+
name: str,
|
|
143
|
+
group: str = 'DefStream',
|
|
144
|
+
) -> Queue:
|
|
145
|
+
self.logger.debug(
|
|
146
|
+
'Creating Queue: queue=%s group=%s',
|
|
147
|
+
name,
|
|
148
|
+
group,
|
|
149
|
+
)
|
|
150
|
+
return Queue(
|
|
151
|
+
self.connected(),
|
|
152
|
+
self.conf.encode,
|
|
153
|
+
self.conf.decode,
|
|
154
|
+
name,
|
|
155
|
+
group,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def stream(
|
|
159
|
+
self,
|
|
160
|
+
name: str,
|
|
161
|
+
group: str = 'DefStream',
|
|
162
|
+
) -> Queue:
|
|
163
|
+
return self.queue(
|
|
164
|
+
name,
|
|
165
|
+
group,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def consumer(
|
|
169
|
+
self,
|
|
170
|
+
handler: Optional[EventHandler] = None,
|
|
171
|
+
qos: int = 1,
|
|
172
|
+
reclaim_idle: Optional[int] = 60000,
|
|
173
|
+
) -> Consumer:
|
|
174
|
+
self.logger.debug(
|
|
175
|
+
'Creating Consumer: qos=%s reclaim_idle=%sms',
|
|
176
|
+
qos,
|
|
177
|
+
reclaim_idle,
|
|
178
|
+
)
|
|
179
|
+
return Consumer(
|
|
180
|
+
self.connected(),
|
|
181
|
+
self.conf.encode,
|
|
182
|
+
self.conf.decode,
|
|
183
|
+
handler,
|
|
184
|
+
qos,
|
|
185
|
+
reclaim_idle,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def create_queue(
|
|
189
|
+
self,
|
|
190
|
+
name: str,
|
|
191
|
+
group: str = 'DefStream',
|
|
192
|
+
) -> None:
|
|
193
|
+
self.logger.debug(
|
|
194
|
+
'Delegating Consumer Group creation: queue=%s group=%s',
|
|
195
|
+
name,
|
|
196
|
+
group,
|
|
197
|
+
)
|
|
198
|
+
self.queue(
|
|
199
|
+
name,
|
|
200
|
+
group,
|
|
201
|
+
).create_group()
|
|
202
|
+
|
|
203
|
+
def delete_queue(
|
|
204
|
+
self,
|
|
205
|
+
name: Union[str, Queue],
|
|
206
|
+
) -> int:
|
|
207
|
+
queue = str(name)
|
|
208
|
+
self.logger.debug(
|
|
209
|
+
'Deleting Redis stream: queue=%s',
|
|
210
|
+
queue,
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
deleted = self.connected().delete(queue)
|
|
214
|
+
except Exception as error:
|
|
215
|
+
self.logger.error(
|
|
216
|
+
'Redis stream deletion failed: queue=%s error_type=%s',
|
|
217
|
+
queue,
|
|
218
|
+
type(error).__name__,
|
|
219
|
+
)
|
|
220
|
+
raise
|
|
221
|
+
if isinstance(deleted, int) and deleted > 0:
|
|
222
|
+
self.logger.info(
|
|
223
|
+
'Redis stream deletion completed: queue=%s deleted=%s',
|
|
224
|
+
queue,
|
|
225
|
+
deleted,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
self.logger.info(
|
|
229
|
+
'No Redis stream to delete: queue=%s deleted=%s',
|
|
230
|
+
queue,
|
|
231
|
+
deleted,
|
|
232
|
+
)
|
|
233
|
+
return deleted
|
|
234
|
+
|
|
235
|
+
def create_stream(
|
|
236
|
+
self,
|
|
237
|
+
name: str,
|
|
238
|
+
group: str = 'DefStream',
|
|
239
|
+
) -> None:
|
|
240
|
+
self.create_queue(
|
|
241
|
+
name,
|
|
242
|
+
group,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def delete_stream(
|
|
246
|
+
self,
|
|
247
|
+
name: Union[str, Queue],
|
|
248
|
+
) -> int:
|
|
249
|
+
return self.delete_queue(name)
|
|
250
|
+
|
|
251
|
+
def close(self) -> None:
|
|
252
|
+
if self.connection is not None:
|
|
253
|
+
self.logger.info('Closing Redis connection')
|
|
254
|
+
try:
|
|
255
|
+
self.connection.close()
|
|
256
|
+
except Exception as error:
|
|
257
|
+
self.logger.error(
|
|
258
|
+
'Redis connection close failed: error_type=%s',
|
|
259
|
+
type(error).__name__,
|
|
260
|
+
)
|
|
261
|
+
raise
|
|
262
|
+
self.connection = None
|
|
263
|
+
self.logger.info('Redis connection closed')
|
|
264
|
+
else:
|
|
265
|
+
self.logger.debug('No Redis connection to close')
|