python-utils 4.0.1__py3-none-any.whl → 4.1.1__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/logger.py CHANGED
@@ -30,6 +30,7 @@ from __future__ import annotations
30
30
  import abc
31
31
  import collections.abc
32
32
  import logging
33
+ import sys
33
34
  import types
34
35
  import typing
35
36
 
@@ -55,6 +56,10 @@ _ExcInfoType: typing.TypeAlias = (
55
56
  | BaseException
56
57
  | None
57
58
  )
59
+ #: Extra frames between ``Logger.exception`` and its caller. Up to Python 3.10
60
+ #: ``Logger.exception`` calls ``Logger.error``, and ``logging`` counts that
61
+ #: frame against the ``stacklevel``. From Python 3.11 it skips its own frames.
62
+ _EXCEPTION_FRAMES: int = int(sys.version_info < (3, 11))
58
63
  #: Parameter specification capturing a wrapped logger method's arguments.
59
64
  _P = typing.ParamSpec('_P')
60
65
  #: Covariant return-type variable for wrapped logger methods.
@@ -148,6 +153,45 @@ class LoggerProtocol(typing.Protocol):
148
153
  """Log ``msg`` at the integer ``level``."""
149
154
 
150
155
 
156
+ def _create_instance(
157
+ new: collections.abc.Callable[..., _T],
158
+ cls: type[_T],
159
+ args: tuple[typing.Any, ...],
160
+ kwargs: dict[str, typing.Any],
161
+ ) -> _T:
162
+ """Create an instance with the next ``__new__`` in line.
163
+
164
+ The constructor arguments are passed on, so that a class which is created
165
+ in ``__new__`` gets its value, as ``int`` and ``str`` do. A ``__new__``
166
+ that does not take them is called without, the way it always was. That
167
+ second call repeats whatever the first one did before it raised.
168
+
169
+ Args:
170
+ new: The next ``__new__`` in the method resolution order.
171
+ cls: The class to create an instance of.
172
+ args: The positional constructor arguments.
173
+ kwargs: The keyword constructor arguments.
174
+
175
+ Returns:
176
+ The new instance.
177
+
178
+ Raises:
179
+ TypeError: When ``new`` accepts the arguments in neither form. The
180
+ error is the one for the call with the arguments.
181
+ """
182
+ if new is object.__new__:
183
+ # `object.__new__` takes no arguments, they are for `__init__`.
184
+ return new(cls)
185
+
186
+ try:
187
+ return new(cls, *args, **kwargs)
188
+ except TypeError as error:
189
+ try:
190
+ return new(cls)
191
+ except TypeError:
192
+ raise error from None
193
+
194
+
151
195
  class LoggerBase(abc.ABC):
152
196
  """Class which automatically adds logging utilities to your class when
153
197
  inheriting. Expects `logger` to be a logging.Logger or compatible instance.
@@ -182,6 +226,37 @@ class LoggerBase(abc.ABC):
182
226
  """Join the non-empty, stripped ``name_parts`` into a dotted name."""
183
227
  return '.'.join(n.strip() for n in name_parts if n.strip())
184
228
 
229
+ @classmethod
230
+ def _log_kwargs(
231
+ cls,
232
+ exc_info: _ExcInfoType,
233
+ stack_info: bool,
234
+ stacklevel: int,
235
+ extra: collections.abc.Mapping[str, object] | None,
236
+ ) -> dict[str, typing.Any]:
237
+ """Build the keyword arguments that every log method forwards.
238
+
239
+ Subclasses with a logger that does not take the ``logging`` keyword
240
+ arguments can override this method.
241
+
242
+ Args:
243
+ exc_info: Exception information to attach to the record.
244
+ stack_info: Whether to attach the current stack to the record.
245
+ stacklevel: How many frames up the caller of the log method is.
246
+ extra: Extra attributes for the record.
247
+
248
+ Returns:
249
+ The keyword arguments for the method of ``cls.logger``.
250
+ """
251
+ return {
252
+ 'exc_info': exc_info,
253
+ 'stack_info': stack_info,
254
+ # One level more, so the record names the caller of the log
255
+ # method instead of the log method itself.
256
+ 'stacklevel': stacklevel + 1,
257
+ 'extra': extra,
258
+ }
259
+
185
260
  @decorators.wraps_classmethod(logging.Logger.debug)
186
261
  @classmethod
187
262
  def debug(
@@ -197,10 +272,7 @@ class LoggerBase(abc.ABC):
197
272
  return cls.logger.debug( # type: ignore[no-any-return]
198
273
  msg,
199
274
  *args,
200
- exc_info=exc_info,
201
- stack_info=stack_info,
202
- stacklevel=stacklevel,
203
- extra=extra,
275
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
204
276
  )
205
277
 
206
278
  @decorators.wraps_classmethod(logging.Logger.info)
@@ -218,10 +290,7 @@ class LoggerBase(abc.ABC):
218
290
  return cls.logger.info( # type: ignore[no-any-return]
219
291
  msg,
220
292
  *args,
221
- exc_info=exc_info,
222
- stack_info=stack_info,
223
- stacklevel=stacklevel,
224
- extra=extra,
293
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
225
294
  )
226
295
 
227
296
  @decorators.wraps_classmethod(logging.Logger.warning)
@@ -239,10 +308,7 @@ class LoggerBase(abc.ABC):
239
308
  return cls.logger.warning( # type: ignore[no-any-return]
240
309
  msg,
241
310
  *args,
242
- exc_info=exc_info,
243
- stack_info=stack_info,
244
- stacklevel=stacklevel,
245
- extra=extra,
311
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
246
312
  )
247
313
 
248
314
  @decorators.wraps_classmethod(logging.Logger.error)
@@ -260,10 +326,7 @@ class LoggerBase(abc.ABC):
260
326
  return cls.logger.error( # type: ignore[no-any-return]
261
327
  msg,
262
328
  *args,
263
- exc_info=exc_info,
264
- stack_info=stack_info,
265
- stacklevel=stacklevel,
266
- extra=extra,
329
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
267
330
  )
268
331
 
269
332
  @decorators.wraps_classmethod(logging.Logger.critical)
@@ -281,10 +344,7 @@ class LoggerBase(abc.ABC):
281
344
  return cls.logger.critical( # type: ignore[no-any-return]
282
345
  msg,
283
346
  *args,
284
- exc_info=exc_info,
285
- stack_info=stack_info,
286
- stacklevel=stacklevel,
287
- extra=extra,
347
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
288
348
  )
289
349
 
290
350
  @decorators.wraps_classmethod(logging.Logger.exception)
@@ -293,7 +353,7 @@ class LoggerBase(abc.ABC):
293
353
  cls,
294
354
  msg: object,
295
355
  *args: object,
296
- exc_info: _ExcInfoType = None,
356
+ exc_info: _ExcInfoType = True,
297
357
  stack_info: bool = False,
298
358
  stacklevel: int = 1,
299
359
  extra: collections.abc.Mapping[str, object] | None = None,
@@ -302,10 +362,12 @@ class LoggerBase(abc.ABC):
302
362
  return cls.logger.exception( # type: ignore[no-any-return]
303
363
  msg,
304
364
  *args,
305
- exc_info=exc_info,
306
- stack_info=stack_info,
307
- stacklevel=stacklevel,
308
- extra=extra,
365
+ **cls._log_kwargs(
366
+ exc_info,
367
+ stack_info,
368
+ stacklevel + _EXCEPTION_FRAMES,
369
+ extra,
370
+ ),
309
371
  )
310
372
 
311
373
  @decorators.wraps_classmethod(logging.Logger.log)
@@ -325,10 +387,7 @@ class LoggerBase(abc.ABC):
325
387
  level,
326
388
  msg,
327
389
  *args,
328
- exc_info=exc_info,
329
- stack_info=stack_info,
330
- stacklevel=stacklevel,
331
- extra=extra,
390
+ **cls._log_kwargs(exc_info, stack_info, stacklevel, extra),
332
391
  )
333
392
 
334
393
 
@@ -382,4 +441,4 @@ class Logged(LoggerBase):
382
441
  cls.logger = logging.getLogger(
383
442
  cls.__get_name(cls.__module__, cls.__name__)
384
443
  )
385
- return super().__new__(cls)
444
+ return _create_instance(super().__new__, cls, args, kwargs)
python_utils/loguru.py CHANGED
@@ -16,6 +16,7 @@ Usage example:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import collections.abc
19
20
  import typing
20
21
 
21
22
  import loguru
@@ -35,6 +36,36 @@ class Logurud(logger_module.LoggerBase):
35
36
 
36
37
  logger: loguru.Logger
37
38
 
39
+ @classmethod
40
+ def _log_kwargs(
41
+ cls,
42
+ exc_info: object,
43
+ stack_info: bool,
44
+ stacklevel: int,
45
+ extra: collections.abc.Mapping[str, object] | None,
46
+ ) -> dict[str, typing.Any]:
47
+ """Forward only an explicit ``extra`` to the `loguru` logger.
48
+
49
+ `loguru` runs ``str.format`` over the message as soon as a call has
50
+ any arguments. The `logging` keyword arguments would turn every
51
+ message with a brace in it into a broken format string, so they are
52
+ left out. An ``extra`` that the caller passed is still forwarded, as
53
+ it always reached the `loguru` record.
54
+
55
+ Args:
56
+ exc_info: Ignored, `loguru` does not take this argument.
57
+ stack_info: Ignored, `loguru` does not take this argument.
58
+ stacklevel: Ignored, `loguru` does not take this argument.
59
+ extra: Forwarded when it is not ``None``.
60
+
61
+ Returns:
62
+ The keyword arguments for the `loguru` logger.
63
+ """
64
+ if extra is None:
65
+ return {}
66
+
67
+ return {'extra': extra}
68
+
38
69
  def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Logurud:
39
70
  """
40
71
  Creates a new instance of `Logurud` and initializes the `loguru`
@@ -50,4 +81,6 @@ class Logurud(logger_module.LoggerBase):
50
81
  # `logger` is already declared at class scope; assign without
51
82
  # re-annotating to avoid an obscured-declaration error.
52
83
  cls.logger = loguru.logger.opt(depth=1)
53
- return super().__new__(cls)
84
+ return logger_module._create_instance( # pyright: ignore[reportPrivateUsage]
85
+ super().__new__, cls, args, kwargs
86
+ )
python_utils/time.py CHANGED
@@ -54,14 +54,15 @@ def timedelta_to_seconds(delta: datetime.timedelta) -> _aliases.Number:
54
54
  >>> '%.6f' % timedelta_to_seconds(timedelta(microseconds=1))
55
55
  '0.000001'
56
56
  """
57
+ seconds: int = delta.seconds + delta.days * 60 * 60 * 24
58
+
57
59
  # Only convert to float if needed
58
60
  if delta.microseconds:
59
- total = delta.microseconds * 1e-6
61
+ # Divide the whole microseconds once, the way `total_seconds()` does.
62
+ # Adding a float fraction to a large number of seconds loses digits.
63
+ return (seconds * 10**6 + delta.microseconds) / 10**6
60
64
  else:
61
- total = 0
62
- total += delta.seconds
63
- total += delta.days * 60 * 60 * 24
64
- return total
65
+ return seconds
65
66
 
66
67
 
67
68
  def delta_to_seconds(interval: _aliases.delta_type) -> _aliases.Number:
@@ -135,23 +136,22 @@ def format_time(
135
136
  TypeError: Unknown type ...
136
137
 
137
138
  """
138
- precision_seconds = precision.total_seconds()
139
-
140
139
  if isinstance(timestamp, str):
141
140
  timestamp = float(timestamp)
142
141
 
143
142
  if isinstance(timestamp, (int, float)):
144
143
  try:
145
144
  timestamp = datetime.timedelta(seconds=timestamp)
146
- except OverflowError: # pragma: no cover
145
+ except (OverflowError, ValueError):
146
+ # Too large for a timedelta, or not a number at all: nan raises
147
+ # a ValueError where infinity raises an OverflowError.
147
148
  timestamp = None
148
149
 
149
150
  if isinstance(timestamp, datetime.timedelta):
150
- seconds = timestamp.total_seconds()
151
- # Truncate the number to the given precision
152
- seconds = seconds - (seconds % precision_seconds)
153
-
154
- return str(datetime.timedelta(seconds=seconds))
151
+ # Truncate the number to the given precision. A timedelta counts in
152
+ # whole microseconds, which keeps the modulo exact. In float seconds
153
+ # `1.0 % 0.1` is just under 0.1 and the result drops a whole step.
154
+ return str(timestamp - timestamp % precision)
155
155
  elif isinstance(timestamp, datetime.datetime): # pragma: no cover
156
156
  # Python 2 doesn't have the timestamp method
157
157
  if hasattr(timestamp, 'timestamp'):
@@ -159,8 +159,10 @@ def format_time(
159
159
  else:
160
160
  seconds = timedelta_to_seconds(timestamp - epoch)
161
161
 
162
- # Truncate the number to the given precision
163
- seconds = seconds - (seconds % precision_seconds)
162
+ # Truncate the number to the given precision, in whole microseconds
163
+ # for the same reason as above
164
+ since_epoch: datetime.timedelta = datetime.timedelta(seconds=seconds)
165
+ seconds = (since_epoch - since_epoch % precision).total_seconds()
164
166
 
165
167
  try: # pragma: no cover
166
168
  dt = datetime.datetime.fromtimestamp(seconds)
@@ -223,6 +225,10 @@ def timeout_generator(
223
225
  useful for testing slow APIs so you get a small sample of the data in a
224
226
  reasonable amount of time.
225
227
 
228
+ After every sleep the interval is multiplied by `interval_multiplier`. No
229
+ sleep is longer than `maximum_interval`, and that includes the first one.
230
+ A `maximum_interval` of `None` or `0` means that there is no maximum.
231
+
226
232
  >>> for i in timeout_generator(0.1, 0.06):
227
233
  ... # Put your slow code here
228
234
  ... print(i)
@@ -262,7 +268,12 @@ def timeout_generator(
262
268
  if time.perf_counter() >= end:
263
269
  break
264
270
 
265
- time.sleep(float_interval)
271
+ # The maximum holds for the first sleep as well. Zero is not a
272
+ # maximum here, it means the same as `None`.
273
+ if float_maximum_interval:
274
+ time.sleep(min(float_interval, float_maximum_interval))
275
+ else:
276
+ time.sleep(float_interval)
266
277
 
267
278
  float_interval *= interval_multiplier
268
279
  if float_maximum_interval:
@@ -283,10 +294,11 @@ async def aio_timeout_generator(
283
294
  default) until the float_timeout is reached with a configurable
284
295
  float_interval between items.
285
296
 
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.
297
+ After every sleep the interval is multiplied by `interval_multiplier`. To
298
+ double the interval with each run, specify 2. A value below 1 makes the
299
+ interval shorter with each run. No sleep is longer than
300
+ `maximum_interval`, and that includes the first one. A `maximum_interval`
301
+ of `None` or `0` means that there is no maximum.
290
302
 
291
303
  Doctests and asyncio are not friends, so no examples. But this function is
292
304
  effectively the same as the `timeout_generator` but it uses `async for`
@@ -317,7 +329,12 @@ async def aio_timeout_generator(
317
329
  if time.perf_counter() >= end:
318
330
  break
319
331
 
320
- await asyncio.sleep(float_interval)
332
+ # The maximum holds for the first sleep as well. Zero is not a
333
+ # maximum here, it means the same as `None`.
334
+ if float_maximum_interval:
335
+ await asyncio.sleep(min(float_interval, float_maximum_interval))
336
+ else:
337
+ await asyncio.sleep(float_interval)
321
338
 
322
339
  float_interval *= interval_multiplier
323
340
  if float_maximum_interval: # pragma: no branch
@@ -344,9 +361,15 @@ async def aio_generator_timeout_detector(
344
361
  This function is used to detect if an asyncio generator has not yielded
345
362
  an element for a set amount of time.
346
363
 
364
+ The `timeout` is the time a single element may take. A `timeout` of `None`
365
+ or `0` means that there is no timeout per element. The `total_timeout` is
366
+ the time all elements together may take. It is checked between elements,
367
+ so it does not end the wait for an element that does not arrive. Use
368
+ `timeout` for that.
369
+
347
370
  The `on_timeout` argument is called with the `generator`, `timeout`,
348
371
  `total_timeout`, `exception` and the extra `**kwargs` to this function as
349
- arguments.
372
+ arguments. It can be a plain function or a coroutine function.
350
373
  If `on_timeout` is not specified, the exception is reraised.
351
374
  If `on_timeout` is `None`, the exception is silently ignored and the
352
375
  generator will finish as normal.
@@ -377,13 +400,17 @@ async def aio_generator_timeout_detector(
377
400
 
378
401
  except asyncio.TimeoutError as exception: # noqa: PERF203
379
402
  if on_timeout is not None:
380
- await on_timeout(
403
+ result: typing.Any = on_timeout(
381
404
  generator,
382
405
  timeout,
383
406
  total_timeout,
384
407
  exception,
385
408
  **on_timeout_kwargs,
386
409
  )
410
+ # A coroutine function hands back something to await, a
411
+ # plain function has already done its work by now.
412
+ if isinstance(result, collections.abc.Awaitable):
413
+ await result
387
414
  break
388
415
 
389
416
  except StopAsyncIteration:
@@ -411,9 +438,11 @@ def aio_generator_timeout_detector_decorator(
411
438
  """Wrap a generator function with ``aio_generator_timeout_detector``.
412
439
 
413
440
  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.
441
+ timeout: Per-item timeout. If a single yield takes longer,
442
+ ``on_timeout`` fires. ``None`` or ``0`` disables the per-item
443
+ check.
444
+ total_timeout: Overall timeout across the whole generator. It is
445
+ checked between items.
417
446
  on_timeout: Callback invoked on a timeout; defaults to re-raising.
418
447
  **on_timeout_kwargs: Extra keyword arguments passed to ``on_timeout``.
419
448
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-utils
3
- Version: 4.0.1
3
+ Version: 4.1.1
4
4
  Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
5
5
  Keywords: utils,utilities,helpers,typing,typed,async,lazy-imports
6
6
  Author: Rick van Hattem
@@ -35,6 +35,7 @@ Project-URL: Homepage, https://github.com/WoLpH/python-utils
35
35
  Project-URL: Documentation, https://python-utils.readthedocs.io/
36
36
  Project-URL: Repository, https://github.com/WoLpH/python-utils
37
37
  Project-URL: Changelog, https://github.com/WoLpH/python-utils/releases
38
+ Project-URL: Funding, https://github.com/sponsors/wolph
38
39
  Provides-Extra: docs
39
40
  Provides-Extra: loguru
40
41
  Provides-Extra: tests
@@ -48,14 +49,14 @@ Description-Content-Type: text/markdown
48
49
 
49
50
  **The fast, fully-typed stdlib helpers you keep rewriting — in one tiny, dependency-light package.**
50
51
 
51
- [![PyPI version](https://img.shields.io/pypi/v/python-utils.svg?logo=pypi&logoColor=white)](https://pypi.python.org/pypi/python-utils)
52
- [![Python versions](https://img.shields.io/pypi/pyversions/python-utils.svg?logo=python&logoColor=white)](https://pypi.python.org/pypi/python-utils)
53
- [![CI](https://github.com/WoLpH/python-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/WoLpH/python-utils/actions/workflows/ci.yml)
54
- [![Coverage Status](https://coveralls.io/repos/github/WoLpH/python-utils/badge.svg?branch=develop)](https://coveralls.io/github/WoLpH/python-utils?branch=develop)
55
- [![Typed](https://img.shields.io/badge/typed-mypy%20%7C%20pyright%20%7C%20pyrefly-blue.svg)](https://github.com/WoLpH/python-utils)
56
- [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
57
- [![License](https://img.shields.io/pypi/l/python-utils.svg)](https://github.com/WoLpH/python-utils/blob/develop/LICENSE)
58
- [![Downloads](https://img.shields.io/pypi/dm/python-utils.svg?logo=pypi&logoColor=white)](https://pypi.python.org/pypi/python-utils)
52
+ [![PyPI version](https://img.shields.io/pypi/v/python-utils.svg?logo=pypi&logoColor=white&style=flat-square&labelColor=555&color=007ec6)](https://pypi.python.org/pypi/python-utils)
53
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-utils.svg?logo=python&logoColor=white&style=flat-square&labelColor=555&color=007ec6)](https://pypi.python.org/pypi/python-utils)
54
+ [![CI on master](https://img.shields.io/github/actions/workflow/status/wolph/python-utils/ci.yml?branch=master&label=CI&style=flat-square&labelColor=555)](https://github.com/wolph/python-utils/actions/workflows/ci.yml?query=branch%3Amaster)
55
+ [![Coverage on master](https://img.shields.io/coverallsCoverage/github/wolph/python-utils?branch=master&style=flat-square&labelColor=555)](https://coveralls.io/github/wolph/python-utils?branch=master)
56
+ [Typed: mypy, pyright and pyrefly](https://github.com/WoLpH/python-utils)
57
+ [![Ruff](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fastral-sh%2Fruff%2Fmain%2Fassets%2Fbadge%2Fv2.json&style=flat-square&labelColor=555&color=007ec6)](https://github.com/astral-sh/ruff)
58
+ [![License](https://img.shields.io/pypi/l/python-utils.svg?style=flat-square&labelColor=555&color=007ec6)](https://github.com/WoLpH/python-utils/blob/develop/LICENSE)
59
+ [![Monthly downloads](https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fapi.pepy.tech%2Fbadge%2Fpython-utils%2Fmonth&query=%28%2F%2F%2A%5Blocal-name%28%29%3D%22text%22%5D%29%5Blast%28%29%5D&label=downloads%2Fmonth&style=flat-square&labelColor=555&color=007ec6)](https://pepy.tech/projects/python-utils)
59
60
 
60
61
  [**Documentation**](https://python-utils.readthedocs.io/en/latest/) ·
61
62
  [**PyPI**](https://pypi.python.org/pypi/python-utils) ·
@@ -382,6 +383,18 @@ Contributions are very welcome! We keep a strict **100% coverage** bar and run
382
383
  [CONTRIBUTING.md](https://github.com/WoLpH/python-utils/blob/develop/CONTRIBUTING.md)
383
384
  to get set up.
384
385
 
386
+ ## Support
387
+
388
+ python-utils is maintained by [Rick van Hattem](https://github.com/wolph) in his own time. It is a small library that four other packages depend on, which means every change is a compatibility question.
389
+
390
+ If it saved you an afternoon, a tip covers an hour of issue triage:
391
+ [Ko-fi](https://ko-fi.com/wolph_gh) or [GitHub Sponsors](https://github.com/sponsors/wolph).
392
+
393
+ If your company funds its dependencies, this package is on
394
+ [thanks.dev](https://thanks.dev/u/gh/wolph).
395
+
396
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/wolph_gh)
397
+
385
398
  ## 📄 License
386
399
 
387
400
  BSD-3-Clause — see [LICENSE](https://github.com/WoLpH/python-utils/blob/develop/LICENSE).
@@ -0,0 +1,21 @@
1
+ python_utils/__about__.py,sha256=qmqR375psvJc8IPYmLhruuaiouHzhdHu7DozRZG34DM,1343
2
+ python_utils/__init__.py,sha256=atYwTBHh2KNGdPSawwQKYCL9UolHho3L4WFxo901WDg,5958
3
+ python_utils/_aliases.py,sha256=xf_HrYaAa9ZKZAdYYJwIen-OB3nFGI5TTUETH5_ayGY,1641
4
+ python_utils/aio.py,sha256=64uhJ2jEF2vyTI7TQsgxWnTnrda9O23kKVFr6ebnuq0,4235
5
+ python_utils/containers.py,sha256=kHpDgj_sAdoouL0b_CE6IHSd-qtz0KYMs_Nuiba-yEQ,37135
6
+ python_utils/converters.py,sha256=gBOjkVYGwx1aBs7OtjFrMdQgZwI6wbOU_Rvy0u9OVFE,17273
7
+ python_utils/decorators.py,sha256=UyVuV7Phf8kjAt38uA-F9MFH2o8zM4t-fmlgt3KL5cc,8225
8
+ python_utils/exceptions.py,sha256=B5wljs3Id6eQ9eRaur-ZMPGGmhHNwh9F08Sfw6fuLX4,1230
9
+ python_utils/formatters.py,sha256=TgnSRHjfC9TKMp5lNUOlwDNBhFOtIaS-lOFpBcsMaEc,6611
10
+ python_utils/generators.py,sha256=5X2mbjkEWl69syOHTxUaQS18ILZ49n2EvXb4cASVjxg,6151
11
+ python_utils/import_.py,sha256=EnVSLs7Sk1CHrkpiKdXe8i-O6XjS5hXINpjdEohRT4A,5541
12
+ python_utils/logger.py,sha256=-QuF7Zfo7pxc4nTmAPc9Pm-oz5wsxXIX7NeCnYEPI6I,14043
13
+ python_utils/loguru.py,sha256=qsap-aXByGXsd4KrYmtneTSUARVIdeVu9xgmrFPtqSI,2696
14
+ python_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ python_utils/terminal.py,sha256=GL3n2_rT98LBNmxy-amY-hezGKStCo-9_d_pR-TMgv8,6823
16
+ python_utils/time.py,sha256=fZz00bpTeLXFFhbAo03LiMLWuzSJU4iPbyGSe7PYshU,16979
17
+ python_utils/types.py,sha256=N1O2uFE2IuwVJk_vcwz8XNkf-taL-vJ5iNNG6FUXjTg,4336
18
+ python_utils-4.1.1.dist-info/licenses/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
19
+ python_utils-4.1.1.dist-info/WHEEL,sha256=xTCBFIZIIyRSxyowOv5Wqg_jFU6KsdIffuulBB09Ph8,80
20
+ python_utils-4.1.1.dist-info/METADATA,sha256=LObK8d9NeYLUJN3Jpi3EyfXgIR7KSrSn0z5rJm5Ew7M,15988
21
+ python_utils-4.1.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.12.7
2
+ Generator: uv 0.13.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,21 +0,0 @@
1
- python_utils/__about__.py,sha256=qmqR375psvJc8IPYmLhruuaiouHzhdHu7DozRZG34DM,1343
2
- python_utils/__init__.py,sha256=atYwTBHh2KNGdPSawwQKYCL9UolHho3L4WFxo901WDg,5958
3
- python_utils/_aliases.py,sha256=xf_HrYaAa9ZKZAdYYJwIen-OB3nFGI5TTUETH5_ayGY,1641
4
- python_utils/aio.py,sha256=59Zd-N9gF4jx__mEHlne1UIHFMlmNYuQOIy4_Nb6-QI,3999
5
- python_utils/containers.py,sha256=FX3naenzdHuNQvNYsB_KdiFXgCXKVklS3GvSkbK95OU,19785
6
- python_utils/converters.py,sha256=XgGqmtHjQ4czgYOiptTGM38Of5-NMwnDTnxuxPBkl2E,14949
7
- python_utils/decorators.py,sha256=5r-T7fEBWqbhaWEEdi9NWS7f6uVVlvq1Uk8K0PGC3qA,7124
8
- python_utils/exceptions.py,sha256=B5wljs3Id6eQ9eRaur-ZMPGGmhHNwh9F08Sfw6fuLX4,1230
9
- python_utils/formatters.py,sha256=ypKLXAL1t0Z3NJ-hKAbGFrQ18F5aGKh93WVvL_lvwrM,5452
10
- python_utils/generators.py,sha256=ftkIbx0HQntGfe87rudMZJma5fIfv-xCKMvL_fYaPGQ,3882
11
- python_utils/import_.py,sha256=0w1jHkRFcAKO0Sr15cY2_Lz4ZZYjP5p5qrl0wRyohm4,3916
12
- python_utils/logger.py,sha256=Fe0OYGucs-5oPSp4to_PTjaJkfGS2Kflr86Y73ycOrM,11627
13
- python_utils/loguru.py,sha256=mcUzxk16EDhK2MoAimG3dCVxXQCbImj6MWmtac1h8pg,1476
14
- python_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- python_utils/terminal.py,sha256=GL3n2_rT98LBNmxy-amY-hezGKStCo-9_d_pR-TMgv8,6823
16
- python_utils/time.py,sha256=amOrCsbQuEaVKSKkd35chDJU2_lHh-PCazM_J7hqeGE,15006
17
- python_utils/types.py,sha256=N1O2uFE2IuwVJk_vcwz8XNkf-taL-vJ5iNNG6FUXjTg,4336
18
- python_utils-4.0.1.dist-info/licenses/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
19
- python_utils-4.0.1.dist-info/WHEEL,sha256=ZFFp7t7R4RYQ5KYZkmiFWoQvHay7SrTmn-6ZYfoFZ3U,80
20
- python_utils-4.0.1.dist-info/METADATA,sha256=ZfEiMfzJg7fEOUHd-EWrhNo8XxEttCPGuY5tRovYriw,14910
21
- python_utils-4.0.1.dist-info/RECORD,,