python-utils 2.5.6__py3-none-any.whl → 4.0.0__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.
python_utils/time.py CHANGED
@@ -1,14 +1,45 @@
1
- import six
1
+ """
2
+ This module provides utility functions for handling time-related operations.
3
+
4
+ Functions::
5
+
6
+ timedelta_to_seconds: Convert a timedelta to seconds (microseconds as
7
+ fraction).
8
+ delta_to_seconds: Convert a timedelta or numeric interval to seconds.
9
+ delta_to_seconds_or_none: Convert a timedelta to seconds or return None.
10
+ format_time: Format a timestamp (timedelta, datetime, or seconds).
11
+ timeout_generator: Generate items from an iterable until a timeout.
12
+ aio_timeout_generator: Async generate items from an iterable until a
13
+ timeout.
14
+ aio_generator_timeout_detector: Detect if an async generator has stalled.
15
+ aio_generator_timeout_detector_decorator: Decorator for the detector.
16
+ """
17
+
18
+ # pyright: reportUnnecessaryIsInstance=false
19
+ import collections.abc
2
20
  import datetime
21
+ import functools
22
+ import itertools
23
+ import time
24
+ import typing
25
+
26
+ import python_utils
27
+ from python_utils import _aliases, exceptions
28
+
29
+ #: Item type produced by the time/timeout generators.
30
+ _T = typing.TypeVar('_T')
31
+ #: Parameter specification for the timeout-detector decorator's target.
32
+ _P = typing.ParamSpec('_P')
3
33
 
4
34
 
5
- # There might be a better way to get the epoch with tzinfo, please create
6
- # a pull request if you know a better way that functions for Python 2 and 3
35
+ #: The Unix epoch (1970-01-01) as a naive ``datetime``, used as a reference.
36
+ # There might be a better way to get the epoch with tzinfo; please open a
37
+ # pull request if you know one.
7
38
  epoch = datetime.datetime(year=1970, month=1, day=1)
8
39
 
9
40
 
10
- def timedelta_to_seconds(delta):
11
- '''Convert a timedelta to seconds with the microseconds as fraction
41
+ def timedelta_to_seconds(delta: datetime.timedelta) -> _aliases.Number:
42
+ """Convert a timedelta to seconds with the microseconds as fraction.
12
43
 
13
44
  Note that this method has become largely obsolete with the
14
45
  `timedelta.total_seconds()` method introduced in Python 2.7.
@@ -22,7 +53,7 @@ def timedelta_to_seconds(delta):
22
53
  '1.000001'
23
54
  >>> '%.6f' % timedelta_to_seconds(timedelta(microseconds=1))
24
55
  '0.000001'
25
- '''
56
+ """
26
57
  # Only convert to float if needed
27
58
  if delta.microseconds:
28
59
  total = delta.microseconds * 1e-6
@@ -33,8 +64,56 @@ def timedelta_to_seconds(delta):
33
64
  return total
34
65
 
35
66
 
36
- def format_time(timestamp, precision=datetime.timedelta(seconds=1)):
37
- '''Formats timedelta/datetime/seconds
67
+ def delta_to_seconds(interval: _aliases.delta_type) -> _aliases.Number:
68
+ """
69
+ Convert a timedelta to seconds.
70
+
71
+ >>> delta_to_seconds(datetime.timedelta(seconds=1))
72
+ 1
73
+ >>> delta_to_seconds(datetime.timedelta(seconds=1, microseconds=1))
74
+ 1.000001
75
+ >>> delta_to_seconds(1)
76
+ 1
77
+ >>> delta_to_seconds('whatever') # doctest: +ELLIPSIS
78
+ Traceback (most recent call last):
79
+ ...
80
+ TypeError: Unknown type ...
81
+ """
82
+ if isinstance(interval, datetime.timedelta):
83
+ return timedelta_to_seconds(interval)
84
+ elif isinstance(interval, (int, float)):
85
+ return interval
86
+ else:
87
+ raise TypeError(f'Unknown type {type(interval)}: {interval!r}')
88
+
89
+
90
+ def delta_to_seconds_or_none(
91
+ interval: _aliases.delta_type | None,
92
+ ) -> _aliases.Number | None:
93
+ """Convert a timedelta to seconds, passing ``None`` through unchanged.
94
+
95
+ Args:
96
+ interval: A timedelta or a number of seconds, or ``None``.
97
+
98
+ Returns:
99
+ The interval in seconds, or ``None`` when ``interval`` is ``None``.
100
+
101
+ >>> delta_to_seconds_or_none(datetime.timedelta(seconds=2))
102
+ 2
103
+ >>> delta_to_seconds_or_none(None) is None
104
+ True
105
+ """
106
+ if interval is None:
107
+ return None
108
+ else:
109
+ return delta_to_seconds(interval)
110
+
111
+
112
+ def format_time(
113
+ timestamp: _aliases.timestamp_type,
114
+ precision: datetime.timedelta = datetime.timedelta(seconds=1),
115
+ ) -> str:
116
+ """Formats timedelta/datetime/seconds.
38
117
 
39
118
  >>> format_time('1')
40
119
  '0:00:01'
@@ -55,13 +134,15 @@ def format_time(timestamp, precision=datetime.timedelta(seconds=1)):
55
134
  ...
56
135
  TypeError: Unknown type ...
57
136
 
58
- '''
137
+ """
59
138
  precision_seconds = precision.total_seconds()
60
139
 
61
- if isinstance(timestamp, six.string_types + six.integer_types + (float, )):
140
+ if isinstance(timestamp, str):
141
+ timestamp = float(timestamp)
142
+
143
+ if isinstance(timestamp, (int, float)):
62
144
  try:
63
- castfunc = six.integer_types[-1]
64
- timestamp = datetime.timedelta(seconds=castfunc(timestamp))
145
+ timestamp = datetime.timedelta(seconds=timestamp)
65
146
  except OverflowError: # pragma: no cover
66
147
  timestamp = None
67
148
 
@@ -82,11 +163,8 @@ def format_time(timestamp, precision=datetime.timedelta(seconds=1)):
82
163
  seconds = seconds - (seconds % precision_seconds)
83
164
 
84
165
  try: # pragma: no cover
85
- if six.PY3:
86
- dt = datetime.datetime.fromtimestamp(seconds)
87
- else:
88
- dt = datetime.datetime.utcfromtimestamp(seconds)
89
- except ValueError: # pragma: no cover
166
+ dt = datetime.datetime.fromtimestamp(seconds)
167
+ except (ValueError, OSError): # pragma: no cover
90
168
  dt = datetime.datetime.max
91
169
  return str(dt)
92
170
  elif isinstance(timestamp, datetime.date):
@@ -94,4 +172,279 @@ def format_time(timestamp, precision=datetime.timedelta(seconds=1)):
94
172
  elif timestamp is None:
95
173
  return '--:--:--'
96
174
  else:
97
- raise TypeError('Unknown type %s: %r' % (type(timestamp), timestamp))
175
+ raise TypeError(f'Unknown type {type(timestamp)}: {timestamp!r}')
176
+
177
+
178
+ @typing.overload
179
+ def _to_iterable(
180
+ iterable: collections.abc.Callable[[], collections.abc.AsyncIterable[_T]]
181
+ | collections.abc.AsyncIterable[_T],
182
+ ) -> collections.abc.AsyncIterable[_T]:
183
+ """Async overload: async iterable or factory in, async iterable out."""
184
+
185
+
186
+ @typing.overload
187
+ def _to_iterable(
188
+ iterable: collections.abc.Callable[[], collections.abc.Iterable[_T]]
189
+ | collections.abc.Iterable[_T],
190
+ ) -> collections.abc.Iterable[_T]:
191
+ """Sync overload: sync iterable or factory in, sync iterable out."""
192
+
193
+
194
+ def _to_iterable(
195
+ iterable: collections.abc.Iterable[_T]
196
+ | collections.abc.Callable[[], collections.abc.Iterable[_T]]
197
+ | collections.abc.AsyncIterable[_T]
198
+ | collections.abc.Callable[[], collections.abc.AsyncIterable[_T]],
199
+ ) -> collections.abc.Iterable[_T] | collections.abc.AsyncIterable[_T]:
200
+ """Return ``iterable``, calling it first if it is a zero-arg callable."""
201
+ if callable(iterable):
202
+ return iterable()
203
+ else:
204
+ return iterable
205
+
206
+
207
+ def timeout_generator(
208
+ timeout: _aliases.delta_type,
209
+ interval: _aliases.delta_type = datetime.timedelta(seconds=1),
210
+ iterable: collections.abc.Iterable[_T]
211
+ | collections.abc.Callable[
212
+ [], collections.abc.Iterable[_T]
213
+ ] = itertools.count, # type: ignore[assignment]
214
+ interval_multiplier: float = 1.0,
215
+ maximum_interval: _aliases.delta_type | None = None,
216
+ ) -> collections.abc.Iterable[_T]:
217
+ """
218
+ Generator that walks through the given iterable (a counter by default)
219
+ until the float_timeout is reached with a configurable float_interval
220
+ between items.
221
+
222
+ This can be used to limit the time spent on a slow operation. This can be
223
+ useful for testing slow APIs so you get a small sample of the data in a
224
+ reasonable amount of time.
225
+
226
+ >>> for i in timeout_generator(0.1, 0.06):
227
+ ... # Put your slow code here
228
+ ... print(i)
229
+ 0
230
+ 1
231
+ 2
232
+ >>> timeout = datetime.timedelta(seconds=0.1)
233
+ >>> interval = datetime.timedelta(seconds=0.06)
234
+ >>> for i in timeout_generator(timeout, interval, itertools.count()):
235
+ ... print(i)
236
+ 0
237
+ 1
238
+ 2
239
+ >>> for i in timeout_generator(1, interval=0.1, iterable='ab'):
240
+ ... print(i)
241
+ a
242
+ b
243
+
244
+ >>> timeout = datetime.timedelta(seconds=0.1)
245
+ >>> interval = datetime.timedelta(seconds=0.06)
246
+ >>> for i in timeout_generator(timeout, interval, interval_multiplier=2):
247
+ ... print(i)
248
+ 0
249
+ 1
250
+ 2
251
+ """
252
+ float_interval: float = delta_to_seconds(interval)
253
+ float_maximum_interval: float | None = delta_to_seconds_or_none(
254
+ maximum_interval
255
+ )
256
+ iterable_ = _to_iterable(iterable)
257
+
258
+ end = delta_to_seconds(timeout) + time.perf_counter()
259
+ for item in iterable_:
260
+ yield item
261
+
262
+ if time.perf_counter() >= end:
263
+ break
264
+
265
+ time.sleep(float_interval)
266
+
267
+ float_interval *= interval_multiplier
268
+ if float_maximum_interval:
269
+ float_interval = min(float_interval, float_maximum_interval)
270
+
271
+
272
+ async def aio_timeout_generator(
273
+ timeout: _aliases.delta_type, # noqa: ASYNC109
274
+ interval: _aliases.delta_type = datetime.timedelta(seconds=1),
275
+ iterable: collections.abc.AsyncIterable[_T]
276
+ | collections.abc.Callable[..., collections.abc.AsyncIterable[_T]]
277
+ | None = None,
278
+ interval_multiplier: float = 1.0,
279
+ maximum_interval: _aliases.delta_type | None = None,
280
+ ) -> collections.abc.AsyncGenerator[_T, None]:
281
+ """
282
+ Async generator that walks through the given async iterable (a counter by
283
+ default) until the float_timeout is reached with a configurable
284
+ float_interval between items.
285
+
286
+ The interval_exponent automatically increases the float_timeout with each
287
+ run. Note that if the float_interval is less than 1, 1/interval_exponent
288
+ will be used so the float_interval is always growing. To double the
289
+ float_interval with each run, specify 2.
290
+
291
+ Doctests and asyncio are not friends, so no examples. But this function is
292
+ effectively the same as the `timeout_generator` but it uses `async for`
293
+ instead.
294
+ """
295
+ # Imported lazily so that importing `python_utils.time` for its
296
+ # synchronous helpers (e.g. ``format_time``) does not pull in ``asyncio``.
297
+ import asyncio
298
+
299
+ from python_utils import aio
300
+
301
+ if iterable is None:
302
+ iterable = typing.cast(
303
+ collections.abc.Callable[[], collections.abc.AsyncIterable[_T]],
304
+ aio.acount,
305
+ )
306
+
307
+ float_interval: float = delta_to_seconds(interval)
308
+ float_maximum_interval: float | None = delta_to_seconds_or_none(
309
+ maximum_interval
310
+ )
311
+ iterable_ = _to_iterable(iterable)
312
+
313
+ end = delta_to_seconds(timeout) + time.perf_counter()
314
+ async for item in iterable_: # pragma: no branch
315
+ yield item
316
+
317
+ if time.perf_counter() >= end:
318
+ break
319
+
320
+ await asyncio.sleep(float_interval)
321
+
322
+ float_interval *= interval_multiplier
323
+ if float_maximum_interval: # pragma: no branch
324
+ float_interval = min(float_interval, float_maximum_interval)
325
+
326
+
327
+ async def aio_generator_timeout_detector(
328
+ generator: collections.abc.AsyncGenerator[_T, None],
329
+ timeout: _aliases.delta_type | None = None, # noqa: ASYNC109
330
+ total_timeout: _aliases.delta_type | None = None,
331
+ on_timeout: collections.abc.Callable[
332
+ [
333
+ collections.abc.AsyncGenerator[_T, None],
334
+ _aliases.delta_type | None,
335
+ _aliases.delta_type | None,
336
+ BaseException,
337
+ ],
338
+ typing.Any,
339
+ ]
340
+ | None = exceptions.reraise,
341
+ **on_timeout_kwargs: collections.abc.Mapping[str, typing.Any],
342
+ ) -> collections.abc.AsyncGenerator[_T, None]:
343
+ """
344
+ This function is used to detect if an asyncio generator has not yielded
345
+ an element for a set amount of time.
346
+
347
+ The `on_timeout` argument is called with the `generator`, `timeout`,
348
+ `total_timeout`, `exception` and the extra `**kwargs` to this function as
349
+ arguments.
350
+ If `on_timeout` is not specified, the exception is reraised.
351
+ If `on_timeout` is `None`, the exception is silently ignored and the
352
+ generator will finish as normal.
353
+ """
354
+ # Imported lazily so importing `python_utils.time` stays asyncio-free.
355
+ import asyncio
356
+
357
+ if total_timeout is None:
358
+ total_timeout_end = None
359
+ else:
360
+ total_timeout_end = time.perf_counter() + delta_to_seconds(
361
+ total_timeout
362
+ )
363
+
364
+ timeout_s = python_utils.delta_to_seconds_or_none(timeout)
365
+
366
+ while True:
367
+ try:
368
+ if total_timeout_end and time.perf_counter() >= total_timeout_end:
369
+ raise asyncio.TimeoutError( # noqa: TRY301
370
+ 'Total timeout reached'
371
+ )
372
+
373
+ if timeout_s:
374
+ yield await asyncio.wait_for(generator.__anext__(), timeout_s)
375
+ else:
376
+ yield await generator.__anext__()
377
+
378
+ except asyncio.TimeoutError as exception: # noqa: PERF203
379
+ if on_timeout is not None:
380
+ await on_timeout(
381
+ generator,
382
+ timeout,
383
+ total_timeout,
384
+ exception,
385
+ **on_timeout_kwargs,
386
+ )
387
+ break
388
+
389
+ except StopAsyncIteration:
390
+ break
391
+
392
+
393
+ def aio_generator_timeout_detector_decorator(
394
+ timeout: _aliases.delta_type | None = None,
395
+ total_timeout: _aliases.delta_type | None = None,
396
+ on_timeout: collections.abc.Callable[
397
+ [
398
+ collections.abc.AsyncGenerator[typing.Any, None],
399
+ _aliases.delta_type | None,
400
+ _aliases.delta_type | None,
401
+ BaseException,
402
+ ],
403
+ typing.Any,
404
+ ]
405
+ | None = exceptions.reraise,
406
+ **on_timeout_kwargs: collections.abc.Mapping[str, typing.Any],
407
+ ) -> collections.abc.Callable[
408
+ [collections.abc.Callable[_P, collections.abc.AsyncGenerator[_T, None]]],
409
+ collections.abc.Callable[_P, collections.abc.AsyncGenerator[_T, None]],
410
+ ]:
411
+ """Wrap a generator function with ``aio_generator_timeout_detector``.
412
+
413
+ Args:
414
+ timeout: Per-item timeout; if a single yield takes longer,
415
+ ``on_timeout`` fires. ``None`` disables the per-item check.
416
+ total_timeout: Overall timeout across the whole generator.
417
+ on_timeout: Callback invoked on a timeout; defaults to re-raising.
418
+ **on_timeout_kwargs: Extra keyword arguments passed to ``on_timeout``.
419
+
420
+ Returns:
421
+ A decorator that wraps an async-generator function so every call is
422
+ guarded against stalls.
423
+ """
424
+
425
+ def _timeout_detector_decorator(
426
+ generator: collections.abc.Callable[
427
+ _P, collections.abc.AsyncGenerator[_T, None]
428
+ ],
429
+ ) -> collections.abc.Callable[
430
+ _P, collections.abc.AsyncGenerator[_T, None]
431
+ ]:
432
+ """Wrap ``generator`` so each call is timeout-guarded."""
433
+
434
+ @functools.wraps(generator)
435
+ def wrapper(
436
+ *args: _P.args,
437
+ **kwargs: _P.kwargs,
438
+ ) -> collections.abc.AsyncGenerator[_T, None]:
439
+ """Forward the call to ``aio_generator_timeout_detector``."""
440
+ return aio_generator_timeout_detector(
441
+ generator(*args, **kwargs),
442
+ timeout,
443
+ total_timeout,
444
+ on_timeout,
445
+ **on_timeout_kwargs,
446
+ )
447
+
448
+ return wrapper
449
+
450
+ return _timeout_detector_decorator
python_utils/types.py ADDED
@@ -0,0 +1,179 @@
1
+ """
2
+ This module provides type definitions and utility functions for type hinting.
3
+
4
+ It includes:
5
+ - Shorthand for commonly used types such as Optional and Union.
6
+ - Type aliases for various data structures and common types.
7
+ - Importing all types from the `typing` and `typing_extensions` modules.
8
+ - Importing specific types from the `types` module.
9
+
10
+ The module also configures Pyright to ignore wildcard import warnings.
11
+ """
12
+ # pyright: reportWildcardImportFromLibrary=false
13
+ # ruff: noqa: F405
14
+
15
+ # Kept as public module attributes for backwards compatibility:
16
+ # `from python_utils.types import datetime, decimal` worked in 3.x.
17
+ import datetime as datetime
18
+ import decimal as decimal
19
+ from re import Match, Pattern
20
+ from types import * # pragma: no cover # noqa: F403
21
+ from typing import * # pragma: no cover # noqa: F403
22
+
23
+ # `import *` does not import these on all Python versions. `O` / `U` are kept
24
+ # as backwards-compatible shorthands; new code should prefer `X | None`.
25
+ from typing import (
26
+ IO,
27
+ BinaryIO,
28
+ Optional as O, # noqa: N817
29
+ TextIO,
30
+ Union as U, # noqa: N817
31
+ )
32
+
33
+ from typing_extensions import * # type: ignore[no-redef,assignment] # noqa: F403
34
+
35
+ # Lightweight aliases live in a stdlib-only module so importers don't pull in
36
+ # typing_extensions; re-exported here to keep the public surface unchanged.
37
+ # The redundant `X as X` form marks these as intentional re-exports so strict
38
+ # type checkers and ruff see `python_utils.types.X` without adding them to
39
+ # `__all__` (which would change `from python_utils.types import *`).
40
+ from ._aliases import (
41
+ DecimalNumber as DecimalNumber,
42
+ ExceptionType as ExceptionType,
43
+ ExceptionsType as ExceptionsType,
44
+ Number as Number,
45
+ OptionalScope as OptionalScope,
46
+ Scope as Scope,
47
+ StringTypes as StringTypes,
48
+ delta_type as delta_type,
49
+ timestamp_type as timestamp_type,
50
+ )
51
+
52
+ __all__ = [
53
+ 'IO',
54
+ 'TYPE_CHECKING',
55
+ # ABCs (from collections.abc).
56
+ 'AbstractSet',
57
+ # The types from the typing module.
58
+ # Super-special typing primitives.
59
+ 'Annotated',
60
+ 'Any',
61
+ # One-off things.
62
+ 'AnyStr',
63
+ 'AsyncContextManager',
64
+ 'AsyncGenerator',
65
+ 'AsyncGeneratorType',
66
+ 'AsyncIterable',
67
+ 'AsyncIterator',
68
+ 'Awaitable',
69
+ # Other concrete types.
70
+ 'BinaryIO',
71
+ 'BuiltinFunctionType',
72
+ 'BuiltinMethodType',
73
+ 'ByteString',
74
+ 'Callable',
75
+ # Concrete collection types.
76
+ 'ChainMap',
77
+ 'ClassMethodDescriptorType',
78
+ 'ClassVar',
79
+ 'CodeType',
80
+ 'Collection',
81
+ 'Concatenate',
82
+ 'Container',
83
+ 'ContextManager',
84
+ 'Coroutine',
85
+ 'CoroutineType',
86
+ 'Counter',
87
+ 'DecimalNumber',
88
+ 'DefaultDict',
89
+ 'Deque',
90
+ 'Dict',
91
+ 'DynamicClassAttribute',
92
+ 'Final',
93
+ 'ForwardRef',
94
+ 'FrameType',
95
+ 'FrozenSet',
96
+ # Types from the `types` module.
97
+ 'FunctionType',
98
+ 'Generator',
99
+ 'GeneratorType',
100
+ 'Generic',
101
+ 'GetSetDescriptorType',
102
+ 'Hashable',
103
+ 'ItemsView',
104
+ 'Iterable',
105
+ 'Iterator',
106
+ 'KeysView',
107
+ 'LambdaType',
108
+ 'List',
109
+ 'Literal',
110
+ 'Mapping',
111
+ 'MappingProxyType',
112
+ 'MappingView',
113
+ 'Match',
114
+ 'MemberDescriptorType',
115
+ 'MethodDescriptorType',
116
+ 'MethodType',
117
+ 'MethodWrapperType',
118
+ 'ModuleType',
119
+ 'MutableMapping',
120
+ 'MutableSequence',
121
+ 'MutableSet',
122
+ 'NamedTuple', # Not really a type.
123
+ 'NewType',
124
+ 'NoReturn',
125
+ 'Number',
126
+ 'O',
127
+ 'Optional',
128
+ 'OptionalScope',
129
+ 'OrderedDict',
130
+ 'ParamSpec',
131
+ 'ParamSpecArgs',
132
+ 'ParamSpecKwargs',
133
+ 'Pattern',
134
+ 'Protocol',
135
+ # Structural checks, a.k.a. protocols.
136
+ 'Reversible',
137
+ 'Sequence',
138
+ 'Set',
139
+ 'SimpleNamespace',
140
+ 'Sized',
141
+ 'SupportsAbs',
142
+ 'SupportsBytes',
143
+ 'SupportsComplex',
144
+ 'SupportsFloat',
145
+ 'SupportsIndex',
146
+ 'SupportsIndex',
147
+ 'SupportsInt',
148
+ 'SupportsRound',
149
+ 'Text',
150
+ 'TextIO',
151
+ 'TracebackType',
152
+ 'TracebackType',
153
+ 'Tuple',
154
+ 'Type',
155
+ 'TypeAlias',
156
+ 'TypeGuard',
157
+ 'TypeVar',
158
+ 'TypedDict', # Not really a type.
159
+ 'U',
160
+ 'Union',
161
+ 'ValuesView',
162
+ 'WrapperDescriptorType',
163
+ 'cast',
164
+ 'coroutine',
165
+ 'delta_type',
166
+ 'final',
167
+ 'get_args',
168
+ 'get_origin',
169
+ 'get_type_hints',
170
+ 'is_typeddict',
171
+ 'new_class',
172
+ 'no_type_check',
173
+ 'no_type_check_decorator',
174
+ 'overload',
175
+ 'prepare_class',
176
+ 'resolve_bases',
177
+ 'runtime_checkable',
178
+ 'timestamp_type',
179
+ ]