DefStream 0.1.9__25-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 +414 -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 +314 -0
  22. DefStream/Inspector/Extensions/Specifications.py +175 -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 +311 -0
  47. DefStream/Worker/Specification.py +91 -0
  48. DefStream/Worker/Startup.py +135 -0
  49. DefStream/Worker/Worker.py +416 -0
  50. DefStream/Worker/__init__.py +47 -0
  51. DefStream/__init__.py +0 -0
  52. defstream-0.1.9.dist-info/METADATA +674 -0
  53. defstream-0.1.9.dist-info/RECORD +56 -0
  54. defstream-0.1.9.dist-info/WHEEL +6 -0
  55. defstream-0.1.9.dist-info/licenses/LICENSE +674 -0
  56. defstream-0.1.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,245 @@
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('DefStream.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.logger.info(
60
+ 'Pool configured: concurrency=%s',
61
+ max,
62
+ )
63
+
64
+ def submit(
65
+ self,
66
+ run: Type[Run],
67
+ body: Dict[str, Any],
68
+ callback: Optional[Callable[[Any], None]] = None,
69
+ error_callback: Optional[Callable[[BaseException], None]] = None,
70
+ ) -> Future:
71
+ with self.lock:
72
+ if self.closed:
73
+ raise RuntimeError('Pool is closed')
74
+ if not self.slots.acquire(blocking=False):
75
+ self.logger.debug(
76
+ 'Pool capacity exhausted: active=%s capacity=%s',
77
+ len(self.futures),
78
+ self.max,
79
+ )
80
+ raise PoolFull('Pool capacity is exhausted')
81
+ try:
82
+ future = self.pool.submit(
83
+ run(),
84
+ **body,
85
+ )
86
+ except BaseException:
87
+ self.slots.release()
88
+ raise
89
+ self.futures.add(future)
90
+ self.logger.debug(
91
+ 'Task submission accepted: task=%s.%s active=%s',
92
+ run.__module__,
93
+ run.__qualname__,
94
+ len(self.futures),
95
+ )
96
+ future.add_done_callback(lambda completed: self.complete(
97
+ completed,
98
+ callback,
99
+ error_callback,
100
+ ))
101
+ return future
102
+
103
+ def complete(
104
+ self,
105
+ future: Future,
106
+ callback: Optional[Callable[[Any], None]] = None,
107
+ error_callback: Optional[Callable[[BaseException], None]] = None,
108
+ ) -> None:
109
+ with self.lock:
110
+ self.futures.remove(future)
111
+ self.slots.release()
112
+ self.logger.debug(
113
+ 'Task slot released: active=%s',
114
+ len(self.futures),
115
+ )
116
+ try:
117
+ result = future.result()
118
+ except BaseException as error:
119
+ self.logger.debug(
120
+ 'Delivering failed result: error_type=%s',
121
+ type(error).__qualname__,
122
+ )
123
+ if error_callback is not None:
124
+ error_callback(error)
125
+ else:
126
+ self.logger.debug('Delivering successful result')
127
+ if callback is not None:
128
+ callback(result)
129
+
130
+ def waits(
131
+ self,
132
+ timeout: Optional[float] = None,
133
+ ) -> None:
134
+ with self.lock:
135
+ tasks = tuple(self.futures)
136
+ self.logger.debug(
137
+ 'Waiting for Pool tasks: active=%s timeout=%s',
138
+ len(tasks),
139
+ timeout,
140
+ )
141
+ if tasks and wait(
142
+ tasks,
143
+ timeout=timeout,
144
+ ).not_done:
145
+ self.logger.warning(
146
+ 'Pool task wait timed out: timeout=%s',
147
+ timeout,
148
+ )
149
+ raise TimeoutError('Pool tasks did not complete in time')
150
+
151
+ def stop(self) -> None:
152
+ self.logger.info('Stopping Pool')
153
+ with self.lock:
154
+ self.closed = True
155
+ self.pool.shutdown(wait=True)
156
+ self.logger.info('Pool shutdown completed')
157
+
158
+ def shutdown(self) -> None:
159
+ self.stop()
160
+
161
+ def size(self) -> int:
162
+ return self.max
163
+
164
+ def count(self) -> int:
165
+ with self.lock:
166
+ return len(self.futures)
167
+
168
+
169
+ class PoolSetup(ABC):
170
+ @abstractmethod
171
+ def __call__(self) -> None:
172
+ raise NotImplementedError
173
+
174
+
175
+ class PoolCleanup(ABC):
176
+ @abstractmethod
177
+ def __call__(self) -> None:
178
+ raise NotImplementedError
179
+
180
+
181
+ def pool_hooks(
182
+ hooks: Optional[Union[Callable[[], None], Sequence[Callable[[], None]]]],
183
+ ) -> Tuple[Callable[[], None], ...]:
184
+ result = () if hooks is None else tuple(hooks) if isinstance(
185
+ hooks,
186
+ Sequence,
187
+ ) else (hooks,)
188
+ if not all(callable(hook) for hook in result):
189
+ raise TypeError('Pool hooks must be callable')
190
+ return result
191
+
192
+
193
+ def pool_size(max: Optional[int]) -> int:
194
+ if max is None:
195
+ return cpu_count() or 1
196
+ if isinstance(
197
+ max,
198
+ bool,
199
+ ) or not isinstance(
200
+ max,
201
+ int,
202
+ ) or max < 1:
203
+ raise ValueError('Pool size must be a positive integer')
204
+ return max
205
+
206
+
207
+ class ThreadPool(Pool):
208
+ logger = getLogger('DefStream.ThreadPool')
209
+
210
+ def __init__(
211
+ self,
212
+ max: Optional[int] = None,
213
+ setups: Optional[Union[PoolSetup, Sequence[PoolSetup]]] = None,
214
+ cleanups: Optional[Union[PoolCleanup, Sequence[PoolCleanup]]] = None,
215
+ ):
216
+ size = pool_size(max)
217
+ super().__init__(
218
+ Executor(
219
+ size,
220
+ pool_hooks(setups),
221
+ pool_hooks(cleanups),
222
+ ),
223
+ size,
224
+ )
225
+
226
+
227
+ class ProcessPool(Pool):
228
+ logger = getLogger('DefStream.ProcessPool')
229
+
230
+ def __init__(
231
+ self,
232
+ max: Optional[int] = None,
233
+ setups: Optional[Union[PoolSetup, Sequence[PoolSetup]]] = None,
234
+ cleanups: Optional[Union[PoolCleanup, Sequence[PoolCleanup]]] = None,
235
+ ):
236
+ size = pool_size(max)
237
+ super().__init__(
238
+ Executor(
239
+ size,
240
+ pool_hooks(setups),
241
+ pool_hooks(cleanups),
242
+ process=True,
243
+ ),
244
+ size,
245
+ )
@@ -0,0 +1,311 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from collections.abc import Mapping
6
+ from logging import getLogger
7
+ from time import monotonic
8
+ from typing import (
9
+ Any,
10
+ Optional,
11
+ Sequence,
12
+ Tuple,
13
+ Union,
14
+ )
15
+
16
+ from ..Broker.Event import EventError
17
+ from ..Broker.Priority import validate_priority
18
+ from .Specification import Specification
19
+
20
+ __all__ = (
21
+ 'Context',
22
+ 'RunSetup',
23
+ 'RunComplete',
24
+ 'RunError',
25
+ 'Run',
26
+ )
27
+
28
+
29
+ def priority_setting(
30
+ parameters: Any,
31
+ name: str,
32
+ ) -> Optional[int]:
33
+ if isinstance(
34
+ parameters,
35
+ Mapping,
36
+ ):
37
+ return parameters.get(name)
38
+ return getattr(
39
+ parameters,
40
+ name,
41
+ None,
42
+ )
43
+
44
+
45
+ class Context:
46
+ def __init__(
47
+ self,
48
+ *args,
49
+ **kwargs,
50
+ ):
51
+ self.args = args
52
+ self.kwargs = kwargs
53
+
54
+
55
+ class RunSetup(ABC):
56
+ @abstractmethod
57
+ def __call__(
58
+ self,
59
+ context: Context,
60
+ ) -> None:
61
+ raise NotImplementedError
62
+
63
+
64
+ class RunComplete(ABC):
65
+ @abstractmethod
66
+ def __call__(
67
+ self,
68
+ context: Context,
69
+ completion: Any,
70
+ ) -> None:
71
+ raise NotImplementedError
72
+
73
+
74
+ class RunError(ABC):
75
+ @abstractmethod
76
+ def __call__(
77
+ self,
78
+ context: Context,
79
+ error: Exception,
80
+ ) -> None:
81
+ raise NotImplementedError
82
+
83
+
84
+ class Run(ABC):
85
+ logger = getLogger('DefStream.Run')
86
+ parameters: Any = None
87
+ on_error: Optional[EventError] = None
88
+ setups: Tuple[RunSetup, ...] = ()
89
+ completes: Tuple[RunComplete, ...] = ()
90
+ errors: Tuple[RunError, ...] = ()
91
+ request: Optional[Specification] = None
92
+ response: Optional[Specification] = None
93
+
94
+ def __init_subclass__(
95
+ cls,
96
+ *,
97
+ parameters: Any = None,
98
+ on_error: Optional[EventError] = None,
99
+ setups: Optional[Union[RunSetup, Sequence[RunSetup]]] = None,
100
+ completes: Optional[Union[RunComplete, Sequence[RunComplete]]] = None,
101
+ errors: Optional[Union[RunError, Sequence[RunError]]] = None,
102
+ request: Optional[Specification] = None,
103
+ response: Optional[Specification] = None,
104
+ **kwargs,
105
+ ):
106
+ super().__init_subclass__(**kwargs)
107
+ parents = tuple(base for base in cls.__mro__[1:] if issubclass(
108
+ base,
109
+ Run,
110
+ ))
111
+ if parameters is not None:
112
+ cls.parameters = parameters
113
+ for name in (
114
+ 'priority',
115
+ 'max_priority',
116
+ ):
117
+ if any(priority_setting(
118
+ base.parameters,
119
+ name,
120
+ ) is not None for base in parents) and priority_setting(
121
+ cls.parameters,
122
+ name,
123
+ ) is None:
124
+ raise ValueError('Inherited task ' + name + ' cannot be cleared')
125
+ maximum = priority_setting(
126
+ cls.parameters,
127
+ 'max_priority',
128
+ )
129
+ validate_priority(
130
+ maximum,
131
+ None,
132
+ minimum=1,
133
+ name='max_priority',
134
+ )
135
+ validate_priority(
136
+ priority_setting(
137
+ cls.parameters,
138
+ 'priority',
139
+ ),
140
+ maximum,
141
+ )
142
+ for name, specification in (
143
+ (
144
+ 'request',
145
+ request,
146
+ ),
147
+ (
148
+ 'response',
149
+ response,
150
+ ),
151
+ ):
152
+ if specification is not None:
153
+ if not isinstance(
154
+ specification,
155
+ Specification,
156
+ ):
157
+ raise TypeError(name + ' must be a Specification')
158
+ setattr(
159
+ cls,
160
+ name,
161
+ specification,
162
+ )
163
+ if on_error is not None:
164
+ if not callable(on_error):
165
+ raise TypeError('on_error must be a callable event error policy')
166
+ cls.on_error = on_error
167
+ if setups is not None:
168
+ cls.setups = tuple(setups) if isinstance(
169
+ setups,
170
+ Sequence,
171
+ ) else (setups,)
172
+ if completes is not None:
173
+ cls.completes = tuple(completes) if isinstance(
174
+ completes,
175
+ Sequence,
176
+ ) else (completes,)
177
+ if errors is not None:
178
+ cls.errors = tuple(errors) if isinstance(
179
+ errors,
180
+ Sequence,
181
+ ) else (errors,)
182
+ for hooks in (
183
+ cls.setups,
184
+ cls.completes,
185
+ cls.errors,
186
+ ):
187
+ if not all(callable(hook) for hook in hooks):
188
+ raise TypeError('Run hooks must be callable')
189
+ return
190
+
191
+ def __call__(
192
+ self,
193
+ *args,
194
+ **kwargs,
195
+ ) -> Any:
196
+ task = type(self).__module__ + '.' + type(self).__qualname__
197
+ started = monotonic()
198
+ self.logger.debug(
199
+ 'Starting task execution: task=%s',
200
+ task,
201
+ )
202
+ context = Context(
203
+ *args,
204
+ **kwargs,
205
+ )
206
+ for setup in self.setups:
207
+ self.logger.debug(
208
+ 'Starting task setup: task=%s hook=%s',
209
+ task,
210
+ type(setup).__qualname__,
211
+ )
212
+ setup(context)
213
+ self.logger.debug(
214
+ 'Task setup completed: task=%s hook=%s',
215
+ task,
216
+ type(setup).__qualname__,
217
+ )
218
+ try:
219
+ if self.request is not None:
220
+ self.logger.debug(
221
+ 'Validating task input: task=%s',
222
+ task,
223
+ )
224
+ if context.args:
225
+ raise TypeError('A specified task request requires keyword arguments')
226
+ body = self.request(context.kwargs)
227
+ if not isinstance(
228
+ body,
229
+ Mapping,
230
+ ) or not all(isinstance(
231
+ key,
232
+ str,
233
+ ) for key in body):
234
+ raise TypeError('A task request must remain a mapping with string keys')
235
+ context.kwargs = dict(body)
236
+ self.logger.debug(
237
+ 'Task input validated: task=%s',
238
+ task,
239
+ )
240
+ self.logger.debug(
241
+ 'Calling user task: task=%s',
242
+ task,
243
+ )
244
+ result = self.run(
245
+ *context.args,
246
+ **context.kwargs,
247
+ )
248
+ self.logger.debug(
249
+ 'User task returned: task=%s',
250
+ task,
251
+ )
252
+ if self.response is not None:
253
+ self.logger.debug(
254
+ 'Validating task output: task=%s',
255
+ task,
256
+ )
257
+ result = self.response(result)
258
+ self.logger.debug(
259
+ 'Task output validated: task=%s',
260
+ task,
261
+ )
262
+ except Exception as error:
263
+ self.logger.debug(
264
+ 'Task execution or validation failed: task=%s error_type=%s',
265
+ task,
266
+ type(error).__qualname__,
267
+ )
268
+ for handler in self.errors:
269
+ self.logger.debug(
270
+ 'Starting task error hook: task=%s hook=%s',
271
+ task,
272
+ type(handler).__qualname__,
273
+ )
274
+ handler(
275
+ context,
276
+ error,
277
+ )
278
+ self.logger.debug(
279
+ 'Task error hook completed: task=%s hook=%s',
280
+ task,
281
+ type(handler).__qualname__,
282
+ )
283
+ raise
284
+ for complete in self.completes:
285
+ self.logger.debug(
286
+ 'Starting task completion hook: task=%s hook=%s',
287
+ task,
288
+ type(complete).__qualname__,
289
+ )
290
+ complete(
291
+ context,
292
+ result,
293
+ )
294
+ self.logger.debug(
295
+ 'Task completion hook completed: task=%s hook=%s',
296
+ task,
297
+ type(complete).__qualname__,
298
+ )
299
+ self.logger.debug(
300
+ 'Task execution completed: task=%s duration=%s',
301
+ task,
302
+ monotonic() - started,
303
+ )
304
+ return result
305
+
306
+ @abstractmethod
307
+ def run(
308
+ self,
309
+ **kwargs,
310
+ ) -> Any:
311
+ raise NotImplementedError
@@ -0,0 +1,91 @@
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
+ description: Optional[str] = None,
33
+ format: str = 'application/json',
34
+ ):
35
+ if not isinstance(
36
+ format,
37
+ str,
38
+ ) or fullmatch(
39
+ r"[!#$%&'+.^_`|~0-9A-Za-z-]+/[!#$%&'+.^_`|~0-9A-Za-z-]+(?:[ \t]*;[^\r\n]+)?",
40
+ format,
41
+ ) is None:
42
+ raise ValueError('Specification format must be a concrete media type')
43
+ if validator is not None and not callable(validator):
44
+ raise TypeError('A Specification validator must be callable')
45
+ if isinstance(
46
+ schema,
47
+ type,
48
+ ) and is_dataclass(schema):
49
+ schema = ToSchema(schema)
50
+ if schema is not None and not isinstance(
51
+ schema,
52
+ (
53
+ Schema,
54
+ Descriptor,
55
+ Condition,
56
+ Mapping,
57
+ ),
58
+ ):
59
+ raise TypeError('A Descriptor schema, schema mapping or dataclass type is required')
60
+ self.schema = schema
61
+ self.validator = validator
62
+ self.example = example
63
+ self.description = description
64
+ self.format = format
65
+ self.document()
66
+
67
+ def __call__(
68
+ self,
69
+ value: Any,
70
+ ) -> Any:
71
+ return self.validator(value) if self.validator is not None else value
72
+
73
+ def document(self) -> dict:
74
+ payload = {} if self.schema is None else DumpSchema(
75
+ self.schema if isinstance(
76
+ self.schema,
77
+ Schema,
78
+ ) else Schema(
79
+ 'Payload',
80
+ self.schema,
81
+ ),
82
+ )
83
+ message = {
84
+ 'contentType': self.format,
85
+ 'payload': payload,
86
+ }
87
+ if self.description is not None:
88
+ message['description'] = self.description
89
+ if self.example is not ...:
90
+ message['examples'] = [{'payload': deepcopy(self.example)}]
91
+ return message