redzed 25.12.30__py3-none-any.whl → 26.1.28__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 +4 -4
- redzed/base_block.py +2 -2
- redzed/block.py +13 -17
- redzed/blocklib/counter.py +4 -0
- redzed/blocklib/fsm.py +81 -52
- redzed/blocklib/inputs.py +38 -66
- redzed/blocklib/outputs.py +53 -38
- redzed/blocklib/timedate.py +3 -3
- redzed/blocklib/timeinterval.py +4 -0
- redzed/blocklib/timer.py +3 -4
- redzed/blocklib/validator.py +46 -0
- redzed/circuit.py +5 -5
- redzed/cron_service.py +137 -123
- redzed/debug.py +70 -53
- redzed/formula_trigger.py +4 -8
- redzed/initializers.py +6 -7
- redzed/py.typed +0 -0
- redzed/signal_shutdown.py +3 -3
- redzed/undef.py +2 -2
- redzed/utils/data_utils.py +20 -44
- {redzed-25.12.30.dist-info → redzed-26.1.28.dist-info}/METADATA +1 -1
- redzed-26.1.28.dist-info/RECORD +30 -0
- {redzed-25.12.30.dist-info → redzed-26.1.28.dist-info}/WHEEL +1 -1
- {redzed-25.12.30.dist-info → redzed-26.1.28.dist-info}/licenses/LICENSE.txt +1 -1
- redzed-25.12.30.dist-info/RECORD +0 -28
- {redzed-25.12.30.dist-info → redzed-26.1.28.dist-info}/top_level.txt +0 -0
redzed/blocklib/outputs.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Output blocks.
|
|
3
3
|
- - - - - -
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Home: https://github.com/xitop/redzed/
|
|
6
7
|
"""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
@@ -14,6 +15,7 @@ import typing as t
|
|
|
14
15
|
|
|
15
16
|
import redzed
|
|
16
17
|
from redzed.utils import BufferShutDown, cancel_shield, func_call_string, MsgSync, time_period
|
|
18
|
+
from .validator import _Validate
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class OutputFunc(redzed.Block):
|
|
@@ -37,13 +39,13 @@ class OutputFunc(redzed.Block):
|
|
|
37
39
|
self.event('put', value)
|
|
38
40
|
|
|
39
41
|
def _event_put(self, edata: redzed.EventData) -> t.Any:
|
|
40
|
-
|
|
42
|
+
evalue = edata['evalue']
|
|
41
43
|
if redzed.get_debug_level() >= 1:
|
|
42
|
-
self.log_debug("Running %s", func_call_string(self._func, (
|
|
44
|
+
self.log_debug("Running %s", func_call_string(self._func, (evalue,)))
|
|
43
45
|
try:
|
|
44
|
-
result = self._func(
|
|
46
|
+
result = self._func(evalue)
|
|
45
47
|
except Exception as err:
|
|
46
|
-
func_args = func_call_string(self._func, (
|
|
48
|
+
func_args = func_call_string(self._func, (evalue,))
|
|
47
49
|
self.log_error("Output function failed; call: %s; error: %r", func_args, err)
|
|
48
50
|
err.add_note(f"Error occurred in function call {func_args}")
|
|
49
51
|
self.circuit.abort(err)
|
|
@@ -56,19 +58,31 @@ class OutputFunc(redzed.Block):
|
|
|
56
58
|
self._event_put({'evalue': self._stop_value})
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
class _Buffer(redzed.Block):
|
|
61
|
+
class _Buffer(_Validate, redzed.Block):
|
|
60
62
|
def __init__(
|
|
61
63
|
self, *args,
|
|
62
64
|
stop_value: t.Any = redzed.UNDEF,
|
|
63
65
|
triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
|
|
64
66
|
**kwargs) -> None:
|
|
65
67
|
super().__init__(*args, **kwargs)
|
|
66
|
-
|
|
68
|
+
# the stop_value is stored already validated
|
|
69
|
+
self._stop_value = \
|
|
70
|
+
redzed.UNDEF if stop_value is redzed.UNDEF else self._validate(stop_value)
|
|
67
71
|
if triggered_by is not redzed.UNDEF:
|
|
68
72
|
@redzed.triggered
|
|
69
73
|
def trigger(value=triggered_by) -> None:
|
|
70
74
|
self.event('put', value)
|
|
71
75
|
|
|
76
|
+
def _event_put(self, edata: redzed.EventData) -> None:
|
|
77
|
+
"""Put an item into the queue."""
|
|
78
|
+
# not aggregating following two lines in order to have a clear traceback
|
|
79
|
+
evalue = edata['evalue']
|
|
80
|
+
evalue = self._validate(evalue)
|
|
81
|
+
self.rz_put_value(evalue)
|
|
82
|
+
|
|
83
|
+
def rz_put_value(self, value: t.Any) -> None:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
72
86
|
def rz_get_size(self) -> int:
|
|
73
87
|
raise NotImplementedError
|
|
74
88
|
|
|
@@ -91,13 +105,10 @@ class QueueBuffer(_Buffer):
|
|
|
91
105
|
self._queue: asyncio.Queue[t.Any] = queue_type(maxsize)
|
|
92
106
|
# Queue.shutdown is available in Python 3.13, but we want to support 3.11+
|
|
93
107
|
self._waiters = 0
|
|
108
|
+
self._shutdown = False
|
|
94
109
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
evalue = edata['evalue']
|
|
98
|
-
if evalue is redzed.UNDEF:
|
|
99
|
-
raise ValueError(f"{self}: Cannot put UNDEF into the buffer")
|
|
100
|
-
self._queue.put_nowait(evalue)
|
|
110
|
+
def rz_put_value(self, value: t.Any) -> None:
|
|
111
|
+
self._queue.put_nowait(value)
|
|
101
112
|
|
|
102
113
|
def rz_get_size(self) -> int:
|
|
103
114
|
"""Get the number of items in the buffer."""
|
|
@@ -107,11 +118,14 @@ class QueueBuffer(_Buffer):
|
|
|
107
118
|
return self.rz_get_size()
|
|
108
119
|
|
|
109
120
|
def rz_stop(self) -> None:
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
waiters = self._waiters
|
|
122
|
+
if self._stop_value is not redzed.UNDEF:
|
|
123
|
+
self.rz_put_value(self._stop_value)
|
|
124
|
+
waiters -= 1
|
|
125
|
+
# unblock waiters
|
|
126
|
+
for _ in range(waiters):
|
|
127
|
+
self.rz_put_value(redzed.UNDEF)
|
|
128
|
+
self._shutdown = True
|
|
115
129
|
|
|
116
130
|
async def rz_buffer_get(self) -> t.Any:
|
|
117
131
|
"""
|
|
@@ -121,7 +135,7 @@ class QueueBuffer(_Buffer):
|
|
|
121
135
|
After the shutdown drain the queue
|
|
122
136
|
and then raise BufferShutDown to each caller.
|
|
123
137
|
"""
|
|
124
|
-
if self.
|
|
138
|
+
if self._shutdown and self._queue.qsize() == 0:
|
|
125
139
|
raise BufferShutDown("The buffer was shut down")
|
|
126
140
|
self._waiters += 1
|
|
127
141
|
try:
|
|
@@ -156,9 +170,8 @@ class MemoryBuffer(_Buffer):
|
|
|
156
170
|
def _event__get_size(self, _edata: redzed.EventData) -> int:
|
|
157
171
|
return self.rz_get_size()
|
|
158
172
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
self._cell.send(edata['evalue'])
|
|
173
|
+
def rz_put_value(self, value: t.Any) -> None:
|
|
174
|
+
self._cell.send(value)
|
|
162
175
|
|
|
163
176
|
def rz_stop(self) -> None:
|
|
164
177
|
"""Shut down"""
|
|
@@ -189,20 +202,20 @@ class MemoryBuffer(_Buffer):
|
|
|
189
202
|
|
|
190
203
|
class OutputWorker(redzed.Block):
|
|
191
204
|
"""
|
|
192
|
-
Run
|
|
205
|
+
Run an awaitable for each value from a buffer.
|
|
193
206
|
"""
|
|
194
207
|
|
|
195
208
|
def __init__(
|
|
196
209
|
self, *args,
|
|
197
210
|
buffer: str|redzed.Block,
|
|
198
|
-
|
|
211
|
+
aw_func: Callable[[t.Any], Awaitable[t.Any]], # e.g. an async function
|
|
199
212
|
workers: int = 1,
|
|
200
213
|
**kwargs) -> None:
|
|
201
214
|
super().__init__(*args, **kwargs)
|
|
202
215
|
if workers < 1:
|
|
203
216
|
raise ValueError(f"{self}: At least one worker is required")
|
|
204
217
|
self._buffer = buffer
|
|
205
|
-
self.
|
|
218
|
+
self._aw_func = aw_func
|
|
206
219
|
self._workers = workers
|
|
207
220
|
self._worker_tasks: list[asyncio.Task[None]] = []
|
|
208
221
|
|
|
@@ -222,10 +235,9 @@ class OutputWorker(redzed.Block):
|
|
|
222
235
|
try:
|
|
223
236
|
if redzed.get_debug_level() >= 1:
|
|
224
237
|
self.log_debug(
|
|
225
|
-
"%s: Awaiting
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
self.log_debug2("%s: coroutine done", wname)
|
|
238
|
+
"%s: Awaiting %s", wname, func_call_string(self._aw_func, (value,)))
|
|
239
|
+
await self._aw_func(value)
|
|
240
|
+
self.log_debug2("%s: await done", wname)
|
|
229
241
|
except Exception as err:
|
|
230
242
|
err.add_note(f"Processed value was: {value!r}")
|
|
231
243
|
raise
|
|
@@ -241,27 +253,30 @@ class OutputWorker(redzed.Block):
|
|
|
241
253
|
self._worker(wname), auto_cancel=False, name=f"{self}: {wname}"))
|
|
242
254
|
|
|
243
255
|
async def rz_astop(self) -> None:
|
|
256
|
+
worker_tasks = [worker for worker in self._worker_tasks if not worker.done()]
|
|
257
|
+
if not worker_tasks:
|
|
258
|
+
return
|
|
244
259
|
try:
|
|
245
|
-
await asyncio.wait(
|
|
260
|
+
await asyncio.wait(worker_tasks)
|
|
246
261
|
except asyncio.CancelledError:
|
|
247
262
|
# stop_timeout from the circuit runner
|
|
248
|
-
for worker in
|
|
263
|
+
for worker in worker_tasks:
|
|
249
264
|
if not worker.done():
|
|
250
265
|
worker.cancel()
|
|
251
266
|
await asyncio.sleep(0)
|
|
252
|
-
if (running := sum(1 for worker in
|
|
267
|
+
if (running := sum(1 for worker in worker_tasks if not worker.done())) > 0:
|
|
253
268
|
self.log_warning("%d worker(s) did not stop", running)
|
|
254
269
|
raise
|
|
255
270
|
|
|
256
271
|
class OutputController(redzed.Block):
|
|
257
272
|
"""
|
|
258
|
-
Run
|
|
273
|
+
Run an awaitable for the latest value from a buffer.
|
|
259
274
|
"""
|
|
260
275
|
|
|
261
276
|
def __init__(
|
|
262
277
|
self, *args,
|
|
263
278
|
buffer: str|redzed.Block,
|
|
264
|
-
|
|
279
|
+
aw_func: Callable[[t.Any], Awaitable[t.Any]], # e.g. an async function
|
|
265
280
|
rest_time: float|str = 0.0,
|
|
266
281
|
**kwargs) -> None:
|
|
267
282
|
super().__init__(*args, **kwargs)
|
|
@@ -269,7 +284,7 @@ class OutputController(redzed.Block):
|
|
|
269
284
|
if self._rest_time >= self.rz_stop_timeout:
|
|
270
285
|
raise ValueError(f"{self}: rest_time must be shorter than the stop_timeout")
|
|
271
286
|
self._buffer = buffer
|
|
272
|
-
self.
|
|
287
|
+
self._aw_func = aw_func
|
|
273
288
|
self._main_loop_task: asyncio.Task[None]|None = None
|
|
274
289
|
|
|
275
290
|
def rz_pre_init(self) -> None:
|
|
@@ -284,7 +299,7 @@ class OutputController(redzed.Block):
|
|
|
284
299
|
|
|
285
300
|
async def _run_with_rest_time(self, value: t.Any) -> None:
|
|
286
301
|
try:
|
|
287
|
-
await self.
|
|
302
|
+
await self._aw_func(value)
|
|
288
303
|
except asyncio.CancelledError:
|
|
289
304
|
self.log_debug2("Task cancelled")
|
|
290
305
|
await self._rest_time_delay()
|
|
@@ -331,7 +346,7 @@ class OutputController(redzed.Block):
|
|
|
331
346
|
if redzed.get_debug_level() >= 1:
|
|
332
347
|
self.log_debug(
|
|
333
348
|
"Creating task: %s%s",
|
|
334
|
-
func_call_string(self.
|
|
349
|
+
func_call_string(self._aw_func, (value,)),
|
|
335
350
|
" + rest time" if self._rest_time > 0.0 else "",
|
|
336
351
|
)
|
|
337
352
|
task = asyncio.create_task(self._run_with_rest_time(value))
|
redzed/blocklib/timedate.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Periodic events at fixed time/date.
|
|
3
|
-
|
|
4
3
|
- - - - - -
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Home: https://github.com/xitop/redzed/
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
redzed/blocklib/timeinterval.py
CHANGED
|
@@ -13,6 +13,10 @@ Intervals use those objects as endpoints:
|
|
|
13
13
|
- DateTimeInterval defines non-recurring intervals.
|
|
14
14
|
|
|
15
15
|
All intervals support the operation "value in interval".
|
|
16
|
+
- - - - - -
|
|
17
|
+
Part of the redzed package.
|
|
18
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
19
|
+
Home: https://github.com/xitop/redzed/
|
|
16
20
|
"""
|
|
17
21
|
from __future__ import annotations
|
|
18
22
|
|
redzed/blocklib/timer.py
CHANGED
|
@@ -18,10 +18,9 @@ class Timer(FSM):
|
|
|
18
18
|
A timer.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
['on', float("inf"), 'off']
|
|
24
|
-
['off', float("inf"), 'on']]
|
|
21
|
+
STATES = [
|
|
22
|
+
['off', float("inf"), 'on'],
|
|
23
|
+
['on', float("inf"), 'off']]
|
|
25
24
|
EVENTS = [
|
|
26
25
|
['start', ..., 'on'],
|
|
27
26
|
['stop', ..., 'off'],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data validator
|
|
3
|
+
- - - - - -
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Home: https://github.com/xitop/redzed/
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
import typing as t
|
|
12
|
+
|
|
13
|
+
class _Validate:
|
|
14
|
+
"""
|
|
15
|
+
Add a value validator.
|
|
16
|
+
|
|
17
|
+
To be used as a redzed.Block mix-in class.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self, *args,
|
|
22
|
+
validator: Callable[[t.Any], t.Any]|None = None,
|
|
23
|
+
**kwargs) -> None:
|
|
24
|
+
self._validator = validator
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
# mypy: disable-error-code=attr-defined
|
|
28
|
+
# pylint: disable=no-member
|
|
29
|
+
def _validate(self, value: t.Any) -> t.Any:
|
|
30
|
+
"""
|
|
31
|
+
Return the value processed by the validator.
|
|
32
|
+
|
|
33
|
+
Return the value unchanged if a validator was not configured.
|
|
34
|
+
The validator may raise to reject the value.
|
|
35
|
+
"""
|
|
36
|
+
if self._validator is None:
|
|
37
|
+
return value
|
|
38
|
+
try:
|
|
39
|
+
validated = self._validator(value)
|
|
40
|
+
except Exception as err:
|
|
41
|
+
self.log_debug1(
|
|
42
|
+
"Validator rejected value %r with %s: %s", value, type(err).__name__, err)
|
|
43
|
+
raise
|
|
44
|
+
if validated != value:
|
|
45
|
+
self.log_debug2("Validator has rewritten %r -> %r", value, validated)
|
|
46
|
+
return validated
|
redzed/circuit.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
The circuit runner.
|
|
3
3
|
- - - - - -
|
|
4
4
|
Part of the redzed package.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Project home: https://github.com/xitop/redzed/
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
@@ -21,12 +21,12 @@ import typing as t
|
|
|
21
21
|
|
|
22
22
|
from .block import Block, PersistenceFlags
|
|
23
23
|
from .cron_service import Cron
|
|
24
|
-
from .debug import get_debug_level
|
|
24
|
+
from .debug import get_debug_level, set_debug_level
|
|
25
25
|
from .initializers import AsyncInitializer
|
|
26
26
|
from .formula_trigger import Formula, Trigger
|
|
27
27
|
from .signal_shutdown import TerminatingSignal
|
|
28
28
|
from .undef import UNDEF
|
|
29
|
-
from .utils import
|
|
29
|
+
from .utils import check_identifier, tasks_are_eager, time_period
|
|
30
30
|
|
|
31
31
|
_logger = logging.getLogger(__package__)
|
|
32
32
|
_current_circuit: Circuit|None = None
|
|
@@ -448,6 +448,7 @@ class Circuit:
|
|
|
448
448
|
if self.after_shutdown():
|
|
449
449
|
# It looks like a supporting task has failed immediately after the start
|
|
450
450
|
return
|
|
451
|
+
set_debug_level(get_debug_level()) # will check the existence of a logging handler
|
|
451
452
|
|
|
452
453
|
pe_blocks = [blk for blk in self.get_items(Block) if blk.has_method('rz_pre_init')]
|
|
453
454
|
pe_formulas = list(self.get_items(Formula))
|
|
@@ -681,7 +682,6 @@ class Circuit:
|
|
|
681
682
|
"""Create a service task for the circuit."""
|
|
682
683
|
if self.after_shutdown():
|
|
683
684
|
raise RuntimeError("Cannot create a service after shutdown")
|
|
684
|
-
check_async_coro(coro)
|
|
685
685
|
# Python 3.12 and 3.13 only: Eager tasks start to run before their name is set.
|
|
686
686
|
# As a workaround we tell the watchdog wrapper the name.
|
|
687
687
|
task = asyncio.create_task(
|