python-utils 3.8.2__py2.py3-none-any.whl → 3.9.1__py2.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/__about__.py +13 -1
- python_utils/__init__.py +81 -32
- python_utils/aio.py +72 -9
- python_utils/containers.py +283 -33
- python_utils/converters.py +143 -63
- python_utils/decorators.py +47 -24
- python_utils/exceptions.py +20 -2
- python_utils/formatters.py +36 -15
- python_utils/generators.py +38 -6
- python_utils/import_.py +34 -14
- python_utils/logger.py +134 -17
- python_utils/loguru.py +36 -1
- python_utils/terminal.py +46 -20
- python_utils/time.py +98 -51
- python_utils/types.py +109 -92
- {python_utils-3.8.2.dist-info → python_utils-3.9.1.dist-info}/METADATA +20 -16
- python_utils-3.9.1.dist-info/RECORD +21 -0
- {python_utils-3.8.2.dist-info → python_utils-3.9.1.dist-info}/WHEEL +1 -1
- python_utils/compat.py +0 -0
- python_utils-3.8.2.dist-info/RECORD +0 -22
- {python_utils-3.8.2.dist-info → python_utils-3.9.1.dist-info}/LICENSE +0 -0
- {python_utils-3.8.2.dist-info → python_utils-3.9.1.dist-info}/top_level.txt +0 -0
python_utils/converters.py
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for type conversion.
|
|
3
|
+
|
|
4
|
+
Functions:
|
|
5
|
+
- to_int: Convert a string to an integer with optional regular expression
|
|
6
|
+
matching.
|
|
7
|
+
- to_float: Convert a string to a float with optional regular expression
|
|
8
|
+
matching.
|
|
9
|
+
- to_unicode: Convert objects to Unicode strings.
|
|
10
|
+
- to_str: Convert objects to byte strings.
|
|
11
|
+
- scale_1024: Scale a number down to a suitable size based on powers of
|
|
12
|
+
1024.
|
|
13
|
+
- remap: Remap a value from one range to another.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Ignoring all mypy errors because mypy doesn't understand many modern typing
|
|
17
|
+
# constructs... please, use pyright instead if you can.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
1
20
|
import decimal
|
|
2
21
|
import math
|
|
3
22
|
import re
|
|
4
23
|
import typing
|
|
24
|
+
from typing import Union
|
|
5
25
|
|
|
6
26
|
from . import types
|
|
7
27
|
|
|
8
28
|
_TN = types.TypeVar('_TN', bound=types.DecimalNumber)
|
|
9
29
|
|
|
30
|
+
_RegexpType: types.TypeAlias = Union[
|
|
31
|
+
types.Pattern[str], str, types.Literal[True], None
|
|
32
|
+
]
|
|
33
|
+
|
|
10
34
|
|
|
11
35
|
def to_int(
|
|
12
|
-
input_:
|
|
36
|
+
input_: str | None = None,
|
|
13
37
|
default: int = 0,
|
|
14
38
|
exception: types.ExceptionsType = (ValueError, TypeError),
|
|
15
|
-
regexp:
|
|
39
|
+
regexp: _RegexpType = None,
|
|
16
40
|
) -> int:
|
|
17
|
-
r
|
|
18
|
-
Convert the given input to an integer or return default
|
|
41
|
+
r"""
|
|
42
|
+
Convert the given input to an integer or return default.
|
|
19
43
|
|
|
20
44
|
When trying to convert the exceptions given in the exception parameter
|
|
21
45
|
are automatically catched and the default will be returned.
|
|
@@ -74,7 +98,7 @@ def to_int(
|
|
|
74
98
|
Traceback (most recent call last):
|
|
75
99
|
...
|
|
76
100
|
TypeError: unknown argument for regexp parameter: 123
|
|
77
|
-
|
|
101
|
+
"""
|
|
78
102
|
if regexp is True:
|
|
79
103
|
regexp = re.compile(r'(\d+)')
|
|
80
104
|
elif isinstance(regexp, str):
|
|
@@ -82,18 +106,17 @@ def to_int(
|
|
|
82
106
|
elif hasattr(regexp, 'search'):
|
|
83
107
|
pass
|
|
84
108
|
elif regexp is not None:
|
|
85
|
-
raise TypeError('unknown argument for regexp parameter:
|
|
109
|
+
raise TypeError(f'unknown argument for regexp parameter: {regexp!r}')
|
|
86
110
|
|
|
87
111
|
try:
|
|
88
|
-
if regexp and input_:
|
|
89
|
-
|
|
90
|
-
input_ = match.groups()[-1]
|
|
112
|
+
if regexp and input_ and (match := regexp.search(input_)):
|
|
113
|
+
input_ = match.groups()[-1]
|
|
91
114
|
|
|
92
115
|
if input_ is None:
|
|
93
116
|
return default
|
|
94
117
|
else:
|
|
95
118
|
return int(input_)
|
|
96
|
-
except exception:
|
|
119
|
+
except exception:
|
|
97
120
|
return default
|
|
98
121
|
|
|
99
122
|
|
|
@@ -101,10 +124,10 @@ def to_float(
|
|
|
101
124
|
input_: str,
|
|
102
125
|
default: int = 0,
|
|
103
126
|
exception: types.ExceptionsType = (ValueError, TypeError),
|
|
104
|
-
regexp:
|
|
127
|
+
regexp: _RegexpType = None,
|
|
105
128
|
) -> types.Number:
|
|
106
|
-
r
|
|
107
|
-
Convert the given `input_` to an integer or return default
|
|
129
|
+
r"""
|
|
130
|
+
Convert the given `input_` to an integer or return default.
|
|
108
131
|
|
|
109
132
|
When trying to convert the exceptions given in the exception parameter
|
|
110
133
|
are automatically catched and the default will be returned.
|
|
@@ -113,7 +136,7 @@ def to_float(
|
|
|
113
136
|
in a string.
|
|
114
137
|
When True it will automatically match any digit in the string.
|
|
115
138
|
When a (regexp) object (has a search method) is given, that will be used.
|
|
116
|
-
|
|
139
|
+
When a string is given, re.compile will be run over it first
|
|
117
140
|
|
|
118
141
|
The last group of the regexp will be used as value
|
|
119
142
|
|
|
@@ -153,8 +176,7 @@ def to_float(
|
|
|
153
176
|
Traceback (most recent call last):
|
|
154
177
|
...
|
|
155
178
|
TypeError: unknown argument for regexp parameter
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
"""
|
|
158
180
|
if regexp is True:
|
|
159
181
|
regexp = re.compile(r'(\d+(\.\d+|))')
|
|
160
182
|
elif isinstance(regexp, str):
|
|
@@ -165,9 +187,8 @@ def to_float(
|
|
|
165
187
|
raise TypeError('unknown argument for regexp parameter')
|
|
166
188
|
|
|
167
189
|
try:
|
|
168
|
-
if regexp:
|
|
169
|
-
|
|
170
|
-
input_ = match.group(1)
|
|
190
|
+
if regexp and (match := regexp.search(input_)):
|
|
191
|
+
input_ = match.group(1)
|
|
171
192
|
return float(input_)
|
|
172
193
|
except exception:
|
|
173
194
|
return default
|
|
@@ -178,7 +199,7 @@ def to_unicode(
|
|
|
178
199
|
encoding: str = 'utf-8',
|
|
179
200
|
errors: str = 'replace',
|
|
180
201
|
) -> str:
|
|
181
|
-
|
|
202
|
+
"""Convert objects to unicode, if needed decodes string with the given
|
|
182
203
|
encoding and errors settings.
|
|
183
204
|
|
|
184
205
|
:rtype: str
|
|
@@ -187,14 +208,15 @@ def to_unicode(
|
|
|
187
208
|
'a'
|
|
188
209
|
>>> to_unicode('a')
|
|
189
210
|
'a'
|
|
190
|
-
>>> to_unicode(
|
|
211
|
+
>>> to_unicode('a')
|
|
191
212
|
'a'
|
|
192
|
-
>>> class Foo(object):
|
|
213
|
+
>>> class Foo(object):
|
|
214
|
+
... __str__ = lambda s: 'a'
|
|
193
215
|
>>> to_unicode(Foo())
|
|
194
216
|
'a'
|
|
195
217
|
>>> to_unicode(Foo)
|
|
196
218
|
"<class 'python_utils.converters.Foo'>"
|
|
197
|
-
|
|
219
|
+
"""
|
|
198
220
|
if isinstance(input_, bytes):
|
|
199
221
|
input_ = input_.decode(encoding, errors)
|
|
200
222
|
else:
|
|
@@ -207,22 +229,23 @@ def to_str(
|
|
|
207
229
|
encoding: str = 'utf-8',
|
|
208
230
|
errors: str = 'replace',
|
|
209
231
|
) -> bytes:
|
|
210
|
-
|
|
232
|
+
"""Convert objects to string, encodes to the given encoding.
|
|
211
233
|
|
|
212
234
|
:rtype: str
|
|
213
235
|
|
|
214
236
|
>>> to_str('a')
|
|
215
237
|
b'a'
|
|
216
|
-
>>> to_str(
|
|
238
|
+
>>> to_str('a')
|
|
217
239
|
b'a'
|
|
218
240
|
>>> to_str(b'a')
|
|
219
241
|
b'a'
|
|
220
|
-
>>> class Foo(object):
|
|
242
|
+
>>> class Foo(object):
|
|
243
|
+
... __str__ = lambda s: 'a'
|
|
221
244
|
>>> to_str(Foo())
|
|
222
245
|
'a'
|
|
223
246
|
>>> to_str(Foo)
|
|
224
247
|
"<class 'python_utils.converters.Foo'>"
|
|
225
|
-
|
|
248
|
+
"""
|
|
226
249
|
if not isinstance(input_, bytes):
|
|
227
250
|
if not hasattr(input_, 'encode'):
|
|
228
251
|
input_ = str(input_)
|
|
@@ -235,7 +258,7 @@ def scale_1024(
|
|
|
235
258
|
x: types.Number,
|
|
236
259
|
n_prefixes: int,
|
|
237
260
|
) -> types.Tuple[types.Number, types.Number]:
|
|
238
|
-
|
|
261
|
+
"""Scale a number down to a suitable size, based on powers of 1024.
|
|
239
262
|
|
|
240
263
|
Returns the scaled number and the power of 1024 used.
|
|
241
264
|
|
|
@@ -251,7 +274,7 @@ def scale_1024(
|
|
|
251
274
|
(0.5, 0)
|
|
252
275
|
>>> scale_1024(1, 2)
|
|
253
276
|
(1.0, 0)
|
|
254
|
-
|
|
277
|
+
"""
|
|
255
278
|
if x <= 0:
|
|
256
279
|
power = 0
|
|
257
280
|
else:
|
|
@@ -260,14 +283,76 @@ def scale_1024(
|
|
|
260
283
|
return scaled, power
|
|
261
284
|
|
|
262
285
|
|
|
286
|
+
@typing.overload
|
|
287
|
+
def remap(
|
|
288
|
+
value: decimal.Decimal,
|
|
289
|
+
old_min: decimal.Decimal | float,
|
|
290
|
+
old_max: decimal.Decimal | float,
|
|
291
|
+
new_min: decimal.Decimal | float,
|
|
292
|
+
new_max: decimal.Decimal | float,
|
|
293
|
+
) -> decimal.Decimal: ...
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@typing.overload
|
|
297
|
+
def remap(
|
|
298
|
+
value: decimal.Decimal | float,
|
|
299
|
+
old_min: decimal.Decimal,
|
|
300
|
+
old_max: decimal.Decimal | float,
|
|
301
|
+
new_min: decimal.Decimal | float,
|
|
302
|
+
new_max: decimal.Decimal | float,
|
|
303
|
+
) -> decimal.Decimal: ...
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@typing.overload
|
|
307
|
+
def remap(
|
|
308
|
+
value: decimal.Decimal | float,
|
|
309
|
+
old_min: decimal.Decimal | float,
|
|
310
|
+
old_max: decimal.Decimal,
|
|
311
|
+
new_min: decimal.Decimal | float,
|
|
312
|
+
new_max: decimal.Decimal | float,
|
|
313
|
+
) -> decimal.Decimal: ...
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@typing.overload
|
|
263
317
|
def remap(
|
|
318
|
+
value: decimal.Decimal | float,
|
|
319
|
+
old_min: decimal.Decimal | float,
|
|
320
|
+
old_max: decimal.Decimal | float,
|
|
321
|
+
new_min: decimal.Decimal,
|
|
322
|
+
new_max: decimal.Decimal | float,
|
|
323
|
+
) -> decimal.Decimal: ...
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@typing.overload
|
|
327
|
+
def remap(
|
|
328
|
+
value: decimal.Decimal | float,
|
|
329
|
+
old_min: decimal.Decimal | float,
|
|
330
|
+
old_max: decimal.Decimal | float,
|
|
331
|
+
new_min: decimal.Decimal | float,
|
|
332
|
+
new_max: decimal.Decimal,
|
|
333
|
+
) -> decimal.Decimal: ...
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# Note that float captures both int and float types so we don't need to
|
|
337
|
+
# specify them separately
|
|
338
|
+
@typing.overload
|
|
339
|
+
def remap(
|
|
340
|
+
value: float,
|
|
341
|
+
old_min: float,
|
|
342
|
+
old_max: float,
|
|
343
|
+
new_min: float,
|
|
344
|
+
new_max: float,
|
|
345
|
+
) -> float: ...
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def remap( # pyright: ignore[reportInconsistentOverload]
|
|
264
349
|
value: _TN,
|
|
265
350
|
old_min: _TN,
|
|
266
351
|
old_max: _TN,
|
|
267
352
|
new_min: _TN,
|
|
268
353
|
new_max: _TN,
|
|
269
354
|
) -> _TN:
|
|
270
|
-
|
|
355
|
+
"""
|
|
271
356
|
remap a value from one range into another.
|
|
272
357
|
|
|
273
358
|
>>> remap(500, 0, 1000, 0, 100)
|
|
@@ -312,33 +397,26 @@ def remap(
|
|
|
312
397
|
...
|
|
313
398
|
ValueError: Output range (0-0) is empty
|
|
314
399
|
|
|
315
|
-
:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
will get converted. The returned value type will be `decimal.Decimal`
|
|
336
|
-
of any of the passed parameters ar `decimal.Decimal`, the return type
|
|
337
|
-
will be `float` if any of the passed parameters are a `float` otherwise
|
|
338
|
-
the returned type will be `int`.
|
|
339
|
-
|
|
340
|
-
:rtype: int, float, decimal.Decimal
|
|
341
|
-
'''
|
|
400
|
+
Args:
|
|
401
|
+
value (int, float, decimal.Decimal): Value to be converted.
|
|
402
|
+
old_min (int, float, decimal.Decimal): Minimum of the range for the
|
|
403
|
+
value that has been passed.
|
|
404
|
+
old_max (int, float, decimal.Decimal): Maximum of the range for the
|
|
405
|
+
value that has been passed.
|
|
406
|
+
new_min (int, float, decimal.Decimal): The minimum of the new range.
|
|
407
|
+
new_max (int, float, decimal.Decimal): The maximum of the new range.
|
|
408
|
+
|
|
409
|
+
Returns: int, float, decimal.Decimal: Value that has been re-ranged. If
|
|
410
|
+
any of the parameters passed is a `decimal.Decimal`, all of the
|
|
411
|
+
parameters will be converted to `decimal.Decimal`. The same thing also
|
|
412
|
+
happens if one of the parameters is a `float`. Otherwise, all
|
|
413
|
+
parameters will get converted into an `int`. Technically, you can pass
|
|
414
|
+
a `str` of an integer and it will get converted. The returned value
|
|
415
|
+
type will be `decimal.Decimal` if any of the passed parameters are
|
|
416
|
+
`decimal.Decimal`, the return type will be `float` if any of the
|
|
417
|
+
passed parameters are a `float`, otherwise the returned type will be
|
|
418
|
+
`int`.
|
|
419
|
+
"""
|
|
342
420
|
type_: types.Type[types.DecimalNumber]
|
|
343
421
|
if (
|
|
344
422
|
isinstance(value, decimal.Decimal)
|
|
@@ -356,7 +434,6 @@ def remap(
|
|
|
356
434
|
or isinstance(new_max, float)
|
|
357
435
|
):
|
|
358
436
|
type_ = float
|
|
359
|
-
|
|
360
437
|
else:
|
|
361
438
|
type_ = int
|
|
362
439
|
|
|
@@ -377,13 +454,16 @@ def remap(
|
|
|
377
454
|
if new_range == 0:
|
|
378
455
|
raise ValueError(f'Output range ({new_min}-{new_max}) is empty')
|
|
379
456
|
|
|
380
|
-
|
|
457
|
+
# The current state of Python typing makes it impossible to use the
|
|
458
|
+
# generic type system in this case. Or so extremely verbose that it's not
|
|
459
|
+
# worth it.
|
|
460
|
+
new_value = (value - old_min) * new_range # type: ignore[operator] # pyright: ignore[reportOperatorIssue, reportUnknownVariableType]
|
|
381
461
|
|
|
382
|
-
if type_
|
|
383
|
-
new_value //= old_range #
|
|
462
|
+
if type_ is int:
|
|
463
|
+
new_value //= old_range # pyright: ignore[reportUnknownVariableType]
|
|
384
464
|
else:
|
|
385
|
-
new_value /= old_range #
|
|
465
|
+
new_value /= old_range # pyright: ignore[reportUnknownVariableType]
|
|
386
466
|
|
|
387
|
-
new_value += new_min # type: ignore
|
|
467
|
+
new_value += new_min # type: ignore[operator] # pyright: ignore[reportOperatorIssue, reportUnknownVariableType]
|
|
388
468
|
|
|
389
469
|
return types.cast(_TN, new_value)
|
python_utils/decorators.py
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides various utility decorators for Python functions
|
|
3
|
+
and methods.
|
|
4
|
+
|
|
5
|
+
The decorators include:
|
|
6
|
+
|
|
7
|
+
1. `set_attributes`: Sets attributes on functions and classes.
|
|
8
|
+
2. `listify`: Converts any generator to a list or other collection.
|
|
9
|
+
3. `sample`: Limits calls to a function based on a sample rate.
|
|
10
|
+
4. `wraps_classmethod`: Wraps classmethods with type info from a
|
|
11
|
+
regular method.
|
|
12
|
+
|
|
13
|
+
Each decorator is designed to enhance the functionality of Python
|
|
14
|
+
functions and methods in a simple and reusable manner.
|
|
15
|
+
"""
|
|
16
|
+
|
|
1
17
|
import contextlib
|
|
2
18
|
import functools
|
|
3
19
|
import logging
|
|
4
20
|
import random
|
|
21
|
+
|
|
5
22
|
from . import types
|
|
6
23
|
|
|
7
24
|
_T = types.TypeVar('_T')
|
|
8
|
-
_TC = types.TypeVar('_TC', bound=types.Container[types.Any])
|
|
9
25
|
_P = types.ParamSpec('_P')
|
|
10
26
|
_S = types.TypeVar('_S', covariant=True)
|
|
11
27
|
|
|
12
28
|
|
|
13
29
|
def set_attributes(**kwargs: types.Any) -> types.Callable[..., types.Any]:
|
|
14
|
-
|
|
30
|
+
"""Decorator to set attributes on functions and classes.
|
|
15
31
|
|
|
16
32
|
A common usage for this pattern is the Django Admin where
|
|
17
33
|
functions can get an optional short_description. To illustrate:
|
|
@@ -23,19 +39,19 @@ def set_attributes(**kwargs: types.Any) -> types.Callable[..., types.Any]:
|
|
|
23
39
|
|
|
24
40
|
>>> @set_attributes(short_description='Name')
|
|
25
41
|
... def upper_case_name(self, obj):
|
|
26
|
-
... return (
|
|
42
|
+
... return ('%s %s' % (obj.first_name, obj.last_name)).upper()
|
|
27
43
|
|
|
28
44
|
The standard Django version:
|
|
29
45
|
|
|
30
46
|
>>> def upper_case_name(obj):
|
|
31
|
-
... return (
|
|
47
|
+
... return ('%s %s' % (obj.first_name, obj.last_name)).upper()
|
|
32
48
|
|
|
33
49
|
>>> upper_case_name.short_description = 'Name'
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
"""
|
|
36
52
|
|
|
37
53
|
def _set_attributes(
|
|
38
|
-
function: types.Callable[_P, _T]
|
|
54
|
+
function: types.Callable[_P, _T],
|
|
39
55
|
) -> types.Callable[_P, _T]:
|
|
40
56
|
for key, value in kwargs.items():
|
|
41
57
|
setattr(function, key, value)
|
|
@@ -46,14 +62,14 @@ def set_attributes(**kwargs: types.Any) -> types.Callable[..., types.Any]:
|
|
|
46
62
|
|
|
47
63
|
def listify(
|
|
48
64
|
collection: types.Callable[
|
|
49
|
-
[types.Iterable[_T]],
|
|
50
|
-
] = list,
|
|
65
|
+
[types.Iterable[_T]], types.Collection[_T]
|
|
66
|
+
] = list,
|
|
51
67
|
allow_empty: bool = True,
|
|
52
68
|
) -> types.Callable[
|
|
53
69
|
[types.Callable[..., types.Optional[types.Iterable[_T]]]],
|
|
54
|
-
types.Callable[...,
|
|
70
|
+
types.Callable[..., types.Collection[_T]],
|
|
55
71
|
]:
|
|
56
|
-
|
|
72
|
+
"""
|
|
57
73
|
Convert any generator to a list or other type of collection.
|
|
58
74
|
|
|
59
75
|
>>> @listify()
|
|
@@ -97,12 +113,14 @@ def listify(
|
|
|
97
113
|
|
|
98
114
|
>>> dict_generator()
|
|
99
115
|
{'a': 1, 'b': 2}
|
|
100
|
-
|
|
116
|
+
"""
|
|
101
117
|
|
|
102
118
|
def _listify(
|
|
103
|
-
function: types.Callable[..., types.Optional[types.Iterable[_T]]]
|
|
104
|
-
) -> types.Callable[...,
|
|
105
|
-
def __listify(
|
|
119
|
+
function: types.Callable[..., types.Optional[types.Iterable[_T]]],
|
|
120
|
+
) -> types.Callable[..., types.Collection[_T]]:
|
|
121
|
+
def __listify(
|
|
122
|
+
*args: types.Any, **kwargs: types.Any
|
|
123
|
+
) -> types.Collection[_T]:
|
|
106
124
|
result: types.Optional[types.Iterable[_T]] = function(
|
|
107
125
|
*args, **kwargs
|
|
108
126
|
)
|
|
@@ -122,8 +140,13 @@ def listify(
|
|
|
122
140
|
return _listify
|
|
123
141
|
|
|
124
142
|
|
|
125
|
-
def sample(
|
|
126
|
-
|
|
143
|
+
def sample(
|
|
144
|
+
sample_rate: float,
|
|
145
|
+
) -> types.Callable[
|
|
146
|
+
[types.Callable[_P, _T]],
|
|
147
|
+
types.Callable[_P, types.Optional[_T]],
|
|
148
|
+
]:
|
|
149
|
+
"""
|
|
127
150
|
Limit calls to a function based on given sample rate.
|
|
128
151
|
Number of calls to the function will be roughly equal to
|
|
129
152
|
sample_rate percentage.
|
|
@@ -135,10 +158,10 @@ def sample(sample_rate: float):
|
|
|
135
158
|
... return 1
|
|
136
159
|
|
|
137
160
|
Calls to *demo_function* will be limited to 50% approximatly.
|
|
138
|
-
|
|
161
|
+
"""
|
|
139
162
|
|
|
140
163
|
def _sample(
|
|
141
|
-
function: types.Callable[_P, _T]
|
|
164
|
+
function: types.Callable[_P, _T],
|
|
142
165
|
) -> types.Callable[_P, types.Optional[_T]]:
|
|
143
166
|
@functools.wraps(function)
|
|
144
167
|
def __sample(
|
|
@@ -152,7 +175,7 @@ def sample(sample_rate: float):
|
|
|
152
175
|
function,
|
|
153
176
|
args,
|
|
154
177
|
kwargs,
|
|
155
|
-
)
|
|
178
|
+
)
|
|
156
179
|
return None
|
|
157
180
|
|
|
158
181
|
return __sample
|
|
@@ -166,16 +189,16 @@ def wraps_classmethod(
|
|
|
166
189
|
[
|
|
167
190
|
types.Callable[types.Concatenate[types.Any, _P], _T],
|
|
168
191
|
],
|
|
169
|
-
types.Callable[types.Concatenate[
|
|
192
|
+
types.Callable[types.Concatenate[_S, _P], _T],
|
|
170
193
|
]:
|
|
171
|
-
|
|
194
|
+
"""
|
|
172
195
|
Like `functools.wraps`, but for wrapping classmethods with the type info
|
|
173
|
-
from a regular method
|
|
174
|
-
|
|
196
|
+
from a regular method.
|
|
197
|
+
"""
|
|
175
198
|
|
|
176
199
|
def _wraps_classmethod(
|
|
177
200
|
wrapper: types.Callable[types.Concatenate[types.Any, _P], _T],
|
|
178
|
-
) -> types.Callable[types.Concatenate[
|
|
201
|
+
) -> types.Callable[types.Concatenate[_S, _P], _T]:
|
|
179
202
|
# For some reason `functools.update_wrapper` fails on some test
|
|
180
203
|
# runs but not while running actual code
|
|
181
204
|
with contextlib.suppress(AttributeError):
|
python_utils/exceptions.py
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for raising and reraising exceptions.
|
|
3
|
+
|
|
4
|
+
Functions:
|
|
5
|
+
raise_exception(exception_class, *args, **kwargs):
|
|
6
|
+
Returns a function that raises an exception of the given type with
|
|
7
|
+
the given arguments.
|
|
8
|
+
|
|
9
|
+
reraise(*args, **kwargs):
|
|
10
|
+
Reraises the current exception.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
from . import types
|
|
2
14
|
|
|
3
15
|
|
|
@@ -6,7 +18,7 @@ def raise_exception(
|
|
|
6
18
|
*args: types.Any,
|
|
7
19
|
**kwargs: types.Any,
|
|
8
20
|
) -> types.Callable[..., None]:
|
|
9
|
-
|
|
21
|
+
"""
|
|
10
22
|
Returns a function that raises an exception of the given type with the
|
|
11
23
|
given arguments.
|
|
12
24
|
|
|
@@ -14,7 +26,7 @@ def raise_exception(
|
|
|
14
26
|
Traceback (most recent call last):
|
|
15
27
|
...
|
|
16
28
|
ValueError: spam
|
|
17
|
-
|
|
29
|
+
"""
|
|
18
30
|
|
|
19
31
|
def raise_(*args_: types.Any, **kwargs_: types.Any) -> types.Any:
|
|
20
32
|
raise exception_class(*args, **kwargs)
|
|
@@ -23,4 +35,10 @@ def raise_exception(
|
|
|
23
35
|
|
|
24
36
|
|
|
25
37
|
def reraise(*args: types.Any, **kwargs: types.Any) -> types.Any:
|
|
38
|
+
"""
|
|
39
|
+
Reraises the current exception.
|
|
40
|
+
|
|
41
|
+
This function seems useless, but it can be useful when you need to pass
|
|
42
|
+
a callable to another function that raises an exception.
|
|
43
|
+
"""
|
|
26
44
|
raise
|
python_utils/formatters.py
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for formatting strings and dates.
|
|
3
|
+
|
|
4
|
+
Functions:
|
|
5
|
+
camel_to_underscore(name: str) -> str:
|
|
6
|
+
Convert camel case style naming to underscore/snake case style naming.
|
|
7
|
+
|
|
8
|
+
apply_recursive(function: Callable[[str], str], data: OptionalScope = None,
|
|
9
|
+
**kwargs: Any) -> OptionalScope:
|
|
10
|
+
Apply a function to all keys in a scope recursively.
|
|
11
|
+
|
|
12
|
+
timesince(dt: Union[datetime.datetime, datetime.timedelta],
|
|
13
|
+
default: str = 'just now') -> str:
|
|
14
|
+
Returns string representing 'time since' e.g. 3 days ago, 5 hours ago.
|
|
15
|
+
"""
|
|
16
|
+
|
|
1
17
|
# pyright: reportUnnecessaryIsInstance=false
|
|
2
18
|
import datetime
|
|
3
19
|
|
|
@@ -5,7 +21,7 @@ from python_utils import types
|
|
|
5
21
|
|
|
6
22
|
|
|
7
23
|
def camel_to_underscore(name: str) -> str:
|
|
8
|
-
|
|
24
|
+
"""Convert camel case style naming to underscore/snake case style naming.
|
|
9
25
|
|
|
10
26
|
If there are existing underscores they will be collapsed with the
|
|
11
27
|
to-be-added underscores. Multiple consecutive capital letters will not be
|
|
@@ -21,7 +37,7 @@ def camel_to_underscore(name: str) -> str:
|
|
|
21
37
|
'__spam_and_bacon__'
|
|
22
38
|
>>> camel_to_underscore('__SpamANDBacon__')
|
|
23
39
|
'__spam_and_bacon__'
|
|
24
|
-
|
|
40
|
+
"""
|
|
25
41
|
output: types.List[str] = []
|
|
26
42
|
for i, c in enumerate(name):
|
|
27
43
|
if i > 0:
|
|
@@ -47,14 +63,19 @@ def apply_recursive(
|
|
|
47
63
|
data: types.OptionalScope = None,
|
|
48
64
|
**kwargs: types.Any,
|
|
49
65
|
) -> types.OptionalScope:
|
|
50
|
-
|
|
51
|
-
Apply a function to all keys in a scope recursively
|
|
66
|
+
"""
|
|
67
|
+
Apply a function to all keys in a scope recursively.
|
|
52
68
|
|
|
53
69
|
>>> apply_recursive(camel_to_underscore, {'SpamEggsAndBacon': 'spam'})
|
|
54
70
|
{'spam_eggs_and_bacon': 'spam'}
|
|
55
|
-
>>> apply_recursive(
|
|
56
|
-
...
|
|
57
|
-
...
|
|
71
|
+
>>> apply_recursive(
|
|
72
|
+
... camel_to_underscore,
|
|
73
|
+
... {
|
|
74
|
+
... 'SpamEggsAndBacon': {
|
|
75
|
+
... 'SpamEggsAndBacon': 'spam',
|
|
76
|
+
... }
|
|
77
|
+
... },
|
|
78
|
+
... )
|
|
58
79
|
{'spam_eggs_and_bacon': {'spam_eggs_and_bacon': 'spam'}}
|
|
59
80
|
|
|
60
81
|
>>> a = {'a_b_c': 123, 'def': {'DeF': 456}}
|
|
@@ -63,7 +84,7 @@ def apply_recursive(
|
|
|
63
84
|
{'a_b_c': 123, 'def': {'de_f': 456}}
|
|
64
85
|
|
|
65
86
|
>>> apply_recursive(camel_to_underscore, None)
|
|
66
|
-
|
|
87
|
+
"""
|
|
67
88
|
if data is None:
|
|
68
89
|
return None
|
|
69
90
|
|
|
@@ -80,7 +101,7 @@ def timesince(
|
|
|
80
101
|
dt: types.Union[datetime.datetime, datetime.timedelta],
|
|
81
102
|
default: str = 'just now',
|
|
82
103
|
) -> str:
|
|
83
|
-
|
|
104
|
+
"""
|
|
84
105
|
Returns string representing 'time since' e.g.
|
|
85
106
|
3 days ago, 5 hours ago etc.
|
|
86
107
|
|
|
@@ -121,7 +142,7 @@ def timesince(
|
|
|
121
142
|
'1 hour and 2 minutes ago'
|
|
122
143
|
>>> timesince(datetime.timedelta(seconds=3721))
|
|
123
144
|
'1 hour and 2 minutes ago'
|
|
124
|
-
|
|
145
|
+
"""
|
|
125
146
|
if isinstance(dt, datetime.timedelta):
|
|
126
147
|
diff = dt
|
|
127
148
|
else:
|
|
@@ -140,11 +161,11 @@ def timesince(
|
|
|
140
161
|
|
|
141
162
|
output: types.List[str] = []
|
|
142
163
|
for period, singular, plural in periods:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
int_period = int(period)
|
|
165
|
+
if int_period == 1:
|
|
166
|
+
output.append(f'{int_period} {singular}')
|
|
167
|
+
elif int_period:
|
|
168
|
+
output.append(f'{int_period} {plural}')
|
|
148
169
|
|
|
149
170
|
if output:
|
|
150
171
|
return f'{" and ".join(output[:2])} ago'
|