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 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
+ ]
@@ -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
+ ]
@@ -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
+ ![Language](https://img.shields.io/badge/Python-3776AB?style=flat&logo=python&logoColor=white)
39
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
40
+ [![PyPI](https://img.shields.io/pypi/v/asynkio.svg)](https://pypi.org/project/asynkio/)
41
+ [![GitHub release](https://img.shields.io/github/v/release/synesissoftware/asynkio.svg)](https://github.com/synesissoftware/asynkio/releases/latest)
42
+ ![Python](https://img.shields.io/badge/Python-3.11+-lightgrey)
43
+ [![CI](https://github.com/synesissoftware/asynkio/actions/workflows/python-package.yml/badge.svg)](https://github.com/synesissoftware/asynkio/actions/workflows/python-package.yml)
44
+ [![PyPI project](https://img.shields.io/badge/documentation-PyPI-lightgrey)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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