redzed 25.12.30__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 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
- # Docs: https://redzed.readthedocs.io/en/latest/
15
- # Home: https://github.com/xitop/redzed/
14
+ Docs: https://redzed.readthedocs.io/en/latest/
15
+ Home: https://github.com/xitop/redzed/
16
16
  """
17
17
 
18
- __version_info__ = (25, 12, 30)
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/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
- # Docs: https://redzed.readthedocs.io/en/latest/
6
- # Project home: https://github.com/xitop/redzed/
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
- # Docs: https://redzed.readthedocs.io/en/latest/
6
- # Project home: https://github.com/xitop/redzed/
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, init_types):
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
- init_cnt = sum(1 for init in initial if isinstance(init, init_types))
117
- if init_cnt == 0:
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):
@@ -229,6 +225,9 @@ class Block(BlockOrFormula):
229
225
  # pylint: disable-next=no-member
230
226
  return self.rz_export_state() # type: ignore[attr-defined]
231
227
 
228
+ def rz_is_shut_down(self) -> bool:
229
+ return self.circuit.is_shut_down()
230
+
232
231
  def event(self, etype: str, /, evalue: t.Any = UNDEF, **edata: t.Any) -> t.Any:
233
232
  """
234
233
  An entry point for events.
@@ -236,13 +235,13 @@ class Block(BlockOrFormula):
236
235
  Call the specialized _event_ETYPE() method if it exists.
237
236
  Otherwise call the _default_event_handler() as the last resort.
238
237
  """
239
- if self.circuit.after_shutdown() and not etype.startswith('_get_'):
238
+ check_identifier(etype, "Event type")
239
+ if not etype.startswith('_get_') and self.rz_is_shut_down():
240
240
  raise CircuitShutDown("The circuit was shut down")
241
241
 
242
242
  if evalue is not UNDEF:
243
243
  edata['evalue'] = evalue
244
244
 
245
- check_identifier(etype, "Event type")
246
245
  if get_debug_level() >= 1:
247
246
  if not edata:
248
247
  self.log_debug("Got event '%s'", etype)
@@ -1,5 +1,9 @@
1
1
  """
2
2
  Counter.
3
+ - - - - - -
4
+ Part of the redzed package.
5
+ Docs: https://redzed.readthedocs.io/en/latest/
6
+ Home: https://github.com/xitop/redzed/
3
7
  """
4
8
 
5
9
  from __future__ import annotations
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 any(p.kind not in _ALLOWED_KINDS for p in params.values()):
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
- ALL_STATES: Sequence[str] = []
60
- TIMED_STATES: Sequence[Sequence] = []
61
- # each timed state: [state, duration, next_state]
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 ALL_STATES)
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 ALL_STATES)")
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 ALL_STATES, TIMED_STATES and EVENTS.
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.ALL_STATES) or not cls.ALL_STATES:
111
- raise ValueError("ALL_STATES: Expecting non-empty sequence of names")
112
- for state in cls.ALL_STATES:
113
- check_identifier(state, "FSM state name")
114
- cls._ct_states = set(cls.ALL_STATES)
115
- cls._ct_default_state = cls.ALL_STATES[0]
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 cls.TIMED_STATES:
120
- cls._check_state(state)
121
- cls._check_state(next_state)
122
- if state in cls._ct_timed_states:
123
- raise ValueError(f"TIMED_STATES: Multiple rules for timed state '{state}'")
124
- if duration is not None:
125
- try:
126
- cls._ct_duration[state] = time_period(duration, zero_ok=True)
127
- except (ValueError, TypeError) as err:
128
- raise ValueError(
129
- f"TIMED_STATES: could not convert duration of state '{state}' "
130
- + f"to seconds: {err}") from None
131
- cls._ct_timed_states[state] = next_state
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
- check_identifier(event, "FSM event name")
138
- if event in cls._edt_handlers:
139
- raise ValueError(
140
- f"Ambiguous event '{event}': "
141
- + "the name is used for both FSM and Block event")
142
- cls._ct_events.add(event)
143
- if next_state is not None:
144
- cls._check_state(next_state)
145
- if from_states is ...:
146
- # The ellipsis means any state. In control tables we are using None instead
147
- cls._add_transition(event, None, next_state)
148
- else:
149
- if not is_multiple(from_states):
150
- exc = ValueError(
151
- "Expected is a literal ellipsis (...) or a sequence of states, "
152
- + f"got {from_states!r}")
153
- exc.add_note(
154
- f"Problem was found in the transition rule for event '{event}'")
155
- if from_states in cls._ct_states:
156
- exc.add_note(f"Did you mean: ['{from_states}'] ?")
157
- raise exc
158
- for fstate in from_states:
159
- cls._check_state(fstate)
160
- cls._add_transition(event, fstate, next_state)
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://edzed.readthedocs.io/en/latest/
6
- Home: https://github.com/xitop/edzed/
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
- return self._store_value(evalue)
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._store_value(init_value)
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
- self._store_value(state)
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
- ALL_STATES = ['expired', 'valid']
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
- self._expired = self._validate(expired)
102
- if self._expired is redzed.UNDEF:
103
- raise ValueError(
104
- f"{self} The 'expired' argument {expired!r} was rejected by the validator")
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 _event_store(self, edata: redzed.EventData) -> bool:
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
- if (validated := self._validate(init_value)) is redzed.UNDEF:
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(_Validate, redzed.Block):
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 (value := self._validate(value)) is redzed.UNDEF:
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
- def rz_restore_state(self, state: t.Any, /) -> None:
209
- if (value := self._validate(state)) is not redzed.UNDEF:
210
- self._set_output(value)
182
+ rz_restore_state = rz_init