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/cron_service.py
CHANGED
|
@@ -3,8 +3,8 @@ Call the .rz_cron_event() method of all registered blocks at given times of day.
|
|
|
3
3
|
|
|
4
4
|
Blocks acting on given time, date and weekdays are implemented
|
|
5
5
|
on top of this low-level service.
|
|
6
|
-
|
|
7
6
|
- - - - - -
|
|
7
|
+
Part of the redzed package.
|
|
8
8
|
Docs: https://redzed.readthedocs.io/en/latest/
|
|
9
9
|
Home: https://github.com/xitop/redzed/
|
|
10
10
|
"""
|
|
@@ -12,23 +12,41 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import bisect
|
|
15
|
+
from collections import deque
|
|
15
16
|
from collections.abc import Collection
|
|
16
17
|
import datetime as dt
|
|
17
|
-
import time
|
|
18
18
|
import typing as t
|
|
19
19
|
|
|
20
20
|
from .block import Block, EventData
|
|
21
21
|
from .debug import get_debug_level
|
|
22
22
|
from .utils import SEC_PER_HOUR, SEC_PER_MIN, SEC_PER_DAY
|
|
23
23
|
|
|
24
|
-
# time tracking
|
|
25
|
-
|
|
26
|
-
_TT_WARNING = 0.
|
|
27
|
-
_TT_ERROR = 2.5 #
|
|
28
|
-
|
|
24
|
+
# time tracking settings (in seconds)
|
|
25
|
+
_TT_OVERHEAD = 0.001 # asyncio sleep overhead estimation; real value will be measured
|
|
26
|
+
_TT_WARNING = 0.2 # timing difference threshold for a warning message
|
|
27
|
+
_TT_ERROR = 2.5 # timing difference threshold for a reset
|
|
28
|
+
|
|
29
|
+
# hourly wake ups for precise time tracking and early detection of DST changes
|
|
30
|
+
_SET24H = frozenset(dt.time(hour, 0, 0) for hour in range(24))
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
def _wait_time(t1: dt.time, t2: dt.time) -> float:
|
|
34
|
+
"""
|
|
35
|
+
Return seconds from *t1* to *t2* on a 24 hour clock.
|
|
36
|
+
|
|
37
|
+
The result is always between -1 and 23 hours (in seconds).
|
|
38
|
+
Positive values are normal wait times (when *t2* is after *t1*).
|
|
39
|
+
Negative values correspond to delays after a missed event
|
|
40
|
+
(when *t2* is less than 1 hour before *t1*).
|
|
41
|
+
"""
|
|
42
|
+
# datetime.time does not support time arithmetic
|
|
43
|
+
diff = (SEC_PER_HOUR*(t2.hour - t1.hour)
|
|
44
|
+
+ SEC_PER_MIN*(t2.minute - t1.minute)
|
|
45
|
+
+ (t2.second - t1.second)
|
|
46
|
+
+ (t2.microsecond - t1.microsecond) / 1_000_000.0)
|
|
47
|
+
if -SEC_PER_HOUR < diff <= 23 * SEC_PER_HOUR:
|
|
48
|
+
return diff
|
|
49
|
+
return diff + SEC_PER_DAY if diff < 0 else diff - SEC_PER_DAY
|
|
32
50
|
|
|
33
51
|
|
|
34
52
|
class Cron(Block):
|
|
@@ -44,7 +62,10 @@ class Cron(Block):
|
|
|
44
62
|
def __init__(self, *args, utc: bool, **kwargs) -> None:
|
|
45
63
|
super().__init__(*args, **kwargs)
|
|
46
64
|
self._utc = bool(utc)
|
|
47
|
-
self._alarms: dict[dt.time, set[Block]] = {}
|
|
65
|
+
self._alarms: dict[dt.time, set[Block]] = {tod: set() for tod in _SET24H}
|
|
66
|
+
self._timetable: list[dt.time] = sorted(_SET24H)
|
|
67
|
+
# timetable = sorted list (for bisection) of wake-up times (i.e. _alarms keys)
|
|
68
|
+
self._tt_len = 24
|
|
48
69
|
self._reversed: dict[Block, set[dt.time]] = {}
|
|
49
70
|
self._do_reload = asyncio.Event()
|
|
50
71
|
|
|
@@ -69,144 +90,137 @@ class Cron(Block):
|
|
|
69
90
|
raise ValueError("time_of_day must not contain timezone data")
|
|
70
91
|
|
|
71
92
|
def set_schedule(self, blk: Block, times_of_day: Collection[dt.time]) -> None:
|
|
72
|
-
"""
|
|
73
|
-
Add a block to be activated at given times or update its schedule.
|
|
74
|
-
|
|
75
|
-
The block's 'rz_cron_event' method will be called at given time
|
|
76
|
-
and also when this service is started, reset or reloaded.
|
|
77
|
-
|
|
78
|
-
A datetime.datetime object will be passed to the blk as its
|
|
79
|
-
only argument.
|
|
80
|
-
"""
|
|
93
|
+
"""Add a block to be activated at given times or update its schedule."""
|
|
81
94
|
if not hasattr(blk, 'rz_cron_event'):
|
|
82
95
|
raise TypeError(f"{blk} is not compatible with the cron service")
|
|
96
|
+
for tod in times_of_day:
|
|
97
|
+
self._check_tz(tod)
|
|
98
|
+
self.log_debug2("Got a new schedule for %s", blk)
|
|
99
|
+
times_of_day = set(times_of_day)
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
# remove old times of day
|
|
101
|
+
# remove old times of day; it is not necessary to notify the main loop
|
|
86
102
|
old_times = self._reversed.get(blk, set())
|
|
87
103
|
for tod in old_times - times_of_day:
|
|
88
104
|
self._alarms[tod].discard(blk)
|
|
89
|
-
|
|
90
|
-
do_reload = False
|
|
91
105
|
# add new times of day
|
|
106
|
+
do_reload = False
|
|
92
107
|
for tod in times_of_day - old_times:
|
|
93
|
-
self._check_tz(tod)
|
|
94
108
|
if tod in self._alarms:
|
|
95
109
|
self._alarms[tod].add(blk)
|
|
96
110
|
else:
|
|
97
111
|
self._alarms[tod] = {blk}
|
|
98
|
-
|
|
99
|
-
|
|
112
|
+
# new entry added to timetable; the main loop must be notified
|
|
113
|
+
do_reload = True
|
|
100
114
|
self._reversed[blk] = times_of_day
|
|
101
|
-
|
|
102
|
-
# cleanup
|
|
103
|
-
unused = [tod for tod, blkset in self._alarms.items() if not blkset]
|
|
104
|
-
for tod in unused:
|
|
105
|
-
del self._alarms[tod]
|
|
106
|
-
if tod not in _SET24:
|
|
107
|
-
do_reload = True # empty entry removed from the timetable
|
|
108
115
|
if do_reload:
|
|
109
116
|
self._do_reload.set()
|
|
117
|
+
# cleanup
|
|
118
|
+
for tod in [
|
|
119
|
+
tod for tod, blks in self._alarms.items() if tod not in _SET24H and not blks]:
|
|
120
|
+
del self._alarms[tod]
|
|
121
|
+
|
|
122
|
+
self._timetable = sorted(self._alarms)
|
|
123
|
+
self._tt_len = len(self._timetable)
|
|
124
|
+
|
|
110
125
|
|
|
111
126
|
async def _cron_daemon(self) -> t.NoReturn:
|
|
112
127
|
"""Recalculate registered blocks according to the schedule."""
|
|
113
|
-
overhead = _TT_OK # initial value, will be adjusted
|
|
114
|
-
# the sleeptime is reduced by this value
|
|
115
128
|
reset_flag = False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
tlen = len(timetable)
|
|
122
|
-
self.log_debug1("time schedule reloaded")
|
|
123
|
-
index = None
|
|
124
|
-
reload_flag = False
|
|
129
|
+
overhead = _TT_OVERHEAD # an estimate to start with
|
|
130
|
+
measured_overheads: deque[float] = deque(maxlen=8)
|
|
131
|
+
prev_sleeptime: float
|
|
132
|
+
long_sleep: bool
|
|
133
|
+
wakeup: dt.time|None = None
|
|
125
134
|
|
|
135
|
+
while True:
|
|
126
136
|
nowdt = self.dtnow()
|
|
127
137
|
nowt = nowdt.time()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
|
|
139
|
+
if wakeup is None or self._do_reload.is_set():
|
|
140
|
+
index = bisect.bisect_left(self._timetable, nowt)
|
|
141
|
+
next_wakeup = self._timetable[index % self._tt_len]
|
|
142
|
+
# next_wakeup is new and wakeup was not processed yet,
|
|
143
|
+
# all we need is to select which one comes first.
|
|
144
|
+
if wakeup is None \
|
|
145
|
+
or next_wakeup != wakeup and _wait_time(next_wakeup, wakeup) > 0:
|
|
146
|
+
wakeup = next_wakeup
|
|
147
|
+
# else: the current wakeup is confirmed
|
|
148
|
+
self._do_reload.clear()
|
|
149
|
+
self.log_debug1("wake-up time after reload: %s", wakeup)
|
|
150
|
+
else:
|
|
151
|
+
# return the next entry (in circular manner)
|
|
152
|
+
index = bisect.bisect_right(self._timetable, wakeup)
|
|
153
|
+
wakeup = self._timetable[index % self._tt_len]
|
|
154
|
+
self.log_debug1("wake-up time: %s", wakeup)
|
|
155
|
+
|
|
156
|
+
# sleep until the wake-up time:
|
|
157
|
+
# step 0: main sleep
|
|
158
|
+
# compute the delay until wake-up time and sleep
|
|
159
|
+
# steps 1 and 2: fine adjustment
|
|
160
|
+
# check the current time, adjust overhead estimate and
|
|
161
|
+
# - finish if the time is correct, or
|
|
162
|
+
# - add a tiny sleep if woken up too early, because
|
|
163
|
+
# that is not acceptable, or
|
|
164
|
+
# - do a reset if the time is way off
|
|
165
|
+
# steps 3 and 4: safety net
|
|
166
|
+
# like above, just for the case the computer clock
|
|
167
|
+
# does something unexpected
|
|
168
|
+
for step in range(5):
|
|
169
|
+
sleeptime = _wait_time(nowt, wakeup) # negative = we are late
|
|
170
|
+
debug2 = get_debug_level() >= 2
|
|
156
171
|
if step == 0:
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
if debug2:
|
|
173
|
+
self.log_debug("sleep until wake up: %.3f sec", sleeptime)
|
|
174
|
+
else:
|
|
159
175
|
diff = abs(sleeptime)
|
|
160
|
-
|
|
161
|
-
msg = 'BEFORE' if sleeptime > 0 else 'after'
|
|
162
|
-
self.log_debug(
|
|
163
|
-
"step %d, diff %.2f ms %s, estimated overhead: %.2f ms",
|
|
164
|
-
step, 1000*diff, msg, 1000*overhead)
|
|
176
|
+
after = sleeptime <= 0.0
|
|
165
177
|
if diff > _TT_WARNING:
|
|
166
178
|
self.log_warning(
|
|
167
|
-
"expected time: %s, current time: %s, diff: %.
|
|
168
|
-
wakeup, nowt,
|
|
169
|
-
|
|
179
|
+
"expected time: %s, current time: %s, diff: %.1f sec %s",
|
|
180
|
+
wakeup, nowt, diff, 'after' if after else 'BEFORE')
|
|
181
|
+
elif debug2:
|
|
182
|
+
self.log_debug(
|
|
183
|
+
"iteration %d, diff %.2f ms %s",
|
|
184
|
+
step, 1000*diff, 'after' if after else 'BEFORE')
|
|
185
|
+
# prev_sleeptime and long_sleep has been set in previous step
|
|
186
|
+
# pylint: disable=used-before-assignment
|
|
187
|
+
if diff > _TT_ERROR or sleeptime > prev_sleeptime or step == 4:
|
|
188
|
+
# something is wrong with the computer clock
|
|
170
189
|
reset_flag = True
|
|
171
|
-
if reset_flag:
|
|
172
190
|
break
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
if long_sleep:
|
|
192
|
+
saved = overhead
|
|
193
|
+
measured_overheads.append(overhead - sleeptime)
|
|
194
|
+
# Using smallest overhead recently measured, because to be late
|
|
195
|
+
# by a tiny amount is much better than to wake up early by a tiny
|
|
196
|
+
# amount. The latter case must be corrected by another sleep.
|
|
197
|
+
overhead = min(measured_overheads)
|
|
198
|
+
if debug2 and overhead != saved:
|
|
199
|
+
self.log_debug("estimated overhead >= %.2f ms", 1000*overhead)
|
|
200
|
+
if after:
|
|
176
201
|
break
|
|
177
|
-
if
|
|
202
|
+
if debug2:
|
|
178
203
|
self.log_debug("additional sleep %.2f ms", 1000*sleeptime)
|
|
204
|
+
prev_sleeptime = sleeptime
|
|
179
205
|
|
|
180
|
-
if sleeptime
|
|
181
|
-
pass # how likely is this?
|
|
182
|
-
elif sleeptime <= _SYNC_SLEEP:
|
|
183
|
-
short_sleep = True
|
|
184
|
-
# breaking the asyncio rules for max time tracking accuracy:
|
|
185
|
-
# doing a blocking sleep, but only for a fraction of a millisecond
|
|
186
|
-
time.sleep(sleeptime)
|
|
187
|
-
elif sleeptime <= overhead:
|
|
188
|
-
short_sleep = True
|
|
189
|
-
await asyncio.sleep(sleeptime)
|
|
190
|
-
else:
|
|
191
|
-
short_sleep = False
|
|
206
|
+
if (long_sleep := sleeptime > overhead):
|
|
192
207
|
try:
|
|
193
208
|
async with asyncio.timeout(sleeptime - overhead):
|
|
194
209
|
await self._do_reload.wait()
|
|
195
210
|
except TimeoutError:
|
|
196
|
-
pass
|
|
211
|
+
pass # no reload request
|
|
197
212
|
else:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
break # reload request arrived
|
|
214
|
+
elif sleeptime > 0.0:
|
|
215
|
+
await asyncio.sleep(sleeptime)
|
|
201
216
|
nowdt = self.dtnow()
|
|
202
217
|
nowt = nowdt.time()
|
|
218
|
+
# --- end for loop ---
|
|
203
219
|
|
|
204
220
|
if reset_flag:
|
|
205
221
|
# DST begin/end or other computer clock related reason
|
|
206
|
-
if (not self._utc
|
|
207
|
-
and
|
|
208
|
-
and abs(diff - SEC_PER_HOUR) <= _TT_ERROR
|
|
209
|
-
):
|
|
222
|
+
if (not self._utc and nowdt.isoweekday() >= 6
|
|
223
|
+
and abs(diff - SEC_PER_HOUR) <= _TT_ERROR):
|
|
210
224
|
self.log_warning("Apparently a DST (summer time) clock change has occured.")
|
|
211
225
|
self.log_warning(
|
|
212
226
|
"Resetting due to a time tracking problem. "
|
|
@@ -215,29 +229,29 @@ class Cron(Block):
|
|
|
215
229
|
for blk in list(self._reversed): # all blocks
|
|
216
230
|
assert hasattr(blk, 'rz_cron_event')
|
|
217
231
|
blk.rz_cron_event(nowdt)
|
|
218
|
-
index = None
|
|
219
232
|
reset_flag = False
|
|
233
|
+
wakeup = None
|
|
220
234
|
continue
|
|
221
|
-
if
|
|
235
|
+
if self._do_reload.is_set():
|
|
222
236
|
continue
|
|
223
237
|
|
|
224
|
-
if wakeup in self._alarms:
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
if wakeup not in self._alarms:
|
|
239
|
+
continue # entry removed in the meantime
|
|
240
|
+
# .rz_cron_event() may alter the set we are iterating over
|
|
241
|
+
block_list = list(self._alarms[wakeup])
|
|
242
|
+
if get_debug_level() >= 1:
|
|
243
|
+
self.log_debug(
|
|
244
|
+
"Notifying block(s): %s", ", ".join(blk.name for blk in block_list))
|
|
245
|
+
for blk in block_list:
|
|
246
|
+
assert hasattr(blk, 'rz_cron_event')
|
|
247
|
+
blk.rz_cron_event(nowdt)
|
|
234
248
|
|
|
235
249
|
def _event__get_config(self, _edata: EventData) -> dict[str, dict[str, list[str]]]:
|
|
236
250
|
"""Return the internal scheduling data for debugging or monitoring."""
|
|
237
251
|
return {
|
|
238
252
|
'alarms': {
|
|
239
|
-
str(
|
|
240
|
-
for
|
|
241
|
-
'blocks': {blk.name: sorted(str(
|
|
242
|
-
for blk,
|
|
253
|
+
str(tod): sorted(blk.name for blk in blks)
|
|
254
|
+
for tod, blks in self._alarms.items() if blks},
|
|
255
|
+
'blocks': {blk.name: sorted(str(tod) for tod in tods)
|
|
256
|
+
for blk, tods in self._reversed.items()},
|
|
243
257
|
}
|
redzed/debug.py
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Debug
|
|
2
|
+
Debug levels.
|
|
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
|
from __future__ import annotations
|
|
5
9
|
|
|
6
|
-
__all__ = ['get_debug_level', 'set_debug_level']
|
|
7
|
-
|
|
8
10
|
import logging
|
|
9
11
|
import os
|
|
10
12
|
|
|
11
13
|
from . import circuit
|
|
12
14
|
|
|
15
|
+
__all__ = ['get_debug_level', 'set_debug_level']
|
|
16
|
+
|
|
13
17
|
_logger = logging.getLogger(__package__)
|
|
14
|
-
_debug_handler: logging.StreamHandler|None = None
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
def get_level_from_env() -> int|None:
|
|
21
|
+
if (env_level := os.environ.get('REDZED_DEBUG', '')) == '':
|
|
22
|
+
return None
|
|
23
|
+
try:
|
|
24
|
+
level = int(env_level)
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
else:
|
|
28
|
+
if 0 <= level <= 3:
|
|
29
|
+
return level
|
|
30
|
+
_logger.warning(
|
|
31
|
+
"Envvar 'REDZED_DEBUG' should be: 0 (disabled), 1 (normal), "
|
|
32
|
+
+ "2 (verbose) or 3 (verbose with circuit timestamps)")
|
|
33
|
+
_logger.error("Ignoring REDZED_DEBUG='%s'. Please use a correct value.", env_level)
|
|
34
|
+
return None
|
|
24
35
|
|
|
25
36
|
|
|
26
37
|
class _DebugLevel:
|
|
@@ -31,56 +42,62 @@ class _DebugLevel:
|
|
|
31
42
|
"""
|
|
32
43
|
|
|
33
44
|
def __init__(self) -> None:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return 0
|
|
42
|
-
level = level.strip()
|
|
43
|
-
if not level:
|
|
44
|
-
return 0
|
|
45
|
-
if level in {'0', '1', '2', '3'}:
|
|
46
|
-
return int(level)
|
|
47
|
-
_logger.warning(
|
|
48
|
-
"Envvar 'REDZED_DEBUG' should be: 0 (disabled), 1 (normal), "
|
|
49
|
-
+ "2 (verbose) or 3 (verbose with circuit timestamps)")
|
|
50
|
-
_logger.warning("Ignoring REDZED_DEBUG='%s'. Please use a correct value.", level)
|
|
51
|
-
return 0
|
|
52
|
-
|
|
53
|
-
def get_level(self) -> int:
|
|
45
|
+
if (level := get_level_from_env()) is None:
|
|
46
|
+
level = 0
|
|
47
|
+
self._level = level
|
|
48
|
+
_logger.debug("[Logging] Debug level: %d", level)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def level(self) -> int:
|
|
54
52
|
return self._level
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
@level.setter
|
|
55
|
+
def level(self, level: int) -> None:
|
|
57
56
|
"""Set debug level."""
|
|
58
|
-
global _debug_handler # pylint: disable=global-statement
|
|
59
|
-
|
|
60
|
-
if not isinstance(level, int):
|
|
61
|
-
raise TypeError(f"Expected an integer, got {level!r}")
|
|
62
57
|
if level == self._level:
|
|
63
58
|
return
|
|
59
|
+
if not isinstance(level, int):
|
|
60
|
+
raise TypeError(f"Expected an integer, got {level!r}")
|
|
64
61
|
if not 0 <= level <= 3:
|
|
65
62
|
raise ValueError(f"Debug level must be an integer 0 to 3, but got {level}")
|
|
66
|
-
|
|
67
|
-
if self._level >= 0:
|
|
68
|
-
_logger.debug("Debug level: %d -> %d", self._level, level)
|
|
69
|
-
elif level > 0:
|
|
70
|
-
_logger.debug("Debug level: %d", level)
|
|
63
|
+
_logger.debug("[Logging] Debug level: %d -> %d", self._level, level)
|
|
71
64
|
self._level = level
|
|
72
|
-
if level == 0:
|
|
73
|
-
if _debug_handler is not None:
|
|
74
|
-
_logger.removeHandler(_debug_handler)
|
|
75
|
-
_debug_handler = None
|
|
76
|
-
_logger.setLevel(logging.WARNING)
|
|
77
|
-
elif not _logger.hasHandlers() and _debug_handler is None:
|
|
78
|
-
_logger.addHandler(_debug_handler := logging.StreamHandler())
|
|
79
|
-
_debug_handler.setFormatter(_CircuitTimeFormatter("%(levelname)s - %(message)s"))
|
|
80
|
-
_logger.setLevel(logging.DEBUG)
|
|
81
65
|
|
|
82
66
|
|
|
83
|
-
|
|
67
|
+
_debug_level = _DebugLevel()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_debug_level():
|
|
71
|
+
"""Get the debug level."""
|
|
72
|
+
return _debug_level.level
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
set_debug_level
|
|
74
|
+
|
|
75
|
+
def set_debug_level(level):
|
|
76
|
+
"""
|
|
77
|
+
Set the debug level.
|
|
78
|
+
|
|
79
|
+
Also make sure debug messages will be logged or printed.
|
|
80
|
+
For this purpose, if there is no handler, add an own stream handler.
|
|
81
|
+
"""
|
|
82
|
+
_debug_level.level = level
|
|
83
|
+
if circuit.get_circuit().get_state() in [
|
|
84
|
+
circuit.CircuitState.UNDER_CONSTRUCTION, circuit.CircuitState.CLOSED]:
|
|
85
|
+
return
|
|
86
|
+
if level > 0:
|
|
87
|
+
if not _logger.hasHandlers():
|
|
88
|
+
_logger.addHandler(logging.StreamHandler())
|
|
89
|
+
_logger.propagate = False
|
|
90
|
+
_logger.setLevel(logging.DEBUG)
|
|
91
|
+
else:
|
|
92
|
+
_logger.setLevel(logging.NOTSET if _logger.propagate else logging.INFO)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _CircuitTimeFilter(logging.Filter):
|
|
96
|
+
"""Filter adding timestamsps in debug level 3."""
|
|
97
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
98
|
+
if _debug_level.level >= 3 and isinstance(record.msg, str):
|
|
99
|
+
record.msg = f"{circuit.get_circuit().runtime():.03f} {record.msg}"
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_logger.addFilter(_CircuitTimeFilter())
|
redzed/formula_trigger.py
CHANGED
|
@@ -194,12 +194,8 @@ class Formula(base_block.BlockOrFormula):
|
|
|
194
194
|
self._evaluate_active = False
|
|
195
195
|
return triggers
|
|
196
196
|
|
|
197
|
-
def formula(
|
|
197
|
+
def formula(func: _FUNC) -> _FUNC:
|
|
198
198
|
"""@formula() creates a Formula block with the decorated function."""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def decorator(func: _FUNC) -> _FUNC:
|
|
203
|
-
Formula(name, *args, func=func, **kwargs)
|
|
204
|
-
return func
|
|
205
|
-
return decorator
|
|
199
|
+
comment = '' if func.__doc__ is None else inspect.cleandoc(func.__doc__).partition('\n')[0]
|
|
200
|
+
Formula(func.__name__, func=func, comment=comment)
|
|
201
|
+
return func
|
redzed/initializers.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Block initializers.
|
|
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
|
|
|
@@ -18,7 +18,7 @@ import typing as t
|
|
|
18
18
|
|
|
19
19
|
from . import block
|
|
20
20
|
from .undef import UNDEF, UndefType
|
|
21
|
-
from .utils import
|
|
21
|
+
from .utils import time_period
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
_LONG_TIMEOUT = 60 # threshold for a "long timeout" debug message
|
|
@@ -225,19 +225,18 @@ class InitTask(AsyncInitializer):
|
|
|
225
225
|
|
|
226
226
|
def __init__(
|
|
227
227
|
self,
|
|
228
|
-
|
|
228
|
+
aw_func: Callable[..., Awaitable[t.Any]],
|
|
229
229
|
*args: t.Any,
|
|
230
230
|
timeout: float|str = 10.0
|
|
231
231
|
) -> None:
|
|
232
232
|
"""Similar to InitFunction, but asynchonous."""
|
|
233
|
-
check_async_func(coro_func)
|
|
234
233
|
super().__init__(timeout)
|
|
235
|
-
self.
|
|
234
|
+
self._aw_func = aw_func
|
|
236
235
|
self._args = args
|
|
237
236
|
|
|
238
237
|
async def _async_get_init(self) -> t.Any:
|
|
239
238
|
async with asyncio.timeout(self._timeout):
|
|
240
|
-
return await self.
|
|
239
|
+
return await self._aw_func(*self._args)
|
|
241
240
|
|
|
242
241
|
|
|
243
242
|
class InitWait(AsyncInitializer):
|
redzed/py.typed
ADDED
|
File without changes
|
redzed/signal_shutdown.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Stop the runner with a signal.
|
|
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
|
|
|
@@ -55,7 +55,7 @@ class TerminatingSignal:
|
|
|
55
55
|
"""A signal handler."""
|
|
56
56
|
# - we need the _threadsafe variant of call_soon
|
|
57
57
|
# - get_running loop() and get_circuit() will succeed,
|
|
58
|
-
# because this handler is active only during
|
|
58
|
+
# because this handler is active only during redzed.run()
|
|
59
59
|
msg = f"Signal {self._signame!r} caught"
|
|
60
60
|
call_soon = asyncio.get_running_loop().call_soon_threadsafe
|
|
61
61
|
call_soon(_logger.warning, "%s", msg)
|
redzed/undef.py
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
The UNDEF singleton constant.
|
|
3
3
|
- - - - - -
|
|
4
4
|
Part of the redzed package.
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|