DefStream-Redis 0.1.3__9-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/Implements/Redis/Configuration.py +59 -0
- DefStream/Broker/Implements/Redis/Connection.py +174 -0
- DefStream/Broker/Implements/Redis/Consumer.py +209 -0
- DefStream/Broker/Implements/Redis/Event.py +83 -0
- DefStream/Broker/Implements/Redis/Queue.py +151 -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_redis-0.1.3.dist-info/METADATA +255 -0
- defstream_redis-0.1.3.dist-info/RECORD +13 -0
- defstream_redis-0.1.3.dist-info/WHEEL +6 -0
- defstream_redis-0.1.3.dist-info/licenses/LICENSE +674 -0
- defstream_redis-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -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,174 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Optional,
|
|
4
|
+
Union,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from redis import Redis
|
|
8
|
+
from redis.cluster import RedisCluster
|
|
9
|
+
|
|
10
|
+
from DefStream.Broker import (
|
|
11
|
+
Connection as BaseConnection,
|
|
12
|
+
GetQueue,
|
|
13
|
+
GetStream,
|
|
14
|
+
GetConsumer,
|
|
15
|
+
Declarable,
|
|
16
|
+
TaskSender,
|
|
17
|
+
EventHandler,
|
|
18
|
+
)
|
|
19
|
+
from .Configuration import (
|
|
20
|
+
Configuration,
|
|
21
|
+
ConnectionType,
|
|
22
|
+
)
|
|
23
|
+
from .Consumer import Consumer
|
|
24
|
+
from .Queue import Queue
|
|
25
|
+
from .Subscription import Subscription
|
|
26
|
+
|
|
27
|
+
__all__ = (
|
|
28
|
+
'Connection',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Connection(
|
|
33
|
+
BaseConnection,
|
|
34
|
+
GetQueue,
|
|
35
|
+
GetStream,
|
|
36
|
+
GetConsumer,
|
|
37
|
+
Declarable,
|
|
38
|
+
TaskSender,
|
|
39
|
+
):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
conf: Configuration,
|
|
43
|
+
):
|
|
44
|
+
self.conf = conf
|
|
45
|
+
self.connection: Optional[Union[Redis, RedisCluster]] = None
|
|
46
|
+
|
|
47
|
+
def connect(self) -> None:
|
|
48
|
+
if self.connection is not None:
|
|
49
|
+
return
|
|
50
|
+
parameters = {
|
|
51
|
+
'host': self.conf.host,
|
|
52
|
+
'port': self.conf.port,
|
|
53
|
+
'username': self.conf.username,
|
|
54
|
+
'password': self.conf.password,
|
|
55
|
+
'ssl': self.conf.secure,
|
|
56
|
+
'max_connections': self.conf.max,
|
|
57
|
+
'socket_connect_timeout': self.conf.timeout,
|
|
58
|
+
'socket_timeout': self.conf.timeout,
|
|
59
|
+
'decode_responses': False,
|
|
60
|
+
}
|
|
61
|
+
if self.conf.type == ConnectionType.Cluster:
|
|
62
|
+
connection = RedisCluster(**parameters)
|
|
63
|
+
else:
|
|
64
|
+
connection = Redis(
|
|
65
|
+
db=self.conf.database,
|
|
66
|
+
**parameters,
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
connection.ping()
|
|
70
|
+
except Exception:
|
|
71
|
+
connection.close()
|
|
72
|
+
raise
|
|
73
|
+
self.connection = connection
|
|
74
|
+
|
|
75
|
+
def connected(self) -> Union[Redis, RedisCluster]:
|
|
76
|
+
if self.connection is None:
|
|
77
|
+
raise RuntimeError('Connect the Redis broker before use')
|
|
78
|
+
return self.connection
|
|
79
|
+
|
|
80
|
+
def declare(
|
|
81
|
+
self,
|
|
82
|
+
parameters: Any,
|
|
83
|
+
) -> None:
|
|
84
|
+
subscription = Subscription.parse(parameters)
|
|
85
|
+
self.create_queue(
|
|
86
|
+
subscription.queue,
|
|
87
|
+
group=subscription.group,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def send(
|
|
91
|
+
self,
|
|
92
|
+
body: Any,
|
|
93
|
+
parameters: Any,
|
|
94
|
+
) -> str:
|
|
95
|
+
subscription = Subscription.parse(parameters)
|
|
96
|
+
return self.queue(
|
|
97
|
+
subscription.queue,
|
|
98
|
+
group=subscription.group,
|
|
99
|
+
).send(body)
|
|
100
|
+
|
|
101
|
+
def queue(
|
|
102
|
+
self,
|
|
103
|
+
name: str,
|
|
104
|
+
group: str = 'DefStream',
|
|
105
|
+
) -> Queue:
|
|
106
|
+
return Queue(
|
|
107
|
+
self.connected(),
|
|
108
|
+
self.conf.encode,
|
|
109
|
+
self.conf.decode,
|
|
110
|
+
name,
|
|
111
|
+
group,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def stream(
|
|
115
|
+
self,
|
|
116
|
+
name: str,
|
|
117
|
+
group: str = 'DefStream',
|
|
118
|
+
) -> Queue:
|
|
119
|
+
return self.queue(
|
|
120
|
+
name,
|
|
121
|
+
group,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def consumer(
|
|
125
|
+
self,
|
|
126
|
+
handler: Optional[EventHandler] = None,
|
|
127
|
+
qos: int = 1,
|
|
128
|
+
reclaim_idle: Optional[int] = 60000,
|
|
129
|
+
) -> Consumer:
|
|
130
|
+
return Consumer(
|
|
131
|
+
self.connected(),
|
|
132
|
+
self.conf.encode,
|
|
133
|
+
self.conf.decode,
|
|
134
|
+
handler,
|
|
135
|
+
qos,
|
|
136
|
+
reclaim_idle,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def create_queue(
|
|
140
|
+
self,
|
|
141
|
+
name: str,
|
|
142
|
+
group: str = 'DefStream',
|
|
143
|
+
) -> None:
|
|
144
|
+
self.queue(
|
|
145
|
+
name,
|
|
146
|
+
group,
|
|
147
|
+
).create_group()
|
|
148
|
+
|
|
149
|
+
def delete_queue(
|
|
150
|
+
self,
|
|
151
|
+
name: Union[str, Queue],
|
|
152
|
+
) -> int:
|
|
153
|
+
return self.connected().delete(str(name))
|
|
154
|
+
|
|
155
|
+
def create_stream(
|
|
156
|
+
self,
|
|
157
|
+
name: str,
|
|
158
|
+
group: str = 'DefStream',
|
|
159
|
+
) -> None:
|
|
160
|
+
self.create_queue(
|
|
161
|
+
name,
|
|
162
|
+
group,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def delete_stream(
|
|
166
|
+
self,
|
|
167
|
+
name: Union[str, Queue],
|
|
168
|
+
) -> int:
|
|
169
|
+
return self.delete_queue(name)
|
|
170
|
+
|
|
171
|
+
def close(self) -> None:
|
|
172
|
+
if self.connection is not None:
|
|
173
|
+
self.connection.close()
|
|
174
|
+
self.connection = None
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from queue import (
|
|
3
|
+
Empty,
|
|
4
|
+
SimpleQueue,
|
|
5
|
+
)
|
|
6
|
+
from threading import Event as Wakeup
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Callable,
|
|
10
|
+
Dict,
|
|
11
|
+
Optional,
|
|
12
|
+
Tuple,
|
|
13
|
+
Union,
|
|
14
|
+
)
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
from redis import Redis
|
|
18
|
+
from redis.cluster import RedisCluster
|
|
19
|
+
|
|
20
|
+
from DefStream.Broker import (
|
|
21
|
+
Consumer as BaseConsumer,
|
|
22
|
+
Event as BaseEvent,
|
|
23
|
+
EventError,
|
|
24
|
+
EventHandler,
|
|
25
|
+
Requeue,
|
|
26
|
+
)
|
|
27
|
+
from .Event import Event
|
|
28
|
+
from .Queue import Queue
|
|
29
|
+
from .Serializer import (
|
|
30
|
+
Decoder,
|
|
31
|
+
Encoder,
|
|
32
|
+
)
|
|
33
|
+
from .Subscription import Subscription
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
'Consumer',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Consumer(BaseConsumer):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
connection: Union[Redis, RedisCluster],
|
|
44
|
+
encode: Encoder,
|
|
45
|
+
decode: Decoder,
|
|
46
|
+
handler: Optional[EventHandler] = None,
|
|
47
|
+
qos: int = 1,
|
|
48
|
+
reclaim_idle: Optional[int] = 60000,
|
|
49
|
+
):
|
|
50
|
+
if isinstance(
|
|
51
|
+
qos,
|
|
52
|
+
bool,
|
|
53
|
+
) or not isinstance(
|
|
54
|
+
qos,
|
|
55
|
+
int,
|
|
56
|
+
) or qos < 1:
|
|
57
|
+
raise ValueError('Redis qos must be a positive integer')
|
|
58
|
+
if reclaim_idle is not None and reclaim_idle < 1:
|
|
59
|
+
raise ValueError('Redis reclaim_idle must be positive milliseconds or None')
|
|
60
|
+
self.connection = connection
|
|
61
|
+
self.encode = encode
|
|
62
|
+
self.decode = decode
|
|
63
|
+
self.handler = handler
|
|
64
|
+
self.qos = qos
|
|
65
|
+
self.reclaim_idle = reclaim_idle
|
|
66
|
+
self.name = uuid4().hex
|
|
67
|
+
self.subscriptions: Dict[str, Tuple[Queue, EventHandler]] = {}
|
|
68
|
+
self.rotation = deque()
|
|
69
|
+
self.pending: Dict[Tuple[str, str], Event] = {}
|
|
70
|
+
self.cursors: Dict[str, str] = {}
|
|
71
|
+
self.callbacks: SimpleQueue[Callable[[], None]] = SimpleQueue()
|
|
72
|
+
self.wakeup = Wakeup()
|
|
73
|
+
self.stopped = False
|
|
74
|
+
self.closed = False
|
|
75
|
+
|
|
76
|
+
def subs(
|
|
77
|
+
self,
|
|
78
|
+
queue: Any,
|
|
79
|
+
handler: Optional[EventHandler] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
if self.closed:
|
|
82
|
+
raise RuntimeError('Redis consumer is closed')
|
|
83
|
+
subscription = Subscription.parse(queue)
|
|
84
|
+
handler = handler if handler is not None else self.handler
|
|
85
|
+
if handler is None:
|
|
86
|
+
raise ValueError('subs requires a handler or a default handler')
|
|
87
|
+
if subscription.queue in self.subscriptions:
|
|
88
|
+
raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
|
|
89
|
+
stream = Queue(
|
|
90
|
+
self.connection,
|
|
91
|
+
self.encode,
|
|
92
|
+
self.decode,
|
|
93
|
+
subscription.queue,
|
|
94
|
+
subscription.group,
|
|
95
|
+
)
|
|
96
|
+
stream.create_group()
|
|
97
|
+
self.subscriptions[subscription.queue] = (
|
|
98
|
+
stream,
|
|
99
|
+
handler,
|
|
100
|
+
)
|
|
101
|
+
self.cursors[subscription.queue] = '0-0'
|
|
102
|
+
self.rotation.append(subscription.queue)
|
|
103
|
+
|
|
104
|
+
def call(
|
|
105
|
+
self,
|
|
106
|
+
callback: Callable[[], None],
|
|
107
|
+
) -> None:
|
|
108
|
+
if self.closed:
|
|
109
|
+
raise RuntimeError('Redis consumer is closed')
|
|
110
|
+
self.callbacks.put(callback)
|
|
111
|
+
self.wakeup.set()
|
|
112
|
+
|
|
113
|
+
def fail(
|
|
114
|
+
self,
|
|
115
|
+
event: BaseEvent,
|
|
116
|
+
policy: Optional[EventError] = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
(policy if policy is not None else Requeue())(event)
|
|
119
|
+
|
|
120
|
+
def drain(self) -> None:
|
|
121
|
+
while True:
|
|
122
|
+
try:
|
|
123
|
+
callback = self.callbacks.get_nowait()
|
|
124
|
+
except Empty:
|
|
125
|
+
break
|
|
126
|
+
callback()
|
|
127
|
+
self.pending = {
|
|
128
|
+
key: event
|
|
129
|
+
for key, event in self.pending.items()
|
|
130
|
+
if not event.settled
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def deliver(
|
|
134
|
+
self,
|
|
135
|
+
stream: Queue,
|
|
136
|
+
handler: EventHandler,
|
|
137
|
+
entries: list,
|
|
138
|
+
) -> None:
|
|
139
|
+
for id, fields in entries:
|
|
140
|
+
key = (
|
|
141
|
+
stream.queue,
|
|
142
|
+
id.decode('ascii'),
|
|
143
|
+
)
|
|
144
|
+
if key in self.pending:
|
|
145
|
+
continue
|
|
146
|
+
event = stream.event(
|
|
147
|
+
id,
|
|
148
|
+
fields,
|
|
149
|
+
stream.group,
|
|
150
|
+
self.name,
|
|
151
|
+
)
|
|
152
|
+
self.pending[key] = event
|
|
153
|
+
handler(event)
|
|
154
|
+
self.drain()
|
|
155
|
+
|
|
156
|
+
def run(self) -> None:
|
|
157
|
+
if self.closed:
|
|
158
|
+
raise RuntimeError('Redis consumer is closed')
|
|
159
|
+
if not self.subscriptions:
|
|
160
|
+
raise RuntimeError('Subscribe before running the Redis consumer')
|
|
161
|
+
while not self.stopped:
|
|
162
|
+
self.wakeup.clear()
|
|
163
|
+
self.drain()
|
|
164
|
+
for _ in range(len(self.rotation)):
|
|
165
|
+
available = self.qos - len(self.pending)
|
|
166
|
+
if self.stopped or available <= 0:
|
|
167
|
+
break
|
|
168
|
+
name = self.rotation.popleft()
|
|
169
|
+
self.rotation.append(name)
|
|
170
|
+
stream, handler = self.subscriptions[name]
|
|
171
|
+
if self.reclaim_idle is not None:
|
|
172
|
+
claimed = self.connection.xautoclaim(
|
|
173
|
+
stream.queue,
|
|
174
|
+
stream.group,
|
|
175
|
+
self.name,
|
|
176
|
+
self.reclaim_idle,
|
|
177
|
+
start_id=self.cursors[stream.queue],
|
|
178
|
+
count=available,
|
|
179
|
+
)
|
|
180
|
+
self.cursors[stream.queue] = claimed[0].decode('ascii')
|
|
181
|
+
self.deliver(
|
|
182
|
+
stream,
|
|
183
|
+
handler,
|
|
184
|
+
claimed[1],
|
|
185
|
+
)
|
|
186
|
+
available = self.qos - len(self.pending)
|
|
187
|
+
if self.stopped or available <= 0:
|
|
188
|
+
continue
|
|
189
|
+
messages = self.connection.xreadgroup(
|
|
190
|
+
stream.group,
|
|
191
|
+
self.name,
|
|
192
|
+
{stream.queue: '>'},
|
|
193
|
+
count=available,
|
|
194
|
+
)
|
|
195
|
+
for _, entries in messages:
|
|
196
|
+
self.deliver(
|
|
197
|
+
stream,
|
|
198
|
+
handler,
|
|
199
|
+
entries,
|
|
200
|
+
)
|
|
201
|
+
self.wakeup.wait(0.05)
|
|
202
|
+
|
|
203
|
+
def stop(self) -> None:
|
|
204
|
+
self.stopped = True
|
|
205
|
+
self.wakeup.set()
|
|
206
|
+
|
|
207
|
+
def close(self) -> None:
|
|
208
|
+
self.stop()
|
|
209
|
+
self.closed = True
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Optional,
|
|
4
|
+
Union,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from redis import Redis
|
|
8
|
+
from redis.cluster import RedisCluster
|
|
9
|
+
|
|
10
|
+
from DefStream.Broker import Event as BaseEvent
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'Event',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Event(BaseEvent):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
connection: Union[Redis, RedisCluster],
|
|
21
|
+
queue: str,
|
|
22
|
+
id: str,
|
|
23
|
+
body: Any,
|
|
24
|
+
group: Optional[str] = None,
|
|
25
|
+
consumer: Optional[str] = None,
|
|
26
|
+
):
|
|
27
|
+
self.connection = connection
|
|
28
|
+
self.queue = queue
|
|
29
|
+
self.id = id
|
|
30
|
+
self.payload = body
|
|
31
|
+
self.group = group
|
|
32
|
+
self.consumer = consumer
|
|
33
|
+
self.settled = False
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def body(self) -> Any:
|
|
37
|
+
return self.payload
|
|
38
|
+
|
|
39
|
+
@body.setter
|
|
40
|
+
def body(
|
|
41
|
+
self,
|
|
42
|
+
body: Any,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.payload = body
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def src(self) -> str:
|
|
48
|
+
return self.queue
|
|
49
|
+
|
|
50
|
+
def ack(self) -> None:
|
|
51
|
+
if self.group is None:
|
|
52
|
+
raise RuntimeError('Events from read() have no consumer group to acknowledge')
|
|
53
|
+
if not self.settled:
|
|
54
|
+
self.connection.xack(
|
|
55
|
+
self.queue,
|
|
56
|
+
self.group,
|
|
57
|
+
self.id,
|
|
58
|
+
)
|
|
59
|
+
self.settled = True
|
|
60
|
+
|
|
61
|
+
def nack(self) -> None:
|
|
62
|
+
if self.group is None:
|
|
63
|
+
raise RuntimeError('Events from read() have no consumer group to requeue')
|
|
64
|
+
if not self.settled:
|
|
65
|
+
# Requeue and acknowledge atomically on one stream (also valid in Cluster).
|
|
66
|
+
self.connection.eval(
|
|
67
|
+
"""
|
|
68
|
+
local pending = redis.call('XPENDING', KEYS[1], ARGV[1], ARGV[2], ARGV[2], 1)
|
|
69
|
+
if #pending == 0 then return 0 end
|
|
70
|
+
local entries = redis.call('XRANGE', KEYS[1], ARGV[2], ARGV[2])
|
|
71
|
+
if #entries == 0 then return redis.error_reply('Pending entry was deleted') end
|
|
72
|
+
redis.call('XADD', KEYS[1], '*', unpack(entries[1][2]))
|
|
73
|
+
return redis.call('XACK', KEYS[1], ARGV[1], ARGV[2])
|
|
74
|
+
""",
|
|
75
|
+
1,
|
|
76
|
+
self.queue,
|
|
77
|
+
self.group,
|
|
78
|
+
self.id,
|
|
79
|
+
)
|
|
80
|
+
self.settled = True
|
|
81
|
+
|
|
82
|
+
def reject(self) -> None:
|
|
83
|
+
self.ack()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from time import (
|
|
2
|
+
monotonic,
|
|
3
|
+
sleep,
|
|
4
|
+
)
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Iterable,
|
|
8
|
+
Optional,
|
|
9
|
+
Union,
|
|
10
|
+
)
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from redis import Redis
|
|
14
|
+
from redis.cluster import RedisCluster
|
|
15
|
+
from redis.exceptions import ResponseError
|
|
16
|
+
|
|
17
|
+
from DefStream.Broker import (
|
|
18
|
+
Poppable,
|
|
19
|
+
Readable,
|
|
20
|
+
Sendable,
|
|
21
|
+
)
|
|
22
|
+
from .Event import Event
|
|
23
|
+
from .Serializer import (
|
|
24
|
+
Decoder,
|
|
25
|
+
Encoder,
|
|
26
|
+
)
|
|
27
|
+
from .Subscription import Subscription
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
'Queue',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Queue(
|
|
35
|
+
Sendable,
|
|
36
|
+
Poppable,
|
|
37
|
+
Readable,
|
|
38
|
+
):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
connection: Union[Redis, RedisCluster],
|
|
42
|
+
encode: Encoder,
|
|
43
|
+
decode: Decoder,
|
|
44
|
+
name: str,
|
|
45
|
+
group: str = 'DefStream',
|
|
46
|
+
):
|
|
47
|
+
self.subscription = Subscription(
|
|
48
|
+
name,
|
|
49
|
+
group,
|
|
50
|
+
)
|
|
51
|
+
self.connection = connection
|
|
52
|
+
self.encode = encode
|
|
53
|
+
self.decode = decode
|
|
54
|
+
self.queue = name
|
|
55
|
+
self.group = group
|
|
56
|
+
self.consumer = uuid4().hex
|
|
57
|
+
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
return self.queue
|
|
60
|
+
|
|
61
|
+
def create_group(self) -> None:
|
|
62
|
+
try:
|
|
63
|
+
self.connection.xgroup_create(
|
|
64
|
+
self.queue,
|
|
65
|
+
self.group,
|
|
66
|
+
id='0-0',
|
|
67
|
+
mkstream=True,
|
|
68
|
+
)
|
|
69
|
+
except ResponseError as error:
|
|
70
|
+
if not str(error).startswith('BUSYGROUP '):
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
def send(
|
|
74
|
+
self,
|
|
75
|
+
body: Any,
|
|
76
|
+
) -> str:
|
|
77
|
+
id = self.connection.xadd(
|
|
78
|
+
self.queue,
|
|
79
|
+
{'body': self.encode(body)},
|
|
80
|
+
)
|
|
81
|
+
return id.decode('ascii')
|
|
82
|
+
|
|
83
|
+
def event(
|
|
84
|
+
self,
|
|
85
|
+
id: bytes,
|
|
86
|
+
fields: dict,
|
|
87
|
+
group: Optional[str] = None,
|
|
88
|
+
consumer: Optional[str] = None,
|
|
89
|
+
) -> Event:
|
|
90
|
+
return Event(
|
|
91
|
+
self.connection,
|
|
92
|
+
self.queue,
|
|
93
|
+
id.decode('ascii'),
|
|
94
|
+
self.decode(fields[b'body']),
|
|
95
|
+
group,
|
|
96
|
+
consumer,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def pop(
|
|
100
|
+
self,
|
|
101
|
+
timeout: Optional[int] = None,
|
|
102
|
+
) -> Optional[Event]:
|
|
103
|
+
if timeout is not None and timeout < 0:
|
|
104
|
+
raise ValueError('Redis pop timeout must be non-negative milliseconds')
|
|
105
|
+
self.create_group()
|
|
106
|
+
deadline = monotonic() + timeout / 1000 if timeout else None
|
|
107
|
+
while True:
|
|
108
|
+
messages = self.connection.xreadgroup(
|
|
109
|
+
self.group,
|
|
110
|
+
self.consumer,
|
|
111
|
+
{self.queue: '>'},
|
|
112
|
+
count=1,
|
|
113
|
+
)
|
|
114
|
+
for _, entries in messages:
|
|
115
|
+
for id, fields in entries:
|
|
116
|
+
return self.event(
|
|
117
|
+
id,
|
|
118
|
+
fields,
|
|
119
|
+
self.group,
|
|
120
|
+
self.consumer,
|
|
121
|
+
)
|
|
122
|
+
if deadline is not None and monotonic() >= deadline:
|
|
123
|
+
return None
|
|
124
|
+
sleep(0.01)
|
|
125
|
+
|
|
126
|
+
def read(
|
|
127
|
+
self,
|
|
128
|
+
offset: Union[int, str] = 0,
|
|
129
|
+
timeout: Optional[int] = None,
|
|
130
|
+
) -> Iterable[Event]:
|
|
131
|
+
if timeout is not None and timeout < 0:
|
|
132
|
+
raise ValueError('Redis read timeout must be non-negative milliseconds')
|
|
133
|
+
cursor = str(offset)
|
|
134
|
+
deadline = monotonic() + timeout / 1000 if timeout else None
|
|
135
|
+
while True:
|
|
136
|
+
if deadline is not None and monotonic() >= deadline:
|
|
137
|
+
return
|
|
138
|
+
messages = self.connection.xread(
|
|
139
|
+
{self.queue: cursor},
|
|
140
|
+
count=1,
|
|
141
|
+
)
|
|
142
|
+
if not messages:
|
|
143
|
+
sleep(0.01)
|
|
144
|
+
continue
|
|
145
|
+
for _, entries in messages:
|
|
146
|
+
for id, fields in entries:
|
|
147
|
+
cursor = id.decode('ascii')
|
|
148
|
+
yield self.event(
|
|
149
|
+
id,
|
|
150
|
+
fields,
|
|
151
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from json import (
|
|
2
|
+
dumps,
|
|
3
|
+
loads,
|
|
4
|
+
)
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Protocol,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
'Encoder',
|
|
12
|
+
'Decoder',
|
|
13
|
+
'JavaScriptObjectNotationEncoder',
|
|
14
|
+
'JavaScriptObjectNotationDecoder',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Encoder(Protocol):
|
|
19
|
+
def __call__(
|
|
20
|
+
self,
|
|
21
|
+
body: Any,
|
|
22
|
+
) -> bytes: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Decoder(Protocol):
|
|
26
|
+
def __call__(
|
|
27
|
+
self,
|
|
28
|
+
body: bytes,
|
|
29
|
+
) -> Any: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class JavaScriptObjectNotationEncoder(Encoder):
|
|
33
|
+
def __call__(
|
|
34
|
+
self,
|
|
35
|
+
body: Any,
|
|
36
|
+
) -> bytes:
|
|
37
|
+
return dumps(
|
|
38
|
+
body,
|
|
39
|
+
ensure_ascii=False,
|
|
40
|
+
).encode('utf-8')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class JavaScriptObjectNotationDecoder(Decoder):
|
|
44
|
+
def __call__(
|
|
45
|
+
self,
|
|
46
|
+
body: bytes,
|
|
47
|
+
) -> Any:
|
|
48
|
+
return loads(body)
|