DefStream 0.1.27__119-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 (55) hide show
  1. DefStream/Broker/Configuration.py +17 -0
  2. DefStream/Broker/Connection.py +176 -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/Priority.py +26 -0
  8. DefStream/Broker/Protocol.py +54 -0
  9. DefStream/Broker/__init__.py +61 -0
  10. DefStream/Descriptor/DataObject.py +409 -0
  11. DefStream/Descriptor/Types.py +248 -0
  12. DefStream/Descriptor/Value.py +188 -0
  13. DefStream/Descriptor/__init__.py +172 -0
  14. DefStream/Inspector/Extension.py +69 -0
  15. DefStream/Inspector/Extensions/Metrics.py +316 -0
  16. DefStream/Inspector/Extensions/Ping.py +107 -0
  17. DefStream/Inspector/Extensions/Play.html +373 -0
  18. DefStream/Inspector/Extensions/Play.py +415 -0
  19. DefStream/Inspector/Extensions/Specifications.py +176 -0
  20. DefStream/Inspector/Extensions/Status.html +206 -0
  21. DefStream/Inspector/Extensions/Status.py +124 -0
  22. DefStream/Inspector/Extensions/__init__.py +13 -0
  23. DefStream/Inspector/Inspector.py +357 -0
  24. DefStream/Inspector/Response.py +14 -0
  25. DefStream/Inspector/__init__.py +9 -0
  26. DefStream/Validator/Pattern.py +36 -0
  27. DefStream/Validator/Patterns/Array.py +30 -0
  28. DefStream/Validator/Patterns/Common.py +92 -0
  29. DefStream/Validator/Patterns/Compare.py +180 -0
  30. DefStream/Validator/Patterns/Condition.py +60 -0
  31. DefStream/Validator/Patterns/DataObject.py +93 -0
  32. DefStream/Validator/Patterns/DateTime.py +111 -0
  33. DefStream/Validator/Patterns/Number.py +43 -0
  34. DefStream/Validator/Patterns/Object.py +158 -0
  35. DefStream/Validator/Patterns/Size.py +110 -0
  36. DefStream/Validator/Patterns/String.py +92 -0
  37. DefStream/Validator/Patterns/Type.py +355 -0
  38. DefStream/Validator/Patterns/__init__.py +137 -0
  39. DefStream/Validator/Validate.py +29 -0
  40. DefStream/Validator/Validator.py +30 -0
  41. DefStream/Validator/__init__.py +11 -0
  42. DefStream/Worker/Executor.py +525 -0
  43. DefStream/Worker/Message.py +260 -0
  44. DefStream/Worker/Pool.py +282 -0
  45. DefStream/Worker/Run.py +357 -0
  46. DefStream/Worker/Specification.py +87 -0
  47. DefStream/Worker/Startup.py +174 -0
  48. DefStream/Worker/Worker.py +571 -0
  49. DefStream/Worker/__init__.py +47 -0
  50. DefStream/__init__.py +0 -0
  51. defstream-0.1.27.dist-info/METADATA +94 -0
  52. defstream-0.1.27.dist-info/RECORD +55 -0
  53. defstream-0.1.27.dist-info/WHEEL +6 -0
  54. defstream-0.1.27.dist-info/licenses/LICENSE +674 -0
  55. defstream-0.1.27.dist-info/top_level.txt +1 -0
@@ -0,0 +1,260 @@
1
+ from collections.abc import Mapping
2
+ from inspect import isabstract
3
+ from math import isfinite
4
+ from threading import TIMEOUT_MAX
5
+ from time import sleep
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Optional,
10
+ Type,
11
+ Union,
12
+ )
13
+
14
+ from ..Broker.Connection import (
15
+ Connection,
16
+ Declarable,
17
+ Emittable,
18
+ Requestable,
19
+ PrioritySupport,
20
+ )
21
+ from .Run import (
22
+ Run,
23
+ priority_setting,
24
+ )
25
+
26
+ __all__ = (
27
+ 'declare',
28
+ 'emit',
29
+ 'request',
30
+ )
31
+
32
+
33
+ def task_parameters(
34
+ task: Type[Run],
35
+ ) -> Any:
36
+ if not isinstance(
37
+ task,
38
+ type,
39
+ ) or not issubclass(
40
+ task,
41
+ Run,
42
+ ) or isabstract(task):
43
+ raise TypeError('A concrete Run class is required')
44
+ if task.parameters is None:
45
+ raise ValueError('Run subscription parameters are required')
46
+ return task.parameters
47
+
48
+
49
+ def message_body(
50
+ body: Mapping,
51
+ ) -> dict:
52
+ if not isinstance(
53
+ body,
54
+ Mapping,
55
+ ) or not all(isinstance(
56
+ key,
57
+ str,
58
+ ) for key in body):
59
+ raise TypeError('Task body must be a mapping with string keys')
60
+ return dict(body)
61
+
62
+
63
+ def priority_parameters(
64
+ broker: Connection,
65
+ parameters: Any,
66
+ priority: Optional[int],
67
+ ) -> Any:
68
+ if priority_setting(
69
+ parameters,
70
+ 'priority',
71
+ ) is None and priority_setting(
72
+ parameters,
73
+ 'max_priority',
74
+ ) is None and priority is None:
75
+ return parameters
76
+ if not isinstance(
77
+ broker,
78
+ PrioritySupport,
79
+ ):
80
+ raise NotImplementedError('This connection does not support task priorities')
81
+ return broker.priority_parameters(
82
+ parameters,
83
+ )
84
+
85
+
86
+ def _retry_settings(
87
+ retry: Optional[int],
88
+ interval: Union[int, float],
89
+ ) -> int:
90
+ if retry is not None and (
91
+ isinstance(
92
+ retry,
93
+ bool,
94
+ ) or not isinstance(
95
+ retry,
96
+ int,
97
+ )
98
+ ):
99
+ raise TypeError('Retry count must be an integer or None')
100
+ if retry is not None and retry < 0:
101
+ raise ValueError('Retry count must not be negative')
102
+ if isinstance(
103
+ interval,
104
+ bool,
105
+ ) or not isinstance(
106
+ interval,
107
+ (
108
+ int,
109
+ float,
110
+ ),
111
+ ):
112
+ raise TypeError('Interval must be a number of seconds')
113
+ if interval < 0 or interval > TIMEOUT_MAX or not isfinite(interval):
114
+ raise ValueError('Interval must be finite and between 0 and threading.TIMEOUT_MAX')
115
+ return 0 if retry is None else retry
116
+
117
+
118
+ def _retry_call(
119
+ call: Callable[[], Any],
120
+ retry: int,
121
+ interval: Union[int, float],
122
+ ) -> Any:
123
+ while True:
124
+ try:
125
+ return call()
126
+ except Exception:
127
+ if retry == 0:
128
+ raise
129
+ retry -= 1
130
+ sleep(interval)
131
+
132
+
133
+ def declare(
134
+ *,
135
+ task: Type[Run],
136
+ broker: Connection,
137
+ ) -> None:
138
+ parameters = task_parameters(task)
139
+ if not isinstance(
140
+ broker,
141
+ Declarable,
142
+ ):
143
+ raise NotImplementedError('This connection does not support task declarations')
144
+ broker.declare(priority_parameters(
145
+ broker,
146
+ parameters,
147
+ None,
148
+ ))
149
+ return
150
+
151
+
152
+ def emit(
153
+ body: Mapping,
154
+ *,
155
+ task: Type[Run],
156
+ broker: Connection,
157
+ priority: Optional[int] = None,
158
+ retry: Optional[int] = None,
159
+ interval: Union[int, float] = 1,
160
+ ) -> Any:
161
+ retries = _retry_settings(
162
+ retry,
163
+ interval,
164
+ )
165
+ parameters = task_parameters(task)
166
+ payload = message_body(body)
167
+ if not isinstance(
168
+ broker,
169
+ Emittable,
170
+ ):
171
+ raise NotImplementedError('This connection does not support task emission')
172
+ parameters = priority_parameters(
173
+ broker,
174
+ parameters,
175
+ priority,
176
+ )
177
+ effective = priority_setting(
178
+ parameters,
179
+ 'priority',
180
+ ) if priority is None else priority
181
+ if effective is not None:
182
+ if not isinstance(
183
+ broker,
184
+ PrioritySupport,
185
+ ):
186
+ raise NotImplementedError('This connection does not support task priorities')
187
+ return _retry_call(
188
+ lambda: broker.emit(
189
+ payload,
190
+ parameters,
191
+ priority=effective,
192
+ ),
193
+ retries,
194
+ interval,
195
+ )
196
+ return _retry_call(
197
+ lambda: broker.emit(
198
+ payload,
199
+ parameters,
200
+ ),
201
+ retries,
202
+ interval,
203
+ )
204
+
205
+
206
+ def request(
207
+ body: Mapping,
208
+ *,
209
+ task: Type[Run],
210
+ broker: Connection,
211
+ timeout: float = 5,
212
+ priority: Optional[int] = None,
213
+ retry: Optional[int] = None,
214
+ interval: Union[int, float] = 1,
215
+ ) -> Any:
216
+ retries = _retry_settings(
217
+ retry,
218
+ interval,
219
+ )
220
+ parameters = task_parameters(task)
221
+ payload = message_body(body)
222
+ if not isinstance(
223
+ broker,
224
+ Requestable,
225
+ ):
226
+ raise NotImplementedError('This connection does not support task RPC requests')
227
+ parameters = priority_parameters(
228
+ broker,
229
+ parameters,
230
+ priority,
231
+ )
232
+ effective = priority_setting(
233
+ parameters,
234
+ 'priority',
235
+ ) if priority is None else priority
236
+ if effective is not None:
237
+ if not isinstance(
238
+ broker,
239
+ PrioritySupport,
240
+ ):
241
+ raise NotImplementedError('This connection does not support task priorities')
242
+ return _retry_call(
243
+ lambda: broker.request(
244
+ payload,
245
+ parameters,
246
+ timeout=timeout,
247
+ priority=effective,
248
+ ),
249
+ retries,
250
+ interval,
251
+ )
252
+ return _retry_call(
253
+ lambda: broker.request(
254
+ payload,
255
+ parameters,
256
+ timeout=timeout,
257
+ ),
258
+ retries,
259
+ interval,
260
+ )
@@ -0,0 +1,282 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from concurrent.futures import (
6
+ Executor as PyExecutor,
7
+ Future,
8
+ wait,
9
+ )
10
+ from os import cpu_count
11
+ from logging import getLogger
12
+ from threading import (
13
+ BoundedSemaphore,
14
+ Lock,
15
+ )
16
+ from typing import (
17
+ Any,
18
+ Callable,
19
+ Dict,
20
+ Optional,
21
+ Sequence,
22
+ Set,
23
+ Tuple,
24
+ Type,
25
+ Union,
26
+ )
27
+
28
+ from .Run import Run
29
+ from .Executor import Executor
30
+
31
+ __all__ = (
32
+ 'Pool',
33
+ 'PoolFull',
34
+ 'PoolSetup',
35
+ 'PoolCleanup',
36
+ 'ThreadPool',
37
+ 'ProcessPool',
38
+ )
39
+
40
+
41
+ class PoolFull(RuntimeError):
42
+ pass
43
+
44
+
45
+ class Pool:
46
+ logger = getLogger('Worker.Pool')
47
+
48
+ def __init__(
49
+ self,
50
+ executor: PyExecutor,
51
+ max: int,
52
+ ):
53
+ self.pool = executor
54
+ self.max = max
55
+ self.slots = BoundedSemaphore(max)
56
+ self.lock = Lock()
57
+ self.futures: Set[Future] = set()
58
+ self.closed = False
59
+ self.stopped = False
60
+ self.logger.info(
61
+ 'Pool configured: concurrency=%s',
62
+ max,
63
+ )
64
+
65
+ def submit(
66
+ self,
67
+ run: Type[Run],
68
+ body: Dict[str, Any],
69
+ callback: Optional[Callable[[Any], None]] = None,
70
+ error_callback: Optional[Callable[[BaseException], None]] = None,
71
+ ) -> Future:
72
+ with self.lock:
73
+ if self.closed:
74
+ raise RuntimeError('Pool is closed')
75
+ if not self.slots.acquire(blocking=False):
76
+ self.logger.debug(
77
+ 'Pool capacity exhausted: active=%s capacity=%s',
78
+ len(self.futures),
79
+ self.max,
80
+ )
81
+ raise PoolFull('Pool capacity is exhausted')
82
+ try:
83
+ future = self.pool.submit(
84
+ run(),
85
+ **body,
86
+ )
87
+ except BaseException:
88
+ self.slots.release()
89
+ raise
90
+ self.futures.add(future)
91
+ futures = self.futures
92
+ slots = self.slots
93
+ self.logger.debug(
94
+ 'Task submission accepted: task=%s.%s active=%s',
95
+ run.__module__,
96
+ run.__qualname__,
97
+ len(self.futures),
98
+ )
99
+ future.add_done_callback(lambda completed: self.complete(
100
+ completed,
101
+ callback,
102
+ error_callback,
103
+ futures,
104
+ slots,
105
+ ))
106
+ return future
107
+
108
+ def complete(
109
+ self,
110
+ future: Future,
111
+ callback: Optional[Callable[[Any], None]] = None,
112
+ error_callback: Optional[Callable[[BaseException], None]] = None,
113
+ futures: Optional[Set[Future]] = None,
114
+ slots: Optional[BoundedSemaphore] = None,
115
+ ) -> None:
116
+ with self.lock:
117
+ active = self.futures if futures is None else futures
118
+ capacity = self.slots if slots is None else slots
119
+ active.remove(future)
120
+ capacity.release()
121
+ self.logger.debug(
122
+ 'Task slot released: active=%s',
123
+ len(self.futures),
124
+ )
125
+ try:
126
+ result = future.result()
127
+ except BaseException as error:
128
+ self.logger.debug(
129
+ 'Delivering failed result: error_type=%s',
130
+ type(error).__qualname__,
131
+ )
132
+ if error_callback is not None:
133
+ error_callback(error)
134
+ else:
135
+ self.logger.debug('Delivering successful result')
136
+ if callback is not None:
137
+ callback(result)
138
+
139
+ def waits(
140
+ self,
141
+ timeout: Optional[float] = None,
142
+ ) -> None:
143
+ with self.lock:
144
+ tasks = tuple(self.futures)
145
+ self.logger.debug(
146
+ 'Waiting for Pool tasks: active=%s timeout=%s',
147
+ len(tasks),
148
+ timeout,
149
+ )
150
+ if tasks and wait(
151
+ tasks,
152
+ timeout=timeout,
153
+ ).not_done:
154
+ self.logger.warning(
155
+ 'Pool task wait timed out: timeout=%s',
156
+ timeout,
157
+ )
158
+ raise TimeoutError('Pool tasks did not complete in time')
159
+
160
+ def stop(self, *, abort: bool = False) -> None:
161
+ self.logger.info('Stopping Pool')
162
+ with self.lock:
163
+ self.closed = True
164
+ self.stopped = True
165
+ if abort:
166
+ if isinstance(self.pool, Executor):
167
+ self.pool.abort()
168
+ else:
169
+ self.logger.warning('Custom executor shutdown cannot terminate running tasks')
170
+ self.pool.shutdown(wait=False)
171
+ else:
172
+ self.pool.shutdown(wait=True)
173
+ self.logger.info('Pool shutdown completed')
174
+ return
175
+
176
+ def restart(self) -> None:
177
+ """Replace the internal executor while preserving this Pool object."""
178
+ with self.lock:
179
+ if self.closed:
180
+ raise RuntimeError('Pool is closed')
181
+ previous = self.pool
182
+ if not isinstance(previous, Executor):
183
+ raise TypeError('Pool recovery requires a DefStream Executor')
184
+ self.closed = True
185
+ previous.abort()
186
+ with self.lock:
187
+ if self.stopped:
188
+ return
189
+ self.pool = previous.replacement()
190
+ self.futures = set()
191
+ self.slots = BoundedSemaphore(self.max)
192
+ self.closed = False
193
+ return
194
+
195
+ def shutdown(self) -> None:
196
+ self.stop()
197
+
198
+ def size(self) -> int:
199
+ return self.max
200
+
201
+ def count(self) -> int:
202
+ with self.lock:
203
+ return len(self.futures)
204
+
205
+
206
+ class PoolSetup(ABC):
207
+ @abstractmethod
208
+ def __call__(self) -> None:
209
+ raise NotImplementedError
210
+
211
+
212
+ class PoolCleanup(ABC):
213
+ @abstractmethod
214
+ def __call__(self) -> None:
215
+ raise NotImplementedError
216
+
217
+
218
+ def pool_hooks(
219
+ hooks: Optional[Union[Callable[[], None], Sequence[Callable[[], None]]]],
220
+ ) -> Tuple[Callable[[], None], ...]:
221
+ result = () if hooks is None else tuple(hooks) if isinstance(
222
+ hooks,
223
+ Sequence,
224
+ ) else (hooks,)
225
+ if not all(callable(hook) for hook in result):
226
+ raise TypeError('Pool hooks must be callable')
227
+ return result
228
+
229
+
230
+ def pool_size(max: Optional[int]) -> int:
231
+ if max is None:
232
+ return cpu_count() or 1
233
+ if isinstance(
234
+ max,
235
+ bool,
236
+ ) or not isinstance(
237
+ max,
238
+ int,
239
+ ) or max < 1:
240
+ raise ValueError('Pool size must be a positive integer')
241
+ return max
242
+
243
+
244
+ class ThreadPool(Pool):
245
+ logger = getLogger('Worker.ThreadPool')
246
+
247
+ def __init__(
248
+ self,
249
+ max: Optional[int] = None,
250
+ setups: Optional[Union[PoolSetup, Sequence[PoolSetup]]] = None,
251
+ cleanups: Optional[Union[PoolCleanup, Sequence[PoolCleanup]]] = None,
252
+ ):
253
+ size = pool_size(max)
254
+ super().__init__(
255
+ Executor(
256
+ size,
257
+ pool_hooks(setups),
258
+ pool_hooks(cleanups),
259
+ ),
260
+ size,
261
+ )
262
+
263
+
264
+ class ProcessPool(Pool):
265
+ logger = getLogger('Worker.ProcessPool')
266
+
267
+ def __init__(
268
+ self,
269
+ max: Optional[int] = None,
270
+ setups: Optional[Union[PoolSetup, Sequence[PoolSetup]]] = None,
271
+ cleanups: Optional[Union[PoolCleanup, Sequence[PoolCleanup]]] = None,
272
+ ):
273
+ size = pool_size(max)
274
+ super().__init__(
275
+ Executor(
276
+ size,
277
+ pool_hooks(setups),
278
+ pool_hooks(cleanups),
279
+ process=True,
280
+ ),
281
+ size,
282
+ )