DefStream 0.1.17__58-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.
Files changed (75) hide show
  1. DefStream/Broker/Configuration.py +18 -0
  2. DefStream/Broker/Connection.py +141 -0
  3. DefStream/Broker/Consumer.py +59 -0
  4. DefStream/Broker/Errors.py +14 -0
  5. DefStream/Broker/Event.py +35 -0
  6. DefStream/Broker/Implements/RabbitMQ/Configuration.py +48 -0
  7. DefStream/Broker/Implements/RabbitMQ/Connection.py +542 -0
  8. DefStream/Broker/Implements/RabbitMQ/Consumer.py +359 -0
  9. DefStream/Broker/Implements/RabbitMQ/Event.py +127 -0
  10. DefStream/Broker/Implements/RabbitMQ/Exchange.py +278 -0
  11. DefStream/Broker/Implements/RabbitMQ/Policies.py +9 -0
  12. DefStream/Broker/Implements/RabbitMQ/Queue.py +319 -0
  13. DefStream/Broker/Implements/RabbitMQ/Serializer.py +61 -0
  14. DefStream/Broker/Implements/RabbitMQ/Stream.py +179 -0
  15. DefStream/Broker/Implements/RabbitMQ/Subscription.py +74 -0
  16. DefStream/Broker/Implements/RabbitMQ/__init__.py +47 -0
  17. DefStream/Broker/Implements/Redis/Configuration.py +59 -0
  18. DefStream/Broker/Implements/Redis/Connection.py +265 -0
  19. DefStream/Broker/Implements/Redis/Consumer.py +364 -0
  20. DefStream/Broker/Implements/Redis/Event.py +191 -0
  21. DefStream/Broker/Implements/Redis/Queue.py +284 -0
  22. DefStream/Broker/Implements/Redis/Serializer.py +48 -0
  23. DefStream/Broker/Implements/Redis/Subscription.py +46 -0
  24. DefStream/Broker/Implements/Redis/__init__.py +29 -0
  25. DefStream/Broker/Implements/__init__.py +1 -0
  26. DefStream/Broker/Policies.py +25 -0
  27. DefStream/Broker/Priority.py +26 -0
  28. DefStream/Broker/Protocol.py +45 -0
  29. DefStream/Broker/__init__.py +59 -0
  30. DefStream/Descriptor/DataObject.py +409 -0
  31. DefStream/Descriptor/Types.py +248 -0
  32. DefStream/Descriptor/Value.py +188 -0
  33. DefStream/Descriptor/__init__.py +172 -0
  34. DefStream/Inspector/Extension.py +69 -0
  35. DefStream/Inspector/Extensions/Console.html +373 -0
  36. DefStream/Inspector/Extensions/Console.py +415 -0
  37. DefStream/Inspector/Extensions/Metrics.py +316 -0
  38. DefStream/Inspector/Extensions/Ping.py +107 -0
  39. DefStream/Inspector/Extensions/Specifications.py +176 -0
  40. DefStream/Inspector/Extensions/Status.html +206 -0
  41. DefStream/Inspector/Extensions/Status.py +124 -0
  42. DefStream/Inspector/Extensions/__init__.py +13 -0
  43. DefStream/Inspector/Inspector.py +347 -0
  44. DefStream/Inspector/Response.py +14 -0
  45. DefStream/Inspector/__init__.py +9 -0
  46. DefStream/Validator/Pattern.py +36 -0
  47. DefStream/Validator/Patterns/Array.py +30 -0
  48. DefStream/Validator/Patterns/Common.py +92 -0
  49. DefStream/Validator/Patterns/Compare.py +180 -0
  50. DefStream/Validator/Patterns/Condition.py +60 -0
  51. DefStream/Validator/Patterns/DataObject.py +85 -0
  52. DefStream/Validator/Patterns/DateTime.py +111 -0
  53. DefStream/Validator/Patterns/Number.py +43 -0
  54. DefStream/Validator/Patterns/Object.py +150 -0
  55. DefStream/Validator/Patterns/Size.py +110 -0
  56. DefStream/Validator/Patterns/String.py +85 -0
  57. DefStream/Validator/Patterns/Type.py +350 -0
  58. DefStream/Validator/Patterns/__init__.py +137 -0
  59. DefStream/Validator/Validate.py +29 -0
  60. DefStream/Validator/Validator.py +30 -0
  61. DefStream/Validator/__init__.py +11 -0
  62. DefStream/Worker/Executor.py +371 -0
  63. DefStream/Worker/Message.py +180 -0
  64. DefStream/Worker/Pool.py +245 -0
  65. DefStream/Worker/Run.py +357 -0
  66. DefStream/Worker/Specification.py +87 -0
  67. DefStream/Worker/Startup.py +174 -0
  68. DefStream/Worker/Worker.py +421 -0
  69. DefStream/Worker/__init__.py +47 -0
  70. DefStream/__init__.py +0 -0
  71. defstream-0.1.17.dist-info/METADATA +1174 -0
  72. defstream-0.1.17.dist-info/RECORD +75 -0
  73. defstream-0.1.17.dist-info/WHEEL +6 -0
  74. defstream-0.1.17.dist-info/licenses/LICENSE +674 -0
  75. defstream-0.1.17.dist-info/top_level.txt +1 -0
@@ -0,0 +1,284 @@
1
+ from logging import getLogger
2
+ from time import (
3
+ monotonic,
4
+ sleep,
5
+ )
6
+ from typing import (
7
+ Any,
8
+ Iterable,
9
+ Optional,
10
+ Union,
11
+ )
12
+ from uuid import uuid4
13
+
14
+ from redis import Redis
15
+ from redis.cluster import RedisCluster
16
+ from redis.exceptions import ResponseError
17
+
18
+ from DefStream.Broker import (
19
+ Poppable,
20
+ Readable,
21
+ Sendable,
22
+ )
23
+ from .Event import Event
24
+ from .Serializer import (
25
+ Decoder,
26
+ Encoder,
27
+ )
28
+ from .Subscription import Subscription
29
+
30
+ __all__ = (
31
+ 'Queue',
32
+ )
33
+
34
+
35
+ class Queue(
36
+ Sendable,
37
+ Poppable,
38
+ Readable,
39
+ ):
40
+ logger = getLogger('DefStream.Broker.Implements.Redis.Queue')
41
+
42
+ def __init__(
43
+ self,
44
+ connection: Union[Redis, RedisCluster],
45
+ encode: Encoder,
46
+ decode: Decoder,
47
+ name: str,
48
+ group: str = 'DefStream',
49
+ ):
50
+ self.subscription = Subscription(
51
+ name,
52
+ group,
53
+ )
54
+ self.connection = connection
55
+ self.encode = encode
56
+ self.decode = decode
57
+ self.queue = name
58
+ self.group = group
59
+ self.consumer = uuid4().hex
60
+
61
+ def __str__(self) -> str:
62
+ return self.queue
63
+
64
+ def create_group(self) -> None:
65
+ self.logger.debug(
66
+ 'Creating Consumer Group: queue=%s group=%s',
67
+ self.queue,
68
+ self.group,
69
+ )
70
+ try:
71
+ self.connection.xgroup_create(
72
+ self.queue,
73
+ self.group,
74
+ id='0-0',
75
+ mkstream=True,
76
+ )
77
+ except ResponseError as error:
78
+ if not str(error).startswith('BUSYGROUP '):
79
+ self.logger.error(
80
+ 'Consumer Group creation failed: queue=%s group=%s error_type=%s',
81
+ self.queue,
82
+ self.group,
83
+ type(error).__name__,
84
+ )
85
+ raise
86
+ self.logger.debug(
87
+ 'Reusing existing Consumer Group: queue=%s group=%s',
88
+ self.queue,
89
+ self.group,
90
+ )
91
+ return
92
+ except Exception as error:
93
+ self.logger.error(
94
+ 'Consumer Group creation failed: queue=%s group=%s error_type=%s',
95
+ self.queue,
96
+ self.group,
97
+ type(error).__name__,
98
+ )
99
+ raise
100
+ self.logger.info(
101
+ 'Consumer Group creation completed: queue=%s group=%s',
102
+ self.queue,
103
+ self.group,
104
+ )
105
+
106
+ def send(
107
+ self,
108
+ body: Any,
109
+ ) -> str:
110
+ self.logger.debug(
111
+ 'Publishing Redis Stream message: queue=%s',
112
+ self.queue,
113
+ )
114
+ try:
115
+ id = self.connection.xadd(
116
+ self.queue,
117
+ {'body': self.encode(body)},
118
+ )
119
+ message_id = id.decode('ascii')
120
+ except Exception as error:
121
+ self.logger.error(
122
+ 'Redis Stream publication or serialization failed: queue=%s error_type=%s',
123
+ self.queue,
124
+ type(error).__name__,
125
+ )
126
+ raise
127
+ self.logger.debug(
128
+ 'Redis Stream message published: queue=%s id=%s',
129
+ self.queue,
130
+ message_id,
131
+ )
132
+ return message_id
133
+
134
+ def event(
135
+ self,
136
+ id: bytes,
137
+ fields: dict,
138
+ group: Optional[str] = None,
139
+ consumer: Optional[str] = None,
140
+ ) -> Event:
141
+ try:
142
+ message_id = id.decode('ascii')
143
+ body = self.decode(fields[b'body'])
144
+ except Exception as error:
145
+ self.logger.error(
146
+ 'Redis message deserialization or Event conversion failed: queue=%s group=%s id=%s error_type=%s',
147
+ self.queue,
148
+ group,
149
+ id,
150
+ type(error).__name__,
151
+ )
152
+ raise
153
+ self.logger.debug(
154
+ 'Converting message to Event: queue=%s group=%s consumer=%s id=%s',
155
+ self.queue,
156
+ group,
157
+ consumer,
158
+ message_id,
159
+ )
160
+ return Event(
161
+ self.connection,
162
+ self.queue,
163
+ message_id,
164
+ body,
165
+ group,
166
+ consumer,
167
+ )
168
+
169
+ def pop(
170
+ self,
171
+ timeout: Optional[int] = None,
172
+ ) -> Optional[Event]:
173
+ if timeout is not None and timeout < 0:
174
+ raise ValueError('Redis pop timeout must be non-negative milliseconds')
175
+ self.logger.debug(
176
+ 'Starting group-based single-message reception: queue=%s group=%s consumer=%s timeout=%sms',
177
+ self.queue,
178
+ self.group,
179
+ self.consumer,
180
+ timeout,
181
+ )
182
+ self.create_group()
183
+ deadline = monotonic() + timeout / 1000 if timeout else None
184
+ while True:
185
+ try:
186
+ messages = self.connection.xreadgroup(
187
+ self.group,
188
+ self.consumer,
189
+ {self.queue: '>'},
190
+ count=1,
191
+ )
192
+ except Exception as error:
193
+ self.logger.error(
194
+ 'Group-based single-message reception failed: queue=%s group=%s error_type=%s',
195
+ self.queue,
196
+ self.group,
197
+ type(error).__name__,
198
+ )
199
+ raise
200
+ for _, entries in messages:
201
+ for id, fields in entries:
202
+ event = self.event(
203
+ id,
204
+ fields,
205
+ self.group,
206
+ self.consumer,
207
+ )
208
+ self.logger.debug(
209
+ 'Group message received: queue=%s group=%s id=%s',
210
+ self.queue,
211
+ self.group,
212
+ event.id,
213
+ )
214
+ return event
215
+ if deadline is not None and monotonic() >= deadline:
216
+ self.logger.debug(
217
+ 'Group-based single-message wait timed out: queue=%s group=%s timeout=%sms',
218
+ self.queue,
219
+ self.group,
220
+ timeout,
221
+ )
222
+ return None
223
+ sleep(0.01)
224
+
225
+ def read(
226
+ self,
227
+ offset: Union[int, str] = 0,
228
+ timeout: Optional[int] = None,
229
+ ) -> Iterable[Event]:
230
+ if timeout is not None and timeout < 0:
231
+ raise ValueError('Redis read timeout must be non-negative milliseconds')
232
+ cursor = str(offset)
233
+ deadline = monotonic() + timeout / 1000 if timeout else None
234
+ self.logger.debug(
235
+ 'Starting history read without a consumer group: queue=%s offset=%s timeout=%sms',
236
+ self.queue,
237
+ cursor,
238
+ timeout,
239
+ )
240
+ try:
241
+ while True:
242
+ if deadline is not None and monotonic() >= deadline:
243
+ self.logger.debug(
244
+ 'History read without a consumer group timed out: queue=%s cursor=%s timeout=%sms',
245
+ self.queue,
246
+ cursor,
247
+ timeout,
248
+ )
249
+ return
250
+ try:
251
+ messages = self.connection.xread(
252
+ {self.queue: cursor},
253
+ count=1,
254
+ )
255
+ except Exception as error:
256
+ self.logger.error(
257
+ 'History read without a consumer group failed: queue=%s cursor=%s error_type=%s',
258
+ self.queue,
259
+ cursor,
260
+ type(error).__name__,
261
+ )
262
+ raise
263
+ if not messages:
264
+ sleep(0.01)
265
+ continue
266
+ for _, entries in messages:
267
+ for id, fields in entries:
268
+ event = self.event(
269
+ id,
270
+ fields,
271
+ )
272
+ cursor = event.id
273
+ self.logger.debug(
274
+ 'History message received without a consumer group: queue=%s id=%s',
275
+ self.queue,
276
+ event.id,
277
+ )
278
+ yield event
279
+ finally:
280
+ self.logger.debug(
281
+ 'History read without a consumer group ended: queue=%s cursor=%s',
282
+ self.queue,
283
+ cursor,
284
+ )
@@ -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)
@@ -0,0 +1,46 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+ __all__ = (
6
+ 'Subscription',
7
+ )
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Subscription:
12
+ queue: str
13
+ group: str = 'DefStream'
14
+
15
+ def __post_init__(self) -> None:
16
+ for value in (
17
+ self.queue,
18
+ self.group,
19
+ ):
20
+ if not isinstance(
21
+ value,
22
+ str,
23
+ ) or not value:
24
+ raise ValueError('Redis queue and group must be non-empty strings')
25
+
26
+ @classmethod
27
+ def parse(
28
+ cls,
29
+ parameters: Any,
30
+ ) -> 'Subscription':
31
+ if isinstance(
32
+ parameters,
33
+ cls,
34
+ ):
35
+ return parameters
36
+ if isinstance(
37
+ parameters,
38
+ str,
39
+ ):
40
+ return cls(parameters)
41
+ if isinstance(
42
+ parameters,
43
+ Mapping,
44
+ ):
45
+ return cls(**dict(parameters))
46
+ raise TypeError('Redis subscriptions require a queue name, Subscription or mapping')
@@ -0,0 +1,29 @@
1
+ from DefStream.Broker import (
2
+ EventHandler,
3
+ Reject,
4
+ Requeue,
5
+ )
6
+ from .Configuration import (
7
+ Configuration,
8
+ ConnectionType,
9
+ )
10
+ from .Connection import (
11
+ Connection,
12
+ )
13
+ from .Consumer import Consumer
14
+ from .Event import Event
15
+ from .Queue import Queue
16
+ from .Subscription import Subscription
17
+
18
+ __all__ = (
19
+ 'Connection',
20
+ 'Configuration',
21
+ 'ConnectionType',
22
+ 'Consumer',
23
+ 'Event',
24
+ 'EventHandler',
25
+ 'Queue',
26
+ 'Subscription',
27
+ 'Reject',
28
+ 'Requeue',
29
+ )
@@ -0,0 +1 @@
1
+ # -*- coding: utf-8 -*-
@@ -0,0 +1,25 @@
1
+ from .Event import (
2
+ Event,
3
+ EventError,
4
+ )
5
+
6
+ __all__ = (
7
+ 'Requeue',
8
+ 'Reject',
9
+ )
10
+
11
+
12
+ class Requeue(EventError):
13
+ def __call__(
14
+ self,
15
+ event: Event,
16
+ ) -> None:
17
+ event.nack()
18
+
19
+
20
+ class Reject(EventError):
21
+ def __call__(
22
+ self,
23
+ event: Event,
24
+ ) -> None:
25
+ event.reject()
@@ -0,0 +1,26 @@
1
+ from typing import Optional
2
+
3
+
4
+ def validate_priority(
5
+ value: Optional[int],
6
+ maximum: Optional[int],
7
+ *,
8
+ minimum: int = 0,
9
+ name: str = 'priority',
10
+ ) -> None:
11
+ """우선순위 타입과 범위를 검증하며, 최대값이 없으면 하한만 검사한다."""
12
+ if value is None:
13
+ return
14
+ if isinstance(
15
+ value,
16
+ bool,
17
+ ) or not isinstance(
18
+ value,
19
+ int,
20
+ ):
21
+ raise TypeError(name + ' must be an integer or None')
22
+ if value < minimum:
23
+ raise ValueError(f'{name} must be at least {minimum}')
24
+ if maximum is not None and value > maximum:
25
+ raise ValueError(f'{name} must be between {minimum} and {maximum}')
26
+ return
@@ -0,0 +1,45 @@
1
+ from typing import (
2
+ Any,
3
+ Iterable,
4
+ Optional,
5
+ Protocol,
6
+ )
7
+
8
+ from .Event import Event
9
+
10
+ __all__ = (
11
+ 'Sendable',
12
+ 'Callable',
13
+ 'Poppable',
14
+ 'Readable',
15
+ )
16
+
17
+
18
+ class Sendable(Protocol):
19
+ def send(
20
+ self,
21
+ body: Any,
22
+ ) -> Any: ...
23
+
24
+
25
+ class Callable(Protocol):
26
+ def call(
27
+ self,
28
+ body: Any,
29
+ timeout: float = 5,
30
+ ) -> Any: ...
31
+
32
+
33
+ class Poppable(Protocol):
34
+ def pop(
35
+ self,
36
+ timeout: Optional[int] = None,
37
+ ) -> Optional[Event]: ...
38
+
39
+
40
+ class Readable(Protocol):
41
+ def read(
42
+ self,
43
+ offset: int = 0,
44
+ timeout: Optional[int] = None,
45
+ ) -> Iterable[Event]: ...
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .Configuration import Configuration
4
+ from .Connection import (
5
+ Connection,
6
+ BrokerFactory,
7
+ GetExchange,
8
+ GetQueue,
9
+ GetStream,
10
+ GetConsumer,
11
+ Declarable,
12
+ Emittable,
13
+ Requestable,
14
+ PrioritySupport,
15
+ )
16
+ from .Protocol import (
17
+ Sendable,
18
+ Callable,
19
+ Poppable,
20
+ Readable,
21
+ )
22
+ from .Errors import RemoteError
23
+ from .Consumer import Consumer
24
+ from .Policies import (
25
+ Requeue,
26
+ Reject,
27
+ )
28
+ from .Priority import validate_priority
29
+ from .Event import (
30
+ Event,
31
+ EventHandler,
32
+ EventError,
33
+ )
34
+
35
+ __all__ = (
36
+ 'Configuration',
37
+ 'Connection',
38
+ 'BrokerFactory',
39
+ 'GetExchange',
40
+ 'GetQueue',
41
+ 'GetStream',
42
+ 'GetConsumer',
43
+ 'Declarable',
44
+ 'Emittable',
45
+ 'Requestable',
46
+ 'PrioritySupport',
47
+ 'Sendable',
48
+ 'Callable',
49
+ 'RemoteError',
50
+ 'Poppable',
51
+ 'Readable',
52
+ 'Consumer',
53
+ 'EventHandler',
54
+ 'Event',
55
+ 'EventError',
56
+ 'Requeue',
57
+ 'Reject',
58
+ 'validate_priority',
59
+ )