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/__about__.py CHANGED
@@ -1,9 +1,37 @@
1
- __package_name__ = 'python-utils'
2
- __version__ = '2.5.6'
3
- __author__ = 'Rick van Hattem'
4
- __author_email__ = 'Wolph@wol.ph'
5
- __description__ = (
1
+ """
2
+ This module contains metadata about the `python-utils` package.
3
+
4
+ Attributes:
5
+ __package_name__ (str): The name of the package.
6
+ __author__ (str): The author of the package.
7
+ __author_email__ (str): The email of the author.
8
+ __description__ (str): A brief description of the package.
9
+ __url__ (str): The URL of the package's repository.
10
+ __version__ (str): The current version, read from the installed metadata.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from importlib import metadata
16
+
17
+ #: Distribution name as published on PyPI.
18
+ __package_name__: str = 'python-utils'
19
+ #: Primary author's name.
20
+ __author__: str = 'Rick van Hattem'
21
+ #: Primary author's contact email.
22
+ __author_email__: str = 'Wolph@wol.ph'
23
+ #: One-line description of the package.
24
+ __description__: str = (
6
25
  'Python Utils is a module with some convenient utilities not included '
7
- 'with the standard Python install')
8
- __url__ = 'https://github.com/WoLpH/python-utils'
26
+ 'with the standard Python install'
27
+ )
28
+ #: Canonical project/repository URL.
29
+ __url__: str = 'https://github.com/WoLpH/python-utils'
9
30
 
31
+ try:
32
+ # `[project].version` in pyproject.toml is the single source of truth;
33
+ # read it back at runtime so the two never drift.
34
+ __version__: str = metadata.version(__package_name__)
35
+ except metadata.PackageNotFoundError: # pragma: no cover
36
+ # Not installed (e.g. running straight from a source checkout).
37
+ __version__ = '0.0.0'
python_utils/__init__.py CHANGED
@@ -0,0 +1,241 @@
1
+ """
2
+ This module initializes the `python_utils` package by importing various
3
+ submodules and functions.
4
+
5
+ Imports are performed lazily (PEP 562): nothing is imported when you ``import
6
+ python_utils``; each submodule/function is loaded on first access. This keeps
7
+ ``import python_utils`` cheap and, in particular, avoids eagerly importing
8
+ ``asyncio`` (via the async helpers) for consumers that only need the
9
+ synchronous utilities.
10
+
11
+ Submodules::
12
+
13
+ aio
14
+ converters
15
+ decorators
16
+ formatters
17
+ generators
18
+ import_
19
+ logger
20
+ terminal
21
+ time
22
+ types
23
+
24
+ Functions::
25
+
26
+ acount
27
+ remap
28
+ scale_1024
29
+ to_float
30
+ to_int
31
+ to_str
32
+ to_unicode
33
+ listify
34
+ set_attributes
35
+ raise_exception
36
+ reraise
37
+ camel_to_underscore
38
+ timesince
39
+ abatcher
40
+ batcher
41
+ import_global
42
+ get_terminal_size
43
+ aio_generator_timeout_detector
44
+ aio_generator_timeout_detector_decorator
45
+ aio_timeout_generator
46
+ delta_to_seconds
47
+ delta_to_seconds_or_none
48
+ format_time
49
+ timedelta_to_seconds
50
+ timeout_generator
51
+
52
+ Classes::
53
+
54
+ CastedDict
55
+ LazyCastedDict
56
+ UniqueList
57
+ Logged
58
+ LoggerBase
59
+ """
60
+
61
+ import importlib as _importlib
62
+ import typing as _typing
63
+
64
+ if _typing.TYPE_CHECKING: # pragma: no cover
65
+ # Eager imports for type checkers only; the runtime equivalents are loaded
66
+ # lazily by ``__getattr__`` below. Names appear in ``__all__`` so they are
67
+ # treated as re-exports (not unused imports).
68
+ from . import (
69
+ aio,
70
+ converters,
71
+ decorators,
72
+ formatters,
73
+ generators,
74
+ import_,
75
+ logger,
76
+ terminal,
77
+ time,
78
+ types,
79
+ )
80
+ from .__about__ import __version__
81
+ from .aio import acount
82
+ from .containers import CastedDict, LazyCastedDict, UniqueList
83
+ from .converters import (
84
+ remap,
85
+ scale_1024,
86
+ to_float,
87
+ to_int,
88
+ to_str,
89
+ to_unicode,
90
+ )
91
+ from .decorators import listify, set_attributes
92
+ from .exceptions import raise_exception, reraise
93
+ from .formatters import camel_to_underscore, timesince
94
+ from .generators import abatcher, batcher
95
+ from .import_ import import_global
96
+ from .logger import Logged, LoggerBase
97
+ from .terminal import get_terminal_size
98
+ from .time import (
99
+ aio_generator_timeout_detector,
100
+ aio_generator_timeout_detector_decorator,
101
+ aio_timeout_generator,
102
+ delta_to_seconds,
103
+ delta_to_seconds_or_none,
104
+ format_time,
105
+ timedelta_to_seconds,
106
+ timeout_generator,
107
+ )
108
+
109
+ #: Submodules that can be accessed as ``python_utils.<name>``.
110
+ _SUBMODULES: frozenset[str] = frozenset(
111
+ {
112
+ 'aio',
113
+ 'containers',
114
+ 'converters',
115
+ 'decorators',
116
+ 'exceptions',
117
+ 'formatters',
118
+ 'generators',
119
+ 'import_',
120
+ 'logger',
121
+ 'terminal',
122
+ 'time',
123
+ 'types',
124
+ }
125
+ )
126
+
127
+ #: Exported name -> submodule it lives in.
128
+ _NAME_TO_MODULE: dict[str, str] = {
129
+ '__version__': '__about__',
130
+ 'acount': 'aio',
131
+ 'CastedDict': 'containers',
132
+ 'LazyCastedDict': 'containers',
133
+ 'UniqueList': 'containers',
134
+ 'remap': 'converters',
135
+ 'scale_1024': 'converters',
136
+ 'to_float': 'converters',
137
+ 'to_int': 'converters',
138
+ 'to_str': 'converters',
139
+ 'to_unicode': 'converters',
140
+ 'listify': 'decorators',
141
+ 'set_attributes': 'decorators',
142
+ 'raise_exception': 'exceptions',
143
+ 'reraise': 'exceptions',
144
+ 'camel_to_underscore': 'formatters',
145
+ 'timesince': 'formatters',
146
+ 'abatcher': 'generators',
147
+ 'batcher': 'generators',
148
+ 'import_global': 'import_',
149
+ 'Logged': 'logger',
150
+ 'LoggerBase': 'logger',
151
+ 'get_terminal_size': 'terminal',
152
+ 'aio_generator_timeout_detector': 'time',
153
+ 'aio_generator_timeout_detector_decorator': 'time',
154
+ 'aio_timeout_generator': 'time',
155
+ 'delta_to_seconds': 'time',
156
+ 'delta_to_seconds_or_none': 'time',
157
+ 'format_time': 'time',
158
+ 'timedelta_to_seconds': 'time',
159
+ 'timeout_generator': 'time',
160
+ }
161
+
162
+
163
+ def __getattr__(name: str) -> _typing.Any:
164
+ """Lazily import a submodule or exported name on first access (PEP 562).
165
+
166
+ Args:
167
+ name: Attribute requested on the ``python_utils`` package.
168
+
169
+ Returns:
170
+ The imported submodule or object. It is cached in ``globals()`` so
171
+ this hook runs only once per name.
172
+
173
+ Raises:
174
+ AttributeError: If ``name`` is not a known submodule or export.
175
+ """
176
+ if name in _SUBMODULES:
177
+ module = _importlib.import_module(f'.{name}', __name__)
178
+ elif name in _NAME_TO_MODULE:
179
+ module = _importlib.import_module(
180
+ f'.{_NAME_TO_MODULE[name]}', __name__
181
+ )
182
+ value = getattr(module, name)
183
+ globals()[name] = value # cache so __getattr__ runs only once
184
+ return value
185
+ else:
186
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
187
+
188
+ globals()[name] = module
189
+ return module
190
+
191
+
192
+ def __dir__() -> list[str]:
193
+ """List all eager and lazily-available names (for tab-completion)."""
194
+ return sorted(
195
+ set(globals()) | set(__all__) | _SUBMODULES | set(_NAME_TO_MODULE)
196
+ )
197
+
198
+
199
+ __all__ = [
200
+ 'CastedDict',
201
+ 'LazyCastedDict',
202
+ 'Logged',
203
+ 'LoggerBase',
204
+ 'UniqueList',
205
+ '__version__',
206
+ 'abatcher',
207
+ 'acount',
208
+ 'aio',
209
+ 'aio_generator_timeout_detector',
210
+ 'aio_generator_timeout_detector_decorator',
211
+ 'aio_timeout_generator',
212
+ 'batcher',
213
+ 'camel_to_underscore',
214
+ 'converters',
215
+ 'decorators',
216
+ 'delta_to_seconds',
217
+ 'delta_to_seconds_or_none',
218
+ 'format_time',
219
+ 'formatters',
220
+ 'generators',
221
+ 'get_terminal_size',
222
+ 'import_',
223
+ 'import_global',
224
+ 'listify',
225
+ 'logger',
226
+ 'raise_exception',
227
+ 'remap',
228
+ 'reraise',
229
+ 'scale_1024',
230
+ 'set_attributes',
231
+ 'terminal',
232
+ 'time',
233
+ 'timedelta_to_seconds',
234
+ 'timeout_generator',
235
+ 'timesince',
236
+ 'to_float',
237
+ 'to_int',
238
+ 'to_str',
239
+ 'to_unicode',
240
+ 'types',
241
+ ]
@@ -0,0 +1,53 @@
1
+ """Lightweight, stdlib-only type aliases shared across python_utils.
2
+
3
+ These live here (rather than in ``python_utils.types``) so internal modules can
4
+ import them without dragging in ``typing_extensions``. ``python_utils.types``
5
+ re-exports everything defined here, so the public names are unchanged.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import datetime
11
+ import decimal
12
+ from typing import Any
13
+
14
+ __all__ = [
15
+ 'DecimalNumber',
16
+ 'ExceptionType',
17
+ 'ExceptionsType',
18
+ 'Number',
19
+ 'OptionalScope',
20
+ 'Scope',
21
+ 'StringTypes',
22
+ 'delta_type',
23
+ 'timestamp_type',
24
+ ]
25
+
26
+ #: A namespace mapping, e.g. ``locals()``/``globals()`` (name -> value).
27
+ Scope = dict[str, Any]
28
+ #: A :data:`Scope`, or ``None`` when no namespace is supplied.
29
+ OptionalScope = Scope | None
30
+ #: Any plain (non-decimal) number: an ``int`` or a ``float``.
31
+ Number = int | float
32
+ #: A :data:`Number` or a :class:`decimal.Decimal`, for precise arithmetic.
33
+ DecimalNumber = Number | decimal.Decimal
34
+ #: An exception class (not an instance), e.g. ``ValueError``.
35
+ ExceptionType = type[Exception]
36
+ #: One exception class or a tuple of them, as accepted by ``except``.
37
+ ExceptionsType = tuple[ExceptionType, ...] | ExceptionType
38
+ #: Text-like data: ``str`` or ``bytes``.
39
+ StringTypes = str | bytes
40
+
41
+ #: A time interval expressed as a ``timedelta`` or a number of seconds.
42
+ delta_type = datetime.timedelta | int | float
43
+ #: Anything :func:`~python_utils.time.format_time` can render: a duration, a
44
+ #: date/datetime, a numeric/str timestamp, or ``None``.
45
+ timestamp_type = (
46
+ datetime.timedelta
47
+ | datetime.date
48
+ | datetime.datetime
49
+ | str
50
+ | int
51
+ | float
52
+ | None
53
+ )
python_utils/aio.py ADDED
@@ -0,0 +1,133 @@
1
+ """Asyncio equivalents of common synchronous helpers.
2
+
3
+ These bring ``itertools``-style ergonomics to ``async for``: ``acount`` is an
4
+ async counter, while ``acontainer`` and ``adict`` collect an async iterable
5
+ into a concrete container.
6
+ """
7
+
8
+ import asyncio
9
+ import collections.abc
10
+ import itertools
11
+ import typing
12
+
13
+ #: Numeric type (``int`` or ``float``) produced by ``acount``.
14
+ _N = typing.TypeVar('_N', int, float)
15
+ #: Element type of the async iterables being consumed.
16
+ _T = typing.TypeVar('_T')
17
+ #: Key type when collecting an async iterable of pairs into a dict.
18
+ _K = typing.TypeVar('_K')
19
+ #: Value type when collecting an async iterable of pairs into a dict.
20
+ _V = typing.TypeVar('_V')
21
+
22
+
23
+ async def acount(
24
+ start: _N = 0,
25
+ step: _N = 1,
26
+ delay: float = 0,
27
+ stop: _N | None = None,
28
+ ) -> collections.abc.AsyncIterator[_N]:
29
+ """Async equivalent of ``itertools.count()``.
30
+
31
+ Counts from ``start`` in steps of ``step``, sleeping ``delay`` seconds
32
+ between values, and stops once ``stop`` (when given) is reached.
33
+
34
+ Args:
35
+ start: First value to yield.
36
+ step: Amount added between successive values.
37
+ delay: Seconds to ``asyncio.sleep`` between yields.
38
+ stop: Exclusive upper bound; ``None`` counts forever.
39
+
40
+ Yields:
41
+ The successive counter values.
42
+
43
+ >>> async def demo():
44
+ ... return [i async for i in acount(stop=3)]
45
+ >>> asyncio.run(demo())
46
+ [0, 1, 2]
47
+ """
48
+ for item in itertools.count(start, step): # pragma: no branch
49
+ if stop is not None and item >= stop:
50
+ break
51
+
52
+ yield item
53
+ await asyncio.sleep(delay)
54
+
55
+
56
+ @typing.overload
57
+ async def acontainer(
58
+ iterable: collections.abc.AsyncIterable[_T]
59
+ | collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
60
+ container: type[tuple[_T, ...]],
61
+ ) -> tuple[_T, ...]:
62
+ """Overload: collect the async iterable into a ``tuple``."""
63
+
64
+
65
+ @typing.overload
66
+ async def acontainer(
67
+ iterable: collections.abc.AsyncIterable[_T]
68
+ | collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
69
+ container: type[list[_T]] = list,
70
+ ) -> list[_T]:
71
+ """Overload: collect the async iterable into a ``list`` (the default)."""
72
+
73
+
74
+ @typing.overload
75
+ async def acontainer(
76
+ iterable: collections.abc.AsyncIterable[_T]
77
+ | collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
78
+ container: type[set[_T]],
79
+ ) -> set[_T]:
80
+ """Overload: collect the async iterable into a ``set``."""
81
+
82
+
83
+ async def acontainer(
84
+ iterable: collections.abc.AsyncIterable[_T]
85
+ | collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
86
+ container: collections.abc.Callable[
87
+ [collections.abc.Iterable[_T]], collections.abc.Collection[_T]
88
+ ] = list,
89
+ ) -> collections.abc.Collection[_T]:
90
+ """
91
+ Asyncio version of list()/set()/tuple()/etc() using an async for loop.
92
+
93
+ So instead of doing `[item async for item in iterable]` you can do
94
+ `await acontainer(iterable)`.
95
+
96
+ """
97
+ iterable_: collections.abc.AsyncIterable[_T]
98
+ if callable(iterable):
99
+ iterable_ = iterable()
100
+ else:
101
+ iterable_ = iterable
102
+
103
+ items: list[_T] = [item async for item in iterable_]
104
+
105
+ return container(items)
106
+
107
+
108
+ async def adict(
109
+ iterable: collections.abc.AsyncIterable[tuple[_K, _V]]
110
+ | collections.abc.Callable[
111
+ ..., collections.abc.AsyncIterable[tuple[_K, _V]]
112
+ ],
113
+ container: collections.abc.Callable[
114
+ [collections.abc.Iterable[tuple[_K, _V]]],
115
+ collections.abc.Mapping[_K, _V],
116
+ ] = dict,
117
+ ) -> collections.abc.Mapping[_K, _V]:
118
+ """
119
+ Asyncio version of dict() using an async for loop.
120
+
121
+ So instead of doing `{key: value async for key, value in iterable}` you
122
+ can do `await adict(iterable)`.
123
+
124
+ """
125
+ iterable_: collections.abc.AsyncIterable[tuple[_K, _V]]
126
+ if callable(iterable):
127
+ iterable_ = iterable()
128
+ else:
129
+ iterable_ = iterable
130
+
131
+ items: list[tuple[_K, _V]] = [item async for item in iterable_]
132
+
133
+ return container(items)