redzed 25.12.30__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 +44 -0
- redzed/base_block.py +132 -0
- redzed/block.py +290 -0
- redzed/blocklib/__init__.py +26 -0
- redzed/blocklib/counter.py +45 -0
- redzed/blocklib/fsm.py +554 -0
- redzed/blocklib/inputs.py +210 -0
- redzed/blocklib/outputs.py +361 -0
- redzed/blocklib/repeat.py +72 -0
- redzed/blocklib/timedate.py +150 -0
- redzed/blocklib/timeinterval.py +192 -0
- redzed/blocklib/timer.py +50 -0
- redzed/circuit.py +756 -0
- redzed/cron_service.py +243 -0
- redzed/debug.py +86 -0
- redzed/formula_trigger.py +205 -0
- redzed/initializers.py +249 -0
- redzed/signal_shutdown.py +64 -0
- redzed/undef.py +38 -0
- redzed/utils/__init__.py +14 -0
- redzed/utils/async_utils.py +145 -0
- redzed/utils/data_utils.py +116 -0
- redzed/utils/time_utils.py +262 -0
- redzed-25.12.30.dist-info/METADATA +52 -0
- redzed-25.12.30.dist-info/RECORD +28 -0
- redzed-25.12.30.dist-info/WHEEL +5 -0
- redzed-25.12.30.dist-info/licenses/LICENSE.txt +21 -0
- redzed-25.12.30.dist-info/top_level.txt +1 -0
redzed/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redzed is a library for building small automated systems.
|
|
3
|
+
|
|
4
|
+
The redzed package allows to build a so-called "circuit" containing:
|
|
5
|
+
- logic Blocks with outputs reacting to events
|
|
6
|
+
- Triggers sending events when certain outputs change
|
|
7
|
+
|
|
8
|
+
The application code must connect the circuit with outside world.
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 Vlado Potisk <redzed@poti.sk>.
|
|
11
|
+
|
|
12
|
+
Released under the MIT License.
|
|
13
|
+
|
|
14
|
+
# Docs: https://redzed.readthedocs.io/en/latest/
|
|
15
|
+
# Home: https://github.com/xitop/redzed/
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version_info__ = (25, 12, 30)
|
|
19
|
+
__version__ = '.'.join(str(n) for n in __version_info__)
|
|
20
|
+
|
|
21
|
+
from . import circuit, block, debug, formula_trigger, initializers, undef
|
|
22
|
+
|
|
23
|
+
from .block import *
|
|
24
|
+
from .circuit import *
|
|
25
|
+
from .debug import *
|
|
26
|
+
from .formula_trigger import *
|
|
27
|
+
from .initializers import *
|
|
28
|
+
from .undef import *
|
|
29
|
+
# .utils not imported
|
|
30
|
+
|
|
31
|
+
# block library
|
|
32
|
+
from . import blocklib
|
|
33
|
+
from .blocklib import *
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'__version__', '__version_info__',
|
|
37
|
+
*block.__all__,
|
|
38
|
+
*blocklib.__all__,
|
|
39
|
+
*circuit.__all__,
|
|
40
|
+
*debug.__all__,
|
|
41
|
+
*formula_trigger.__all__,
|
|
42
|
+
*initializers.__all__,
|
|
43
|
+
*undef.__all__,
|
|
44
|
+
]
|
redzed/base_block.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class of logic Blocks and Formulas.
|
|
3
|
+
- - - - - -
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
# Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
# Project home: https://github.com/xitop/redzed/
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import logging
|
|
12
|
+
import textwrap
|
|
13
|
+
import typing as t
|
|
14
|
+
|
|
15
|
+
from . import circuit
|
|
16
|
+
from .debug import get_debug_level
|
|
17
|
+
from .undef import UNDEF
|
|
18
|
+
from .utils import check_identifier
|
|
19
|
+
|
|
20
|
+
_logger = logging.getLogger(__package__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BlockOrFormula:
|
|
24
|
+
"""
|
|
25
|
+
The common part of a logic Block or Formula.
|
|
26
|
+
|
|
27
|
+
Check the name and register the new item.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name: str, *, comment: str = "") -> None:
|
|
31
|
+
"""Create new circuit component. Add it to the circuit."""
|
|
32
|
+
# These are the only two allowed concrete subsclasses
|
|
33
|
+
if not isinstance(self, (block.Block, formula_trigger.Formula)):
|
|
34
|
+
raise TypeError("Cannot instantiate an abstract class")
|
|
35
|
+
self.circuit = circuit.get_circuit()
|
|
36
|
+
check_identifier(name, "Block/Formula name")
|
|
37
|
+
if name.startswith('_') and not getattr(type(self), 'RZ_RESERVED', False):
|
|
38
|
+
raise ValueError(f"Name '{name}' is reserved (starting with an underscore)")
|
|
39
|
+
# reserved blocks cannot be created by simple mistake,
|
|
40
|
+
# because types of reserved blocks (e.g. Cron) are not public
|
|
41
|
+
self.name = name
|
|
42
|
+
self.comment = str(comment)
|
|
43
|
+
self._str_cached: str|None = None # cache for __str__ value
|
|
44
|
+
self.circuit.rz_add_item(self)
|
|
45
|
+
self._dependent_formulas: set[formula_trigger.Formula] = set()
|
|
46
|
+
self._dependent_triggers: set[formula_trigger.Trigger] = set()
|
|
47
|
+
self._output = UNDEF
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def type_name(self) -> str:
|
|
51
|
+
return type(self).__name__
|
|
52
|
+
|
|
53
|
+
def has_method(self, method_name: str, async_method: bool = False) -> bool:
|
|
54
|
+
if not callable(method := getattr(self, method_name, None)):
|
|
55
|
+
return False
|
|
56
|
+
if async_method and not inspect.iscoroutinefunction(method):
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def rz_add_formula(self, formula: formula_trigger.Formula) -> None:
|
|
61
|
+
"""Add a formula block depending on our output value."""
|
|
62
|
+
self._dependent_formulas.add(formula)
|
|
63
|
+
|
|
64
|
+
def rz_add_trigger(self, trigger: formula_trigger.Trigger) -> None:
|
|
65
|
+
"""Add a trigger block depending on our output value."""
|
|
66
|
+
self._dependent_triggers.add(trigger)
|
|
67
|
+
|
|
68
|
+
def _set_output(self, output: t.Any) -> bool:
|
|
69
|
+
"""Set output."""
|
|
70
|
+
if output is UNDEF:
|
|
71
|
+
raise ValueError(f"{self}: Cannot set output to <UNDEF>.")
|
|
72
|
+
if output == self._output:
|
|
73
|
+
return False
|
|
74
|
+
if get_debug_level() >= 1:
|
|
75
|
+
self.log_debug("Output: %r -> %r", self.get(), output)
|
|
76
|
+
self._output = output
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
def get(self) -> t.Any:
|
|
80
|
+
return self._output
|
|
81
|
+
|
|
82
|
+
def is_undef(self) -> bool:
|
|
83
|
+
return self._output is UNDEF
|
|
84
|
+
|
|
85
|
+
def log_msg(self, msg: str, *args: t.Any, level: int, **kwargs: t.Any) -> None:
|
|
86
|
+
"""Add own name and log the message with given severity level."""
|
|
87
|
+
_logger.log(level, f"{self} {msg}", *args, **kwargs)
|
|
88
|
+
|
|
89
|
+
def log_debug(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
90
|
+
"""Log a message with DEBUG severity."""
|
|
91
|
+
self.log_msg(msg, *args, level=logging.DEBUG, **kwargs)
|
|
92
|
+
|
|
93
|
+
def log_debug1(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
94
|
+
"""Log a message if debugging is enabled."""
|
|
95
|
+
if get_debug_level() >= 1:
|
|
96
|
+
self.log_msg(msg, *args, level=logging.DEBUG, **kwargs)
|
|
97
|
+
|
|
98
|
+
def log_debug2(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
99
|
+
"""Log a message if verbose debugging is enabled."""
|
|
100
|
+
if get_debug_level() >= 2:
|
|
101
|
+
self.log_msg(msg, *args, level=logging.DEBUG, **kwargs)
|
|
102
|
+
|
|
103
|
+
def log_info(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
104
|
+
"""Log a message with _INFO_ severity."""
|
|
105
|
+
self.log_msg(msg, *args, level=logging.INFO, **kwargs)
|
|
106
|
+
|
|
107
|
+
def log_warning(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
108
|
+
"""Log a message with _WARNING_ severity."""
|
|
109
|
+
self.log_msg(msg, *args, level=logging.WARNING, **kwargs)
|
|
110
|
+
|
|
111
|
+
def log_error(self, msg: str, *args: t.Any, **kwargs: t.Any) -> None:
|
|
112
|
+
"""Log a message with _ERROR_ severity."""
|
|
113
|
+
self.log_msg(msg, *args, level=logging.ERROR, **kwargs)
|
|
114
|
+
|
|
115
|
+
def __str__(self) -> str:
|
|
116
|
+
if self._str_cached is not None:
|
|
117
|
+
return self._str_cached
|
|
118
|
+
if not hasattr(self, 'name'):
|
|
119
|
+
# a subclass did not call super().__init__(name, ...) yet
|
|
120
|
+
return f"<{self.type_name} N/A id={hex(id(self))}>"
|
|
121
|
+
parts = [self.type_name, self.name]
|
|
122
|
+
if self.comment:
|
|
123
|
+
short_comment = textwrap.shorten(self.comment, width=40, placeholder="...")
|
|
124
|
+
parts.append(f"comment='{short_comment}'")
|
|
125
|
+
self._str_cached = f"<{' '.join(parts)}>"
|
|
126
|
+
return self._str_cached
|
|
127
|
+
|
|
128
|
+
# Importing at the end resolves a circular import issue.
|
|
129
|
+
# formula_trigger.Formula and block.Block are subclasses of BlockOrFormula defined here.
|
|
130
|
+
# pylint: disable=wrong-import-position
|
|
131
|
+
from . import formula_trigger
|
|
132
|
+
from . import block
|
redzed/block.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logic Blocks.
|
|
3
|
+
- - - - - -
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
# Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
# Project home: https://github.com/xitop/redzed/
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = ['Block', 'CircuitShutDown', 'EventData', 'PersistenceFlags', 'UnknownEvent']
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
import enum
|
|
15
|
+
import logging
|
|
16
|
+
import typing as t
|
|
17
|
+
|
|
18
|
+
from .base_block import BlockOrFormula
|
|
19
|
+
from .debug import get_debug_level
|
|
20
|
+
from . import initializers
|
|
21
|
+
from .undef import UNDEF, UndefType
|
|
22
|
+
from .utils import check_identifier, is_multiple, time_period
|
|
23
|
+
|
|
24
|
+
_logger = logging.getLogger(__package__)
|
|
25
|
+
_DEFAULT_STOP_TIMEOUT = 10.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
EventData: t.TypeAlias = dict[str, t.Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnknownEvent(Exception):
|
|
32
|
+
"""Event type not supported."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CircuitShutDown(RuntimeError):
|
|
36
|
+
"""Cannot perform an action after shutdown."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PersistenceFlags(enum.IntFlag):
|
|
40
|
+
"""When and how to save block's internal state to the persistent storage."""
|
|
41
|
+
ENABLED = enum.auto() # persistent state is enabled
|
|
42
|
+
# additional options, they may be set only if ENABLED,
|
|
43
|
+
# EVENT and INTERVAL are mutually exclusive
|
|
44
|
+
EVENT = enum.auto() # save after each event
|
|
45
|
+
INTERVAL = enum.auto() # save periodically
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Block(BlockOrFormula):
|
|
49
|
+
"""
|
|
50
|
+
A circuit block maintaining its own state.
|
|
51
|
+
|
|
52
|
+
Logic blocks accept events and modify their state in response
|
|
53
|
+
to them. They have one output depending solely on the state.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# event handling methods _event_NAME
|
|
57
|
+
_edt_handlers: dict[str, Callable[[t.Self, EventData], t.Any]]
|
|
58
|
+
# class attr RZ_PERSISTENCE: is persistent state supported?
|
|
59
|
+
# instance attr rz_persistence: see PersistenceFlags
|
|
60
|
+
RZ_PERSISTENCE: bool
|
|
61
|
+
|
|
62
|
+
def __init_subclass__(cls, *args, **kwargs) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Create event dispatch table (edt) of all specialized event handlers.
|
|
65
|
+
|
|
66
|
+
Add flag if persistent state is supported.
|
|
67
|
+
"""
|
|
68
|
+
super().__init_subclass__(*args, **kwargs)
|
|
69
|
+
cls._edt_handlers = {}
|
|
70
|
+
for mro in cls.__mro__:
|
|
71
|
+
if issubclass(mro, Block):
|
|
72
|
+
for mname, method in vars(mro).items():
|
|
73
|
+
if not callable(method):
|
|
74
|
+
continue # it's a plain attr
|
|
75
|
+
# When multiple handlers exist, save only the first one, because it has
|
|
76
|
+
# the highest rank in the MRO hierarchy.
|
|
77
|
+
if len(mname) > 7 and mname.startswith('_event_'):
|
|
78
|
+
cls._edt_handlers.setdefault(mname[7:], method)
|
|
79
|
+
|
|
80
|
+
cls.RZ_PERSISTENCE = (callable(getattr(cls, 'rz_export_state', None))
|
|
81
|
+
and callable(getattr(cls, 'rz_restore_state', None)))
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self, *args,
|
|
85
|
+
initial: t.Any = UNDEF,
|
|
86
|
+
stop_timeout: float|str|UndefType = UNDEF,
|
|
87
|
+
output_counter: bool = False,
|
|
88
|
+
output_previous: bool = False,
|
|
89
|
+
**kwargs
|
|
90
|
+
) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Process arguments.
|
|
93
|
+
|
|
94
|
+
Check if given arguments are valid for the particular block type.
|
|
95
|
+
"""
|
|
96
|
+
if type(self) is Block: # pylint: disable=unidiomatic-typecheck
|
|
97
|
+
raise TypeError("Cannot instantiate the base class 'Block'")
|
|
98
|
+
# remove x_arg=... and X_ARG=... from kwargs before calling super().__init__
|
|
99
|
+
for key in list(kwargs):
|
|
100
|
+
if key.startswith(('x_', 'X_')):
|
|
101
|
+
setattr(self, key, kwargs.pop(key))
|
|
102
|
+
super().__init__(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
if initial is UNDEF:
|
|
105
|
+
self.rz_initializers = []
|
|
106
|
+
else:
|
|
107
|
+
if not self.has_method('rz_init'):
|
|
108
|
+
raise TypeError(
|
|
109
|
+
f"{self}: Keyword argument 'initial' is not supported by this block type")
|
|
110
|
+
init_types = (initializers.SyncInitializer, initializers.AsyncInitializer)
|
|
111
|
+
if not is_multiple(initial):
|
|
112
|
+
if not isinstance(initial, init_types):
|
|
113
|
+
initial = initializers.InitValue(initial)
|
|
114
|
+
self.rz_initializers = [initial]
|
|
115
|
+
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.")
|
|
127
|
+
|
|
128
|
+
restore_initializers = [
|
|
129
|
+
init for init in self.rz_initializers
|
|
130
|
+
if isinstance(init, initializers.RestoreState)]
|
|
131
|
+
self.rz_persistence = PersistenceFlags(0)
|
|
132
|
+
if restore_initializers:
|
|
133
|
+
if not type(self).RZ_PERSISTENCE:
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"{self}: 'RestoreState' initializer is not supported by this block type")
|
|
136
|
+
if len(restore_initializers) != 1:
|
|
137
|
+
raise ValueError("Multiple 'RestoreState' initializers are not allowed")
|
|
138
|
+
self.rz_persistence |= PersistenceFlags.ENABLED # keep the int type
|
|
139
|
+
init = restore_initializers[0]
|
|
140
|
+
checkpoints = init.rz_checkpoints
|
|
141
|
+
if checkpoints == 'event':
|
|
142
|
+
self.rz_persistence |= PersistenceFlags.EVENT
|
|
143
|
+
elif checkpoints == 'interval':
|
|
144
|
+
self.rz_persistence |= PersistenceFlags.INTERVAL
|
|
145
|
+
self.rz_key = f"{self.type_name}:{self.name}"
|
|
146
|
+
|
|
147
|
+
has_astop = self.has_method('rz_astop')
|
|
148
|
+
self.rz_stop_timeout: float|None
|
|
149
|
+
if stop_timeout is UNDEF:
|
|
150
|
+
if has_astop:
|
|
151
|
+
self.log_debug2("Using default: stop_timeout=%.1f", _DEFAULT_STOP_TIMEOUT)
|
|
152
|
+
self.rz_stop_timeout = _DEFAULT_STOP_TIMEOUT
|
|
153
|
+
else:
|
|
154
|
+
self.rz_stop_timeout = None
|
|
155
|
+
else:
|
|
156
|
+
if not has_astop:
|
|
157
|
+
raise TypeError(
|
|
158
|
+
f"{self}: Keyword argument 'stop_timeout' "
|
|
159
|
+
+ "is not supported by this block type")
|
|
160
|
+
self.rz_stop_timeout = time_period(stop_timeout)
|
|
161
|
+
self._etypes_active: set[str] = set() # events in-progress, disallow event recursion
|
|
162
|
+
if output_counter and output_previous:
|
|
163
|
+
raise TypeError(
|
|
164
|
+
"Options 'output_counter' and 'output_previous' are mutually exclusive.")
|
|
165
|
+
self._counter = 0 if output_counter else -1
|
|
166
|
+
self._previous = bool(output_previous)
|
|
167
|
+
self._init_task: asyncio.Task[t.Any]|None = None
|
|
168
|
+
|
|
169
|
+
def rz_set_inittask(self, task: asyncio.Task[t.Any]) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Give a reference to the task initializing this block.
|
|
172
|
+
|
|
173
|
+
We will cancel it when the initialization is done.
|
|
174
|
+
"""
|
|
175
|
+
self._init_task = task
|
|
176
|
+
|
|
177
|
+
def _set_output(self, output: t.Any) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Set output, recalculate dependent formulas, run affected triggers.
|
|
180
|
+
"""
|
|
181
|
+
if self._output is UNDEF and self._init_task is not None:
|
|
182
|
+
if not self._init_task.done():
|
|
183
|
+
self._init_task.cancel()
|
|
184
|
+
self._init_task = None
|
|
185
|
+
if self._counter >= 0:
|
|
186
|
+
output = (output, self._counter)
|
|
187
|
+
self._counter += 1
|
|
188
|
+
if self._counter >= 2**48: # more than enough for this purpose
|
|
189
|
+
self._counter = 0
|
|
190
|
+
if self._previous:
|
|
191
|
+
output = (output, self._output)
|
|
192
|
+
if not super()._set_output(output):
|
|
193
|
+
return False
|
|
194
|
+
triggers = self._dependent_triggers.copy()
|
|
195
|
+
for frm in self._dependent_formulas:
|
|
196
|
+
triggers |= frm.evaluate()
|
|
197
|
+
for trg in triggers:
|
|
198
|
+
trg.run()
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def rz_init_default(self) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Set output to None for blocks that do not have init functions.
|
|
204
|
+
|
|
205
|
+
It is assumed that these blocks do not use their output.
|
|
206
|
+
"""
|
|
207
|
+
if self.is_undef() and not self.has_method('rz_init'):
|
|
208
|
+
self._set_output(None)
|
|
209
|
+
|
|
210
|
+
def _default_event_handler(self, etype: str, edata: EventData) -> t.Any:
|
|
211
|
+
"""Default event handler."""
|
|
212
|
+
raise UnknownEvent(f"{self}: Unknown event type {etype!r}")
|
|
213
|
+
|
|
214
|
+
# note the double underscore: ...vent_ + _get...
|
|
215
|
+
def _event__get_names(self, _edata: EventData) -> t.Any:
|
|
216
|
+
"""Handle '_get_names' monitoring event."""
|
|
217
|
+
return {'type': self.type_name, 'name': self.name, 'comment': self.comment}
|
|
218
|
+
|
|
219
|
+
def _event__get_output(self, _edata: EventData) -> t.Any:
|
|
220
|
+
"""Handle '_get_state' monitoring event."""
|
|
221
|
+
return self.get()
|
|
222
|
+
|
|
223
|
+
def _event__get_state(self, _edata: EventData) -> t.Any:
|
|
224
|
+
"""Handle '_get_state' monitoring event."""
|
|
225
|
+
if not self.has_method('rz_export_state'):
|
|
226
|
+
raise UnknownEvent(f"{self.type_name} blocks do not support this event")
|
|
227
|
+
if self.is_undef():
|
|
228
|
+
raise RuntimeError("Not initialized yet")
|
|
229
|
+
# pylint: disable-next=no-member
|
|
230
|
+
return self.rz_export_state() # type: ignore[attr-defined]
|
|
231
|
+
|
|
232
|
+
def event(self, etype: str, /, evalue: t.Any = UNDEF, **edata: t.Any) -> t.Any:
|
|
233
|
+
"""
|
|
234
|
+
An entry point for events.
|
|
235
|
+
|
|
236
|
+
Call the specialized _event_ETYPE() method if it exists.
|
|
237
|
+
Otherwise call the _default_event_handler() as the last resort.
|
|
238
|
+
"""
|
|
239
|
+
if self.circuit.after_shutdown() and not etype.startswith('_get_'):
|
|
240
|
+
raise CircuitShutDown("The circuit was shut down")
|
|
241
|
+
|
|
242
|
+
if evalue is not UNDEF:
|
|
243
|
+
edata['evalue'] = evalue
|
|
244
|
+
|
|
245
|
+
check_identifier(etype, "Event type")
|
|
246
|
+
if get_debug_level() >= 1:
|
|
247
|
+
if not edata:
|
|
248
|
+
self.log_debug("Got event '%s'", etype)
|
|
249
|
+
elif len(edata) == 1 and 'evalue' in edata:
|
|
250
|
+
self.log_debug("Got event '%s', evalue: %r", etype, edata['evalue'])
|
|
251
|
+
else:
|
|
252
|
+
self.log_debug("Got event '%s', edata: %s", etype, edata)
|
|
253
|
+
if etype in self._etypes_active:
|
|
254
|
+
# we must report the error both to the caller and to the runner
|
|
255
|
+
exc = RuntimeError(
|
|
256
|
+
f"{self}: Event '{etype}' generated another event of the same type")
|
|
257
|
+
self.circuit.abort(exc)
|
|
258
|
+
raise exc
|
|
259
|
+
self._etypes_active.add(etype)
|
|
260
|
+
try:
|
|
261
|
+
if self.is_undef():
|
|
262
|
+
# We will allow the event, because:
|
|
263
|
+
# - some blocks need an event for their initialization
|
|
264
|
+
# - this event could have arrived during initialization by chance or race
|
|
265
|
+
self.log_debug2("Pending event, initializing now")
|
|
266
|
+
self.circuit.init_block_sync(self)
|
|
267
|
+
handler = type(self)._edt_handlers.get(etype)
|
|
268
|
+
try:
|
|
269
|
+
if handler:
|
|
270
|
+
# handler is an unbound method => must add 'self'
|
|
271
|
+
retval = handler(self, edata)
|
|
272
|
+
else:
|
|
273
|
+
retval = self._default_event_handler(etype, edata)
|
|
274
|
+
except UnknownEvent:
|
|
275
|
+
self.log_debug1("Unknown event error raised")
|
|
276
|
+
raise
|
|
277
|
+
except Exception as err:
|
|
278
|
+
err.add_note(f"Error occurred in {self} during handling of event '{etype}'; "
|
|
279
|
+
+ f"event data was: {edata if edata else '<EMPTY>'}")
|
|
280
|
+
if isinstance(err, KeyError) and err.args[0] == 'evalue':
|
|
281
|
+
err.add_note("A required event value is almost certainly missing")
|
|
282
|
+
raise
|
|
283
|
+
if (self.rz_persistence & PersistenceFlags.EVENT
|
|
284
|
+
and not etype.startswith('_get_') # nothing has changed
|
|
285
|
+
and not self.is_undef()): # nothing to save
|
|
286
|
+
self.circuit.save_persistent_state(self)
|
|
287
|
+
self.log_debug2("Event '%s' returned: %r", etype, retval)
|
|
288
|
+
return retval
|
|
289
|
+
finally:
|
|
290
|
+
self._etypes_active.remove(etype)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A library of pre-defined blocks.
|
|
3
|
+
- - - - - -
|
|
4
|
+
Part of the redzed package.
|
|
5
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
6
|
+
Home: https://github.com/xitop/redzed/
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import counter, fsm, inputs, outputs, repeat, timedate, timer
|
|
10
|
+
from .counter import *
|
|
11
|
+
from .fsm import *
|
|
12
|
+
from .inputs import *
|
|
13
|
+
from .outputs import *
|
|
14
|
+
from .repeat import *
|
|
15
|
+
from .timedate import *
|
|
16
|
+
from .timer import *
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
*counter.__all__,
|
|
20
|
+
*fsm.__all__,
|
|
21
|
+
*inputs.__all__,
|
|
22
|
+
*outputs.__all__,
|
|
23
|
+
*repeat.__all__,
|
|
24
|
+
*timedate.__all__,
|
|
25
|
+
*timer.__all__,
|
|
26
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Counter.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import redzed
|
|
8
|
+
|
|
9
|
+
__all__ = ['Counter']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Counter(redzed.Block):
|
|
13
|
+
"""
|
|
14
|
+
Counter. If modulo is set to a number M, count modulo M.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, modulo: int|None = None, **kwargs) -> None:
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
if modulo == 0:
|
|
20
|
+
raise ValueError("modulo must not be zero")
|
|
21
|
+
self._mod = modulo
|
|
22
|
+
|
|
23
|
+
def _setmod(self, value: int) -> int:
|
|
24
|
+
output = value if self._mod is None else value % self._mod
|
|
25
|
+
self._set_output(output)
|
|
26
|
+
return output
|
|
27
|
+
|
|
28
|
+
def _event_inc(self, edata: redzed.EventData) -> int:
|
|
29
|
+
return self._setmod(self._output + edata.get('evalue', 1))
|
|
30
|
+
|
|
31
|
+
def _event_dec(self, edata: redzed.EventData) -> int:
|
|
32
|
+
return self._setmod(self._output - edata.get('evalue', 1))
|
|
33
|
+
|
|
34
|
+
def _event_put(self, edata: redzed.EventData) -> int:
|
|
35
|
+
return self._setmod(edata['evalue'])
|
|
36
|
+
|
|
37
|
+
def rz_init_default(self) -> None:
|
|
38
|
+
self._set_output(0)
|
|
39
|
+
|
|
40
|
+
rz_init = _setmod
|
|
41
|
+
|
|
42
|
+
def rz_export_state(self):
|
|
43
|
+
return self.get()
|
|
44
|
+
|
|
45
|
+
rz_restore_state = _setmod
|