redzed 26.1.28__py3-none-any.whl → 26.2.4__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.
- redzed/__init__.py +1 -1
- redzed/block.py +5 -2
- redzed/blocklib/outputs.py +68 -51
- redzed/blocklib/repeat.py +29 -13
- redzed/circuit.py +61 -36
- redzed/utils/async_utils.py +1 -100
- {redzed-26.1.28.dist-info → redzed-26.2.4.dist-info}/METADATA +2 -2
- {redzed-26.1.28.dist-info → redzed-26.2.4.dist-info}/RECORD +11 -11
- {redzed-26.1.28.dist-info → redzed-26.2.4.dist-info}/WHEEL +0 -0
- {redzed-26.1.28.dist-info → redzed-26.2.4.dist-info}/licenses/LICENSE.txt +0 -0
- {redzed-26.1.28.dist-info → redzed-26.2.4.dist-info}/top_level.txt +0 -0
redzed/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ Docs: https://redzed.readthedocs.io/en/latest/
|
|
|
15
15
|
Home: https://github.com/xitop/redzed/
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version_info__ = (26,
|
|
18
|
+
__version_info__ = (26, 2, 4)
|
|
19
19
|
__version__ = '.'.join(str(n) for n in __version_info__)
|
|
20
20
|
|
|
21
21
|
from . import circuit, block, debug, formula_trigger, initializers, undef
|
redzed/block.py
CHANGED
|
@@ -225,6 +225,9 @@ class Block(BlockOrFormula):
|
|
|
225
225
|
# pylint: disable-next=no-member
|
|
226
226
|
return self.rz_export_state() # type: ignore[attr-defined]
|
|
227
227
|
|
|
228
|
+
def rz_is_shut_down(self) -> bool:
|
|
229
|
+
return self.circuit.is_shut_down()
|
|
230
|
+
|
|
228
231
|
def event(self, etype: str, /, evalue: t.Any = UNDEF, **edata: t.Any) -> t.Any:
|
|
229
232
|
"""
|
|
230
233
|
An entry point for events.
|
|
@@ -232,13 +235,13 @@ class Block(BlockOrFormula):
|
|
|
232
235
|
Call the specialized _event_ETYPE() method if it exists.
|
|
233
236
|
Otherwise call the _default_event_handler() as the last resort.
|
|
234
237
|
"""
|
|
235
|
-
|
|
238
|
+
check_identifier(etype, "Event type")
|
|
239
|
+
if not etype.startswith('_get_') and self.rz_is_shut_down():
|
|
236
240
|
raise CircuitShutDown("The circuit was shut down")
|
|
237
241
|
|
|
238
242
|
if evalue is not UNDEF:
|
|
239
243
|
edata['evalue'] = evalue
|
|
240
244
|
|
|
241
|
-
check_identifier(etype, "Event type")
|
|
242
245
|
if get_debug_level() >= 1:
|
|
243
246
|
if not edata:
|
|
244
247
|
self.log_debug("Got event '%s'", etype)
|
redzed/blocklib/outputs.py
CHANGED
|
@@ -14,7 +14,7 @@ from collections.abc import Callable, Awaitable
|
|
|
14
14
|
import typing as t
|
|
15
15
|
|
|
16
16
|
import redzed
|
|
17
|
-
from redzed.utils import BufferShutDown, cancel_shield, func_call_string,
|
|
17
|
+
from redzed.utils import BufferShutDown, cancel_shield, func_call_string, time_period
|
|
18
18
|
from .validator import _Validate
|
|
19
19
|
|
|
20
20
|
|
|
@@ -30,9 +30,14 @@ class OutputFunc(redzed.Block):
|
|
|
30
30
|
triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
|
|
31
31
|
**kwargs) -> None:
|
|
32
32
|
super().__init__(*args, **kwargs)
|
|
33
|
-
self._stop_value = stop_value
|
|
34
33
|
self._func = func
|
|
35
|
-
self.
|
|
34
|
+
self._shutdown = False
|
|
35
|
+
if stop_value is not redzed.UNDEF:
|
|
36
|
+
@redzed.stop_function
|
|
37
|
+
def stop_function_():
|
|
38
|
+
self._event_put({'evalue': stop_value})
|
|
39
|
+
stop_function_.__qualname__ += self.name
|
|
40
|
+
stop_function_.__name__ += self.name
|
|
36
41
|
if triggered_by is not redzed.UNDEF:
|
|
37
42
|
@redzed.triggered
|
|
38
43
|
def trigger(value=triggered_by) -> None:
|
|
@@ -53,9 +58,11 @@ class OutputFunc(redzed.Block):
|
|
|
53
58
|
self.log_debug2("output function returned: %r", result)
|
|
54
59
|
return result
|
|
55
60
|
|
|
61
|
+
def rz_is_shut_down(self) -> bool:
|
|
62
|
+
return self._shutdown
|
|
63
|
+
|
|
56
64
|
def rz_stop(self) -> None:
|
|
57
|
-
|
|
58
|
-
self._event_put({'evalue': self._stop_value})
|
|
65
|
+
self._shutdown = True
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
class _Buffer(_Validate, redzed.Block):
|
|
@@ -65,9 +72,14 @@ class _Buffer(_Validate, redzed.Block):
|
|
|
65
72
|
triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
|
|
66
73
|
**kwargs) -> None:
|
|
67
74
|
super().__init__(*args, **kwargs)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
self._shutdown = False
|
|
76
|
+
if stop_value is not redzed.UNDEF:
|
|
77
|
+
stop_value = self._validate(stop_value)
|
|
78
|
+
@redzed.stop_function
|
|
79
|
+
def stop_function_():
|
|
80
|
+
self.rz_put_value(stop_value)
|
|
81
|
+
stop_function_.__qualname__ += self.name
|
|
82
|
+
stop_function_.__name__ += self.name
|
|
71
83
|
if triggered_by is not redzed.UNDEF:
|
|
72
84
|
@redzed.triggered
|
|
73
85
|
def trigger(value=triggered_by) -> None:
|
|
@@ -80,6 +92,15 @@ class _Buffer(_Validate, redzed.Block):
|
|
|
80
92
|
evalue = self._validate(evalue)
|
|
81
93
|
self.rz_put_value(evalue)
|
|
82
94
|
|
|
95
|
+
def _event__get_size(self, _edata: redzed.EventData) -> int:
|
|
96
|
+
return self.rz_get_size()
|
|
97
|
+
|
|
98
|
+
def rz_is_shut_down(self) -> bool:
|
|
99
|
+
return self._shutdown
|
|
100
|
+
|
|
101
|
+
def rz_stop(self) -> None:
|
|
102
|
+
self._shutdown = True
|
|
103
|
+
|
|
83
104
|
def rz_put_value(self, value: t.Any) -> None:
|
|
84
105
|
raise NotImplementedError
|
|
85
106
|
|
|
@@ -89,6 +110,12 @@ class _Buffer(_Validate, redzed.Block):
|
|
|
89
110
|
async def rz_buffer_get(self) -> t.Any:
|
|
90
111
|
raise NotImplementedError
|
|
91
112
|
|
|
113
|
+
def rz_close(self) -> None:
|
|
114
|
+
if (size := self.rz_get_size()) == 1:
|
|
115
|
+
self.log_error("One value was not retrieved from buffer")
|
|
116
|
+
elif size > 1:
|
|
117
|
+
self.log_error("%d values were not retrieved from buffer", size)
|
|
118
|
+
|
|
92
119
|
|
|
93
120
|
class QueueBuffer(_Buffer):
|
|
94
121
|
"""
|
|
@@ -105,7 +132,6 @@ class QueueBuffer(_Buffer):
|
|
|
105
132
|
self._queue: asyncio.Queue[t.Any] = queue_type(maxsize)
|
|
106
133
|
# Queue.shutdown is available in Python 3.13, but we want to support 3.11+
|
|
107
134
|
self._waiters = 0
|
|
108
|
-
self._shutdown = False
|
|
109
135
|
|
|
110
136
|
def rz_put_value(self, value: t.Any) -> None:
|
|
111
137
|
self._queue.put_nowait(value)
|
|
@@ -114,18 +140,10 @@ class QueueBuffer(_Buffer):
|
|
|
114
140
|
"""Get the number of items in the buffer."""
|
|
115
141
|
return self._queue.qsize()
|
|
116
142
|
|
|
117
|
-
def _event__get_size(self, _edata: redzed.EventData) -> int:
|
|
118
|
-
return self.rz_get_size()
|
|
119
|
-
|
|
120
143
|
def rz_stop(self) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.rz_put_value(self._stop_value)
|
|
124
|
-
waiters -= 1
|
|
125
|
-
# unblock waiters
|
|
126
|
-
for _ in range(waiters):
|
|
144
|
+
super().rz_stop()
|
|
145
|
+
for _ in range(self._waiters):
|
|
127
146
|
self.rz_put_value(redzed.UNDEF)
|
|
128
|
-
self._shutdown = True
|
|
129
147
|
|
|
130
148
|
async def rz_buffer_get(self) -> t.Any:
|
|
131
149
|
"""
|
|
@@ -147,10 +165,6 @@ class QueueBuffer(_Buffer):
|
|
|
147
165
|
raise BufferShutDown("The buffer was shut down")
|
|
148
166
|
return value
|
|
149
167
|
|
|
150
|
-
def rz_close(self) -> None:
|
|
151
|
-
if (size := self._queue.qsize()) > 0:
|
|
152
|
-
self.log_warning("%d unprocessed item(s) left in the buffer", size)
|
|
153
|
-
|
|
154
168
|
|
|
155
169
|
class MemoryBuffer(_Buffer):
|
|
156
170
|
"""
|
|
@@ -159,45 +173,47 @@ class MemoryBuffer(_Buffer):
|
|
|
159
173
|
|
|
160
174
|
def __init__(self, *args, **kwargs) -> None:
|
|
161
175
|
super().__init__(*args, **kwargs)
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
# allowed states:
|
|
177
|
+
# _shutdown value has_value
|
|
178
|
+
# - has value: False, not UNDEF, set
|
|
179
|
+
# - is empty: False, UNDEF, cleared
|
|
180
|
+
# - draining: True, not UNDEF, set
|
|
181
|
+
# - shut down: True, UNDEF, set
|
|
182
|
+
self._value = redzed.UNDEF
|
|
183
|
+
self._has_value = asyncio.Event()
|
|
164
184
|
|
|
165
185
|
def rz_get_size(self) -> int:
|
|
166
186
|
"""Get the number of items (0 or 1) in the buffer."""
|
|
167
|
-
has_data = self.
|
|
187
|
+
has_data = self._value is not redzed.UNDEF
|
|
168
188
|
return 1 if has_data else 0
|
|
169
189
|
|
|
170
|
-
def _event__get_size(self, _edata: redzed.EventData) -> int:
|
|
171
|
-
return self.rz_get_size()
|
|
172
|
-
|
|
173
190
|
def rz_put_value(self, value: t.Any) -> None:
|
|
174
|
-
|
|
191
|
+
if value is redzed.UNDEF:
|
|
192
|
+
raise ValueError(f"{self}: Cannot put UNDEF into the buffer")
|
|
193
|
+
self._value = value
|
|
194
|
+
self._has_value.set()
|
|
175
195
|
|
|
176
196
|
def rz_stop(self) -> None:
|
|
177
197
|
"""Shut down"""
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
super().rz_stop()
|
|
199
|
+
if not self._has_value.is_set():
|
|
200
|
+
assert self._value is redzed.UNDEF
|
|
201
|
+
self._has_value.set() # unblock reader(s)
|
|
180
202
|
|
|
181
203
|
async def rz_buffer_get(self) -> t.Any:
|
|
182
|
-
"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
self._stop_pending = False
|
|
196
|
-
return self._stop_value
|
|
197
|
-
|
|
198
|
-
def rz_close(self) -> None:
|
|
199
|
-
if self._stop_pending:
|
|
200
|
-
self.log_error("The stop_value was not retrieved")
|
|
204
|
+
"""Remove and return an item from the memory cell."""
|
|
205
|
+
while True:
|
|
206
|
+
await self._has_value.wait()
|
|
207
|
+
if self._value is redzed.UNDEF:
|
|
208
|
+
if self._shutdown:
|
|
209
|
+
raise BufferShutDown("The buffer was shut down")
|
|
210
|
+
if not self._has_value.is_set():
|
|
211
|
+
raise RuntimeError("BUG!: busy loop prevented")
|
|
212
|
+
continue
|
|
213
|
+
value, self._value = self._value, redzed.UNDEF
|
|
214
|
+
if not self._shutdown:
|
|
215
|
+
self._has_value.clear()
|
|
216
|
+
return value
|
|
201
217
|
|
|
202
218
|
|
|
203
219
|
class OutputWorker(redzed.Block):
|
|
@@ -268,6 +284,7 @@ class OutputWorker(redzed.Block):
|
|
|
268
284
|
self.log_warning("%d worker(s) did not stop", running)
|
|
269
285
|
raise
|
|
270
286
|
|
|
287
|
+
|
|
271
288
|
class OutputController(redzed.Block):
|
|
272
289
|
"""
|
|
273
290
|
Run an awaitable for the latest value from a buffer.
|
redzed/blocklib/repeat.py
CHANGED
|
@@ -13,7 +13,7 @@ import asyncio
|
|
|
13
13
|
import typing as t
|
|
14
14
|
|
|
15
15
|
import redzed
|
|
16
|
-
from redzed.utils import
|
|
16
|
+
from redzed.utils import time_period
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class Repeat(redzed.Block):
|
|
@@ -27,20 +27,27 @@ class Repeat(redzed.Block):
|
|
|
27
27
|
**kwargs
|
|
28
28
|
) -> None:
|
|
29
29
|
self._dest = dest
|
|
30
|
-
self.
|
|
30
|
+
self._default_interval = time_period(interval)
|
|
31
31
|
if count is not None and count < 0:
|
|
32
32
|
# count = 0 (no repeating) is accepted
|
|
33
33
|
raise ValueError("argument 'count' must not be negative")
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
34
|
+
self._default_count = count
|
|
35
|
+
self._got_event = False
|
|
36
|
+
self._new_event = asyncio.Event()
|
|
37
|
+
# current event
|
|
38
|
+
self._etype: str
|
|
39
|
+
self._edata: redzed.EventData
|
|
40
|
+
self._interval: float
|
|
41
|
+
self._count: int|None
|
|
37
42
|
super().__init__(*args, **kwargs)
|
|
38
43
|
|
|
39
44
|
def rz_pre_init(self) -> None:
|
|
40
45
|
"""Resolve destination block name."""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
dest = self.circuit.resolve_name(self._dest)
|
|
47
|
+
if isinstance(dest, redzed.Formula):
|
|
48
|
+
raise TypeError(f"{self}: {dest} is a Formula; cannot send events to it.")
|
|
49
|
+
if isinstance(dest, type(self)):
|
|
50
|
+
raise TypeError(f"{self}: {dest} is another Repeat block; this is not allowed")
|
|
44
51
|
self._dest = dest
|
|
45
52
|
self.circuit.create_service(self._repeater(), name=f"Event repeating task at {self}")
|
|
46
53
|
|
|
@@ -52,21 +59,30 @@ class Repeat(redzed.Block):
|
|
|
52
59
|
repeat = 0 # prevent pylint warning
|
|
53
60
|
while True:
|
|
54
61
|
try:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
repeat = 0
|
|
62
|
+
async with asyncio.timeout(self._interval if repeating else None):
|
|
63
|
+
await self._new_event.wait()
|
|
58
64
|
except asyncio.TimeoutError:
|
|
65
|
+
pass
|
|
66
|
+
# getting a timeout does not necessarily mean the event was not set
|
|
67
|
+
if self._new_event.is_set():
|
|
68
|
+
self._new_event.clear()
|
|
69
|
+
repeat = 0
|
|
70
|
+
else:
|
|
59
71
|
repeat += 1
|
|
60
72
|
|
|
61
73
|
if repeat > 0: # skip the original event
|
|
62
74
|
self._set_output(repeat)
|
|
63
75
|
assert isinstance(self._dest, redzed.Block) # @mypy: name resolved
|
|
64
|
-
self._dest.event(
|
|
76
|
+
self._dest.event(self._etype, **(self._edata | {'repeat': repeat}))
|
|
65
77
|
repeating = self._count is None or repeat < self._count
|
|
66
78
|
|
|
67
79
|
def _default_event_handler(self, etype: str, edata: redzed.EventData) -> None:
|
|
68
80
|
# send the original event synchronously
|
|
69
81
|
self._set_output(0)
|
|
70
82
|
assert isinstance(self._dest, redzed.Block) # mypy: name is resolved
|
|
83
|
+
self._etype = etype
|
|
84
|
+
self._edata = edata
|
|
85
|
+
self._interval = edata.pop('repeat_interval', self._default_interval)
|
|
86
|
+
self._count = edata.pop('repeat_count', self._default_count)
|
|
71
87
|
self._dest.event(etype, **(edata | {'repeat': 0}))
|
|
72
|
-
self.
|
|
88
|
+
self._new_event.set()
|
redzed/circuit.py
CHANGED
|
@@ -7,10 +7,11 @@ Project home: https://github.com/xitop/redzed/
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
__all__ = [
|
|
10
|
+
__all__ = [
|
|
11
|
+
'CircuitState', 'stop_function', 'get_circuit', 'reset_circuit', 'run', 'unique_name']
|
|
11
12
|
|
|
12
13
|
import asyncio
|
|
13
|
-
from collections.abc import Coroutine, Iterable, MutableMapping, Sequence
|
|
14
|
+
from collections.abc import Callable, Coroutine, Iterable, MutableMapping, Sequence
|
|
14
15
|
import contextlib
|
|
15
16
|
import enum
|
|
16
17
|
import itertools
|
|
@@ -63,6 +64,13 @@ def unique_name(prefix: str = 'auto') -> str:
|
|
|
63
64
|
return get_circuit().rz_unique_name(prefix)
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
_StopFunction: t.TypeAlias = Callable[[], t.Any]
|
|
68
|
+
|
|
69
|
+
def stop_function(func: _StopFunction) -> _StopFunction:
|
|
70
|
+
get_circuit().rz_add_stop_function(func)
|
|
71
|
+
return func
|
|
72
|
+
|
|
73
|
+
|
|
66
74
|
class CircuitState(enum.IntEnum):
|
|
67
75
|
"""
|
|
68
76
|
Circuit state.
|
|
@@ -83,16 +91,16 @@ class _TerminateTaskGroup(Exception):
|
|
|
83
91
|
|
|
84
92
|
|
|
85
93
|
@contextlib.contextmanager
|
|
86
|
-
def error_debug(
|
|
94
|
+
def error_debug(exc_source: t.Any, suppress_error: bool = False) -> t.Iterator[None]:
|
|
87
95
|
"""Add a note to raised exception -or- log and suppress exception."""
|
|
88
96
|
try:
|
|
89
97
|
yield None
|
|
90
98
|
except Exception as err:
|
|
91
99
|
if not suppress_error:
|
|
92
|
-
err.add_note(f"This {type(err).__name__} occurred in {
|
|
100
|
+
err.add_note(f"This {type(err).__name__} occurred in {exc_source}")
|
|
93
101
|
raise
|
|
94
102
|
# errors should be suppressed only during the shutdown & cleanup
|
|
95
|
-
_logger.error("[Circuit] %s: Suppressing %s: %s",
|
|
103
|
+
_logger.error("[Circuit] %s: Suppressing %s: %s", exc_source, type(err).__name__, err)
|
|
96
104
|
|
|
97
105
|
|
|
98
106
|
class Circuit:
|
|
@@ -108,6 +116,7 @@ class Circuit:
|
|
|
108
116
|
self._blocks: dict[str, Block|Formula] = {}
|
|
109
117
|
# all Blocks and Formulas belonging to this circuit stored by name
|
|
110
118
|
self._triggers: list[Trigger] = [] # all triggers belonging to this circuit
|
|
119
|
+
self._stops: list[_StopFunction] = []
|
|
111
120
|
self._errors: list[Exception] = [] # exceptions occurred in the runner
|
|
112
121
|
self.rz_persistent_dict: MutableMapping[str, t.Any]|None = None
|
|
113
122
|
# persistent state data back-end
|
|
@@ -169,6 +178,9 @@ class Circuit:
|
|
|
169
178
|
|
|
170
179
|
# --- circuit components storage ---
|
|
171
180
|
|
|
181
|
+
def rz_add_stop_function(self, func: _StopFunction) -> None:
|
|
182
|
+
self._stops.append(func)
|
|
183
|
+
|
|
172
184
|
def rz_add_item(self, item: Block|Formula|Trigger) -> None:
|
|
173
185
|
"""Add a circuit item."""
|
|
174
186
|
self._check_not_started()
|
|
@@ -279,7 +291,7 @@ class Circuit:
|
|
|
279
291
|
|
|
280
292
|
def _check_not_started(self) -> None:
|
|
281
293
|
"""Raise an error if the circuit runner has started already."""
|
|
282
|
-
if self._state
|
|
294
|
+
if self._state is CircuitState.CLOSED:
|
|
283
295
|
# A circuit may be closed before start (see shutdown),
|
|
284
296
|
# let's use this message instead of the one below.
|
|
285
297
|
raise RuntimeError("The circuit was closed")
|
|
@@ -287,7 +299,7 @@ class Circuit:
|
|
|
287
299
|
if self._state > CircuitState.INIT_CIRCUIT:
|
|
288
300
|
raise RuntimeError("Not allowed after the start")
|
|
289
301
|
|
|
290
|
-
def
|
|
302
|
+
def is_shut_down(self) -> bool:
|
|
291
303
|
"""Test if we are past the shutdown() call."""
|
|
292
304
|
return self._state >= CircuitState.SHUTDOWN
|
|
293
305
|
|
|
@@ -444,11 +456,12 @@ class Circuit:
|
|
|
444
456
|
async def _runner_init(self) -> None:
|
|
445
457
|
"""Run the circuit during the initialization phase."""
|
|
446
458
|
self._set_state(CircuitState.INIT_CIRCUIT)
|
|
459
|
+
# ensure a logging handler (won't work in UNDER_CONSTRUCTION level)
|
|
460
|
+
set_debug_level(get_debug_level())
|
|
447
461
|
await asyncio.sleep(0) # allow reached_state() synchronization
|
|
448
|
-
if self.
|
|
462
|
+
if self.is_shut_down():
|
|
449
463
|
# It looks like a supporting task has failed immediately after the start
|
|
450
464
|
return
|
|
451
|
-
set_debug_level(get_debug_level()) # will check the existence of a logging handler
|
|
452
465
|
|
|
453
466
|
pe_blocks = [blk for blk in self.get_items(Block) if blk.has_method('rz_pre_init')]
|
|
454
467
|
pe_formulas = list(self.get_items(Formula))
|
|
@@ -497,14 +510,24 @@ class Circuit:
|
|
|
497
510
|
self.save_persistent_state(blk, now)
|
|
498
511
|
|
|
499
512
|
stop_triggers = list(self.get_items(Trigger))
|
|
513
|
+
if stop_triggers:
|
|
514
|
+
self._log_debug2_blocks("Stopping triggers", stop_triggers)
|
|
515
|
+
for tstop in stop_triggers:
|
|
516
|
+
with error_debug(tstop, suppress_error=True):
|
|
517
|
+
tstop.rz_stop()
|
|
518
|
+
|
|
519
|
+
if self._stops:
|
|
520
|
+
self.log_debug2("Running %d stop function(s)", len(self._stops))
|
|
521
|
+
for fstop in self._stops:
|
|
522
|
+
with error_debug(f"Stop function {fstop.__name__}()", suppress_error=True):
|
|
523
|
+
fstop()
|
|
524
|
+
|
|
500
525
|
stop_blocks = [blk for blk in self.get_items(Block) if blk.has_method('rz_stop')]
|
|
501
526
|
if stop_blocks:
|
|
502
|
-
self._log_debug2_blocks("Stopping (sync)",
|
|
503
|
-
for
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
# union-attr: checked with .has_method()
|
|
507
|
-
stop.rz_stop() # type: ignore[union-attr]
|
|
527
|
+
self._log_debug2_blocks("Stopping (sync)", stop_blocks)
|
|
528
|
+
for bstop in stop_blocks:
|
|
529
|
+
with error_debug(bstop, suppress_error=True):
|
|
530
|
+
bstop.rz_stop()
|
|
508
531
|
|
|
509
532
|
if self._auto_cancel_tasks:
|
|
510
533
|
self.log_debug2("Cancelling %d service task(s)")
|
|
@@ -544,10 +567,6 @@ class Circuit:
|
|
|
544
567
|
|
|
545
568
|
When the runner terminates, it cannot be invoked again.
|
|
546
569
|
"""
|
|
547
|
-
if self._state == CircuitState.CLOSED:
|
|
548
|
-
raise RuntimeError("Cannot restart a closed circuit.")
|
|
549
|
-
if self._state != CircuitState.UNDER_CONSTRUCTION:
|
|
550
|
-
raise RuntimeError("The circuit is already running.")
|
|
551
570
|
if not self._blocks:
|
|
552
571
|
raise RuntimeError("The circuit is empty")
|
|
553
572
|
if tasks_are_eager():
|
|
@@ -561,7 +580,7 @@ class Circuit:
|
|
|
561
580
|
self.abort(err)
|
|
562
581
|
else:
|
|
563
582
|
# There might be errors reported with abort().
|
|
564
|
-
# In such case the state has been set to SHUTDOWN.
|
|
583
|
+
# In such case the state has been set to SHUTDOWN or even CLOSED.
|
|
565
584
|
# _set_state(RUNNING) will be silently ignored.
|
|
566
585
|
self._set_state(CircuitState.RUNNING)
|
|
567
586
|
# wait until cancelled from the task group; possible causes:
|
|
@@ -573,17 +592,18 @@ class Circuit:
|
|
|
573
592
|
# will be re-raised at the end if there won't be other exceptions
|
|
574
593
|
pass
|
|
575
594
|
# cancellation causes 2 and 3 do not modify the state
|
|
576
|
-
if not self.
|
|
595
|
+
if not self.is_shut_down():
|
|
577
596
|
self._set_state(CircuitState.SHUTDOWN)
|
|
578
597
|
await asyncio.sleep(0)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
598
|
+
if self._state < CircuitState.CLOSED:
|
|
599
|
+
try:
|
|
600
|
+
await self._runner_shutdown()
|
|
601
|
+
except Exception as err:
|
|
602
|
+
# If an exception is propagated from _runner_shutdown, it is probably a bug.
|
|
603
|
+
# Calling abort is not necessary when shutting down, but the call will log
|
|
604
|
+
# and register the exception to be included in the final ExceptionGroup.
|
|
605
|
+
self.abort(err)
|
|
606
|
+
self._set_state(CircuitState.CLOSED)
|
|
587
607
|
|
|
588
608
|
if self._errors:
|
|
589
609
|
raise ExceptionGroup("_runner_core() errors", self._errors)
|
|
@@ -605,7 +625,7 @@ class Circuit:
|
|
|
605
625
|
# the same error may be reported from several places
|
|
606
626
|
return
|
|
607
627
|
self._errors.append(err)
|
|
608
|
-
if self.
|
|
628
|
+
if self.is_shut_down():
|
|
609
629
|
self.log_error("Unhandled error during shutdown: %r", err)
|
|
610
630
|
else:
|
|
611
631
|
self.log_warning("Aborting due to an exception: %r", err)
|
|
@@ -617,9 +637,9 @@ class Circuit:
|
|
|
617
637
|
|
|
618
638
|
Prevent the runner from starting if it wasn't started yet.
|
|
619
639
|
"""
|
|
620
|
-
if self.
|
|
640
|
+
if self.is_shut_down():
|
|
621
641
|
return
|
|
622
|
-
if self._state
|
|
642
|
+
if self._state <= CircuitState.INIT_CIRCUIT:
|
|
623
643
|
self._set_state(CircuitState.CLOSED)
|
|
624
644
|
return
|
|
625
645
|
self._set_state(CircuitState.SHUTDOWN)
|
|
@@ -656,7 +676,7 @@ class Circuit:
|
|
|
656
676
|
try:
|
|
657
677
|
await coro # return value of a service is ignored
|
|
658
678
|
except asyncio.CancelledError:
|
|
659
|
-
if self.
|
|
679
|
+
if self.is_shut_down():
|
|
660
680
|
self.log_debug1("%s was cancelled", longname)
|
|
661
681
|
raise
|
|
662
682
|
err = RuntimeError(f"{longname} was cancelled before shutdown")
|
|
@@ -666,7 +686,7 @@ class Circuit:
|
|
|
666
686
|
err.add_note(f"Error occurred in {longname}")
|
|
667
687
|
self.abort(err)
|
|
668
688
|
raise
|
|
669
|
-
if self.
|
|
689
|
+
if self.is_shut_down():
|
|
670
690
|
self.log_debug1("%s terminated", longname)
|
|
671
691
|
return
|
|
672
692
|
exc = RuntimeError(f"{longname} terminated before shutdown")
|
|
@@ -680,7 +700,7 @@ class Circuit:
|
|
|
680
700
|
**task_kwargs
|
|
681
701
|
) -> asyncio.Task[None]:
|
|
682
702
|
"""Create a service task for the circuit."""
|
|
683
|
-
if self.
|
|
703
|
+
if self.is_shut_down():
|
|
684
704
|
raise RuntimeError("Cannot create a service after shutdown")
|
|
685
705
|
# Python 3.12 and 3.13 only: Eager tasks start to run before their name is set.
|
|
686
706
|
# As a workaround we tell the watchdog wrapper the name.
|
|
@@ -734,8 +754,13 @@ async def run(*coroutines: Coroutine[t.Any, t.Any, t.Any], catch_sigterm: bool =
|
|
|
734
754
|
|
|
735
755
|
If errors occur, raise an exception group with all exceptions.
|
|
736
756
|
"""
|
|
757
|
+
circuit = get_circuit()
|
|
758
|
+
state = circuit.get_state()
|
|
759
|
+
if state is CircuitState.CLOSED:
|
|
760
|
+
raise RuntimeError("Cannot restart a closed circuit.")
|
|
761
|
+
if state is not CircuitState.UNDER_CONSTRUCTION:
|
|
762
|
+
raise RuntimeError("The circuit is already running.")
|
|
737
763
|
with TerminatingSignal(signal.SIGTERM) if catch_sigterm else contextlib.nullcontext():
|
|
738
|
-
circuit = get_circuit()
|
|
739
764
|
try:
|
|
740
765
|
async with asyncio.TaskGroup() as tg:
|
|
741
766
|
tg.create_task(circuit.rz_runner(), name="Circuit runner")
|
redzed/utils/async_utils.py
CHANGED
|
@@ -8,7 +8,7 @@ import asyncio
|
|
|
8
8
|
from collections.abc import Awaitable
|
|
9
9
|
import typing as t
|
|
10
10
|
|
|
11
|
-
__all__ = ['BufferShutDown', 'cancel_shield'
|
|
11
|
+
__all__ = ['BufferShutDown', 'cancel_shield']
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
14
|
BufferShutDown = asyncio.QueueShutDown # Python 3.13+
|
|
@@ -18,105 +18,6 @@ except AttributeError:
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
_T = t.TypeVar("_T")
|
|
21
|
-
_MISSING = object()
|
|
22
|
-
|
|
23
|
-
class MsgSync(t.Generic[_T]):
|
|
24
|
-
"""
|
|
25
|
-
Task synchronization based on sending messages of type _T.
|
|
26
|
-
|
|
27
|
-
The count of unread message is always either zero or one,
|
|
28
|
-
because messages are not queued. A new message replaces
|
|
29
|
-
the previous unread one, if any.
|
|
30
|
-
|
|
31
|
-
An existing unread message is available immediately.
|
|
32
|
-
Otherwise the recipient must wait and will be awakened when
|
|
33
|
-
a new message arrives. Reading a message consumes it,
|
|
34
|
-
so each message can reach only one receiver.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
def __init__(self) -> None:
|
|
38
|
-
self._msg: t.Any = _MISSING
|
|
39
|
-
self._shutdown = False
|
|
40
|
-
self._has_data = asyncio.Event() # valid before shutdown
|
|
41
|
-
self._draining = False # valid after shutdown
|
|
42
|
-
|
|
43
|
-
def shutdown(self) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Disallow sending immediately. Disallow receiving after the MsgSync gets empty.
|
|
46
|
-
|
|
47
|
-
Calls to .send() will raise BufferShutDown.
|
|
48
|
-
|
|
49
|
-
If a message was sent before shutdown and is waiting, it can be
|
|
50
|
-
normally received before the receiver shuts down too. After
|
|
51
|
-
the shutdown all blocked callers will be unblocked with BufferShutDown.
|
|
52
|
-
Calls to .recv() will raise BufferShutDown as well.
|
|
53
|
-
"""
|
|
54
|
-
self._shutdown = True
|
|
55
|
-
self._draining = self._has_data.is_set()
|
|
56
|
-
if not self._draining:
|
|
57
|
-
# wakeup all waiters
|
|
58
|
-
self._has_data.set()
|
|
59
|
-
|
|
60
|
-
def is_shutdown(self) -> bool:
|
|
61
|
-
"""Check if the buffer was shut down."""
|
|
62
|
-
return self._shutdown
|
|
63
|
-
|
|
64
|
-
def send(self, msg: _T) -> None:
|
|
65
|
-
"""
|
|
66
|
-
Send a message to one receiver.
|
|
67
|
-
|
|
68
|
-
If the previously sent message hasn't been received yet,
|
|
69
|
-
the new message overwrites it.
|
|
70
|
-
"""
|
|
71
|
-
if self._shutdown:
|
|
72
|
-
raise BufferShutDown("The buffer was shut down")
|
|
73
|
-
self._msg = msg
|
|
74
|
-
self._has_data.set()
|
|
75
|
-
|
|
76
|
-
def clear(self) -> None:
|
|
77
|
-
"""Remove the waiting message."""
|
|
78
|
-
if self._shutdown:
|
|
79
|
-
self._draining = False
|
|
80
|
-
else:
|
|
81
|
-
self._has_data.clear()
|
|
82
|
-
self._msg = _MISSING
|
|
83
|
-
|
|
84
|
-
def has_data(self) -> bool:
|
|
85
|
-
"""Return True only if there is a waiting message."""
|
|
86
|
-
return self._draining if self._shutdown else self._has_data.is_set()
|
|
87
|
-
|
|
88
|
-
async def _recv(self) -> _T:
|
|
89
|
-
"""Receive (consume) a message."""
|
|
90
|
-
while not self._has_data.is_set():
|
|
91
|
-
await self._has_data.wait()
|
|
92
|
-
if not self._shutdown:
|
|
93
|
-
self._has_data.clear()
|
|
94
|
-
elif self._draining:
|
|
95
|
-
self._draining = False
|
|
96
|
-
else:
|
|
97
|
-
raise BufferShutDown("The buffer was shut down")
|
|
98
|
-
msg, self._msg = self._msg, _MISSING
|
|
99
|
-
return t.cast(_T, msg)
|
|
100
|
-
|
|
101
|
-
async def recv(self, timeout: float | None = None, default: t.Any = _MISSING) -> t.Any:
|
|
102
|
-
"""
|
|
103
|
-
Receive a message with optional timeout.
|
|
104
|
-
|
|
105
|
-
If a message is not available, wait until it arrives.
|
|
106
|
-
|
|
107
|
-
If a timeout [seconds] is given, return the default value
|
|
108
|
-
if no message is received before the timeout period elapses.
|
|
109
|
-
Without a default, raise the TimeoutError.
|
|
110
|
-
"""
|
|
111
|
-
if timeout is None:
|
|
112
|
-
return await self._recv()
|
|
113
|
-
try:
|
|
114
|
-
async with asyncio.timeout(timeout):
|
|
115
|
-
return await self._recv()
|
|
116
|
-
except TimeoutError:
|
|
117
|
-
if default is not _MISSING:
|
|
118
|
-
return default
|
|
119
|
-
raise
|
|
120
21
|
|
|
121
22
|
|
|
122
23
|
async def cancel_shield(aw: Awaitable[_T]) -> _T:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redzed
|
|
3
|
-
Version: 26.
|
|
3
|
+
Version: 26.2.4
|
|
4
4
|
Summary: An asyncio-based library for building small automated systems
|
|
5
5
|
Author-email: Vlado Potisk <redzed@poti.sk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: homepage, https://github.com/xitop/redzed
|
|
|
8
8
|
Project-URL: repository, https://github.com/xitop/redzed
|
|
9
9
|
Project-URL: documentation, https://redzed.readthedocs.io/en/latest/
|
|
10
10
|
Keywords: automation,finite-state machine
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
redzed/__init__.py,sha256=
|
|
1
|
+
redzed/__init__.py,sha256=IVRc3KnMDB9N_M0fiii8X84I6_AEjhmOqtrzAJbvuzY,1097
|
|
2
2
|
redzed/base_block.py,sha256=A1ZxEIH8jsNXj3z3HvzgAVshUsMoFtqIoR41-U2vMaw,5187
|
|
3
|
-
redzed/block.py,sha256=
|
|
4
|
-
redzed/circuit.py,sha256=
|
|
3
|
+
redzed/block.py,sha256=_Qp8DQ1Ojb_PuGcYy2xS4gkhUeQymIx69iJaKHUM9jQ,11768
|
|
4
|
+
redzed/circuit.py,sha256=6C1mmg83Z_mjCHUpLlU9mdK4297cjrBycIuUUnjSB5E,31894
|
|
5
5
|
redzed/cron_service.py,sha256=0RSIte6zkbF4mSojrPoVFMHkVep1OZ2Mv2BtG3YiPTs,11372
|
|
6
6
|
redzed/debug.py,sha256=hsHbBxPOOUVL_utfiRnkRh49FszDvHsCZ9xAqxawUIU,2868
|
|
7
7
|
redzed/formula_trigger.py,sha256=lkBMeLVrcvqA71WGGapj9hitm52-dATMgT5QFhC1lOY,7360
|
|
@@ -13,18 +13,18 @@ redzed/blocklib/__init__.py,sha256=au7jbjO_RUFzK5U4STk-7pc1DTVA0OVh2W_aKsx1Y3s,5
|
|
|
13
13
|
redzed/blocklib/counter.py,sha256=pUT5Np31gk1rP2LqaTjJdzdyyS8NhpD9104Re_MtLpY,1251
|
|
14
14
|
redzed/blocklib/fsm.py,sha256=3UVQyKwZstL96jf3HYfuvN6R49uOMXVo20MFz9EpGkc,24746
|
|
15
15
|
redzed/blocklib/inputs.py,sha256=YtKtbDUtXyQyUTNadniwyxCA_-UPCgrrY17EipfXcaI,6223
|
|
16
|
-
redzed/blocklib/outputs.py,sha256=
|
|
17
|
-
redzed/blocklib/repeat.py,sha256=
|
|
16
|
+
redzed/blocklib/outputs.py,sha256=77FwFzh3q1Ri7tsAi39fmojD0vMmwBTSAYEjf1ZABpg,13964
|
|
17
|
+
redzed/blocklib/repeat.py,sha256=R4f0WjTTnqpiYOHDCHucmGdp6ChyVozLaZDaE-Nx7Tc,3146
|
|
18
18
|
redzed/blocklib/timedate.py,sha256=bmYZx6ToYsuD3UQzc1zxD2ddvovzF0A63-y4Pnkrm6Q,5225
|
|
19
19
|
redzed/blocklib/timeinterval.py,sha256=alK68glDdWPD9SWxZ97pEzhQIReC41RMdXrRIRZsyuE,6878
|
|
20
20
|
redzed/blocklib/timer.py,sha256=fCQTbmBOqo7pc6k32yj7JoyUM9XbdgHOXzu2tRiOiJE,1483
|
|
21
21
|
redzed/blocklib/validator.py,sha256=6r0qtPIJ2neCP32WhI7tGxEUus_ji6kx_jrzHd2_M9I,1312
|
|
22
22
|
redzed/utils/__init__.py,sha256=Yo8cj1f1HQj862UOdCsXOkMg4kfq1c5S3HJDhuiE5os,326
|
|
23
|
-
redzed/utils/async_utils.py,sha256=
|
|
23
|
+
redzed/utils/async_utils.py,sha256=ABwloG7YKqRwfIwWsXNaGlNPc7MdEdq29-aRGRDsBgk,1075
|
|
24
24
|
redzed/utils/data_utils.py,sha256=hesExhPcM2-IZPbDXAuGCUMpCI4YiNodp_ZJUj0_h9I,2952
|
|
25
25
|
redzed/utils/time_utils.py,sha256=eCqk4T4ipn4hgzL8-4Usn1W6kkTyRCuQ9-BMSvnELww,8524
|
|
26
|
-
redzed-26.
|
|
27
|
-
redzed-26.
|
|
28
|
-
redzed-26.
|
|
29
|
-
redzed-26.
|
|
30
|
-
redzed-26.
|
|
26
|
+
redzed-26.2.4.dist-info/licenses/LICENSE.txt,sha256=brj9B7uNdzUvTJON_5Eibf7zeLRhcMuBYMInalu0KlI,1091
|
|
27
|
+
redzed-26.2.4.dist-info/METADATA,sha256=nQARn1_dngD-r1UBeYDVthqAwmqy6taWhJhDlP14kNo,2055
|
|
28
|
+
redzed-26.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
29
|
+
redzed-26.2.4.dist-info/top_level.txt,sha256=7Rt0BRMqaJ0AGAmrd2JDqmqSY4cmQeW--7u6KDV1gZg,7
|
|
30
|
+
redzed-26.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|