python-utils 4.0.1__py3-none-any.whl → 4.1.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/aio.py +5 -2
- python_utils/containers.py +537 -41
- python_utils/converters.py +70 -15
- python_utils/decorators.py +29 -3
- python_utils/formatters.py +25 -8
- python_utils/generators.py +82 -34
- python_utils/import_.py +49 -8
- python_utils/logger.py +89 -30
- python_utils/loguru.py +34 -1
- python_utils/time.py +55 -26
- {python_utils-4.0.1.dist-info → python_utils-4.1.0.dist-info}/METADATA +22 -9
- python_utils-4.1.0.dist-info/RECORD +21 -0
- {python_utils-4.0.1.dist-info → python_utils-4.1.0.dist-info}/WHEEL +1 -1
- python_utils-4.0.1.dist-info/RECORD +0 -21
- {python_utils-4.0.1.dist-info → python_utils-4.1.0.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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__
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
151
|
-
#
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
415
|
-
``on_timeout`` fires. ``None`` disables the per-item
|
|
416
|
-
|
|
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
|
|
3
|
+
Version: 4.1.0
|
|
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
|
-
[](https://pypi.python.org/pypi/python-utils)
|
|
52
|
-
[](https://pypi.python.org/pypi/python-utils)
|
|
53
|
-
[](https://github.com/WoLpH/python-utils/blob/develop/LICENSE)
|
|
58
|
-
[](https://pypi.python.org/pypi/python-utils)
|
|
53
|
+
[](https://pypi.python.org/pypi/python-utils)
|
|
54
|
+
[](https://github.com/wolph/python-utils/actions/workflows/ci.yml?query=branch%3Amaster)
|
|
55
|
+
[](https://coveralls.io/github/wolph/python-utils?branch=master)
|
|
56
|
+
[Typed: mypy, pyright and pyrefly](https://github.com/WoLpH/python-utils)
|
|
57
|
+
[](https://github.com/astral-sh/ruff)
|
|
58
|
+
[](https://github.com/WoLpH/python-utils/blob/develop/LICENSE)
|
|
59
|
+
[](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
|
+
[](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=OQo-dgtKLG76OMc72jXVMA7x1N1l8h8C025G9TwgZAc,6248
|
|
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.0.dist-info/licenses/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
|
|
19
|
+
python_utils-4.1.0.dist-info/WHEEL,sha256=rp-k_VZaX8oABFEtLwG_Df95p8LSKt9-uPyQ4jKH0P4,81
|
|
20
|
+
python_utils-4.1.0.dist-info/METADATA,sha256=H9vBD5TFJQLYKsZSY1GlK_qwb5IUELI_aWRR4v28rGE,15988
|
|
21
|
+
python_utils-4.1.0.dist-info/RECORD,,
|
|
@@ -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,,
|
|
File without changes
|