asynkio 0.0.3__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.
- asynkio/__init__.py +21 -0
- asynkio/time/__init__.py +19 -0
- asynkio/time/interval.py +253 -0
- asynkio/time/types.py +543 -0
- asynkio-0.0.3.dist-info/METADATA +184 -0
- asynkio-0.0.3.dist-info/RECORD +9 -0
- asynkio-0.0.3.dist-info/WHEEL +5 -0
- asynkio-0.0.3.dist-info/licenses/LICENSE +33 -0
- asynkio-0.0.3.dist-info/top_level.txt +1 -0
asynkio/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
__version__ = '0.0.3'
|
|
3
|
+
|
|
4
|
+
from .time import (
|
|
5
|
+
Duration,
|
|
6
|
+
Instant,
|
|
7
|
+
Interval,
|
|
8
|
+
MissedTickBehavior,
|
|
9
|
+
MissedTickBehaviour,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'__version__',
|
|
15
|
+
|
|
16
|
+
'Duration',
|
|
17
|
+
'Instant',
|
|
18
|
+
'Interval',
|
|
19
|
+
'MissedTickBehavior',
|
|
20
|
+
'MissedTickBehaviour',
|
|
21
|
+
]
|
asynkio/time/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
from .interval import (
|
|
3
|
+
Interval,
|
|
4
|
+
MissedTickBehavior,
|
|
5
|
+
MissedTickBehaviour,
|
|
6
|
+
)
|
|
7
|
+
from .types import (
|
|
8
|
+
Duration,
|
|
9
|
+
Instant,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'Duration',
|
|
15
|
+
'Instant',
|
|
16
|
+
'Interval',
|
|
17
|
+
'MissedTickBehavior',
|
|
18
|
+
'MissedTickBehaviour',
|
|
19
|
+
]
|
asynkio/time/interval.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
|
|
2
|
+
from .types import (
|
|
3
|
+
Duration,
|
|
4
|
+
Instant,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MissedTickBehaviour(enum.IntEnum):
|
|
12
|
+
"""
|
|
13
|
+
Defines the behaviour of an `Interval` when it misses a tick.
|
|
14
|
+
|
|
15
|
+
See
|
|
16
|
+
---
|
|
17
|
+
https://docs.rs/tokio/latest/tokio/time/enum.MissedTickBehavior.html
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
Ticks as fast as possible until caught up.
|
|
22
|
+
"""
|
|
23
|
+
BURST = 1
|
|
24
|
+
"""
|
|
25
|
+
Tick at multiples of period from when tick was called, rather than from
|
|
26
|
+
start.
|
|
27
|
+
"""
|
|
28
|
+
DELAY = 2
|
|
29
|
+
"""
|
|
30
|
+
Skips missed ticks and tick on the next multiple of period from start.
|
|
31
|
+
"""
|
|
32
|
+
SKIP = 3
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_str(self):
|
|
36
|
+
|
|
37
|
+
if MissedTickBehaviour.BURST == self:
|
|
38
|
+
|
|
39
|
+
return 'BURST'
|
|
40
|
+
|
|
41
|
+
if MissedTickBehaviour.DELAY == self:
|
|
42
|
+
|
|
43
|
+
return 'DELAY'
|
|
44
|
+
|
|
45
|
+
if MissedTickBehaviour.SKIP == self:
|
|
46
|
+
|
|
47
|
+
return 'SKIP'
|
|
48
|
+
|
|
49
|
+
return NotImplemented
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def try_parse(s : str):
|
|
53
|
+
"""
|
|
54
|
+
Attempts, in a case-insensitive manner, to interpret a string into
|
|
55
|
+
a named member of the class, i.e. a recognised enumerator; returns
|
|
56
|
+
`None` if not recogised.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
|
|
61
|
+
return MissedTickBehaviour[str(s).upper()]
|
|
62
|
+
except KeyError:
|
|
63
|
+
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
|
|
68
|
+
return f"<{self.__module__}.{self.__class__.__name__}.{self._to_str()}>"
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
|
|
72
|
+
return enum.Enum.__str__(self)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
MissedTickBehavior = MissedTickBehaviour
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Interval:
|
|
79
|
+
"""
|
|
80
|
+
Supports wait operations with a certain periodicity and behaviour for
|
|
81
|
+
late ticking.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
__slots__ = (
|
|
85
|
+
# invariant fields
|
|
86
|
+
'_period_ns',
|
|
87
|
+
'_missed_tick_behaviour',
|
|
88
|
+
'_name',
|
|
89
|
+
'_negative_bias',
|
|
90
|
+
'_reference_instant',
|
|
91
|
+
# variant fields
|
|
92
|
+
'_recent_instant',
|
|
93
|
+
'_event_count',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
period : Duration | int,
|
|
99
|
+
missed_tick_behaviour : MissedTickBehaviour=MissedTickBehaviour.SKIP,
|
|
100
|
+
name=None,
|
|
101
|
+
negative_bias=None,
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Creates an instance, based on the given parameters.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
assert isinstance(period, (Duration, int)), "`period` must be instance of `Duration` or `int` (which specifies nanoseconds)"
|
|
108
|
+
|
|
109
|
+
assert negative_bias is None or negative_bias < int(period), "invalid `negative_bias` ({negative_bias}) given the `period` ({period)}"
|
|
110
|
+
|
|
111
|
+
self._period_ns = int(period)
|
|
112
|
+
self._missed_tick_behaviour = missed_tick_behaviour
|
|
113
|
+
self._name = str(name) if name else ''
|
|
114
|
+
self._negative_bias = negative_bias if isinstance(negative_bias, int) else 400_000 if self._period_ns > 100_000_000 else 0
|
|
115
|
+
self._reference_instant = Instant.now()
|
|
116
|
+
|
|
117
|
+
self._recent_instant = None
|
|
118
|
+
self._event_count = 0
|
|
119
|
+
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
|
|
122
|
+
return "" \
|
|
123
|
+
f"<{self.__module__}.{self.__class__.__name__}: " \
|
|
124
|
+
f"_period_ns: {self._period_ns:,}; " \
|
|
125
|
+
f"_missed_tick_behaviour: {self._missed_tick_behaviour:}; " \
|
|
126
|
+
f"_name: {self._name}; " \
|
|
127
|
+
f"_negative_bias: {self._negative_bias}; " \
|
|
128
|
+
f"_reference_instant: {self._reference_instant:}; " \
|
|
129
|
+
f"_recent_instant: {self._recent_instant:}; " \
|
|
130
|
+
f"_event_count: {self._event_count:,}; " \
|
|
131
|
+
">"
|
|
132
|
+
|
|
133
|
+
def __await__(self):
|
|
134
|
+
"""
|
|
135
|
+
.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
self._event_count += 1
|
|
139
|
+
|
|
140
|
+
now = Instant.now()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if self._missed_tick_behaviour == MissedTickBehaviour.DELAY:
|
|
144
|
+
|
|
145
|
+
# simply wait for given duration
|
|
146
|
+
|
|
147
|
+
self._recent_instant = now
|
|
148
|
+
|
|
149
|
+
return asyncio.sleep((self._period_ns - self._negative_bias) / 1_000_000_000).__await__()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
duration : Duration = now - self._reference_instant
|
|
153
|
+
duration_ns = duration.as_nanos()
|
|
154
|
+
|
|
155
|
+
# calculate number of intervals (`q`) and remainder (`r`)
|
|
156
|
+
|
|
157
|
+
q, r = divmod(duration_ns, self._period_ns)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if self._missed_tick_behaviour == MissedTickBehavior.BURST:
|
|
161
|
+
|
|
162
|
+
# either ...
|
|
163
|
+
|
|
164
|
+
if self._event_count <= q:
|
|
165
|
+
|
|
166
|
+
# ... respond immediately if more than a full interval, or ...
|
|
167
|
+
|
|
168
|
+
self._recent_instant = now
|
|
169
|
+
|
|
170
|
+
return asyncio.sleep(0).__await__()
|
|
171
|
+
|
|
172
|
+
if self._event_count + 1 == q:
|
|
173
|
+
|
|
174
|
+
# ... wait for the last bit of the interval.
|
|
175
|
+
|
|
176
|
+
self._recent_instant = now
|
|
177
|
+
|
|
178
|
+
return asyncio.sleep(r / 1_000_000_000).__await__()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# `SKIP` behaviour:
|
|
182
|
+
#
|
|
183
|
+
# T.B.C.
|
|
184
|
+
|
|
185
|
+
# get the time _now_ and then calculate how much time has
|
|
186
|
+
# elapsed, ...
|
|
187
|
+
|
|
188
|
+
# ... and from this calculate how long the next wait should be
|
|
189
|
+
|
|
190
|
+
p_ns = self._period_ns - r
|
|
191
|
+
if p_ns > self._negative_bias:
|
|
192
|
+
|
|
193
|
+
p_ns -= self._negative_bias
|
|
194
|
+
|
|
195
|
+
self._recent_instant = now
|
|
196
|
+
|
|
197
|
+
return asyncio.sleep(p_ns / 1_000_000_000).__await__()
|
|
198
|
+
|
|
199
|
+
def event_count(self) -> int:
|
|
200
|
+
"""
|
|
201
|
+
The number of times that the interval has been awaited.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
return self._event_count
|
|
205
|
+
|
|
206
|
+
def missed_tick_behaviour(self) -> str:
|
|
207
|
+
"""
|
|
208
|
+
The interval's missed-tick behaviour.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
return self._missed_tick_behaviour
|
|
212
|
+
|
|
213
|
+
def missed_tick_behavior(self) -> str:
|
|
214
|
+
"""
|
|
215
|
+
The interval's missed-tick behaviour.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
return self.missed_tick_behaviour()
|
|
219
|
+
|
|
220
|
+
def name(self) -> str:
|
|
221
|
+
"""
|
|
222
|
+
The interval's name.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
return self._name
|
|
226
|
+
|
|
227
|
+
def negative_bias(self) -> Duration:
|
|
228
|
+
"""
|
|
229
|
+
The interval's negative bias.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
return Duration.from_nanos(self._negative_bias)
|
|
233
|
+
|
|
234
|
+
def period(self) -> Duration:
|
|
235
|
+
"""
|
|
236
|
+
The interval's period.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
return Duration.from_nanos(self._period_ns)
|
|
240
|
+
|
|
241
|
+
def recent_instant(self) -> Instant:
|
|
242
|
+
"""
|
|
243
|
+
The instance's most recent await instant.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
return self._recent_instant
|
|
247
|
+
|
|
248
|
+
def reference_instant(self) -> Instant:
|
|
249
|
+
"""
|
|
250
|
+
The instance's reference instant.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
return self._reference_instant
|
asynkio/time/types.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import (
|
|
3
|
+
datetime,
|
|
4
|
+
timezone,
|
|
5
|
+
)
|
|
6
|
+
import time
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Duration:
|
|
11
|
+
"""
|
|
12
|
+
Represents a span of time.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__slots__ = (
|
|
16
|
+
'_duration',
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
from_ns,
|
|
22
|
+
to_ns,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Creates a new instance from the number of nanoseconds represented by
|
|
26
|
+
`to_ns - from_ns`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
self._duration = to_ns - from_ns
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_nanos(t_ns : int) -> Self:
|
|
33
|
+
"""
|
|
34
|
+
Creates a new instance from the specified number of nanoseconds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
return Duration(0, t_ns)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def from_micros(t_us : int) -> Self:
|
|
41
|
+
"""
|
|
42
|
+
Creates a new instance from the specified number of microseconds.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return Duration(0, t_us * 1_000)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def from_millis(t_ms : int) -> Self:
|
|
49
|
+
"""
|
|
50
|
+
Creates a new instance from the specified number of milliseconds.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
return Duration(0, t_ms * 1_000_000)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def from_secs(t_s : int) -> Self:
|
|
57
|
+
"""
|
|
58
|
+
Creates a new instance from the specified number of seconds.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
return Duration(0, t_s * 1_000_000_000)
|
|
62
|
+
|
|
63
|
+
def as_nanos(self) -> int:
|
|
64
|
+
"""
|
|
65
|
+
Total number of whole nanoseconds contained by this instance.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
return self._duration
|
|
69
|
+
|
|
70
|
+
def as_micros(self) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Total number of whole microseconds contained by this instance.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
return self._duration // 1_000
|
|
76
|
+
|
|
77
|
+
def as_millis(self) -> int:
|
|
78
|
+
"""
|
|
79
|
+
Total number of whole milliseconds contained by this instance.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
return self._duration // 1_000_000
|
|
83
|
+
|
|
84
|
+
def as_secs(self) -> int:
|
|
85
|
+
"""
|
|
86
|
+
Total number of whole seconds contained by this instance.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return self._duration // 1_000_000_000
|
|
90
|
+
|
|
91
|
+
def as_secs_f(self) -> float:
|
|
92
|
+
"""
|
|
93
|
+
The fractional number of seconds contained by this instance.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
return self._duration / 1_000_000_000.0
|
|
97
|
+
|
|
98
|
+
def subsec_nanos(self) -> int:
|
|
99
|
+
"""
|
|
100
|
+
The fractional part of this instance, in nanoseconds.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
return self._duration % 1_000_000_000
|
|
104
|
+
|
|
105
|
+
def subsec_micros(self) -> int:
|
|
106
|
+
"""
|
|
107
|
+
The fractional part of this instance, in microseconds.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
return (self._duration % 1_000_000_000) // 1_000
|
|
111
|
+
|
|
112
|
+
def subsec_millis(self) -> int:
|
|
113
|
+
"""
|
|
114
|
+
The fractional part of this instance, in milliseconds.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
return (self._duration % 1_000_000_000) // 1_000_000
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _scale_index(n : int) -> tuple[int, int]:
|
|
121
|
+
|
|
122
|
+
_SCALES = [
|
|
123
|
+
1,
|
|
124
|
+
10,
|
|
125
|
+
100,
|
|
126
|
+
1_000,
|
|
127
|
+
10_000,
|
|
128
|
+
100_000,
|
|
129
|
+
1_000_000,
|
|
130
|
+
10_000_000,
|
|
131
|
+
100_000_000,
|
|
132
|
+
1_000_000_000,
|
|
133
|
+
10_000_000_000,
|
|
134
|
+
100_000_000_000,
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
assert n > 0
|
|
138
|
+
assert len(_SCALES) == 12
|
|
139
|
+
|
|
140
|
+
if n >= 100_000_000_000:
|
|
141
|
+
|
|
142
|
+
return (11, _SCALES[11])
|
|
143
|
+
|
|
144
|
+
l = 0
|
|
145
|
+
h = 11
|
|
146
|
+
|
|
147
|
+
count = 0
|
|
148
|
+
|
|
149
|
+
while l <= h:
|
|
150
|
+
|
|
151
|
+
count += 1
|
|
152
|
+
|
|
153
|
+
assert count < 5, f"too many loops while trying to scale {n}"
|
|
154
|
+
|
|
155
|
+
m = (h + l) // 2
|
|
156
|
+
|
|
157
|
+
b = _SCALES[m]
|
|
158
|
+
|
|
159
|
+
if n == b:
|
|
160
|
+
|
|
161
|
+
return (m, b)
|
|
162
|
+
|
|
163
|
+
if n < b:
|
|
164
|
+
|
|
165
|
+
h = m
|
|
166
|
+
|
|
167
|
+
continue
|
|
168
|
+
else:
|
|
169
|
+
assert n > b
|
|
170
|
+
|
|
171
|
+
if n < b * 10:
|
|
172
|
+
|
|
173
|
+
return (m, b)
|
|
174
|
+
else:
|
|
175
|
+
|
|
176
|
+
l = m
|
|
177
|
+
|
|
178
|
+
return (11, _SCALES[11])
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def duration_to_string(
|
|
182
|
+
duration : Self | int,
|
|
183
|
+
format_spec : str = '',
|
|
184
|
+
) -> str:
|
|
185
|
+
|
|
186
|
+
v = int(duration)
|
|
187
|
+
|
|
188
|
+
if v < 0:
|
|
189
|
+
|
|
190
|
+
v = -v
|
|
191
|
+
sign = '-'
|
|
192
|
+
else:
|
|
193
|
+
|
|
194
|
+
if '+' in format_spec:
|
|
195
|
+
|
|
196
|
+
sign = '+'
|
|
197
|
+
else:
|
|
198
|
+
|
|
199
|
+
sign = ''
|
|
200
|
+
|
|
201
|
+
if v == 0:
|
|
202
|
+
|
|
203
|
+
return "0s"
|
|
204
|
+
|
|
205
|
+
oom, divisor = Duration._scale_index(v)
|
|
206
|
+
|
|
207
|
+
suffixes = [
|
|
208
|
+
'ns',
|
|
209
|
+
'µs',
|
|
210
|
+
'ms',
|
|
211
|
+
's',
|
|
212
|
+
]
|
|
213
|
+
suffix = suffixes[oom // 3]
|
|
214
|
+
|
|
215
|
+
def fmt(
|
|
216
|
+
sign,
|
|
217
|
+
whole,
|
|
218
|
+
frac,
|
|
219
|
+
suffix,
|
|
220
|
+
):
|
|
221
|
+
|
|
222
|
+
if frac == 0:
|
|
223
|
+
|
|
224
|
+
return f"{sign}{whole}{suffix}"
|
|
225
|
+
else:
|
|
226
|
+
|
|
227
|
+
if whole > 999:
|
|
228
|
+
|
|
229
|
+
return f"{sign}{whole}{suffix}"
|
|
230
|
+
|
|
231
|
+
if whole > 99:
|
|
232
|
+
|
|
233
|
+
return f"{sign}{whole}.{frac}{suffix}"
|
|
234
|
+
|
|
235
|
+
if whole > 9:
|
|
236
|
+
|
|
237
|
+
return f"{sign}{whole}.{frac:02d}{suffix}"
|
|
238
|
+
|
|
239
|
+
return f"{sign}{whole}.{frac}{suffix}"
|
|
240
|
+
|
|
241
|
+
if oom < 3:
|
|
242
|
+
|
|
243
|
+
return f"{sign}{v}{suffix}"
|
|
244
|
+
else:
|
|
245
|
+
|
|
246
|
+
divisor_0 = divisor // 1_000
|
|
247
|
+
|
|
248
|
+
i = oom % 3
|
|
249
|
+
|
|
250
|
+
if i == 0:
|
|
251
|
+
|
|
252
|
+
divisor_1 = 1_000
|
|
253
|
+
elif i == 1:
|
|
254
|
+
|
|
255
|
+
divisor_1 = 100
|
|
256
|
+
else:
|
|
257
|
+
|
|
258
|
+
divisor_1 = 10
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
v //= divisor_0
|
|
262
|
+
|
|
263
|
+
whole = v // divisor_1
|
|
264
|
+
frac = v - (whole * divisor_1)
|
|
265
|
+
|
|
266
|
+
return fmt(
|
|
267
|
+
sign,
|
|
268
|
+
whole,
|
|
269
|
+
frac,
|
|
270
|
+
suffix,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def __eq__(self, rhs : Self | float | int) -> bool:
|
|
274
|
+
|
|
275
|
+
if isinstance(rhs, Duration):
|
|
276
|
+
|
|
277
|
+
return self._duration == rhs._duration
|
|
278
|
+
|
|
279
|
+
if isinstance(rhs, (float, int)):
|
|
280
|
+
|
|
281
|
+
return self._duration == int(rhs)
|
|
282
|
+
|
|
283
|
+
return NotImplemented
|
|
284
|
+
|
|
285
|
+
def __format__(self, format_spec) -> str:
|
|
286
|
+
|
|
287
|
+
return self.duration_to_string(
|
|
288
|
+
self._duration,
|
|
289
|
+
format_spec=format_spec,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def __str__(self) -> str:
|
|
293
|
+
|
|
294
|
+
return self.duration_to_string(
|
|
295
|
+
self._duration,
|
|
296
|
+
format_spec='',
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def __repr__(self) -> str:
|
|
300
|
+
|
|
301
|
+
return f"<{self.__module__}.{self.__class__.__name__}: _duration={self._duration}>"
|
|
302
|
+
|
|
303
|
+
def __int__(self) -> int:
|
|
304
|
+
"""
|
|
305
|
+
Converts an instance into an integer, representing the number of
|
|
306
|
+
nanoseconds in the duration.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
return self._duration
|
|
310
|
+
|
|
311
|
+
def __add__(self, rhs : Self) -> Self:
|
|
312
|
+
|
|
313
|
+
if isinstance(rhs, Duration):
|
|
314
|
+
|
|
315
|
+
return Duration.from_nanos(self._duration + rhs._duration)
|
|
316
|
+
|
|
317
|
+
return NotImplemented
|
|
318
|
+
|
|
319
|
+
def __sub__(self, rhs : Self) -> Self:
|
|
320
|
+
|
|
321
|
+
if isinstance(rhs, Duration):
|
|
322
|
+
|
|
323
|
+
return Duration.from_nanos(self._duration - rhs._duration)
|
|
324
|
+
|
|
325
|
+
return NotImplemented
|
|
326
|
+
|
|
327
|
+
def __mul__(self, rhs : float | int) -> Self:
|
|
328
|
+
|
|
329
|
+
if isinstance(rhs, float):
|
|
330
|
+
|
|
331
|
+
return Duration.from_nanos(int(self._duration * rhs))
|
|
332
|
+
|
|
333
|
+
if isinstance(rhs, int):
|
|
334
|
+
|
|
335
|
+
return Duration.from_nanos(self._duration * rhs)
|
|
336
|
+
|
|
337
|
+
return NotImplemented
|
|
338
|
+
|
|
339
|
+
def __truediv__(self, rhs : Self | float | int) -> Self:
|
|
340
|
+
|
|
341
|
+
if isinstance(rhs, (Duration, int)):
|
|
342
|
+
|
|
343
|
+
return Duration.from_nanos(self._duration / int(rhs))
|
|
344
|
+
|
|
345
|
+
if isinstance(rhs, float):
|
|
346
|
+
|
|
347
|
+
return Duration.from_nanos(int(self._duration / rhs))
|
|
348
|
+
|
|
349
|
+
return NotImplemented
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class Instant:
|
|
353
|
+
"""
|
|
354
|
+
Represents a moment in time.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
__slots__ = (
|
|
358
|
+
'_t',
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def __init__(self, t_ns):
|
|
362
|
+
"""
|
|
363
|
+
Initialises with the given time instant (specified in nanoseconds).
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
self._t = t_ns
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _t_ns_to_d_utc(t_ns : int) -> datetime:
|
|
370
|
+
"""
|
|
371
|
+
Convert the number of nanoseconds since epoch into a datetime
|
|
372
|
+
instance assuming UTC.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
return datetime.fromtimestamp(t_ns / 1_000_000_000.0, tz=timezone.utc)
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def now() -> Self:
|
|
379
|
+
"""
|
|
380
|
+
Initialises with the current time instant.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
t_now_ns = time.time_ns()
|
|
384
|
+
|
|
385
|
+
return Instant(t_now_ns)
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def instant_to_string(
|
|
389
|
+
instant : Self | int,
|
|
390
|
+
format_spec : str = '',
|
|
391
|
+
) -> str:
|
|
392
|
+
|
|
393
|
+
typed = None
|
|
394
|
+
|
|
395
|
+
def set_type_or_raise(
|
|
396
|
+
typed,
|
|
397
|
+
t,
|
|
398
|
+
c,
|
|
399
|
+
b,
|
|
400
|
+
):
|
|
401
|
+
|
|
402
|
+
# local as_type
|
|
403
|
+
|
|
404
|
+
if typed is not None:
|
|
405
|
+
|
|
406
|
+
raise ValueError(f"cannot specify type `{t}` as type already specified as `{typed[0]}`")
|
|
407
|
+
|
|
408
|
+
else:
|
|
409
|
+
|
|
410
|
+
return (t, c, b)
|
|
411
|
+
|
|
412
|
+
show_plus = False
|
|
413
|
+
show_base = False
|
|
414
|
+
|
|
415
|
+
for c in format_spec:
|
|
416
|
+
|
|
417
|
+
if c == 'd':
|
|
418
|
+
|
|
419
|
+
typed = set_type_or_raise(typed, int, c, '')
|
|
420
|
+
|
|
421
|
+
if c == 'o':
|
|
422
|
+
|
|
423
|
+
typed = set_type_or_raise(typed, int, c, '0o')
|
|
424
|
+
|
|
425
|
+
if c == 'x':
|
|
426
|
+
|
|
427
|
+
typed = set_type_or_raise(typed, int, c, '0x')
|
|
428
|
+
|
|
429
|
+
if c == 'X':
|
|
430
|
+
|
|
431
|
+
typed = set_type_or_raise(typed, int, c, '0X')
|
|
432
|
+
|
|
433
|
+
if c == '+':
|
|
434
|
+
|
|
435
|
+
show_plus = True
|
|
436
|
+
|
|
437
|
+
if c == '#':
|
|
438
|
+
|
|
439
|
+
show_base = True
|
|
440
|
+
|
|
441
|
+
if typed:
|
|
442
|
+
|
|
443
|
+
if typed[0] == int:
|
|
444
|
+
|
|
445
|
+
fmt = ''
|
|
446
|
+
|
|
447
|
+
if show_plus:
|
|
448
|
+
|
|
449
|
+
fmt += '+'
|
|
450
|
+
|
|
451
|
+
if show_base:
|
|
452
|
+
|
|
453
|
+
fmt += '#'
|
|
454
|
+
|
|
455
|
+
fmt += typed[1]
|
|
456
|
+
|
|
457
|
+
return format(instant, fmt)
|
|
458
|
+
else:
|
|
459
|
+
|
|
460
|
+
return NotImplemented
|
|
461
|
+
else:
|
|
462
|
+
|
|
463
|
+
d = Instant._t_ns_to_d_utc(instant)
|
|
464
|
+
|
|
465
|
+
return d.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
466
|
+
|
|
467
|
+
def __format__(self, format_spec) -> str:
|
|
468
|
+
|
|
469
|
+
return Instant.instant_to_string(
|
|
470
|
+
self._t,
|
|
471
|
+
format_spec=format_spec,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def __str__(self) -> str:
|
|
475
|
+
|
|
476
|
+
return Instant.instant_to_string(
|
|
477
|
+
self._t,
|
|
478
|
+
format_spec='',
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def __repr__(self) -> str:
|
|
482
|
+
|
|
483
|
+
return f"<{self.__module__}.{self.__class__.__name__}: _t={self._t}>"
|
|
484
|
+
|
|
485
|
+
def __int__(self) -> int:
|
|
486
|
+
"""
|
|
487
|
+
Converts an instance into an integer, representing the number of
|
|
488
|
+
nanoseconds since the epoch.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
return self._t
|
|
492
|
+
|
|
493
|
+
def __lt__(self, rhs) -> bool:
|
|
494
|
+
"""
|
|
495
|
+
Determines whether the called instance is less-than the `rhs`
|
|
496
|
+
instance of `Instant`.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
if isinstance(rhs, Instant):
|
|
500
|
+
|
|
501
|
+
return self._t < rhs._t
|
|
502
|
+
|
|
503
|
+
raise NotImplemented
|
|
504
|
+
|
|
505
|
+
def __le__(self, rhs) -> bool:
|
|
506
|
+
"""
|
|
507
|
+
Determines whether the called instance is less-than-or-equal-to the
|
|
508
|
+
`rhs` instance of `Instant`.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
if isinstance(rhs, Instant):
|
|
512
|
+
|
|
513
|
+
return self._t <= rhs._t
|
|
514
|
+
|
|
515
|
+
raise NotImplemented
|
|
516
|
+
|
|
517
|
+
def __sub__(self, rhs) -> Duration | Self:
|
|
518
|
+
"""
|
|
519
|
+
Subtracts the `rhs` - which may be an instance of `Instant` or an
|
|
520
|
+
instance of `Duration` - from the called instance.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
if isinstance(rhs, Instant):
|
|
524
|
+
|
|
525
|
+
return Duration(rhs._t, self._t)
|
|
526
|
+
|
|
527
|
+
if isinstance(rhs, Duration):
|
|
528
|
+
|
|
529
|
+
return Instant(self._t - rhs.as_nanos())
|
|
530
|
+
|
|
531
|
+
return NotImplemented
|
|
532
|
+
|
|
533
|
+
def __add__(self, rhs) -> Self:
|
|
534
|
+
"""
|
|
535
|
+
Adds the `rhs` instance of `Duration` to the called instance.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
if isinstance(rhs, Duration):
|
|
539
|
+
|
|
540
|
+
return Instant(self._t + rhs.as_nanos())
|
|
541
|
+
|
|
542
|
+
return NotImplemented
|
|
543
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asynkio
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Tokio-like functionality for Python
|
|
5
|
+
Author-email: Matt Wilson <matthew@synesis.com.au>
|
|
6
|
+
Project-URL: Changelog, https://github.com/synesissoftware/asynkio/releases
|
|
7
|
+
Project-URL: Homepage, https://github.com/synesissoftware/asynkio
|
|
8
|
+
Project-URL: Issues, https://github.com/synesissoftware/asynkio/issues
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: aiofiles; extra == "dev"
|
|
24
|
+
Requires-Dist: diagnosticism; extra == "dev"
|
|
25
|
+
Requires-Dist: pyclasp>=0.8.10; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest>=8.4.1; extra == "dev"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: aiofiles; extra == "test"
|
|
29
|
+
Requires-Dist: diagnosticism; extra == "test"
|
|
30
|
+
Requires-Dist: pyclasp>=0.8.10; extra == "test"
|
|
31
|
+
Requires-Dist: pytest>=8.4.1; extra == "test"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# asynkio - README <!-- omit in toc -->
|
|
35
|
+
|
|
36
|
+
Tokio-like functionality for Python
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
40
|
+
[](https://pypi.org/project/asynkio/)
|
|
41
|
+
[](https://github.com/synesissoftware/asynkio/releases/latest)
|
|
42
|
+

|
|
43
|
+
[](https://github.com/synesissoftware/asynkio/actions/workflows/python-package.yml)
|
|
44
|
+
[](https://pypi.org/project/asynkio/)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Table of contents <!-- omit in toc -->
|
|
48
|
+
|
|
49
|
+
- [Introduction](#introduction)
|
|
50
|
+
- [Installation \& Usage](#installation--usage)
|
|
51
|
+
- [Components](#components)
|
|
52
|
+
- [Examples](#examples)
|
|
53
|
+
- [Project Information](#project-information)
|
|
54
|
+
- [Where to get help](#where-to-get-help)
|
|
55
|
+
- [Contribution guidelines](#contribution-guidelines)
|
|
56
|
+
- [Dependencies](#dependencies)
|
|
57
|
+
- [Efferent (fan-out)](#efferent-fan-out)
|
|
58
|
+
- [Development Dependencies](#development-dependencies)
|
|
59
|
+
- [Afferent (fan-in)](#afferent-fan-in)
|
|
60
|
+
- [Related projects](#related-projects)
|
|
61
|
+
- [License](#license)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Introduction
|
|
65
|
+
|
|
66
|
+
**asynkio** - a portmanteau of **asyncio** and **Tokio**, pronounced "ay sink ee-oh" - is a small, simple **Python** library that provides some of the features of the **Rust** **Tokio** library's features, such as `Duration`, `Instant`, and `Interval`.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Installation & Usage
|
|
70
|
+
|
|
71
|
+
Install via **pip** or **pip3**, as in:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
$ pip3 install asynkio
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use via **import**:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from asynkio import (
|
|
81
|
+
Duration,
|
|
82
|
+
Instant,
|
|
83
|
+
Interval,
|
|
84
|
+
MissedTickBehaviour,
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
or:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from asynkio.time import (
|
|
92
|
+
Duration,
|
|
93
|
+
Instant,
|
|
94
|
+
Interval,
|
|
95
|
+
MissedTickBehaviour,
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`MissedTickBehavior` (US spelling) is also exported as an alias for
|
|
100
|
+
`MissedTickBehaviour`.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Components
|
|
104
|
+
|
|
105
|
+
| Symbol | Description |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `Duration` | Elapsed time, in nanoseconds (Tokio-like) |
|
|
108
|
+
| `Instant` | Point in time, as nanoseconds since the epoch |
|
|
109
|
+
| `Interval` | Async periodic timer with missed-tick policy |
|
|
110
|
+
| `MissedTickBehaviour` | Missed-tick policy (`BURST`, `DELAY`, `SKIP`) |
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
Examples are provided in the `examples/` directory. See
|
|
116
|
+
[EXAMPLES.md](./EXAMPLES.md) for a summary of each script.
|
|
117
|
+
|
|
118
|
+
Run a script with, for example:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
$ uv sync
|
|
122
|
+
$ uv run python examples/interval_skip.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Project Information
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
### Where to get help
|
|
130
|
+
|
|
131
|
+
[GitHub Page](https://github.com/synesissoftware/asynkio "GitHub Page")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
### Contribution guidelines
|
|
135
|
+
|
|
136
|
+
Defect reports, feature requests, and pull requests are welcome on https://github.com/synesissoftware/asynkio.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
### Dependencies
|
|
140
|
+
|
|
141
|
+
**asynkio** has no runtime dependencies beyond the **Python** standard
|
|
142
|
+
library (`asyncio`).
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
#### Efferent (fan-out)
|
|
146
|
+
|
|
147
|
+
Libraries upon which **asynkio** depends:
|
|
148
|
+
|
|
149
|
+
None.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
##### Development Dependencies
|
|
153
|
+
|
|
154
|
+
* [**aiofiles**](https://pypi.org/project/aiofiles/) — development and
|
|
155
|
+
demonstration tooling;
|
|
156
|
+
* [**diagnosticism**](https://pypi.org/project/diagnosticism/) — example
|
|
157
|
+
scripts under `examples/`;
|
|
158
|
+
* [**pyclasp**](https://pypi.org/project/pyclasp/) — development and
|
|
159
|
+
demonstration tooling;
|
|
160
|
+
* [**pytest**](https://pypi.org/project/pytest/) — unit-test runner;
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
#### Afferent (fan-in)
|
|
164
|
+
|
|
165
|
+
Projects that depend on **asynkio**:
|
|
166
|
+
|
|
167
|
+
None (currently).
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
### Related projects
|
|
171
|
+
|
|
172
|
+
* [**Tokio**](https://github.com/tokio-rs/tokio) (**Rust**) — the library
|
|
173
|
+
whose time types **asynkio** emulates;
|
|
174
|
+
* [**Diagnosticism.Python**](https://github.com/synesissoftware/Diagnosticism.Python/)
|
|
175
|
+
— used in `examples/` scripts;
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### License
|
|
179
|
+
|
|
180
|
+
**asynkio** is released under the 3-clause BSD license. See [LICENSE](./LICENSE) for details.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
<!-- ########################### end of file ########################### -->
|
|
184
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
asynkio/__init__.py,sha256=vPNapu_A_-OlUNcCAdUsabjRAXBUbfto80e4n1THlwU,272
|
|
2
|
+
asynkio/time/__init__.py,sha256=P64pof2xFF-nfnjMBVdPzdzsuweJexq-GzPjcvd-RGs,256
|
|
3
|
+
asynkio/time/interval.py,sha256=uHq81-jgr_Sb1CnNnl-bs0Ukrurvcj8VIZqeTWEI_C4,6090
|
|
4
|
+
asynkio/time/types.py,sha256=054tfvyIbrydNjQ4rzl19va1DbTfJR5BioqRFarsxGo,11177
|
|
5
|
+
asynkio-0.0.3.dist-info/licenses/LICENSE,sha256=TWGuvCKcXKdUounnwoRzT57axIWWCEvUfZMYzsAcR7M,1566
|
|
6
|
+
asynkio-0.0.3.dist-info/METADATA,sha256=lc1HIfgRBk7i7s1TE6f4LP7ZidxT3Ix4OQVr99ivzCY,5423
|
|
7
|
+
asynkio-0.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
asynkio-0.0.3.dist-info/top_level.txt,sha256=MpyOQdFFxdfwkd7nu3LQGQzOzos1nLIzAqFIHsI1pgs,8
|
|
9
|
+
asynkio-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
asynkio - BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Matthew Wilson and Synesis Information Systems
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are
|
|
9
|
+
met:
|
|
10
|
+
|
|
11
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
+
this list of conditions and the following disclaimer in the documentation
|
|
16
|
+
and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
19
|
+
contributors may be used to endorse or promote products derived from
|
|
20
|
+
this software without specific prior written permission.
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
25
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
26
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
27
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
28
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
29
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
30
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
asynkio
|