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/__init__.py
CHANGED
|
@@ -7,15 +7,15 @@ The redzed package allows to build a so-called "circuit" containing:
|
|
|
7
7
|
|
|
8
8
|
The application code must connect the circuit with outside world.
|
|
9
9
|
|
|
10
|
-
Copyright (c) 2025 Vlado Potisk <redzed@poti.sk>.
|
|
10
|
+
Copyright (c) 2025-2026 Vlado Potisk <redzed@poti.sk>.
|
|
11
11
|
|
|
12
12
|
Released under the MIT License.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
15
|
+
Home: https://github.com/xitop/redzed/
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version_info__ = (
|
|
18
|
+
__version_info__ = (26, 1, 28)
|
|
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/base_block.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Base class of logic Blocks and Formulas.
|
|
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
|
|
redzed/block.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Logic Blocks.
|
|
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
|
|
|
@@ -45,6 +45,9 @@ class PersistenceFlags(enum.IntFlag):
|
|
|
45
45
|
INTERVAL = enum.auto() # save periodically
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
_INIT_TYPES = (initializers.SyncInitializer, initializers.AsyncInitializer)
|
|
49
|
+
|
|
50
|
+
|
|
48
51
|
class Block(BlockOrFormula):
|
|
49
52
|
"""
|
|
50
53
|
A circuit block maintaining its own state.
|
|
@@ -107,23 +110,18 @@ class Block(BlockOrFormula):
|
|
|
107
110
|
if not self.has_method('rz_init'):
|
|
108
111
|
raise TypeError(
|
|
109
112
|
f"{self}: Keyword argument 'initial' is not supported by this block type")
|
|
110
|
-
init_types = (initializers.SyncInitializer, initializers.AsyncInitializer)
|
|
111
113
|
if not is_multiple(initial):
|
|
112
|
-
if not isinstance(initial,
|
|
114
|
+
if not isinstance(initial, _INIT_TYPES):
|
|
113
115
|
initial = initializers.InitValue(initial)
|
|
114
116
|
self.rz_initializers = [initial]
|
|
117
|
+
elif any(isinstance(init, _INIT_TYPES) for init in initial):
|
|
118
|
+
# sequence of initializers
|
|
119
|
+
self.rz_initializers = [
|
|
120
|
+
init if isinstance(init, _INIT_TYPES) else initializers.InitValue(init)
|
|
121
|
+
for init in initial]
|
|
115
122
|
else:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# single value which happens to be a sequence
|
|
119
|
-
self.rz_initializers = [initializers.InitValue(initial)]
|
|
120
|
-
elif init_cnt == len(initial):
|
|
121
|
-
# sequence of initializers
|
|
122
|
-
self.rz_initializers = initial
|
|
123
|
-
else:
|
|
124
|
-
raise TypeError(
|
|
125
|
-
f"{self}: Check the initial argument. "
|
|
126
|
-
+ "A non-initializer was found in the sequence of initializers.")
|
|
123
|
+
# single value which happens to be a sequence
|
|
124
|
+
self.rz_initializers = [initializers.InitValue(initial)]
|
|
127
125
|
|
|
128
126
|
restore_initializers = [
|
|
129
127
|
init for init in self.rz_initializers
|
|
@@ -185,8 +183,6 @@ class Block(BlockOrFormula):
|
|
|
185
183
|
if self._counter >= 0:
|
|
186
184
|
output = (output, self._counter)
|
|
187
185
|
self._counter += 1
|
|
188
|
-
if self._counter >= 2**48: # more than enough for this purpose
|
|
189
|
-
self._counter = 0
|
|
190
186
|
if self._previous:
|
|
191
187
|
output = (output, self._output)
|
|
192
188
|
if not super()._set_output(output):
|
redzed/blocklib/counter.py
CHANGED
redzed/blocklib/fsm.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Event driven finite-state machine (FSM) extended with optional timers.
|
|
3
|
-
|
|
4
3
|
- - - - - -
|
|
4
|
+
Part of the redzed package.
|
|
5
5
|
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
6
|
Home: https://github.com/xitop/redzed/
|
|
7
7
|
"""
|
|
@@ -43,7 +43,7 @@ def _hook_args(func: Callable[..., t.Any]) -> int:
|
|
|
43
43
|
"""Check if *func* takes 0 or 1 argument."""
|
|
44
44
|
params = inspect.signature(func).parameters
|
|
45
45
|
plen = len(params)
|
|
46
|
-
if plen > 1 or
|
|
46
|
+
if plen > 1 or plen == 1 and next(iter(params.values())).kind not in _ALLOWED_KINDS:
|
|
47
47
|
raise TypeError(
|
|
48
48
|
f"Function {func.__name__} is not usable as an FSM hook "
|
|
49
49
|
+ "(incompatible call signature)")
|
|
@@ -56,15 +56,15 @@ class FSM(redzed.Block):
|
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
58
|
# subclasses should define:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#
|
|
59
|
+
STATES: Sequence[str|Sequence] = []
|
|
60
|
+
# each state: non-timed: state
|
|
61
|
+
# or timed: [state, duration, next_state]
|
|
62
62
|
EVENTS: Sequence[Sequence] = []
|
|
63
63
|
# each item: [event, [state1, state2, ..., stateN], next_state]
|
|
64
64
|
# or: [event, ..., next_state] <- literal ellipsis
|
|
65
65
|
# --- and redzed will translate that to: ---
|
|
66
66
|
_ct_default_state: str
|
|
67
|
-
# the default initial state (first item of
|
|
67
|
+
# the default initial state (first item of STATES)
|
|
68
68
|
_ct_duration: dict[str, float]
|
|
69
69
|
# {timed_state: default_duration_in_seconds}
|
|
70
70
|
_ct_events: set[str]
|
|
@@ -85,7 +85,7 @@ class FSM(redzed.Block):
|
|
|
85
85
|
@classmethod
|
|
86
86
|
def _check_state(cls, state: str) -> None:
|
|
87
87
|
if state not in cls._ct_states:
|
|
88
|
-
raise ValueError(f"FSM state '{state}' is unknown (missing in
|
|
88
|
+
raise ValueError(f"FSM state '{state}' is unknown (missing in STATES)")
|
|
89
89
|
|
|
90
90
|
@classmethod
|
|
91
91
|
def _add_transition(cls, event: str, state: str|None, next_state: str|None) -> None:
|
|
@@ -100,64 +100,93 @@ class FSM(redzed.Block):
|
|
|
100
100
|
@classmethod
|
|
101
101
|
def _build_tables(cls) -> None:
|
|
102
102
|
"""
|
|
103
|
-
Build control tables from
|
|
103
|
+
Build control tables from STATES and EVENTS.
|
|
104
104
|
|
|
105
105
|
Control tables must be created for each subclass. The original
|
|
106
106
|
tables are left unchanged. All control tables are class
|
|
107
107
|
variables and have the '_ct_' prefix.
|
|
108
108
|
"""
|
|
109
109
|
# states
|
|
110
|
-
if not is_multiple(cls.
|
|
111
|
-
raise ValueError("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
if not is_multiple(cls.STATES) or not cls.STATES:
|
|
111
|
+
raise ValueError("STATES: Expecting a non-empty sequence of states")
|
|
112
|
+
cls._ct_states = set()
|
|
113
|
+
timed_states: list[tuple[int, Sequence]] = []
|
|
114
|
+
for i, entry in enumerate(cls.STATES, start=1):
|
|
115
|
+
try:
|
|
116
|
+
if is_multiple(entry):
|
|
117
|
+
if len(entry) != 3:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Invalid timed state definition. "
|
|
120
|
+
+ "Expected are three values: state, duration, next_state")
|
|
121
|
+
timed_states.append((i, entry))
|
|
122
|
+
state = entry[0]
|
|
123
|
+
else:
|
|
124
|
+
state = entry
|
|
125
|
+
check_identifier(state, "FSM state name")
|
|
126
|
+
if i == 1:
|
|
127
|
+
cls._ct_default_state = state
|
|
128
|
+
elif state in cls._ct_states:
|
|
129
|
+
raise ValueError(f"Duplicate definition for state '{state}'")
|
|
130
|
+
cls._ct_states.add(state)
|
|
131
|
+
except (ValueError, TypeError) as err:
|
|
132
|
+
err.add_note(f"Offending entry: STATES table, item {i}")
|
|
133
|
+
raise
|
|
134
|
+
|
|
116
135
|
# timed states
|
|
117
136
|
cls._ct_duration = {}
|
|
118
137
|
cls._ct_timed_states = {}
|
|
119
|
-
for state, duration, next_state in
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
for i, (state, duration, next_state) in timed_states:
|
|
139
|
+
try:
|
|
140
|
+
# state was checked already, also for duplicates
|
|
141
|
+
cls._check_state(next_state)
|
|
142
|
+
if duration is not None:
|
|
143
|
+
try:
|
|
144
|
+
cls._ct_duration[state] = time_period(duration, zero_ok=True)
|
|
145
|
+
except (ValueError, TypeError) as err:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Could not convert duration of state '{state}' to seconds: {err}"
|
|
148
|
+
) from None
|
|
149
|
+
cls._ct_timed_states[state] = next_state
|
|
150
|
+
except (ValueError, TypeError) as err:
|
|
151
|
+
err.add_note(f"Offending entry: STATES table, item {i}")
|
|
152
|
+
raise
|
|
153
|
+
|
|
132
154
|
|
|
133
155
|
# events and state transitions
|
|
134
156
|
cls._ct_transition = {}
|
|
135
157
|
cls._ct_events = set()
|
|
136
|
-
for event, from_states, next_state in cls.EVENTS:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
cls.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
for i, (event, from_states, next_state) in enumerate(cls.EVENTS, start=1):
|
|
159
|
+
try:
|
|
160
|
+
j = 1
|
|
161
|
+
check_identifier(event, "FSM event name")
|
|
162
|
+
if event in cls._edt_handlers:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Ambiguous event '{event}': "
|
|
165
|
+
+ "the name is used for both FSM and Block event")
|
|
166
|
+
cls._ct_events.add(event)
|
|
167
|
+
j = 2
|
|
168
|
+
if from_states is ...:
|
|
169
|
+
# The ellipsis means any state. In control tables we are using None instead
|
|
170
|
+
cls._add_transition(event, None, next_state)
|
|
171
|
+
else:
|
|
172
|
+
if not is_multiple(from_states):
|
|
173
|
+
if from_states in cls._ct_states:
|
|
174
|
+
hint = f" Did you mean: ['{from_states}'] ?"
|
|
175
|
+
else:
|
|
176
|
+
hint = ""
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"Expected is a literal ellipsis (...) or a sequence of states, "
|
|
179
|
+
+ f"got {from_states!r}{hint}")
|
|
180
|
+
for fstate in from_states:
|
|
181
|
+
cls._check_state(fstate)
|
|
182
|
+
cls._add_transition(event, fstate, next_state)
|
|
183
|
+
j = 3
|
|
184
|
+
if next_state is not None:
|
|
185
|
+
cls._check_state(next_state)
|
|
186
|
+
except (ValueError, TypeError) as err:
|
|
187
|
+
# pylint: disable-next=used-before-assignment
|
|
188
|
+
err.add_note(f"Offending entry: EVENTS table, item {i}, position: {j}/3")
|
|
189
|
+
raise
|
|
161
190
|
|
|
162
191
|
# helper table: name 'prefix_suffix' is valid if prefix is a dict key
|
|
163
192
|
# and suffix is listed in the corresponding dict value
|
redzed/blocklib/inputs.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
A single memory cell blocks for general use.
|
|
3
3
|
- - - - - -
|
|
4
4
|
Part of the redzed package.
|
|
5
|
-
Docs: https://
|
|
6
|
-
Home: https://github.com/xitop/
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Home: https://github.com/xitop/redzed/
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
@@ -17,39 +17,7 @@ import typing as t
|
|
|
17
17
|
import redzed
|
|
18
18
|
from redzed.utils import is_multiple, time_period
|
|
19
19
|
from .fsm import FSM
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class _Validate(redzed.Block):
|
|
23
|
-
"""
|
|
24
|
-
Add a value validator.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self, *args,
|
|
29
|
-
validator: Callable[[t.Any], t.Any]|None = None,
|
|
30
|
-
**kwargs) -> None:
|
|
31
|
-
self._validator = validator
|
|
32
|
-
super().__init__(*args, **kwargs)
|
|
33
|
-
|
|
34
|
-
def _validate(self, value: t.Any) -> t.Any:
|
|
35
|
-
"""
|
|
36
|
-
Return the value processed by the validator.
|
|
37
|
-
|
|
38
|
-
Return UNDEF if the validator rejected the value by raising
|
|
39
|
-
an exception. Return the value unchanged if a validator was
|
|
40
|
-
not configured.
|
|
41
|
-
"""
|
|
42
|
-
if self._validator is None or value is redzed.UNDEF:
|
|
43
|
-
return value
|
|
44
|
-
try:
|
|
45
|
-
validated = self._validator(value)
|
|
46
|
-
except Exception as err:
|
|
47
|
-
self.log_debug1(
|
|
48
|
-
"Validator rejected value %r with %s: %s", value, type(err).__name__, err)
|
|
49
|
-
return redzed.UNDEF
|
|
50
|
-
if validated != value:
|
|
51
|
-
self.log_debug2("Validator has rewritten %r -> %r", value, validated)
|
|
52
|
-
return validated
|
|
20
|
+
from .validator import _Validate
|
|
53
21
|
|
|
54
22
|
|
|
55
23
|
class Memory(_Validate, redzed.Block):
|
|
@@ -59,29 +27,28 @@ class Memory(_Validate, redzed.Block):
|
|
|
59
27
|
Memory is typically used as an input block.
|
|
60
28
|
"""
|
|
61
29
|
|
|
62
|
-
def _store_value(self, value: t.Any) -> bool:
|
|
63
|
-
"""
|
|
64
|
-
Validate and store a value.
|
|
65
|
-
|
|
66
|
-
Return True on success, False on validation error.
|
|
67
|
-
"""
|
|
68
|
-
if (validated := self._validate(value)) is redzed.UNDEF:
|
|
69
|
-
return False
|
|
70
|
-
self._set_output(validated)
|
|
71
|
-
return True
|
|
72
|
-
|
|
73
30
|
def _event_store(self, edata: redzed.EventData) -> bool:
|
|
31
|
+
"""Validate and store a value."""
|
|
74
32
|
evalue = edata['evalue']
|
|
75
|
-
|
|
33
|
+
try:
|
|
34
|
+
validated = self._validate(evalue)
|
|
35
|
+
except Exception:
|
|
36
|
+
if edata.get('suppress', False):
|
|
37
|
+
return False
|
|
38
|
+
raise
|
|
39
|
+
self._set_output(validated)
|
|
40
|
+
return True
|
|
76
41
|
|
|
77
42
|
def rz_init(self, init_value: t.Any, /) -> None:
|
|
78
|
-
self.
|
|
43
|
+
validated = self._validate(init_value)
|
|
44
|
+
self._set_output(validated)
|
|
79
45
|
|
|
80
46
|
def rz_export_state(self) -> t.Any:
|
|
81
47
|
return self.get()
|
|
82
48
|
|
|
83
49
|
def rz_restore_state(self, state: t.Any, /) -> None:
|
|
84
|
-
|
|
50
|
+
# Do not validate. *state* is already validated and thus possibly preprocessed.
|
|
51
|
+
self._set_output(state)
|
|
85
52
|
|
|
86
53
|
|
|
87
54
|
class MemoryExp(_Validate, FSM):
|
|
@@ -89,8 +56,7 @@ class MemoryExp(_Validate, FSM):
|
|
|
89
56
|
Memory cell with an expiration time.
|
|
90
57
|
"""
|
|
91
58
|
|
|
92
|
-
|
|
93
|
-
TIMED_STATES = [ ['valid', None, 'expired'], ]
|
|
59
|
+
STATES = ['expired', ['valid', None, 'expired']]
|
|
94
60
|
|
|
95
61
|
def __init__(
|
|
96
62
|
self, *args,
|
|
@@ -98,25 +64,33 @@ class MemoryExp(_Validate, FSM):
|
|
|
98
64
|
expired: t.Any = None,
|
|
99
65
|
**kwargs) -> None: # kwargs may contain a validator
|
|
100
66
|
super().__init__(*args, t_valid=duration, **kwargs)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
67
|
+
try:
|
|
68
|
+
self._expired = self._validate(expired)
|
|
69
|
+
except Exception as err:
|
|
70
|
+
err.add_note(f"{self}: The validator rejected the 'expired' argument {expired!r}")
|
|
71
|
+
raise
|
|
105
72
|
|
|
106
|
-
def
|
|
107
|
-
evalue = edata['evalue']
|
|
108
|
-
if (validated := self._validate(evalue)) is redzed.UNDEF:
|
|
109
|
-
return False
|
|
73
|
+
def _store(self, validated: t.Any) -> None:
|
|
110
74
|
if validated == self._expired:
|
|
111
75
|
self._goto('expired')
|
|
112
76
|
else:
|
|
113
77
|
self.sdata['memory'] = validated
|
|
114
78
|
self._goto('valid')
|
|
79
|
+
|
|
80
|
+
def _event_store(self, edata: redzed.EventData) -> bool:
|
|
81
|
+
evalue = edata['evalue']
|
|
82
|
+
validated = self._validate(evalue)
|
|
83
|
+
try:
|
|
84
|
+
validated = self._validate(evalue)
|
|
85
|
+
except Exception:
|
|
86
|
+
if edata.get('suppress', False):
|
|
87
|
+
return False
|
|
88
|
+
raise
|
|
89
|
+
self._store(validated)
|
|
115
90
|
return True
|
|
116
91
|
|
|
117
92
|
def rz_init(self, init_value: t.Any, /) -> None:
|
|
118
|
-
|
|
119
|
-
return
|
|
93
|
+
validated = self._validate(init_value)
|
|
120
94
|
if validated == self._expired:
|
|
121
95
|
super().rz_init('expired')
|
|
122
96
|
else:
|
|
@@ -131,7 +105,7 @@ class MemoryExp(_Validate, FSM):
|
|
|
131
105
|
self.sdata['memory'] if self.state == 'valid' else self._expired)
|
|
132
106
|
|
|
133
107
|
|
|
134
|
-
class DataPoll(
|
|
108
|
+
class DataPoll(redzed.Block):
|
|
135
109
|
"""
|
|
136
110
|
A source of sampled or computed values.
|
|
137
111
|
"""
|
|
@@ -180,7 +154,7 @@ class DataPoll(_Validate, redzed.Block):
|
|
|
180
154
|
duration = time.monotonic() - start_ts
|
|
181
155
|
else:
|
|
182
156
|
duration = 0
|
|
183
|
-
if
|
|
157
|
+
if value is redzed.UNDEF:
|
|
184
158
|
failures += 1
|
|
185
159
|
self.log_debug1("Data acquisition failure(s): %d", failures)
|
|
186
160
|
if 0 < self._abort_after_failures <= failures:
|
|
@@ -205,6 +179,4 @@ class DataPoll(_Validate, redzed.Block):
|
|
|
205
179
|
def rz_export_state(self) -> t.Any:
|
|
206
180
|
return self.get()
|
|
207
181
|
|
|
208
|
-
|
|
209
|
-
if (value := self._validate(state)) is not redzed.UNDEF:
|
|
210
|
-
self._set_output(value)
|
|
182
|
+
rz_restore_state = rz_init
|