DefStream 0.1.13__44-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 (56) 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/__init__.py +1 -0
  7. DefStream/Broker/Policies.py +25 -0
  8. DefStream/Broker/Priority.py +26 -0
  9. DefStream/Broker/Protocol.py +45 -0
  10. DefStream/Broker/__init__.py +59 -0
  11. DefStream/Descriptor/DataObject.py +409 -0
  12. DefStream/Descriptor/Types.py +248 -0
  13. DefStream/Descriptor/Value.py +188 -0
  14. DefStream/Descriptor/__init__.py +172 -0
  15. DefStream/Inspector/Extension.py +69 -0
  16. DefStream/Inspector/Extensions/Console.html +373 -0
  17. DefStream/Inspector/Extensions/Console.py +415 -0
  18. DefStream/Inspector/Extensions/Dashboard.html +206 -0
  19. DefStream/Inspector/Extensions/Dashboard.py +124 -0
  20. DefStream/Inspector/Extensions/HealthCheck.py +107 -0
  21. DefStream/Inspector/Extensions/Metrics.py +316 -0
  22. DefStream/Inspector/Extensions/Specifications.py +176 -0
  23. DefStream/Inspector/Extensions/__init__.py +13 -0
  24. DefStream/Inspector/Inspector.py +347 -0
  25. DefStream/Inspector/Response.py +14 -0
  26. DefStream/Inspector/__init__.py +9 -0
  27. DefStream/Validator/Pattern.py +36 -0
  28. DefStream/Validator/Patterns/Array.py +30 -0
  29. DefStream/Validator/Patterns/Common.py +93 -0
  30. DefStream/Validator/Patterns/Compare.py +180 -0
  31. DefStream/Validator/Patterns/Condition.py +60 -0
  32. DefStream/Validator/Patterns/DataObject.py +85 -0
  33. DefStream/Validator/Patterns/DateTime.py +111 -0
  34. DefStream/Validator/Patterns/Number.py +43 -0
  35. DefStream/Validator/Patterns/Object.py +150 -0
  36. DefStream/Validator/Patterns/Size.py +110 -0
  37. DefStream/Validator/Patterns/String.py +85 -0
  38. DefStream/Validator/Patterns/Type.py +350 -0
  39. DefStream/Validator/Patterns/__init__.py +137 -0
  40. DefStream/Validator/Validate.py +29 -0
  41. DefStream/Validator/Validator.py +30 -0
  42. DefStream/Validator/__init__.py +11 -0
  43. DefStream/Worker/Executor.py +371 -0
  44. DefStream/Worker/Message.py +180 -0
  45. DefStream/Worker/Pool.py +245 -0
  46. DefStream/Worker/Run.py +357 -0
  47. DefStream/Worker/Specification.py +87 -0
  48. DefStream/Worker/Startup.py +174 -0
  49. DefStream/Worker/Worker.py +421 -0
  50. DefStream/Worker/__init__.py +47 -0
  51. DefStream/__init__.py +0 -0
  52. defstream-0.1.13.dist-info/METADATA +679 -0
  53. defstream-0.1.13.dist-info/RECORD +56 -0
  54. defstream-0.1.13.dist-info/WHEEL +6 -0
  55. defstream-0.1.13.dist-info/licenses/LICENSE +674 -0
  56. defstream-0.1.13.dist-info/top_level.txt +1 -0
@@ -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
+ )
@@ -0,0 +1,421 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from collections.abc import Mapping
6
+ from logging import getLogger
7
+ from threading import Event as PyEvent
8
+ from typing import (
9
+ Any,
10
+ List,
11
+ Callable,
12
+ Optional,
13
+ Sequence,
14
+ Type,
15
+ Tuple,
16
+ Union,
17
+ )
18
+
19
+ from ..Broker import (
20
+ Connection,
21
+ GetConsumer,
22
+ Consumer,
23
+ EventHandler,
24
+ )
25
+ from ..Broker.Event import Event
26
+ from .Message import task_parameters
27
+ from ..Inspector import Inspector
28
+ from .Pool import (
29
+ Pool,
30
+ PoolFull,
31
+ )
32
+ from .Run import Run
33
+ from .Executor import run_cleanups
34
+ from .Startup import startup
35
+
36
+ __all__ = (
37
+ 'Worker',
38
+ 'WorkerSetup',
39
+ 'WorkerCleanup',
40
+ )
41
+
42
+ class WorkerSetup(ABC):
43
+ @abstractmethod
44
+ def __call__(
45
+ self,
46
+ worker: 'Worker',
47
+ ) -> None:
48
+ raise NotImplementedError
49
+
50
+
51
+ class WorkerCleanup(ABC):
52
+ @abstractmethod
53
+ def __call__(
54
+ self,
55
+ worker: 'Worker',
56
+ ) -> None:
57
+ raise NotImplementedError
58
+
59
+
60
+ def worker_hooks(
61
+ hooks: Optional[Union[Callable[['Worker'], None], Sequence[Callable[['Worker'], None]]]],
62
+ ) -> Tuple[Callable[['Worker'], None], ...]:
63
+ result = () if hooks is None else tuple(hooks) if isinstance(
64
+ hooks,
65
+ Sequence,
66
+ ) else (hooks,)
67
+ if not all(callable(hook) for hook in result):
68
+ raise TypeError('Worker hooks must be callable')
69
+ return result
70
+
71
+
72
+ class Worker:
73
+ logger = getLogger('DefStream.Worker')
74
+
75
+ def __init__(
76
+ self,
77
+ broker: Connection,
78
+ pool: Pool,
79
+ setups: Optional[Union[WorkerSetup, Sequence[WorkerSetup]]] = None,
80
+ cleanups: Optional[Union[WorkerCleanup, Sequence[WorkerCleanup]]] = None,
81
+ inspector: Optional[Inspector] = None,
82
+ ):
83
+ self.broker = broker
84
+ self.pool = pool
85
+ self.tasks: List[Type[Run]] = []
86
+ self.consumer: Optional[Consumer] = None
87
+ self.started = False
88
+ self.running = PyEvent()
89
+ self.stopping = PyEvent()
90
+ self.finished = PyEvent()
91
+ self.pending = 0
92
+ self.setups = worker_hooks(setups)
93
+ self.cleanups = worker_hooks(cleanups)
94
+ self.inspector = inspector
95
+
96
+ def add(
97
+ self,
98
+ run: Type[Run],
99
+ ) -> None:
100
+ if self.started:
101
+ raise RuntimeError('Cannot register runs after starting the worker')
102
+ task_parameters(run)
103
+ if run in self.tasks:
104
+ raise ValueError('Run is already registered')
105
+ if any(
106
+ registered.name == run.name
107
+ for registered in self.tasks
108
+ ):
109
+ raise ValueError('Duplicate Run name: ' + run.name)
110
+ self.tasks.append(run)
111
+ self.logger.debug(
112
+ 'Task registered: task=%s.%s',
113
+ run.__module__,
114
+ run.__qualname__,
115
+ )
116
+
117
+ def run(self) -> None:
118
+ if self.started:
119
+ raise RuntimeError('Worker can only be started once')
120
+ if not self.tasks:
121
+ raise RuntimeError('Register at least one Run before starting')
122
+ self.started = True
123
+ self.logger.info(
124
+ 'Starting Worker: tasks=%s',
125
+ len(self.tasks),
126
+ )
127
+ try:
128
+ startup(self)
129
+ if self.inspector is not None:
130
+ self.inspector.start(self)
131
+ self.logger.debug('Connecting to broker')
132
+ self.broker.connect()
133
+ self.logger.debug('Broker connected')
134
+ for setup in self.setups:
135
+ self.logger.debug(
136
+ 'Starting Worker setup: hook=%s',
137
+ type(setup).__qualname__,
138
+ )
139
+ setup(self)
140
+ self.logger.debug(
141
+ 'Worker setup completed: hook=%s',
142
+ type(setup).__qualname__,
143
+ )
144
+ if not isinstance(self.broker, GetConsumer):
145
+ raise TypeError('Worker broker must support consumer creation')
146
+ self.consumer = self.broker.consumer()
147
+ self.logger.debug('Consumer created')
148
+ for run in self.tasks:
149
+ self.logger.debug(
150
+ 'Subscribing task: task=%s.%s',
151
+ run.__module__,
152
+ run.__qualname__,
153
+ )
154
+ self.consumer.subs(
155
+ run.parameters,
156
+ Handler(
157
+ self,
158
+ run,
159
+ ),
160
+ )
161
+ self.logger.debug(
162
+ 'Task subscribed: task=%s.%s',
163
+ run.__module__,
164
+ run.__qualname__,
165
+ )
166
+ if not self.stopping.is_set():
167
+ self.running.set()
168
+ self.logger.info('Worker ready to consume')
169
+ self.consumer.run()
170
+ finally:
171
+ self.running.clear()
172
+ self.finished.set()
173
+ if self.pending:
174
+ self.logger.error(
175
+ 'Consumer exited with unconfirmed deliveries: pending=%s',
176
+ self.pending,
177
+ )
178
+ try:
179
+ self.logger.info(
180
+ 'Draining Worker tasks: pending=%s',
181
+ self.pending,
182
+ )
183
+ self.pool.stop()
184
+ finally:
185
+ try:
186
+ self.cleanup()
187
+ finally:
188
+ try:
189
+ if self.consumer is not None:
190
+ self.consumer.close()
191
+ finally:
192
+ try:
193
+ self.broker.close()
194
+ finally:
195
+ if self.inspector is not None and self.inspector.worker is self:
196
+ self.inspector.stop()
197
+ self.logger.info('Worker shutdown completed')
198
+
199
+ def cleanup(self) -> None:
200
+ self.logger.debug(
201
+ 'Starting Worker cleanup: hooks=%s',
202
+ len(self.cleanups),
203
+ )
204
+ run_cleanups(
205
+ self.cleanups,
206
+ self,
207
+ )
208
+ self.logger.debug('Worker cleanup completed')
209
+
210
+ def stop(self) -> None:
211
+ if not self.stopping.is_set():
212
+ self.logger.info(
213
+ 'Worker shutdown requested: pending=%s',
214
+ self.pending,
215
+ )
216
+ self.stopping.set()
217
+ if self.consumer is not None and not self.finished.is_set():
218
+ self.consumer.call(self.try_stop)
219
+
220
+ def try_stop(self) -> None:
221
+ if self.stopping.is_set() and self.pending == 0 and self.consumer is not None:
222
+ self.logger.debug('No pending tasks; requesting Consumer stop')
223
+ self.consumer.stop()
224
+
225
+ def receive(
226
+ self,
227
+ run: Type[Run],
228
+ event: Event,
229
+ ) -> None:
230
+ if self.stopping.is_set():
231
+ self.logger.debug(
232
+ 'Requeuing message received during shutdown: task=%s.%s',
233
+ run.__module__,
234
+ run.__qualname__,
235
+ )
236
+ event.nack()
237
+ return
238
+ if not isinstance(
239
+ event.body,
240
+ Mapping,
241
+ ):
242
+ self.failed(
243
+ run,
244
+ event,
245
+ TypeError('Run message body must be a mapping'),
246
+ )
247
+ return
248
+ self.pending += 1
249
+ consumer = self.consumer
250
+ if consumer is None:
251
+ self.pending -= 1
252
+ raise RuntimeError('Worker consumer is not initialized')
253
+ inspector = self.inspector
254
+ task = run.name
255
+ self.logger.debug(
256
+ 'Task message received: task=%s pending=%s',
257
+ task,
258
+ self.pending,
259
+ )
260
+ contexts = inspector.begin(task) if inspector is not None else ()
261
+
262
+ def schedule(
263
+ error: Optional[BaseException] = None,
264
+ result: Any = None,
265
+ ) -> None:
266
+ if inspector is not None:
267
+ inspector.end(
268
+ task,
269
+ contexts,
270
+ error is None,
271
+ )
272
+ if self.finished.is_set():
273
+ self.logger.error(
274
+ 'Cannot confirm delivery after Consumer exit: task=%s',
275
+ task,
276
+ )
277
+ return
278
+ self.logger.debug(
279
+ 'Scheduling task result callback: task=%s success=%s',
280
+ task,
281
+ error is None,
282
+ )
283
+ consumer.call(lambda: self.complete(
284
+ run,
285
+ event,
286
+ error,
287
+ result,
288
+ ))
289
+
290
+ try:
291
+ self.logger.debug(
292
+ 'Submitting task to Pool: task=%s',
293
+ task,
294
+ )
295
+ self.pool.submit(
296
+ run,
297
+ dict(event.body),
298
+ callback=lambda result: schedule(result=result),
299
+ error_callback=schedule,
300
+ )
301
+ self.logger.debug(
302
+ 'Pool task submission accepted: task=%s',
303
+ task,
304
+ )
305
+ except PoolFull:
306
+ self.logger.debug(
307
+ 'Requeuing message because Pool is full: task=%s',
308
+ task,
309
+ )
310
+ if inspector is not None:
311
+ inspector.end(
312
+ task,
313
+ contexts,
314
+ None,
315
+ )
316
+ self.pending -= 1
317
+ event.nack()
318
+ return
319
+ except Exception:
320
+ if inspector is not None:
321
+ inspector.end(
322
+ task,
323
+ contexts,
324
+ None,
325
+ )
326
+ self.pending -= 1
327
+ raise
328
+
329
+ def complete(
330
+ self,
331
+ run: Type[Run],
332
+ event: Event,
333
+ error: Optional[BaseException] = None,
334
+ result: Any = None,
335
+ ) -> None:
336
+ try:
337
+ if error is not None:
338
+ self.failed(
339
+ run,
340
+ event,
341
+ error,
342
+ )
343
+ else:
344
+ if self.consumer is None:
345
+ raise RuntimeError('Worker consumer is not initialized')
346
+ self.consumer.complete(
347
+ event,
348
+ result,
349
+ )
350
+ self.logger.debug(
351
+ 'Consumer completion returned: task=%s.%s',
352
+ run.__module__,
353
+ run.__qualname__,
354
+ )
355
+ finally:
356
+ self.pending -= 1
357
+ self.logger.debug(
358
+ 'Task completion handling finished: pending=%s',
359
+ self.pending,
360
+ )
361
+ self.try_stop()
362
+
363
+ def failed(
364
+ self,
365
+ run: Type[Run],
366
+ event: Event,
367
+ error: BaseException,
368
+ ) -> None:
369
+ if isinstance(
370
+ error,
371
+ TypeError,
372
+ ) and not isinstance(
373
+ event.body,
374
+ Mapping,
375
+ ):
376
+ self.logger.warning(
377
+ 'Rejected a non-mapping task message: task=%s.%s',
378
+ run.__module__,
379
+ run.__qualname__,
380
+ )
381
+ else:
382
+ self.logger.error(
383
+ 'Task failed: task=%s.%s',
384
+ run.__module__,
385
+ run.__qualname__,
386
+ exc_info=(
387
+ type(error),
388
+ error,
389
+ error.__traceback__,
390
+ ),
391
+ )
392
+ if self.consumer is None:
393
+ raise RuntimeError('Worker consumer is not initialized')
394
+ self.consumer.fail(
395
+ event,
396
+ run.on_error,
397
+ )
398
+ self.logger.debug(
399
+ 'Consumer failure policy returned: task=%s.%s',
400
+ run.__module__,
401
+ run.__qualname__,
402
+ )
403
+
404
+
405
+ class Handler(EventHandler):
406
+ def __init__(
407
+ self,
408
+ worker: Worker,
409
+ run: Type[Run],
410
+ ):
411
+ self.worker = worker
412
+ self.run = run
413
+
414
+ def __call__(
415
+ self,
416
+ event: Event,
417
+ ) -> None:
418
+ self.worker.receive(
419
+ self.run,
420
+ event,
421
+ )
@@ -0,0 +1,47 @@
1
+ from .Worker import (
2
+ Worker,
3
+ WorkerSetup,
4
+ WorkerCleanup,
5
+ )
6
+ from .Message import (
7
+ declare,
8
+ emit,
9
+ request,
10
+ )
11
+ from .Run import (
12
+ Run,
13
+ Context,
14
+ RunSetup,
15
+ RunComplete,
16
+ RunError,
17
+ )
18
+ from .Specification import Specification
19
+ from .Pool import (
20
+ Pool,
21
+ PoolFull,
22
+ PoolSetup,
23
+ PoolCleanup,
24
+ ThreadPool,
25
+ ProcessPool,
26
+ )
27
+
28
+ __all__ = (
29
+ 'Worker',
30
+ 'declare',
31
+ 'emit',
32
+ 'request',
33
+ 'WorkerSetup',
34
+ 'WorkerCleanup',
35
+ 'Run',
36
+ 'Specification',
37
+ 'Pool',
38
+ 'PoolFull',
39
+ 'PoolSetup',
40
+ 'PoolCleanup',
41
+ 'ThreadPool',
42
+ 'ProcessPool',
43
+ 'Context',
44
+ 'RunSetup',
45
+ 'RunComplete',
46
+ 'RunError',
47
+ )
DefStream/__init__.py ADDED
File without changes