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/initializers.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Block initializers.
|
|
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__ = [
|
|
11
|
+
'AsyncInitializer', 'SyncInitializer',
|
|
12
|
+
'InitFunction', 'InitTask', 'InitValue', 'InitWait', 'RestoreState']
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from collections.abc import Callable, Awaitable
|
|
16
|
+
import time
|
|
17
|
+
import typing as t
|
|
18
|
+
|
|
19
|
+
from . import block
|
|
20
|
+
from .undef import UNDEF, UndefType
|
|
21
|
+
from .utils import check_async_func, time_period
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LONG_TIMEOUT = 60 # threshold for a "long timeout" debug message
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SyncInitializer:
|
|
28
|
+
"""
|
|
29
|
+
Logic block initializers are used as initial=... arguments.
|
|
30
|
+
|
|
31
|
+
A block can have multiple initializers. They will be called
|
|
32
|
+
in given order until the first one succeeds.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
# Keep track in order to prevent repeated application of the same initializer.
|
|
37
|
+
# It could happen when a block receives an event during initialization.
|
|
38
|
+
self._applied = False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def type_name(self) -> str:
|
|
42
|
+
return type(self).__name__
|
|
43
|
+
|
|
44
|
+
def _get_init(self) -> t.Any:
|
|
45
|
+
"""Return the initial value or UNDEF if not available."""
|
|
46
|
+
raise NotImplementedError()
|
|
47
|
+
|
|
48
|
+
def apply_to(self, blk: block.Block) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Apply this initializer to a logic block *blk*.
|
|
51
|
+
|
|
52
|
+
Log exceptions, but do not propagate them. An error condition
|
|
53
|
+
is when a block doesn't get proper initialization after using
|
|
54
|
+
ALL initializers.
|
|
55
|
+
"""
|
|
56
|
+
if self._applied:
|
|
57
|
+
return
|
|
58
|
+
self._applied = True
|
|
59
|
+
try:
|
|
60
|
+
init_value = self._get_init()
|
|
61
|
+
except Exception as err:
|
|
62
|
+
blk.log_error(
|
|
63
|
+
"%s: could not get the initialization value: %r", self.type_name, err)
|
|
64
|
+
return
|
|
65
|
+
blk.log_debug2("%s: init value: %r", self.type_name, init_value)
|
|
66
|
+
if init_value is UNDEF:
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
blk.rz_init(init_value) # type: ignore[attr-defined]
|
|
70
|
+
except Exception as err:
|
|
71
|
+
blk.log_error(
|
|
72
|
+
"%s could not apply the initialization value: %r", self.type_name, err)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InitFunction(SyncInitializer):
|
|
77
|
+
"""
|
|
78
|
+
Initialize with a calculated value.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, func: Callable[..., t.Any], *args: t.Any) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Usage: InitFunction(func, arg1, arg2, ...)
|
|
84
|
+
Use functools.partial to pass keyword arguments.
|
|
85
|
+
"""
|
|
86
|
+
super().__init__()
|
|
87
|
+
if not callable(func):
|
|
88
|
+
raise TypeError(f"{self.type_name}: {func!r} is not a function")
|
|
89
|
+
self._func = func
|
|
90
|
+
self._args = args
|
|
91
|
+
|
|
92
|
+
def _get_init(self) -> t.Any:
|
|
93
|
+
return self._func(*self._args)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class InitValue(SyncInitializer):
|
|
97
|
+
"""
|
|
98
|
+
Initialize with a literal value.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, value: t.Any) -> None:
|
|
102
|
+
super().__init__()
|
|
103
|
+
if value is UNDEF:
|
|
104
|
+
raise ValueError("<UNDEF> is not a valid initialization value.")
|
|
105
|
+
self._value = value
|
|
106
|
+
|
|
107
|
+
def _get_init(self) -> t.Any:
|
|
108
|
+
return self._value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_CHECKPOINTS = [None, 'event', 'interval']
|
|
112
|
+
|
|
113
|
+
class RestoreState(SyncInitializer):
|
|
114
|
+
"""Restore from saved state."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
checkpoints: None|t.Literal['event', 'interval'] = None,
|
|
119
|
+
expiration: None|float|str = None
|
|
120
|
+
) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
if not checkpoints in _CHECKPOINTS:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"Parameter checkpoints must be one of: "
|
|
125
|
+
+ f"{', '.join(repr(ch) for ch in _CHECKPOINTS)}")
|
|
126
|
+
self.rz_checkpoints = checkpoints
|
|
127
|
+
self._expiration = time_period(expiration, passthrough=None)
|
|
128
|
+
|
|
129
|
+
def _get_init(self) -> t.Any:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def _get_state(self, blk: block.Block) -> t.Any:
|
|
133
|
+
storage = blk.circuit.rz_persistent_dict
|
|
134
|
+
assert storage is not None
|
|
135
|
+
try:
|
|
136
|
+
state, timestamp = storage[blk.rz_key]
|
|
137
|
+
except KeyError:
|
|
138
|
+
blk.log_debug2("No saved state was found")
|
|
139
|
+
return UNDEF
|
|
140
|
+
except Exception as err:
|
|
141
|
+
blk.log_warning("State retrieval error: %r", err)
|
|
142
|
+
return UNDEF
|
|
143
|
+
if self._expiration is None:
|
|
144
|
+
return state
|
|
145
|
+
age = time.time() - timestamp
|
|
146
|
+
if age < 0:
|
|
147
|
+
blk.log_error(
|
|
148
|
+
"The timestamp of saved data is in the future, check the system time")
|
|
149
|
+
elif age > self._expiration:
|
|
150
|
+
blk.log_debug2("The saved state has expired")
|
|
151
|
+
return UNDEF
|
|
152
|
+
return state
|
|
153
|
+
|
|
154
|
+
def apply_to(self, blk: block.Block) -> None:
|
|
155
|
+
if self._applied:
|
|
156
|
+
return
|
|
157
|
+
self._applied = True
|
|
158
|
+
if not blk.rz_persistence:
|
|
159
|
+
return
|
|
160
|
+
try:
|
|
161
|
+
init_state = self._get_state(blk)
|
|
162
|
+
except Exception as err:
|
|
163
|
+
blk.log_error(
|
|
164
|
+
"%s could not get the initialization value: %r", self.type_name, err)
|
|
165
|
+
return
|
|
166
|
+
if init_state is UNDEF:
|
|
167
|
+
# a debug message was logged in _get_state
|
|
168
|
+
return
|
|
169
|
+
blk.log_debug2("%s: saved state: %r", self.type_name, init_state)
|
|
170
|
+
try:
|
|
171
|
+
blk.rz_restore_state(init_state) # type: ignore[attr-defined]
|
|
172
|
+
except Exception as err:
|
|
173
|
+
blk.log_error(
|
|
174
|
+
"%s could not apply the initialization value: %r", self.type_name, err)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AsyncInitializer:
|
|
179
|
+
"""
|
|
180
|
+
Asynchronous initializer.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, timeout: float|str):
|
|
184
|
+
self._applied = False
|
|
185
|
+
self._timeout = time_period(timeout, passthrough=None)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def type_name(self) -> str:
|
|
189
|
+
return type(self).__name__
|
|
190
|
+
|
|
191
|
+
async def _async_get_init(self) -> t.Any:
|
|
192
|
+
"""Async version of _get_init."""
|
|
193
|
+
raise NotImplementedError()
|
|
194
|
+
|
|
195
|
+
async def async_apply_to(self, blk: block.Block) -> None:
|
|
196
|
+
"""Async version of apply(). Do not overwrite existing state."""
|
|
197
|
+
if self._applied:
|
|
198
|
+
return
|
|
199
|
+
self._applied = True
|
|
200
|
+
if self._timeout >= _LONG_TIMEOUT:
|
|
201
|
+
blk.log_debug2("%s has a long timeout of %.1f secs", self.type_name, self._timeout)
|
|
202
|
+
try:
|
|
203
|
+
init_value = await self._async_get_init()
|
|
204
|
+
except TimeoutError:
|
|
205
|
+
blk.log_debug1("%s timed out", self.type_name)
|
|
206
|
+
return
|
|
207
|
+
except Exception as err:
|
|
208
|
+
blk.log_error("Skipping failed %s. Error: %r", self.type_name, err)
|
|
209
|
+
return
|
|
210
|
+
blk.log_debug2("%s: init value: %r", self.type_name, init_value)
|
|
211
|
+
if init_value is UNDEF:
|
|
212
|
+
return
|
|
213
|
+
if not blk.is_undef():
|
|
214
|
+
blk.log_debug1(
|
|
215
|
+
"%s: not applying the init value, because the block "
|
|
216
|
+
+ "has been initialized in the meantime", self.type_name)
|
|
217
|
+
return
|
|
218
|
+
blk.rz_init(init_value) # type: ignore[attr-defined]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class InitTask(AsyncInitializer):
|
|
222
|
+
"""
|
|
223
|
+
Initialize with a value calculated by a coroutine.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
coro_func: Callable[..., Awaitable[t.Any]],
|
|
229
|
+
*args: t.Any,
|
|
230
|
+
timeout: float|str = 10.0
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Similar to InitFunction, but asynchonous."""
|
|
233
|
+
check_async_func(coro_func)
|
|
234
|
+
super().__init__(timeout)
|
|
235
|
+
self._corofunc = coro_func
|
|
236
|
+
self._args = args
|
|
237
|
+
|
|
238
|
+
async def _async_get_init(self) -> t.Any:
|
|
239
|
+
async with asyncio.timeout(self._timeout):
|
|
240
|
+
return await self._corofunc(*self._args)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class InitWait(AsyncInitializer):
|
|
244
|
+
"""
|
|
245
|
+
Passively wait for initialization by an event.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
async def _async_get_init(self) -> UndefType:
|
|
249
|
+
return await asyncio.sleep(self._timeout, result=UNDEF)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stop the runner with a signal.
|
|
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__ = ['TerminatingSignal']
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
import logging
|
|
15
|
+
import signal
|
|
16
|
+
from types import FrameType
|
|
17
|
+
import typing as t
|
|
18
|
+
|
|
19
|
+
from . import circuit
|
|
20
|
+
|
|
21
|
+
_logger = logging.getLogger(__package__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TerminatingSignal:
|
|
25
|
+
"""
|
|
26
|
+
A context manager gracefully stopping the runner after signal.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, signo: int|None) -> None:
|
|
30
|
+
self._signo = signo
|
|
31
|
+
if signo is None:
|
|
32
|
+
return
|
|
33
|
+
self._saved_handler: Callable[[int, FrameType|None], None]|int|None
|
|
34
|
+
self._signame = signal.strsignal(signo) or f"#{signo}"
|
|
35
|
+
|
|
36
|
+
def __enter__(self) -> None:
|
|
37
|
+
if self._signo is None:
|
|
38
|
+
return
|
|
39
|
+
self._saved_handler = signal.getsignal(self._signo)
|
|
40
|
+
if self._saved_handler is None:
|
|
41
|
+
_logger.warning(
|
|
42
|
+
"An incompatible handler for signal %s was found; "
|
|
43
|
+
+ "Redzed will not catch this signal.",
|
|
44
|
+
self._signame
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
signal.signal(self._signo, self._handler)
|
|
48
|
+
|
|
49
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb) -> t.Literal[False]:
|
|
50
|
+
if self._signo is not None and self._saved_handler is not None:
|
|
51
|
+
signal.signal(self._signo, self._saved_handler)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def _handler(self, signo: int, frame: FrameType|None) -> None:
|
|
55
|
+
"""A signal handler."""
|
|
56
|
+
# - we need the _threadsafe variant of call_soon
|
|
57
|
+
# - get_running loop() and get_circuit() will succeed,
|
|
58
|
+
# because this handler is active only during edzed.run()
|
|
59
|
+
msg = f"Signal {self._signame!r} caught"
|
|
60
|
+
call_soon = asyncio.get_running_loop().call_soon_threadsafe
|
|
61
|
+
call_soon(_logger.warning, "%s", msg)
|
|
62
|
+
call_soon(circuit.get_circuit().shutdown)
|
|
63
|
+
if callable(self._saved_handler):
|
|
64
|
+
self._saved_handler(signo, frame)
|
redzed/undef.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The UNDEF singleton constant.
|
|
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
|
+
__all__ = ['UNDEF', 'UndefType']
|
|
11
|
+
|
|
12
|
+
import enum
|
|
13
|
+
import typing as t
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DefaultEnumType(enum.EnumType):
|
|
17
|
+
"""Allow an UndefType() call without parameters."""
|
|
18
|
+
# pylint: disable=keyword-arg-before-vararg
|
|
19
|
+
def __call__(cls, value=None, *args, **kwargs):
|
|
20
|
+
return super().__call__(value, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# See PEP 484 - Support for singleton types in unions
|
|
24
|
+
class UndefType(enum.Enum, metaclass=DefaultEnumType):
|
|
25
|
+
"""Undefined output value"""
|
|
26
|
+
UNDEF = None
|
|
27
|
+
|
|
28
|
+
def __bool__(self) -> bool:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
return '<UNDEF>'
|
|
33
|
+
|
|
34
|
+
__str__ = __repr__
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Uninitialized circuit block's state value
|
|
38
|
+
UNDEF: t.Final = UndefType.UNDEF
|
redzed/utils/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docs: https://redzed.readthedocs.io/en/latest/
|
|
3
|
+
Home: https://github.com/xitop/redzed/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from . import async_utils
|
|
7
|
+
from . import data_utils
|
|
8
|
+
from . import time_utils
|
|
9
|
+
|
|
10
|
+
from .async_utils import *
|
|
11
|
+
from .data_utils import *
|
|
12
|
+
from .time_utils import *
|
|
13
|
+
|
|
14
|
+
__all__ = async_utils.__all__ + data_utils.__all__ + time_utils.__all__
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asynchronous utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from collections.abc import Awaitable
|
|
9
|
+
import typing as t
|
|
10
|
+
|
|
11
|
+
__all__ = ['BufferShutDown', 'cancel_shield', 'MsgSync']
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
BufferShutDown = asyncio.QueueShutDown # Python 3.13+
|
|
15
|
+
except AttributeError:
|
|
16
|
+
class BufferShutDown(Exception): # type: ignore[no-redef]
|
|
17
|
+
"""asyncio.QueueShutDown substitute"""
|
|
18
|
+
|
|
19
|
+
|
|
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
|
+
|
|
121
|
+
|
|
122
|
+
async def cancel_shield(aw: Awaitable[_T]) -> _T:
|
|
123
|
+
"""
|
|
124
|
+
Shield from cancellation while aw is awaited.
|
|
125
|
+
|
|
126
|
+
Any pending CancelledError is raised when aw is finished.
|
|
127
|
+
"""
|
|
128
|
+
task = asyncio.ensure_future(aw)
|
|
129
|
+
cancel_exc = None
|
|
130
|
+
while True:
|
|
131
|
+
try:
|
|
132
|
+
retval = await asyncio.shield(task)
|
|
133
|
+
except asyncio.CancelledError as err:
|
|
134
|
+
if task.done():
|
|
135
|
+
raise
|
|
136
|
+
cancel_exc = err
|
|
137
|
+
else:
|
|
138
|
+
break
|
|
139
|
+
if cancel_exc is not None:
|
|
140
|
+
try:
|
|
141
|
+
raise cancel_exc
|
|
142
|
+
finally:
|
|
143
|
+
# break the reference loop
|
|
144
|
+
cancel_exc = None
|
|
145
|
+
return retval
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small utilities.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'check_async_coro', 'check_async_func', 'check_identifier', 'func_call_string',
|
|
8
|
+
'is_multiple', 'tasks_are_eager', 'to_tuple']
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
12
|
+
import inspect
|
|
13
|
+
import itertools
|
|
14
|
+
import logging
|
|
15
|
+
import typing as t
|
|
16
|
+
|
|
17
|
+
_logger = logging.getLogger(__package__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_identifier(name: t.Any, msg_prefix: str) -> None:
|
|
21
|
+
"""Raise if *name* is not a valid identifier."""
|
|
22
|
+
if not isinstance(name, str):
|
|
23
|
+
raise TypeError(f"{msg_prefix} must be a string, got {name!r}")
|
|
24
|
+
if not name:
|
|
25
|
+
raise ValueError(f"{msg_prefix} cannot be an empty string")
|
|
26
|
+
if not name.isidentifier():
|
|
27
|
+
raise ValueError(f"{msg_prefix} must be a valid identifier, got '{name!r}'")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_multiple(arg: t.Any) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Check if *arg* specifies multiple ordered items.
|
|
33
|
+
|
|
34
|
+
The check is based on the type. The actual item count does not
|
|
35
|
+
matter and can be any value including zero or one.
|
|
36
|
+
A string or a byte-string is considered a single argument.
|
|
37
|
+
"""
|
|
38
|
+
return not isinstance(arg, (str, bytes)) and isinstance(arg, Sequence)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_T_item = t.TypeVar("_T_item")
|
|
42
|
+
def to_tuple(args: _T_item|Sequence[_T_item]) -> tuple[_T_item, ...]:
|
|
43
|
+
"""Transform *args* to a tuple of items."""
|
|
44
|
+
|
|
45
|
+
if isinstance(args, tuple):
|
|
46
|
+
return args
|
|
47
|
+
if is_multiple(args):
|
|
48
|
+
assert isinstance(args, Sequence) # @mypy
|
|
49
|
+
return tuple(args)
|
|
50
|
+
return t.cast(tuple[_T_item], (args,))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# must not touch *kwargs* # pylint: disable-next=dangerous-default-value
|
|
54
|
+
def func_call_string(
|
|
55
|
+
func: Callable[..., t.Any]|None,
|
|
56
|
+
args: Sequence[t.Any],
|
|
57
|
+
kwargs: Mapping[str, t.Any] = {}
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Convert args and kwargs to a printable string."""
|
|
60
|
+
arglist = '(' + ', '.join(itertools.chain(
|
|
61
|
+
(repr(a) for a in args),
|
|
62
|
+
(f"{k}={v!r}" for k, v in kwargs.items()))) + ')'
|
|
63
|
+
if func is None:
|
|
64
|
+
return arglist
|
|
65
|
+
return func.__name__ + arglist
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def tasks_are_eager() -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Detect if eager tasks are enabled.
|
|
71
|
+
|
|
72
|
+
Eager tasks (Python 3.12+) change the order of execution.
|
|
73
|
+
The changed order may violate assumptions made in the
|
|
74
|
+
code written before eager tasks were introduced.
|
|
75
|
+
"""
|
|
76
|
+
if not hasattr(asyncio, "eager_task_factory"):
|
|
77
|
+
return False
|
|
78
|
+
flag = False
|
|
79
|
+
async def test_task() -> None:
|
|
80
|
+
nonlocal flag
|
|
81
|
+
flag = True
|
|
82
|
+
asyncio.create_task(test_task())
|
|
83
|
+
return flag
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_async_coro(arg: t.Any) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Check if arg is a coroutine object.
|
|
89
|
+
|
|
90
|
+
Raise a TypeError with a descriptive message if it isn't.
|
|
91
|
+
"""
|
|
92
|
+
if inspect.iscoroutine(arg):
|
|
93
|
+
return
|
|
94
|
+
if inspect.iscoroutinefunction(arg):
|
|
95
|
+
received = f"an async function '{arg.__name__}'. Did you mean '{arg.__name__}()'?"
|
|
96
|
+
else:
|
|
97
|
+
received = repr(arg)
|
|
98
|
+
raise TypeError(f"Expected a coroutine, but got {received}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_async_func(arg: t.Any) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Check if *arg* is an async function.
|
|
104
|
+
|
|
105
|
+
Raise a TypeError with a descriptive message if it isn't.
|
|
106
|
+
"""
|
|
107
|
+
if inspect.iscoroutinefunction(arg):
|
|
108
|
+
return
|
|
109
|
+
if inspect.iscoroutine(arg):
|
|
110
|
+
received = (f"a coroutine '{arg.__name__}()'. "
|
|
111
|
+
+ f"Did you mean '{arg.__name__}' without parentheses?")
|
|
112
|
+
elif callable(arg):
|
|
113
|
+
received = f"a non-async function/callable '{arg.__name__}'"
|
|
114
|
+
else:
|
|
115
|
+
received = repr(arg)
|
|
116
|
+
raise TypeError(f"Expected an async function, but got {received}")
|