redzed 26.1.28__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
@@ -15,7 +15,7 @@ Docs: https://redzed.readthedocs.io/en/latest/
15
15
  Home: https://github.com/xitop/redzed/
16
16
  """
17
17
 
18
- __version_info__ = (26, 1, 28)
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/block.py CHANGED
@@ -225,6 +225,9 @@ class Block(BlockOrFormula):
225
225
  # pylint: disable-next=no-member
226
226
  return self.rz_export_state() # type: ignore[attr-defined]
227
227
 
228
+ def rz_is_shut_down(self) -> bool:
229
+ return self.circuit.is_shut_down()
230
+
228
231
  def event(self, etype: str, /, evalue: t.Any = UNDEF, **edata: t.Any) -> t.Any:
229
232
  """
230
233
  An entry point for events.
@@ -232,13 +235,13 @@ class Block(BlockOrFormula):
232
235
  Call the specialized _event_ETYPE() method if it exists.
233
236
  Otherwise call the _default_event_handler() as the last resort.
234
237
  """
235
- 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():
236
240
  raise CircuitShutDown("The circuit was shut down")
237
241
 
238
242
  if evalue is not UNDEF:
239
243
  edata['evalue'] = evalue
240
244
 
241
- check_identifier(etype, "Event type")
242
245
  if get_debug_level() >= 1:
243
246
  if not edata:
244
247
  self.log_debug("Got event '%s'", etype)
@@ -14,7 +14,7 @@ from collections.abc import Callable, Awaitable
14
14
  import typing as t
15
15
 
16
16
  import redzed
17
- 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
18
  from .validator import _Validate
19
19
 
20
20
 
@@ -30,9 +30,14 @@ class OutputFunc(redzed.Block):
30
30
  triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
31
31
  **kwargs) -> None:
32
32
  super().__init__(*args, **kwargs)
33
- self._stop_value = stop_value
34
33
  self._func = func
35
- 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
36
41
  if triggered_by is not redzed.UNDEF:
37
42
  @redzed.triggered
38
43
  def trigger(value=triggered_by) -> None:
@@ -53,9 +58,11 @@ class OutputFunc(redzed.Block):
53
58
  self.log_debug2("output function returned: %r", result)
54
59
  return result
55
60
 
61
+ def rz_is_shut_down(self) -> bool:
62
+ return self._shutdown
63
+
56
64
  def rz_stop(self) -> None:
57
- if self._stop_value is not redzed.UNDEF:
58
- self._event_put({'evalue': self._stop_value})
65
+ self._shutdown = True
59
66
 
60
67
 
61
68
  class _Buffer(_Validate, redzed.Block):
@@ -65,9 +72,14 @@ class _Buffer(_Validate, redzed.Block):
65
72
  triggered_by: str|redzed.Block|redzed.Formula|redzed.UndefType = redzed.UNDEF,
66
73
  **kwargs) -> None:
67
74
  super().__init__(*args, **kwargs)
68
- # the stop_value is stored already validated
69
- self._stop_value = \
70
- redzed.UNDEF if stop_value is redzed.UNDEF else self._validate(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
71
83
  if triggered_by is not redzed.UNDEF:
72
84
  @redzed.triggered
73
85
  def trigger(value=triggered_by) -> None:
@@ -80,6 +92,15 @@ class _Buffer(_Validate, redzed.Block):
80
92
  evalue = self._validate(evalue)
81
93
  self.rz_put_value(evalue)
82
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
+
83
104
  def rz_put_value(self, value: t.Any) -> None:
84
105
  raise NotImplementedError
85
106
 
@@ -89,6 +110,12 @@ class _Buffer(_Validate, redzed.Block):
89
110
  async def rz_buffer_get(self) -> t.Any:
90
111
  raise NotImplementedError
91
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
+
92
119
 
93
120
  class QueueBuffer(_Buffer):
94
121
  """
@@ -105,7 +132,6 @@ class QueueBuffer(_Buffer):
105
132
  self._queue: asyncio.Queue[t.Any] = queue_type(maxsize)
106
133
  # Queue.shutdown is available in Python 3.13, but we want to support 3.11+
107
134
  self._waiters = 0
108
- self._shutdown = False
109
135
 
110
136
  def rz_put_value(self, value: t.Any) -> None:
111
137
  self._queue.put_nowait(value)
@@ -114,18 +140,10 @@ class QueueBuffer(_Buffer):
114
140
  """Get the number of items in the buffer."""
115
141
  return self._queue.qsize()
116
142
 
117
- def _event__get_size(self, _edata: redzed.EventData) -> int:
118
- return self.rz_get_size()
119
-
120
143
  def rz_stop(self) -> None:
121
- waiters = self._waiters
122
- if self._stop_value is not redzed.UNDEF:
123
- self.rz_put_value(self._stop_value)
124
- waiters -= 1
125
- # unblock waiters
126
- for _ in range(waiters):
144
+ super().rz_stop()
145
+ for _ in range(self._waiters):
127
146
  self.rz_put_value(redzed.UNDEF)
128
- self._shutdown = True
129
147
 
130
148
  async def rz_buffer_get(self) -> t.Any:
131
149
  """
@@ -147,10 +165,6 @@ class QueueBuffer(_Buffer):
147
165
  raise BufferShutDown("The buffer was shut down")
148
166
  return value
149
167
 
150
- def rz_close(self) -> None:
151
- if (size := self._queue.qsize()) > 0:
152
- self.log_warning("%d unprocessed item(s) left in the buffer", size)
153
-
154
168
 
155
169
  class MemoryBuffer(_Buffer):
156
170
  """
@@ -159,45 +173,47 @@ class MemoryBuffer(_Buffer):
159
173
 
160
174
  def __init__(self, *args, **kwargs) -> None:
161
175
  super().__init__(*args, **kwargs)
162
- self._cell: MsgSync[t.Any] = MsgSync()
163
- 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()
164
184
 
165
185
  def rz_get_size(self) -> int:
166
186
  """Get the number of items (0 or 1) in the buffer."""
167
- has_data = self._cell.has_data() or self._cell.is_shutdown() and self._stop_pending
187
+ has_data = self._value is not redzed.UNDEF
168
188
  return 1 if has_data else 0
169
189
 
170
- def _event__get_size(self, _edata: redzed.EventData) -> int:
171
- return self.rz_get_size()
172
-
173
190
  def rz_put_value(self, value: t.Any) -> None:
174
- self._cell.send(value)
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()
175
195
 
176
196
  def rz_stop(self) -> None:
177
197
  """Shut down"""
178
- self._cell.shutdown()
179
- 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)
180
202
 
181
203
  async def rz_buffer_get(self) -> t.Any:
182
- """
183
- Remove and return an item from the memory cell.
184
-
185
- When the buffer is empty:
186
- - before shutdown wait
187
- - after shutdown return the *stop_value* once (if defined),
188
- then raise BufferShutDown
189
- """
190
- try:
191
- return await self._cell.recv()
192
- except BufferShutDown:
193
- if not self._stop_pending:
194
- raise
195
- self._stop_pending = False
196
- return self._stop_value
197
-
198
- def rz_close(self) -> None:
199
- if self._stop_pending:
200
- 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
201
217
 
202
218
 
203
219
  class OutputWorker(redzed.Block):
@@ -268,6 +284,7 @@ class OutputWorker(redzed.Block):
268
284
  self.log_warning("%d worker(s) did not stop", running)
269
285
  raise
270
286
 
287
+
271
288
  class OutputController(redzed.Block):
272
289
  """
273
290
  Run an awaitable for the latest value from a buffer.
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()
redzed/circuit.py CHANGED
@@ -7,10 +7,11 @@ Project home: https://github.com/xitop/redzed/
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- __all__ = ['CircuitState', 'get_circuit', 'reset_circuit', 'run', 'unique_name']
10
+ __all__ = [
11
+ 'CircuitState', 'stop_function', 'get_circuit', 'reset_circuit', 'run', 'unique_name']
11
12
 
12
13
  import asyncio
13
- from collections.abc import Coroutine, Iterable, MutableMapping, Sequence
14
+ from collections.abc import Callable, Coroutine, Iterable, MutableMapping, Sequence
14
15
  import contextlib
15
16
  import enum
16
17
  import itertools
@@ -63,6 +64,13 @@ def unique_name(prefix: str = 'auto') -> str:
63
64
  return get_circuit().rz_unique_name(prefix)
64
65
 
65
66
 
67
+ _StopFunction: t.TypeAlias = Callable[[], t.Any]
68
+
69
+ def stop_function(func: _StopFunction) -> _StopFunction:
70
+ get_circuit().rz_add_stop_function(func)
71
+ return func
72
+
73
+
66
74
  class CircuitState(enum.IntEnum):
67
75
  """
68
76
  Circuit state.
@@ -83,16 +91,16 @@ class _TerminateTaskGroup(Exception):
83
91
 
84
92
 
85
93
  @contextlib.contextmanager
86
- def error_debug(item: Block|Formula|Trigger, suppress_error: bool = False) -> t.Iterator[None]:
94
+ def error_debug(exc_source: t.Any, suppress_error: bool = False) -> t.Iterator[None]:
87
95
  """Add a note to raised exception -or- log and suppress exception."""
88
96
  try:
89
97
  yield None
90
98
  except Exception as err:
91
99
  if not suppress_error:
92
- err.add_note(f"This {type(err).__name__} occurred in {item}")
100
+ err.add_note(f"This {type(err).__name__} occurred in {exc_source}")
93
101
  raise
94
102
  # errors should be suppressed only during the shutdown & cleanup
95
- _logger.error("[Circuit] %s: Suppressing %s: %s", item, type(err).__name__, err)
103
+ _logger.error("[Circuit] %s: Suppressing %s: %s", exc_source, type(err).__name__, err)
96
104
 
97
105
 
98
106
  class Circuit:
@@ -108,6 +116,7 @@ class Circuit:
108
116
  self._blocks: dict[str, Block|Formula] = {}
109
117
  # all Blocks and Formulas belonging to this circuit stored by name
110
118
  self._triggers: list[Trigger] = [] # all triggers belonging to this circuit
119
+ self._stops: list[_StopFunction] = []
111
120
  self._errors: list[Exception] = [] # exceptions occurred in the runner
112
121
  self.rz_persistent_dict: MutableMapping[str, t.Any]|None = None
113
122
  # persistent state data back-end
@@ -169,6 +178,9 @@ class Circuit:
169
178
 
170
179
  # --- circuit components storage ---
171
180
 
181
+ def rz_add_stop_function(self, func: _StopFunction) -> None:
182
+ self._stops.append(func)
183
+
172
184
  def rz_add_item(self, item: Block|Formula|Trigger) -> None:
173
185
  """Add a circuit item."""
174
186
  self._check_not_started()
@@ -279,7 +291,7 @@ class Circuit:
279
291
 
280
292
  def _check_not_started(self) -> None:
281
293
  """Raise an error if the circuit runner has started already."""
282
- if self._state == CircuitState.CLOSED:
294
+ if self._state is CircuitState.CLOSED:
283
295
  # A circuit may be closed before start (see shutdown),
284
296
  # let's use this message instead of the one below.
285
297
  raise RuntimeError("The circuit was closed")
@@ -287,7 +299,7 @@ class Circuit:
287
299
  if self._state > CircuitState.INIT_CIRCUIT:
288
300
  raise RuntimeError("Not allowed after the start")
289
301
 
290
- def after_shutdown(self) -> bool:
302
+ def is_shut_down(self) -> bool:
291
303
  """Test if we are past the shutdown() call."""
292
304
  return self._state >= CircuitState.SHUTDOWN
293
305
 
@@ -444,11 +456,12 @@ class Circuit:
444
456
  async def _runner_init(self) -> None:
445
457
  """Run the circuit during the initialization phase."""
446
458
  self._set_state(CircuitState.INIT_CIRCUIT)
459
+ # ensure a logging handler (won't work in UNDER_CONSTRUCTION level)
460
+ set_debug_level(get_debug_level())
447
461
  await asyncio.sleep(0) # allow reached_state() synchronization
448
- if self.after_shutdown():
462
+ if self.is_shut_down():
449
463
  # It looks like a supporting task has failed immediately after the start
450
464
  return
451
- set_debug_level(get_debug_level()) # will check the existence of a logging handler
452
465
 
453
466
  pe_blocks = [blk for blk in self.get_items(Block) if blk.has_method('rz_pre_init')]
454
467
  pe_formulas = list(self.get_items(Formula))
@@ -497,14 +510,24 @@ class Circuit:
497
510
  self.save_persistent_state(blk, now)
498
511
 
499
512
  stop_triggers = list(self.get_items(Trigger))
513
+ if stop_triggers:
514
+ self._log_debug2_blocks("Stopping triggers", stop_triggers)
515
+ for tstop in stop_triggers:
516
+ with error_debug(tstop, suppress_error=True):
517
+ tstop.rz_stop()
518
+
519
+ if self._stops:
520
+ self.log_debug2("Running %d stop function(s)", len(self._stops))
521
+ for fstop in self._stops:
522
+ with error_debug(f"Stop function {fstop.__name__}()", suppress_error=True):
523
+ fstop()
524
+
500
525
  stop_blocks = [blk for blk in self.get_items(Block) if blk.has_method('rz_stop')]
501
526
  if stop_blocks:
502
- self._log_debug2_blocks("Stopping (sync)", stop_triggers, stop_blocks)
503
- for stop in itertools.chain(stop_triggers, stop_blocks):
504
- assert isinstance(stop, (Block, Trigger)) # @mypy
505
- with error_debug(stop, suppress_error=True):
506
- # union-attr: checked with .has_method()
507
- stop.rz_stop() # type: ignore[union-attr]
527
+ self._log_debug2_blocks("Stopping (sync)", stop_blocks)
528
+ for bstop in stop_blocks:
529
+ with error_debug(bstop, suppress_error=True):
530
+ bstop.rz_stop()
508
531
 
509
532
  if self._auto_cancel_tasks:
510
533
  self.log_debug2("Cancelling %d service task(s)")
@@ -544,10 +567,6 @@ class Circuit:
544
567
 
545
568
  When the runner terminates, it cannot be invoked again.
546
569
  """
547
- if self._state == CircuitState.CLOSED:
548
- raise RuntimeError("Cannot restart a closed circuit.")
549
- if self._state != CircuitState.UNDER_CONSTRUCTION:
550
- raise RuntimeError("The circuit is already running.")
551
570
  if not self._blocks:
552
571
  raise RuntimeError("The circuit is empty")
553
572
  if tasks_are_eager():
@@ -561,7 +580,7 @@ class Circuit:
561
580
  self.abort(err)
562
581
  else:
563
582
  # There might be errors reported with abort().
564
- # In such case the state has been set to SHUTDOWN.
583
+ # In such case the state has been set to SHUTDOWN or even CLOSED.
565
584
  # _set_state(RUNNING) will be silently ignored.
566
585
  self._set_state(CircuitState.RUNNING)
567
586
  # wait until cancelled from the task group; possible causes:
@@ -573,17 +592,18 @@ class Circuit:
573
592
  # will be re-raised at the end if there won't be other exceptions
574
593
  pass
575
594
  # cancellation causes 2 and 3 do not modify the state
576
- if not self.after_shutdown():
595
+ if not self.is_shut_down():
577
596
  self._set_state(CircuitState.SHUTDOWN)
578
597
  await asyncio.sleep(0)
579
- try:
580
- await self._runner_shutdown()
581
- except Exception as err:
582
- # If an exception is propagated from _runner_shutdown, it is probably a bug.
583
- # Calling abort is not necessary when shutting down, but the call will log
584
- # and register the exception to be included in the final ExceptionGroup.
585
- self.abort(err)
586
- self._set_state(CircuitState.CLOSED)
598
+ if self._state < CircuitState.CLOSED:
599
+ try:
600
+ await self._runner_shutdown()
601
+ except Exception as err:
602
+ # If an exception is propagated from _runner_shutdown, it is probably a bug.
603
+ # Calling abort is not necessary when shutting down, but the call will log
604
+ # and register the exception to be included in the final ExceptionGroup.
605
+ self.abort(err)
606
+ self._set_state(CircuitState.CLOSED)
587
607
 
588
608
  if self._errors:
589
609
  raise ExceptionGroup("_runner_core() errors", self._errors)
@@ -605,7 +625,7 @@ class Circuit:
605
625
  # the same error may be reported from several places
606
626
  return
607
627
  self._errors.append(err)
608
- if self.after_shutdown():
628
+ if self.is_shut_down():
609
629
  self.log_error("Unhandled error during shutdown: %r", err)
610
630
  else:
611
631
  self.log_warning("Aborting due to an exception: %r", err)
@@ -617,9 +637,9 @@ class Circuit:
617
637
 
618
638
  Prevent the runner from starting if it wasn't started yet.
619
639
  """
620
- if self.after_shutdown():
640
+ if self.is_shut_down():
621
641
  return
622
- if self._state == CircuitState.UNDER_CONSTRUCTION:
642
+ if self._state <= CircuitState.INIT_CIRCUIT:
623
643
  self._set_state(CircuitState.CLOSED)
624
644
  return
625
645
  self._set_state(CircuitState.SHUTDOWN)
@@ -656,7 +676,7 @@ class Circuit:
656
676
  try:
657
677
  await coro # return value of a service is ignored
658
678
  except asyncio.CancelledError:
659
- if self.after_shutdown():
679
+ if self.is_shut_down():
660
680
  self.log_debug1("%s was cancelled", longname)
661
681
  raise
662
682
  err = RuntimeError(f"{longname} was cancelled before shutdown")
@@ -666,7 +686,7 @@ class Circuit:
666
686
  err.add_note(f"Error occurred in {longname}")
667
687
  self.abort(err)
668
688
  raise
669
- if self.after_shutdown():
689
+ if self.is_shut_down():
670
690
  self.log_debug1("%s terminated", longname)
671
691
  return
672
692
  exc = RuntimeError(f"{longname} terminated before shutdown")
@@ -680,7 +700,7 @@ class Circuit:
680
700
  **task_kwargs
681
701
  ) -> asyncio.Task[None]:
682
702
  """Create a service task for the circuit."""
683
- if self.after_shutdown():
703
+ if self.is_shut_down():
684
704
  raise RuntimeError("Cannot create a service after shutdown")
685
705
  # Python 3.12 and 3.13 only: Eager tasks start to run before their name is set.
686
706
  # As a workaround we tell the watchdog wrapper the name.
@@ -734,8 +754,13 @@ async def run(*coroutines: Coroutine[t.Any, t.Any, t.Any], catch_sigterm: bool =
734
754
 
735
755
  If errors occur, raise an exception group with all exceptions.
736
756
  """
757
+ circuit = get_circuit()
758
+ state = circuit.get_state()
759
+ if state is CircuitState.CLOSED:
760
+ raise RuntimeError("Cannot restart a closed circuit.")
761
+ if state is not CircuitState.UNDER_CONSTRUCTION:
762
+ raise RuntimeError("The circuit is already running.")
737
763
  with TerminatingSignal(signal.SIGTERM) if catch_sigterm else contextlib.nullcontext():
738
- circuit = get_circuit()
739
764
  try:
740
765
  async with asyncio.TaskGroup() as tg:
741
766
  tg.create_task(circuit.rz_runner(), name="Circuit runner")
@@ -8,7 +8,7 @@ import asyncio
8
8
  from collections.abc import Awaitable
9
9
  import typing as t
10
10
 
11
- __all__ = ['BufferShutDown', 'cancel_shield', 'MsgSync']
11
+ __all__ = ['BufferShutDown', 'cancel_shield']
12
12
 
13
13
  try:
14
14
  BufferShutDown = asyncio.QueueShutDown # Python 3.13+
@@ -18,105 +18,6 @@ except AttributeError:
18
18
 
19
19
 
20
20
  _T = t.TypeVar("_T")
21
- _MISSING = object()
22
-
23
- class MsgSync(t.Generic[_T]):
24
- """
25
- Task synchronization based on sending messages of type _T.
26
-
27
- The count of unread message is always either zero or one,
28
- because messages are not queued. A new message replaces
29
- the previous unread one, if any.
30
-
31
- An existing unread message is available immediately.
32
- Otherwise the recipient must wait and will be awakened when
33
- a new message arrives. Reading a message consumes it,
34
- so each message can reach only one receiver.
35
- """
36
-
37
- def __init__(self) -> None:
38
- self._msg: t.Any = _MISSING
39
- self._shutdown = False
40
- self._has_data = asyncio.Event() # valid before shutdown
41
- self._draining = False # valid after shutdown
42
-
43
- def shutdown(self) -> None:
44
- """
45
- Disallow sending immediately. Disallow receiving after the MsgSync gets empty.
46
-
47
- Calls to .send() will raise BufferShutDown.
48
-
49
- If a message was sent before shutdown and is waiting, it can be
50
- normally received before the receiver shuts down too. After
51
- the shutdown all blocked callers will be unblocked with BufferShutDown.
52
- Calls to .recv() will raise BufferShutDown as well.
53
- """
54
- self._shutdown = True
55
- self._draining = self._has_data.is_set()
56
- if not self._draining:
57
- # wakeup all waiters
58
- self._has_data.set()
59
-
60
- def is_shutdown(self) -> bool:
61
- """Check if the buffer was shut down."""
62
- return self._shutdown
63
-
64
- def send(self, msg: _T) -> None:
65
- """
66
- Send a message to one receiver.
67
-
68
- If the previously sent message hasn't been received yet,
69
- the new message overwrites it.
70
- """
71
- if self._shutdown:
72
- raise BufferShutDown("The buffer was shut down")
73
- self._msg = msg
74
- self._has_data.set()
75
-
76
- def clear(self) -> None:
77
- """Remove the waiting message."""
78
- if self._shutdown:
79
- self._draining = False
80
- else:
81
- self._has_data.clear()
82
- self._msg = _MISSING
83
-
84
- def has_data(self) -> bool:
85
- """Return True only if there is a waiting message."""
86
- return self._draining if self._shutdown else self._has_data.is_set()
87
-
88
- async def _recv(self) -> _T:
89
- """Receive (consume) a message."""
90
- while not self._has_data.is_set():
91
- await self._has_data.wait()
92
- if not self._shutdown:
93
- self._has_data.clear()
94
- elif self._draining:
95
- self._draining = False
96
- else:
97
- raise BufferShutDown("The buffer was shut down")
98
- msg, self._msg = self._msg, _MISSING
99
- return t.cast(_T, msg)
100
-
101
- async def recv(self, timeout: float | None = None, default: t.Any = _MISSING) -> t.Any:
102
- """
103
- Receive a message with optional timeout.
104
-
105
- If a message is not available, wait until it arrives.
106
-
107
- If a timeout [seconds] is given, return the default value
108
- if no message is received before the timeout period elapses.
109
- Without a default, raise the TimeoutError.
110
- """
111
- if timeout is None:
112
- return await self._recv()
113
- try:
114
- async with asyncio.timeout(timeout):
115
- return await self._recv()
116
- except TimeoutError:
117
- if default is not _MISSING:
118
- return default
119
- raise
120
21
 
121
22
 
122
23
  async def cancel_shield(aw: Awaitable[_T]) -> _T:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redzed
3
- Version: 26.1.28
3
+ Version: 26.2.4
4
4
  Summary: An asyncio-based library for building small automated systems
5
5
  Author-email: Vlado Potisk <redzed@poti.sk>
6
6
  License-Expression: MIT
@@ -8,7 +8,7 @@ Project-URL: homepage, https://github.com/xitop/redzed
8
8
  Project-URL: repository, https://github.com/xitop/redzed
9
9
  Project-URL: documentation, https://redzed.readthedocs.io/en/latest/
10
10
  Keywords: automation,finite-state machine
11
- Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Programming Language :: Python :: 3
@@ -1,7 +1,7 @@
1
- redzed/__init__.py,sha256=NxdFNxuOXDhqZRUpCZLhmSFH2W8-khvjkVq_4umGJ9E,1098
1
+ redzed/__init__.py,sha256=IVRc3KnMDB9N_M0fiii8X84I6_AEjhmOqtrzAJbvuzY,1097
2
2
  redzed/base_block.py,sha256=A1ZxEIH8jsNXj3z3HvzgAVshUsMoFtqIoR41-U2vMaw,5187
3
- redzed/block.py,sha256=oviZ1Tonp-eQrgLX4CYnv9KrPSgBPeCHDyUM_yhghJU,11692
4
- redzed/circuit.py,sha256=QO5XtX-zZhSFz_1ixKgnwrPzZ3vaBm98qWI8Y8-7r3I,31151
3
+ redzed/block.py,sha256=_Qp8DQ1Ojb_PuGcYy2xS4gkhUeQymIx69iJaKHUM9jQ,11768
4
+ redzed/circuit.py,sha256=6C1mmg83Z_mjCHUpLlU9mdK4297cjrBycIuUUnjSB5E,31894
5
5
  redzed/cron_service.py,sha256=0RSIte6zkbF4mSojrPoVFMHkVep1OZ2Mv2BtG3YiPTs,11372
6
6
  redzed/debug.py,sha256=hsHbBxPOOUVL_utfiRnkRh49FszDvHsCZ9xAqxawUIU,2868
7
7
  redzed/formula_trigger.py,sha256=lkBMeLVrcvqA71WGGapj9hitm52-dATMgT5QFhC1lOY,7360
@@ -13,18 +13,18 @@ redzed/blocklib/__init__.py,sha256=au7jbjO_RUFzK5U4STk-7pc1DTVA0OVh2W_aKsx1Y3s,5
13
13
  redzed/blocklib/counter.py,sha256=pUT5Np31gk1rP2LqaTjJdzdyyS8NhpD9104Re_MtLpY,1251
14
14
  redzed/blocklib/fsm.py,sha256=3UVQyKwZstL96jf3HYfuvN6R49uOMXVo20MFz9EpGkc,24746
15
15
  redzed/blocklib/inputs.py,sha256=YtKtbDUtXyQyUTNadniwyxCA_-UPCgrrY17EipfXcaI,6223
16
- redzed/blocklib/outputs.py,sha256=TnYjQV9DUfBX86WzXQP1xlSh-Oui-xvuN3uQEWvXIvc,13313
17
- redzed/blocklib/repeat.py,sha256=hkIO7qgm4wS32dcKkkfL_DjnoKWf-l3k2DfRiRUk6iE,2500
16
+ redzed/blocklib/outputs.py,sha256=77FwFzh3q1Ri7tsAi39fmojD0vMmwBTSAYEjf1ZABpg,13964
17
+ redzed/blocklib/repeat.py,sha256=R4f0WjTTnqpiYOHDCHucmGdp6ChyVozLaZDaE-Nx7Tc,3146
18
18
  redzed/blocklib/timedate.py,sha256=bmYZx6ToYsuD3UQzc1zxD2ddvovzF0A63-y4Pnkrm6Q,5225
19
19
  redzed/blocklib/timeinterval.py,sha256=alK68glDdWPD9SWxZ97pEzhQIReC41RMdXrRIRZsyuE,6878
20
20
  redzed/blocklib/timer.py,sha256=fCQTbmBOqo7pc6k32yj7JoyUM9XbdgHOXzu2tRiOiJE,1483
21
21
  redzed/blocklib/validator.py,sha256=6r0qtPIJ2neCP32WhI7tGxEUus_ji6kx_jrzHd2_M9I,1312
22
22
  redzed/utils/__init__.py,sha256=Yo8cj1f1HQj862UOdCsXOkMg4kfq1c5S3HJDhuiE5os,326
23
- redzed/utils/async_utils.py,sha256=Vknijh9rABL8GcJbYv6a7I4qO2VLerMsGTr5wiGWbjo,4486
23
+ redzed/utils/async_utils.py,sha256=ABwloG7YKqRwfIwWsXNaGlNPc7MdEdq29-aRGRDsBgk,1075
24
24
  redzed/utils/data_utils.py,sha256=hesExhPcM2-IZPbDXAuGCUMpCI4YiNodp_ZJUj0_h9I,2952
25
25
  redzed/utils/time_utils.py,sha256=eCqk4T4ipn4hgzL8-4Usn1W6kkTyRCuQ9-BMSvnELww,8524
26
- redzed-26.1.28.dist-info/licenses/LICENSE.txt,sha256=brj9B7uNdzUvTJON_5Eibf7zeLRhcMuBYMInalu0KlI,1091
27
- redzed-26.1.28.dist-info/METADATA,sha256=t_DTA4MdjNI4KoOY8ayUvtU_FNABfmLfUdqQVQddmaI,2057
28
- redzed-26.1.28.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
29
- redzed-26.1.28.dist-info/top_level.txt,sha256=7Rt0BRMqaJ0AGAmrd2JDqmqSY4cmQeW--7u6KDV1gZg,7
30
- redzed-26.1.28.dist-info/RECORD,,
26
+ redzed-26.2.4.dist-info/licenses/LICENSE.txt,sha256=brj9B7uNdzUvTJON_5Eibf7zeLRhcMuBYMInalu0KlI,1091
27
+ redzed-26.2.4.dist-info/METADATA,sha256=nQARn1_dngD-r1UBeYDVthqAwmqy6taWhJhDlP14kNo,2055
28
+ redzed-26.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
29
+ redzed-26.2.4.dist-info/top_level.txt,sha256=7Rt0BRMqaJ0AGAmrd2JDqmqSY4cmQeW--7u6KDV1gZg,7
30
+ redzed-26.2.4.dist-info/RECORD,,