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.
@@ -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)