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.
@@ -1,8 +1,9 @@
1
1
  """
2
2
  Output blocks.
3
3
  - - - - - -
4
- Docs: https://edzed.readthedocs.io/en/latest/
5
- Home: https://github.com/xitop/edzed/
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
 
@@ -13,7 +14,8 @@ from collections.abc import Callable, Awaitable
13
14
  import typing as t
14
15
 
15
16
  import redzed
16
- from redzed.utils import BufferShutDown, cancel_shield, func_call_string, MsgSync, time_period
17
+ from redzed.utils import BufferShutDown, cancel_shield, func_call_string, time_period
18
+ from .validator import _Validate
17
19
 
18
20
 
19
21
  class OutputFunc(redzed.Block):
@@ -28,22 +30,27 @@ class OutputFunc(redzed.Block):
28
30
  triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
29
31
  **kwargs) -> None:
30
32
  super().__init__(*args, **kwargs)
31
- self._stop_value = stop_value
32
33
  self._func = func
33
- self._parameters: list[tuple[str, bool]] = [] # item: (name, is_required)
34
+ self._shutdown = False
35
+ if stop_value is not redzed.UNDEF:
36
+ @redzed.stop_function
37
+ def stop_function_():
38
+ self._event_put({'evalue': stop_value})
39
+ stop_function_.__qualname__ += self.name
40
+ stop_function_.__name__ += self.name
34
41
  if triggered_by is not redzed.UNDEF:
35
42
  @redzed.triggered
36
43
  def trigger(value=triggered_by) -> None:
37
44
  self.event('put', value)
38
45
 
39
46
  def _event_put(self, edata: redzed.EventData) -> t.Any:
40
- arg = edata['evalue']
47
+ evalue = edata['evalue']
41
48
  if redzed.get_debug_level() >= 1:
42
- self.log_debug("Running %s", func_call_string(self._func, (arg,)))
49
+ self.log_debug("Running %s", func_call_string(self._func, (evalue,)))
43
50
  try:
44
- result = self._func(arg)
51
+ result = self._func(evalue)
45
52
  except Exception as err:
46
- func_args = func_call_string(self._func, (arg,))
53
+ func_args = func_call_string(self._func, (evalue,))
47
54
  self.log_error("Output function failed; call: %s; error: %r", func_args, err)
48
55
  err.add_note(f"Error occurred in function call {func_args}")
49
56
  self.circuit.abort(err)
@@ -51,30 +58,64 @@ class OutputFunc(redzed.Block):
51
58
  self.log_debug2("output function returned: %r", result)
52
59
  return result
53
60
 
61
+ def rz_is_shut_down(self) -> bool:
62
+ return self._shutdown
63
+
54
64
  def rz_stop(self) -> None:
55
- if self._stop_value is not redzed.UNDEF:
56
- self._event_put({'evalue': self._stop_value})
65
+ self._shutdown = True
57
66
 
58
67
 
59
- class _Buffer(redzed.Block):
68
+ class _Buffer(_Validate, redzed.Block):
60
69
  def __init__(
61
70
  self, *args,
62
71
  stop_value: t.Any = redzed.UNDEF,
63
72
  triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
64
73
  **kwargs) -> None:
65
74
  super().__init__(*args, **kwargs)
66
- self._stop_value = stop_value
75
+ self._shutdown = False
76
+ if stop_value is not redzed.UNDEF:
77
+ stop_value = self._validate(stop_value)
78
+ @redzed.stop_function
79
+ def stop_function_():
80
+ self.rz_put_value(stop_value)
81
+ stop_function_.__qualname__ += self.name
82
+ stop_function_.__name__ += self.name
67
83
  if triggered_by is not redzed.UNDEF:
68
84
  @redzed.triggered
69
85
  def trigger(value=triggered_by) -> None:
70
86
  self.event('put', value)
71
87
 
88
+ def _event_put(self, edata: redzed.EventData) -> None:
89
+ """Put an item into the queue."""
90
+ # not aggregating following two lines in order to have a clear traceback
91
+ evalue = edata['evalue']
92
+ evalue = self._validate(evalue)
93
+ self.rz_put_value(evalue)
94
+
95
+ def _event__get_size(self, _edata: redzed.EventData) -> int:
96
+ return self.rz_get_size()
97
+
98
+ def rz_is_shut_down(self) -> bool:
99
+ return self._shutdown
100
+
101
+ def rz_stop(self) -> None:
102
+ self._shutdown = True
103
+
104
+ def rz_put_value(self, value: t.Any) -> None:
105
+ raise NotImplementedError
106
+
72
107
  def rz_get_size(self) -> int:
73
108
  raise NotImplementedError
74
109
 
75
110
  async def rz_buffer_get(self) -> t.Any:
76
111
  raise NotImplementedError
77
112
 
113
+ def rz_close(self) -> None:
114
+ if (size := self.rz_get_size()) == 1:
115
+ self.log_error("One value was not retrieved from buffer")
116
+ elif size > 1:
117
+ self.log_error("%d values were not retrieved from buffer", size)
118
+
78
119
 
79
120
  class QueueBuffer(_Buffer):
80
121
  """
@@ -92,26 +133,17 @@ class QueueBuffer(_Buffer):
92
133
  # Queue.shutdown is available in Python 3.13, but we want to support 3.11+
93
134
  self._waiters = 0
94
135
 
95
- def _event_put(self, edata: redzed.EventData) -> None:
96
- """Put an item into the queue."""
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)
136
+ def rz_put_value(self, value: t.Any) -> None:
137
+ self._queue.put_nowait(value)
101
138
 
102
139
  def rz_get_size(self) -> int:
103
140
  """Get the number of items in the buffer."""
104
141
  return self._queue.qsize()
105
142
 
106
- def _event__get_size(self, _edata: redzed.EventData) -> int:
107
- return self.rz_get_size()
108
-
109
143
  def rz_stop(self) -> None:
110
- # stop_value might be UNDEF
111
- self._queue.put_nowait(self._stop_value)
112
- # unblock waiters (-1 accounts for the inserted stop value)
113
- for _ in range(self._waiters - 1):
114
- self._queue.put_nowait(redzed.UNDEF)
144
+ super().rz_stop()
145
+ for _ in range(self._waiters):
146
+ self.rz_put_value(redzed.UNDEF)
115
147
 
116
148
  async def rz_buffer_get(self) -> t.Any:
117
149
  """
@@ -121,7 +153,7 @@ class QueueBuffer(_Buffer):
121
153
  After the shutdown drain the queue
122
154
  and then raise BufferShutDown to each caller.
123
155
  """
124
- if self.circuit.after_shutdown() and self._queue.qsize() == 0:
156
+ if self._shutdown and self._queue.qsize() == 0:
125
157
  raise BufferShutDown("The buffer was shut down")
126
158
  self._waiters += 1
127
159
  try:
@@ -133,10 +165,6 @@ class QueueBuffer(_Buffer):
133
165
  raise BufferShutDown("The buffer was shut down")
134
166
  return value
135
167
 
136
- def rz_close(self) -> None:
137
- if (size := self._queue.qsize()) > 0:
138
- self.log_warning("%d unprocessed item(s) left in the buffer", size)
139
-
140
168
 
141
169
  class MemoryBuffer(_Buffer):
142
170
  """
@@ -145,64 +173,65 @@ class MemoryBuffer(_Buffer):
145
173
 
146
174
  def __init__(self, *args, **kwargs) -> None:
147
175
  super().__init__(*args, **kwargs)
148
- self._cell: MsgSync[t.Any] = MsgSync()
149
- self._stop_pending = False
176
+ # allowed states:
177
+ # _shutdown value has_value
178
+ # - has value: False, not UNDEF, set
179
+ # - is empty: False, UNDEF, cleared
180
+ # - draining: True, not UNDEF, set
181
+ # - shut down: True, UNDEF, set
182
+ self._value = redzed.UNDEF
183
+ self._has_value = asyncio.Event()
150
184
 
151
185
  def rz_get_size(self) -> int:
152
186
  """Get the number of items (0 or 1) in the buffer."""
153
- has_data = self._cell.has_data() or self._cell.is_shutdown() and self._stop_pending
187
+ has_data = self._value is not redzed.UNDEF
154
188
  return 1 if has_data else 0
155
189
 
156
- def _event__get_size(self, _edata: redzed.EventData) -> int:
157
- return self.rz_get_size()
158
-
159
- def _event_put(self, edata: redzed.EventData) -> None:
160
- """Put an item into the memory cell. Existing value will be overwritten."""
161
- self._cell.send(edata['evalue'])
190
+ def rz_put_value(self, value: t.Any) -> None:
191
+ if value is redzed.UNDEF:
192
+ raise ValueError(f"{self}: Cannot put UNDEF into the buffer")
193
+ self._value = value
194
+ self._has_value.set()
162
195
 
163
196
  def rz_stop(self) -> None:
164
197
  """Shut down"""
165
- self._cell.shutdown()
166
- self._stop_pending = self._stop_value is not redzed.UNDEF
198
+ super().rz_stop()
199
+ if not self._has_value.is_set():
200
+ assert self._value is redzed.UNDEF
201
+ self._has_value.set() # unblock reader(s)
167
202
 
168
203
  async def rz_buffer_get(self) -> t.Any:
169
- """
170
- Remove and return an item from the memory cell.
171
-
172
- When the buffer is empty:
173
- - before shutdown wait
174
- - after shutdown return the *stop_value* once (if defined),
175
- then raise BufferShutDown
176
- """
177
- try:
178
- return await self._cell.recv()
179
- except BufferShutDown:
180
- if not self._stop_pending:
181
- raise
182
- self._stop_pending = False
183
- return self._stop_value
184
-
185
- def rz_close(self) -> None:
186
- if self._stop_pending:
187
- self.log_error("The stop_value was not retrieved")
204
+ """Remove and return an item from the memory cell."""
205
+ while True:
206
+ await self._has_value.wait()
207
+ if self._value is redzed.UNDEF:
208
+ if self._shutdown:
209
+ raise BufferShutDown("The buffer was shut down")
210
+ if not self._has_value.is_set():
211
+ raise RuntimeError("BUG!: busy loop prevented")
212
+ continue
213
+ value, self._value = self._value, redzed.UNDEF
214
+ if not self._shutdown:
215
+ self._has_value.clear()
216
+ return value
188
217
 
189
218
 
190
219
  class OutputWorker(redzed.Block):
191
220
  """
192
- Run a coroutine for each value from a buffer.
221
+ Run an awaitable for each value from a buffer.
193
222
  """
194
223
 
195
224
  def __init__(
196
225
  self, *args,
197
226
  buffer: str|redzed.Block,
198
- coro_func: Callable[[t.Any], Awaitable[t.Any]], # i.e. an async function
227
+ aw_func: Callable[[t.Any], Awaitable[t.Any]], # e.g. an async function
199
228
  workers: int = 1,
200
229
  **kwargs) -> None:
201
230
  super().__init__(*args, **kwargs)
202
231
  if workers < 1:
203
232
  raise ValueError(f"{self}: At least one worker is required")
204
233
  self._buffer = buffer
205
- self._corofunc = coro_func
234
+ self._aw_func = aw_func
206
235
  self._workers = workers
207
236
  self._worker_tasks: list[asyncio.Task[None]] = []
208
237
 
@@ -222,10 +251,9 @@ class OutputWorker(redzed.Block):
222
251
  try:
223
252
  if redzed.get_debug_level() >= 1:
224
253
  self.log_debug(
225
- "%s: Awaiting coroutine %s",
226
- wname, func_call_string(self._corofunc, (value,)))
227
- await self._corofunc(value)
228
- self.log_debug2("%s: coroutine done", wname)
254
+ "%s: Awaiting %s", wname, func_call_string(self._aw_func, (value,)))
255
+ await self._aw_func(value)
256
+ self.log_debug2("%s: await done", wname)
229
257
  except Exception as err:
230
258
  err.add_note(f"Processed value was: {value!r}")
231
259
  raise
@@ -241,27 +269,31 @@ class OutputWorker(redzed.Block):
241
269
  self._worker(wname), auto_cancel=False, name=f"{self}: {wname}"))
242
270
 
243
271
  async def rz_astop(self) -> None:
272
+ worker_tasks = [worker for worker in self._worker_tasks if not worker.done()]
273
+ if not worker_tasks:
274
+ return
244
275
  try:
245
- await asyncio.wait(self._worker_tasks)
276
+ await asyncio.wait(worker_tasks)
246
277
  except asyncio.CancelledError:
247
278
  # stop_timeout from the circuit runner
248
- for worker in self._worker_tasks:
279
+ for worker in worker_tasks:
249
280
  if not worker.done():
250
281
  worker.cancel()
251
282
  await asyncio.sleep(0)
252
- if (running := sum(1 for worker in self._worker_tasks if not worker.done())) > 0:
283
+ if (running := sum(1 for worker in worker_tasks if not worker.done())) > 0:
253
284
  self.log_warning("%d worker(s) did not stop", running)
254
285
  raise
255
286
 
287
+
256
288
  class OutputController(redzed.Block):
257
289
  """
258
- Run a coroutine for the latest value from a buffer.
290
+ Run an awaitable for the latest value from a buffer.
259
291
  """
260
292
 
261
293
  def __init__(
262
294
  self, *args,
263
295
  buffer: str|redzed.Block,
264
- coro_func: Callable[[t.Any], Awaitable[t.Any]], # i.e. an async function
296
+ aw_func: Callable[[t.Any], Awaitable[t.Any]], # e.g. an async function
265
297
  rest_time: float|str = 0.0,
266
298
  **kwargs) -> None:
267
299
  super().__init__(*args, **kwargs)
@@ -269,7 +301,7 @@ class OutputController(redzed.Block):
269
301
  if self._rest_time >= self.rz_stop_timeout:
270
302
  raise ValueError(f"{self}: rest_time must be shorter than the stop_timeout")
271
303
  self._buffer = buffer
272
- self._corofunc = coro_func
304
+ self._aw_func = aw_func
273
305
  self._main_loop_task: asyncio.Task[None]|None = None
274
306
 
275
307
  def rz_pre_init(self) -> None:
@@ -284,7 +316,7 @@ class OutputController(redzed.Block):
284
316
 
285
317
  async def _run_with_rest_time(self, value: t.Any) -> None:
286
318
  try:
287
- await self._corofunc(value)
319
+ await self._aw_func(value)
288
320
  except asyncio.CancelledError:
289
321
  self.log_debug2("Task cancelled")
290
322
  await self._rest_time_delay()
@@ -331,7 +363,7 @@ class OutputController(redzed.Block):
331
363
  if redzed.get_debug_level() >= 1:
332
364
  self.log_debug(
333
365
  "Creating task: %s%s",
334
- func_call_string(self._corofunc, (value,)),
366
+ func_call_string(self._aw_func, (value,)),
335
367
  " + rest time" if self._rest_time > 0.0 else "",
336
368
  )
337
369
  task = asyncio.create_task(self._run_with_rest_time(value))
redzed/blocklib/repeat.py CHANGED
@@ -13,7 +13,7 @@ import asyncio
13
13
  import typing as t
14
14
 
15
15
  import redzed
16
- from redzed.utils import MsgSync, time_period
16
+ from redzed.utils import time_period
17
17
 
18
18
 
19
19
  class Repeat(redzed.Block):
@@ -27,20 +27,27 @@ class Repeat(redzed.Block):
27
27
  **kwargs
28
28
  ) -> None:
29
29
  self._dest = dest
30
- self._interval = time_period(interval)
30
+ self._default_interval = time_period(interval)
31
31
  if count is not None and count < 0:
32
32
  # count = 0 (no repeating) is accepted
33
33
  raise ValueError("argument 'count' must not be negative")
34
- self._count = count
35
- self._sync: MsgSync[tuple[str, redzed.EventData]] = MsgSync()
36
- self._warning_logged = False
34
+ self._default_count = count
35
+ self._got_event = False
36
+ self._new_event = asyncio.Event()
37
+ # current event
38
+ self._etype: str
39
+ self._edata: redzed.EventData
40
+ self._interval: float
41
+ self._count: int|None
37
42
  super().__init__(*args, **kwargs)
38
43
 
39
44
  def rz_pre_init(self) -> None:
40
45
  """Resolve destination block name."""
41
- if not isinstance(dest := self.circuit.resolve_name(self._dest), redzed.Block):
42
- raise TypeError(
43
- f"{self}: {dest} is not a Block, but a Formula; cannot send events to it.")
46
+ dest = self.circuit.resolve_name(self._dest)
47
+ if isinstance(dest, redzed.Formula):
48
+ raise TypeError(f"{self}: {dest} is a Formula; cannot send events to it.")
49
+ if isinstance(dest, type(self)):
50
+ raise TypeError(f"{self}: {dest} is another Repeat block; this is not allowed")
44
51
  self._dest = dest
45
52
  self.circuit.create_service(self._repeater(), name=f"Event repeating task at {self}")
46
53
 
@@ -52,21 +59,30 @@ class Repeat(redzed.Block):
52
59
  repeat = 0 # prevent pylint warning
53
60
  while True:
54
61
  try:
55
- etype, edata = await self._sync.recv(
56
- timeout=self._interval if repeating else None)
57
- repeat = 0
62
+ async with asyncio.timeout(self._interval if repeating else None):
63
+ await self._new_event.wait()
58
64
  except asyncio.TimeoutError:
65
+ pass
66
+ # getting a timeout does not necessarily mean the event was not set
67
+ if self._new_event.is_set():
68
+ self._new_event.clear()
69
+ repeat = 0
70
+ else:
59
71
  repeat += 1
60
72
 
61
73
  if repeat > 0: # skip the original event
62
74
  self._set_output(repeat)
63
75
  assert isinstance(self._dest, redzed.Block) # @mypy: name resolved
64
- self._dest.event(etype, **(edata | {'repeat': repeat}))
76
+ self._dest.event(self._etype, **(self._edata | {'repeat': repeat}))
65
77
  repeating = self._count is None or repeat < self._count
66
78
 
67
79
  def _default_event_handler(self, etype: str, edata: redzed.EventData) -> None:
68
80
  # send the original event synchronously
69
81
  self._set_output(0)
70
82
  assert isinstance(self._dest, redzed.Block) # mypy: name is resolved
83
+ self._etype = etype
84
+ self._edata = edata
85
+ self._interval = edata.pop('repeat_interval', self._default_interval)
86
+ self._count = edata.pop('repeat_count', self._default_count)
71
87
  self._dest.event(etype, **(edata | {'repeat': 0}))
72
- self._sync.send((etype, edata))
88
+ self._new_event.set()
@@ -1,9 +1,9 @@
1
1
  """
2
2
  Periodic events at fixed time/date.
3
-
4
3
  - - - - - -
5
- Docs: https://edzed.readthedocs.io/en/latest/
6
- Home: https://github.com/xitop/edzed/
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
@@ -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
- ALL_STATES = ['off', 'on']
22
- TIMED_STATES = [
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