DefStream 0.1.17__70-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 +92 -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
DefStream/Worker/Run.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
ABCMeta,
|
|
4
|
+
abstractmethod,
|
|
5
|
+
)
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from re import fullmatch
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Dict,
|
|
13
|
+
Optional,
|
|
14
|
+
Sequence,
|
|
15
|
+
Tuple,
|
|
16
|
+
Union,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ..Broker.Event import EventError
|
|
20
|
+
from ..Broker.Priority import validate_priority
|
|
21
|
+
from .Specification import Specification
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
'Context',
|
|
25
|
+
'RunSetup',
|
|
26
|
+
'RunComplete',
|
|
27
|
+
'RunError',
|
|
28
|
+
'Run',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def priority_setting(
|
|
33
|
+
parameters: Any,
|
|
34
|
+
name: str,
|
|
35
|
+
) -> Optional[int]:
|
|
36
|
+
if isinstance(
|
|
37
|
+
parameters,
|
|
38
|
+
Mapping,
|
|
39
|
+
):
|
|
40
|
+
return parameters.get(name)
|
|
41
|
+
return getattr(
|
|
42
|
+
parameters,
|
|
43
|
+
name,
|
|
44
|
+
None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Context:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*args,
|
|
52
|
+
**kwargs,
|
|
53
|
+
):
|
|
54
|
+
self.args = args
|
|
55
|
+
self.kwargs = kwargs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RunSetup(ABC):
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def __call__(
|
|
61
|
+
self,
|
|
62
|
+
context: Context,
|
|
63
|
+
) -> None:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RunComplete(ABC):
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
context: Context,
|
|
72
|
+
completion: Any,
|
|
73
|
+
) -> None:
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RunError(ABC):
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def __call__(
|
|
80
|
+
self,
|
|
81
|
+
context: Context,
|
|
82
|
+
error: Exception,
|
|
83
|
+
) -> None:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RunMeta(ABCMeta):
|
|
88
|
+
"""구버전 ABCMeta의 클래스 이름 인자와 Run의 name 키워드 충돌을 방지한다."""
|
|
89
|
+
|
|
90
|
+
def __new__(
|
|
91
|
+
mcls,
|
|
92
|
+
class_name: str,
|
|
93
|
+
bases: Tuple[type, ...],
|
|
94
|
+
namespace: Dict[str, Any],
|
|
95
|
+
/,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> 'RunMeta':
|
|
98
|
+
if 'name' in kwargs:
|
|
99
|
+
kwargs['run_name'] = kwargs.pop('name')
|
|
100
|
+
return super().__new__(
|
|
101
|
+
mcls,
|
|
102
|
+
class_name,
|
|
103
|
+
bases,
|
|
104
|
+
namespace,
|
|
105
|
+
**kwargs,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Run(ABC, metaclass=RunMeta):
|
|
110
|
+
logger = getLogger('DefStream.Run')
|
|
111
|
+
parameters: Any = None
|
|
112
|
+
name: str = __module__ + '.' + __qualname__
|
|
113
|
+
description: Optional[str] = None
|
|
114
|
+
on_error: Optional[EventError] = None
|
|
115
|
+
setups: Tuple[RunSetup, ...] = ()
|
|
116
|
+
completes: Tuple[RunComplete, ...] = ()
|
|
117
|
+
errors: Tuple[RunError, ...] = ()
|
|
118
|
+
request: Optional[Specification] = None
|
|
119
|
+
response: Optional[Specification] = None
|
|
120
|
+
|
|
121
|
+
def __init_subclass__(
|
|
122
|
+
cls,
|
|
123
|
+
*,
|
|
124
|
+
run_name: Optional[str] = None,
|
|
125
|
+
description: Optional[str] = None,
|
|
126
|
+
parameters: Any = None,
|
|
127
|
+
on_error: Optional[EventError] = None,
|
|
128
|
+
setups: Optional[Union[RunSetup, Sequence[RunSetup]]] = None,
|
|
129
|
+
completes: Optional[Union[RunComplete, Sequence[RunComplete]]] = None,
|
|
130
|
+
errors: Optional[Union[RunError, Sequence[RunError]]] = None,
|
|
131
|
+
request: Optional[Specification] = None,
|
|
132
|
+
response: Optional[Specification] = None,
|
|
133
|
+
**kwargs,
|
|
134
|
+
):
|
|
135
|
+
super().__init_subclass__(**kwargs)
|
|
136
|
+
if run_name is not None and not isinstance(
|
|
137
|
+
run_name,
|
|
138
|
+
str,
|
|
139
|
+
):
|
|
140
|
+
raise TypeError('Run name must be a string or None')
|
|
141
|
+
if run_name is not None and fullmatch(
|
|
142
|
+
r'[A-Za-z0-9_.-]+',
|
|
143
|
+
run_name,
|
|
144
|
+
) is None:
|
|
145
|
+
raise ValueError('Run name must contain only ASCII letters, digits, underscores, dots or hyphens')
|
|
146
|
+
if description is not None and not isinstance(
|
|
147
|
+
description,
|
|
148
|
+
str,
|
|
149
|
+
):
|
|
150
|
+
raise TypeError('Run description must be a string or None')
|
|
151
|
+
cls.name = run_name if run_name is not None else cls.__module__ + '.' + cls.__qualname__
|
|
152
|
+
cls.description = description
|
|
153
|
+
parents = tuple(base for base in cls.__mro__[1:] if issubclass(
|
|
154
|
+
base,
|
|
155
|
+
Run,
|
|
156
|
+
))
|
|
157
|
+
if parameters is not None:
|
|
158
|
+
cls.parameters = parameters
|
|
159
|
+
for name in (
|
|
160
|
+
'priority',
|
|
161
|
+
'max_priority',
|
|
162
|
+
):
|
|
163
|
+
if any(priority_setting(
|
|
164
|
+
base.parameters,
|
|
165
|
+
name,
|
|
166
|
+
) is not None for base in parents) and priority_setting(
|
|
167
|
+
cls.parameters,
|
|
168
|
+
name,
|
|
169
|
+
) is None:
|
|
170
|
+
raise ValueError('Inherited task ' + name + ' cannot be cleared')
|
|
171
|
+
maximum = priority_setting(
|
|
172
|
+
cls.parameters,
|
|
173
|
+
'max_priority',
|
|
174
|
+
)
|
|
175
|
+
validate_priority(
|
|
176
|
+
maximum,
|
|
177
|
+
None,
|
|
178
|
+
minimum=1,
|
|
179
|
+
name='max_priority',
|
|
180
|
+
)
|
|
181
|
+
validate_priority(
|
|
182
|
+
priority_setting(
|
|
183
|
+
cls.parameters,
|
|
184
|
+
'priority',
|
|
185
|
+
),
|
|
186
|
+
maximum,
|
|
187
|
+
)
|
|
188
|
+
for name, specification in (
|
|
189
|
+
(
|
|
190
|
+
'request',
|
|
191
|
+
request,
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
'response',
|
|
195
|
+
response,
|
|
196
|
+
),
|
|
197
|
+
):
|
|
198
|
+
if specification is not None:
|
|
199
|
+
if not isinstance(
|
|
200
|
+
specification,
|
|
201
|
+
Specification,
|
|
202
|
+
):
|
|
203
|
+
raise TypeError(name + ' must be a Specification')
|
|
204
|
+
setattr(
|
|
205
|
+
cls,
|
|
206
|
+
name,
|
|
207
|
+
specification,
|
|
208
|
+
)
|
|
209
|
+
if on_error is not None:
|
|
210
|
+
if not callable(on_error):
|
|
211
|
+
raise TypeError('on_error must be a callable event error policy')
|
|
212
|
+
cls.on_error = on_error
|
|
213
|
+
if setups is not None:
|
|
214
|
+
cls.setups = tuple(setups) if isinstance(
|
|
215
|
+
setups,
|
|
216
|
+
Sequence,
|
|
217
|
+
) else (setups,)
|
|
218
|
+
if completes is not None:
|
|
219
|
+
cls.completes = tuple(completes) if isinstance(
|
|
220
|
+
completes,
|
|
221
|
+
Sequence,
|
|
222
|
+
) else (completes,)
|
|
223
|
+
if errors is not None:
|
|
224
|
+
cls.errors = tuple(errors) if isinstance(
|
|
225
|
+
errors,
|
|
226
|
+
Sequence,
|
|
227
|
+
) else (errors,)
|
|
228
|
+
for hooks in (
|
|
229
|
+
cls.setups,
|
|
230
|
+
cls.completes,
|
|
231
|
+
cls.errors,
|
|
232
|
+
):
|
|
233
|
+
if not all(callable(hook) for hook in hooks):
|
|
234
|
+
raise TypeError('Run hooks must be callable')
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
def __call__(
|
|
238
|
+
self,
|
|
239
|
+
*args,
|
|
240
|
+
**kwargs,
|
|
241
|
+
) -> Any:
|
|
242
|
+
task = type(self).__module__ + '.' + type(self).__qualname__
|
|
243
|
+
started = monotonic()
|
|
244
|
+
self.logger.debug(
|
|
245
|
+
'Starting task execution: task=%s',
|
|
246
|
+
task,
|
|
247
|
+
)
|
|
248
|
+
context = Context(
|
|
249
|
+
*args,
|
|
250
|
+
**kwargs,
|
|
251
|
+
)
|
|
252
|
+
for setup in self.setups:
|
|
253
|
+
self.logger.debug(
|
|
254
|
+
'Starting task setup: task=%s hook=%s',
|
|
255
|
+
task,
|
|
256
|
+
type(setup).__qualname__,
|
|
257
|
+
)
|
|
258
|
+
setup(context)
|
|
259
|
+
self.logger.debug(
|
|
260
|
+
'Task setup completed: task=%s hook=%s',
|
|
261
|
+
task,
|
|
262
|
+
type(setup).__qualname__,
|
|
263
|
+
)
|
|
264
|
+
try:
|
|
265
|
+
if self.request is not None:
|
|
266
|
+
self.logger.debug(
|
|
267
|
+
'Validating task input: task=%s',
|
|
268
|
+
task,
|
|
269
|
+
)
|
|
270
|
+
if context.args:
|
|
271
|
+
raise TypeError('A specified task request requires keyword arguments')
|
|
272
|
+
body = self.request(context.kwargs)
|
|
273
|
+
if not isinstance(
|
|
274
|
+
body,
|
|
275
|
+
Mapping,
|
|
276
|
+
) or not all(isinstance(
|
|
277
|
+
key,
|
|
278
|
+
str,
|
|
279
|
+
) for key in body):
|
|
280
|
+
raise TypeError('A task request must remain a mapping with string keys')
|
|
281
|
+
context.kwargs = dict(body)
|
|
282
|
+
self.logger.debug(
|
|
283
|
+
'Task input validated: task=%s',
|
|
284
|
+
task,
|
|
285
|
+
)
|
|
286
|
+
self.logger.debug(
|
|
287
|
+
'Calling user task: task=%s',
|
|
288
|
+
task,
|
|
289
|
+
)
|
|
290
|
+
result = self.run(
|
|
291
|
+
*context.args,
|
|
292
|
+
**context.kwargs,
|
|
293
|
+
)
|
|
294
|
+
self.logger.debug(
|
|
295
|
+
'User task returned: task=%s',
|
|
296
|
+
task,
|
|
297
|
+
)
|
|
298
|
+
if self.response is not None:
|
|
299
|
+
self.logger.debug(
|
|
300
|
+
'Validating task output: task=%s',
|
|
301
|
+
task,
|
|
302
|
+
)
|
|
303
|
+
result = self.response(result)
|
|
304
|
+
self.logger.debug(
|
|
305
|
+
'Task output validated: task=%s',
|
|
306
|
+
task,
|
|
307
|
+
)
|
|
308
|
+
except Exception as error:
|
|
309
|
+
self.logger.debug(
|
|
310
|
+
'Task execution or validation failed: task=%s error_type=%s',
|
|
311
|
+
task,
|
|
312
|
+
type(error).__qualname__,
|
|
313
|
+
)
|
|
314
|
+
for handler in self.errors:
|
|
315
|
+
self.logger.debug(
|
|
316
|
+
'Starting task error hook: task=%s hook=%s',
|
|
317
|
+
task,
|
|
318
|
+
type(handler).__qualname__,
|
|
319
|
+
)
|
|
320
|
+
handler(
|
|
321
|
+
context,
|
|
322
|
+
error,
|
|
323
|
+
)
|
|
324
|
+
self.logger.debug(
|
|
325
|
+
'Task error hook completed: task=%s hook=%s',
|
|
326
|
+
task,
|
|
327
|
+
type(handler).__qualname__,
|
|
328
|
+
)
|
|
329
|
+
raise
|
|
330
|
+
for complete in self.completes:
|
|
331
|
+
self.logger.debug(
|
|
332
|
+
'Starting task completion hook: task=%s hook=%s',
|
|
333
|
+
task,
|
|
334
|
+
type(complete).__qualname__,
|
|
335
|
+
)
|
|
336
|
+
complete(
|
|
337
|
+
context,
|
|
338
|
+
result,
|
|
339
|
+
)
|
|
340
|
+
self.logger.debug(
|
|
341
|
+
'Task completion hook completed: task=%s hook=%s',
|
|
342
|
+
task,
|
|
343
|
+
type(complete).__qualname__,
|
|
344
|
+
)
|
|
345
|
+
self.logger.debug(
|
|
346
|
+
'Task execution completed: task=%s duration=%s',
|
|
347
|
+
task,
|
|
348
|
+
monotonic() - started,
|
|
349
|
+
)
|
|
350
|
+
return result
|
|
351
|
+
|
|
352
|
+
@abstractmethod
|
|
353
|
+
def run(
|
|
354
|
+
self,
|
|
355
|
+
**kwargs,
|
|
356
|
+
) -> Any:
|
|
357
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from dataclasses import is_dataclass
|
|
4
|
+
from re import fullmatch
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Callable,
|
|
8
|
+
Optional,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..Descriptor import (
|
|
12
|
+
DumpSchema,
|
|
13
|
+
Schema,
|
|
14
|
+
ToSchema,
|
|
15
|
+
)
|
|
16
|
+
from ..Descriptor.Value import (
|
|
17
|
+
Condition,
|
|
18
|
+
Descriptor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
'Specification',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Specification:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
schema: Any = None,
|
|
30
|
+
validator: Optional[Callable[[Any], Any]] = None,
|
|
31
|
+
example: Any = ...,
|
|
32
|
+
format: str = 'application/json',
|
|
33
|
+
):
|
|
34
|
+
if not isinstance(
|
|
35
|
+
format,
|
|
36
|
+
str,
|
|
37
|
+
) or fullmatch(
|
|
38
|
+
r"[!#$%&'+.^_`|~0-9A-Za-z-]+/[!#$%&'+.^_`|~0-9A-Za-z-]+(?:[ \t]*;[^\r\n]+)?",
|
|
39
|
+
format,
|
|
40
|
+
) is None:
|
|
41
|
+
raise ValueError('Specification format must be a concrete media type')
|
|
42
|
+
if validator is not None and not callable(validator):
|
|
43
|
+
raise TypeError('A Specification validator must be callable')
|
|
44
|
+
if isinstance(
|
|
45
|
+
schema,
|
|
46
|
+
type,
|
|
47
|
+
) and is_dataclass(schema):
|
|
48
|
+
schema = ToSchema(schema)
|
|
49
|
+
if schema is not None and not isinstance(
|
|
50
|
+
schema,
|
|
51
|
+
(
|
|
52
|
+
Schema,
|
|
53
|
+
Descriptor,
|
|
54
|
+
Condition,
|
|
55
|
+
Mapping,
|
|
56
|
+
),
|
|
57
|
+
):
|
|
58
|
+
raise TypeError('A Descriptor schema, schema mapping or dataclass type is required')
|
|
59
|
+
self.schema = schema
|
|
60
|
+
self.validator = validator
|
|
61
|
+
self.example = example
|
|
62
|
+
self.format = format
|
|
63
|
+
self.document()
|
|
64
|
+
|
|
65
|
+
def __call__(
|
|
66
|
+
self,
|
|
67
|
+
value: Any,
|
|
68
|
+
) -> Any:
|
|
69
|
+
return self.validator(value) if self.validator is not None else value
|
|
70
|
+
|
|
71
|
+
def document(self) -> dict:
|
|
72
|
+
payload = {} if self.schema is None else DumpSchema(
|
|
73
|
+
self.schema if isinstance(
|
|
74
|
+
self.schema,
|
|
75
|
+
Schema,
|
|
76
|
+
) else Schema(
|
|
77
|
+
'Payload',
|
|
78
|
+
self.schema,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
message = {
|
|
82
|
+
'contentType': self.format,
|
|
83
|
+
'payload': payload,
|
|
84
|
+
}
|
|
85
|
+
if self.example is not ...:
|
|
86
|
+
message['examples'] = [{'payload': deepcopy(self.example)}]
|
|
87
|
+
return message
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from os import environ, getpid
|
|
3
|
+
import sys
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .Worker import Worker
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'startup',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOGO = r"""
|
|
18
|
+
▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄
|
|
19
|
+
███▀▀██▄ ██ █████▀▀▀ ██
|
|
20
|
+
███ ███ ▄█▀█▄ ▀██▀ ▀████▄ ▀██▀▀ ████▄ ▄█▀█▄ ▀▀█▄ ███▄███▄
|
|
21
|
+
███ ███ ██▄█▀ ██ ▀████ ██ ██ ▀▀ ██▄█▀ ▄█▀██ ██ ██ ██
|
|
22
|
+
██████▀ ▀█▄▄▄ ██ ███████▀ ██ ██ ▀█▄▄▄ ▀█▄██ ██ ██ ██
|
|
23
|
+
|
|
24
|
+
Just def. Let workers do the work. You don't need to await this.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def supports_color() -> bool:
|
|
29
|
+
return (
|
|
30
|
+
getattr(sys.stdout, 'isatty', lambda: False)()
|
|
31
|
+
and not environ.get('NO_COLOR')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def colorize_logo(logo: str) -> str:
|
|
36
|
+
colors = (
|
|
37
|
+
'\033[38;5;39m',
|
|
38
|
+
'\033[38;5;45m',
|
|
39
|
+
'\033[38;5;51m',
|
|
40
|
+
'\033[38;5;45m',
|
|
41
|
+
'\033[38;5;39m',
|
|
42
|
+
)
|
|
43
|
+
reset = '\033[0m'
|
|
44
|
+
colorized_lines = []
|
|
45
|
+
art_line = 0
|
|
46
|
+
for line in logo.splitlines(keepends=True):
|
|
47
|
+
if line.strip() and art_line < len(colors):
|
|
48
|
+
newline = '\n' if line.endswith('\n') else ''
|
|
49
|
+
colorized_lines.append(
|
|
50
|
+
f'{colors[art_line]}{line[:-1] if newline else line}'
|
|
51
|
+
f'{reset}{newline}',
|
|
52
|
+
)
|
|
53
|
+
art_line += 1
|
|
54
|
+
else:
|
|
55
|
+
colorized_lines.append(line)
|
|
56
|
+
return ''.join(colorized_lines)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def subscription_value(
|
|
60
|
+
parameters: Any,
|
|
61
|
+
name: str,
|
|
62
|
+
) -> Any:
|
|
63
|
+
if isinstance(
|
|
64
|
+
parameters,
|
|
65
|
+
str,
|
|
66
|
+
):
|
|
67
|
+
return parameters if name == 'queue' else None
|
|
68
|
+
if isinstance(
|
|
69
|
+
parameters,
|
|
70
|
+
Mapping,
|
|
71
|
+
):
|
|
72
|
+
return parameters.get(name)
|
|
73
|
+
return getattr(
|
|
74
|
+
parameters,
|
|
75
|
+
name,
|
|
76
|
+
None,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def startup(worker: 'Worker') -> None:
|
|
81
|
+
broker = type(worker.broker)
|
|
82
|
+
broker_name = broker.__qualname__
|
|
83
|
+
for base in broker.__mro__:
|
|
84
|
+
if '.Broker.Implements.' in base.__module__:
|
|
85
|
+
broker_name = base.__module__.split('.Broker.Implements.')[1].split('.')[0]
|
|
86
|
+
break
|
|
87
|
+
pool = type(worker.pool)
|
|
88
|
+
conf = getattr(
|
|
89
|
+
worker.broker,
|
|
90
|
+
'conf',
|
|
91
|
+
None,
|
|
92
|
+
)
|
|
93
|
+
host = getattr(
|
|
94
|
+
conf,
|
|
95
|
+
'host',
|
|
96
|
+
None,
|
|
97
|
+
)
|
|
98
|
+
port = getattr(
|
|
99
|
+
conf,
|
|
100
|
+
'port',
|
|
101
|
+
None,
|
|
102
|
+
)
|
|
103
|
+
if isinstance(
|
|
104
|
+
host,
|
|
105
|
+
str,
|
|
106
|
+
):
|
|
107
|
+
address = f'[{host}]' if ':' in host and not host.startswith('[') else host
|
|
108
|
+
broker_name += f'://{address}'
|
|
109
|
+
if isinstance(
|
|
110
|
+
port,
|
|
111
|
+
(
|
|
112
|
+
str,
|
|
113
|
+
int,
|
|
114
|
+
),
|
|
115
|
+
):
|
|
116
|
+
broker_name += f':{port}'
|
|
117
|
+
vhost = getattr(
|
|
118
|
+
conf,
|
|
119
|
+
'vhost',
|
|
120
|
+
None,
|
|
121
|
+
)
|
|
122
|
+
if isinstance(
|
|
123
|
+
vhost,
|
|
124
|
+
str,
|
|
125
|
+
) and vhost:
|
|
126
|
+
broker_name += '/' + vhost.lstrip('/')
|
|
127
|
+
logo = LOGO.rstrip()
|
|
128
|
+
starting_worker = 'Starting Worker'
|
|
129
|
+
if supports_color():
|
|
130
|
+
logo = colorize_logo(logo)
|
|
131
|
+
starting_worker = f'\033[1;38;5;51m{starting_worker}\033[0m'
|
|
132
|
+
lines = [
|
|
133
|
+
logo,
|
|
134
|
+
'',
|
|
135
|
+
starting_worker,
|
|
136
|
+
f' PID: {getpid()}',
|
|
137
|
+
f' Broker: {broker_name}',
|
|
138
|
+
]
|
|
139
|
+
lines.extend((
|
|
140
|
+
f' Pool: {pool.__name__} (concurrency={worker.pool.size()})',
|
|
141
|
+
f' Tasks: {len(worker.tasks)} tasks',
|
|
142
|
+
))
|
|
143
|
+
queues = []
|
|
144
|
+
for task in worker.tasks:
|
|
145
|
+
name = task.__qualname__
|
|
146
|
+
queue = subscription_value(
|
|
147
|
+
task.parameters,
|
|
148
|
+
'queue',
|
|
149
|
+
)
|
|
150
|
+
lines.append(f' - {name}')
|
|
151
|
+
lines.append(f' queue={queue!r}' if queue is not None else ' queue=<adapter-defined>')
|
|
152
|
+
if queue is not None and queue not in queues:
|
|
153
|
+
queues.append(queue)
|
|
154
|
+
for option in (
|
|
155
|
+
'group',
|
|
156
|
+
'qos',
|
|
157
|
+
'offset',
|
|
158
|
+
'error',
|
|
159
|
+
):
|
|
160
|
+
value = subscription_value(
|
|
161
|
+
task.parameters,
|
|
162
|
+
option,
|
|
163
|
+
)
|
|
164
|
+
if value is not None:
|
|
165
|
+
lines.append(f' {option}={value!r}')
|
|
166
|
+
policy = type(task.on_error).__name__ if task.on_error is not None else 'broker default'
|
|
167
|
+
lines.append(f' on_error={policy}')
|
|
168
|
+
lines.append(f' Queues: {len(queues)} queues')
|
|
169
|
+
lines.extend(f' - {queue!r}' for queue in queues)
|
|
170
|
+
lines.append('\n')
|
|
171
|
+
print(
|
|
172
|
+
'\n'.join(lines),
|
|
173
|
+
flush=True,
|
|
174
|
+
)
|