python-utils 3.9.1__tar.gz → 4.0.0__tar.gz
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-4.0.0/PKG-INFO +389 -0
- python_utils-4.0.0/README.md +347 -0
- python_utils-4.0.0/_python_utils_tests/__init__.py +1 -0
- python_utils-4.0.0/_python_utils_tests/test_aio.py +79 -0
- python_utils-4.0.0/_python_utils_tests/test_aliases.py +76 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_containers.py +7 -0
- python_utils-4.0.0/_python_utils_tests/test_decorators.py +82 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_generators.py +8 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_import.py +9 -0
- python_utils-4.0.0/_python_utils_tests/test_import_footprint.py +106 -0
- python_utils-4.0.0/_python_utils_tests/test_lazy_imports.py +113 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_logger.py +7 -3
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_python_utils.py +3 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/test_time.py +23 -7
- python_utils-4.0.0/pyproject.toml +165 -0
- python_utils-4.0.0/python_utils/__about__.py +37 -0
- python_utils-4.0.0/python_utils/__init__.py +241 -0
- python_utils-4.0.0/python_utils/_aliases.py +53 -0
- python_utils-4.0.0/python_utils/aio.py +133 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/containers.py +92 -78
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/converters.py +58 -47
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/decorators.py +56 -36
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/exceptions.py +11 -8
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/formatters.py +14 -18
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/generators.py +25 -25
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/import_.py +12 -10
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/logger.py +77 -42
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/loguru.py +3 -1
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/terminal.py +41 -12
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/time.py +140 -98
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/types.py +24 -26
- python_utils-4.0.0/tox.ini +87 -0
- python_utils-3.9.1/MANIFEST.in +0 -12
- python_utils-3.9.1/PKG-INFO +0 -309
- python_utils-3.9.1/README.rst +0 -277
- python_utils-3.9.1/_python_utils_tests/__init__.py +0 -0
- python_utils-3.9.1/_python_utils_tests/test_aio.py +0 -68
- python_utils-3.9.1/_python_utils_tests/test_decorators.py +0 -71
- python_utils-3.9.1/coverage.rc +0 -3
- python_utils-3.9.1/pyproject.toml +0 -20
- python_utils-3.9.1/pytest.ini +0 -16
- python_utils-3.9.1/python_utils/__about__.py +0 -22
- python_utils-3.9.1/python_utils/__init__.py +0 -126
- python_utils-3.9.1/python_utils/aio.py +0 -117
- python_utils-3.9.1/python_utils.egg-info/PKG-INFO +0 -309
- python_utils-3.9.1/python_utils.egg-info/SOURCES.txt +0 -41
- python_utils-3.9.1/python_utils.egg-info/dependency_links.txt +0 -1
- python_utils-3.9.1/python_utils.egg-info/requires.txt +0 -23
- python_utils-3.9.1/python_utils.egg-info/top_level.txt +0 -1
- python_utils-3.9.1/requirements.txt +0 -1
- python_utils-3.9.1/setup.cfg +0 -46
- python_utils-3.9.1/setup.py +0 -70
- python_utils-3.9.1/tox.ini +0 -64
- {python_utils-3.9.1 → python_utils-4.0.0}/LICENSE +0 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/_python_utils_tests/requirements.txt +0 -0
- {python_utils-3.9.1 → python_utils-4.0.0}/python_utils/py.typed +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-utils
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
|
|
5
|
+
Keywords: utils,utilities,helpers,typing,typed,async,lazy-imports
|
|
6
|
+
Author: Rick van Hattem
|
|
7
|
+
Author-email: Rick van Hattem <wolph@wol.ph>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: typing-extensions>3.10.0.2
|
|
24
|
+
Requires-Dist: sphinx ; extra == 'docs'
|
|
25
|
+
Requires-Dist: furo ; extra == 'docs'
|
|
26
|
+
Requires-Dist: myst-parser ; extra == 'docs'
|
|
27
|
+
Requires-Dist: loguru ; extra == 'loguru'
|
|
28
|
+
Requires-Dist: pytest ; extra == 'tests'
|
|
29
|
+
Requires-Dist: pytest-cov ; extra == 'tests'
|
|
30
|
+
Requires-Dist: pytest-asyncio ; extra == 'tests'
|
|
31
|
+
Requires-Dist: loguru ; extra == 'tests'
|
|
32
|
+
Requires-Dist: blessings ; extra == 'tests'
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Project-URL: Homepage, https://github.com/WoLpH/python-utils
|
|
35
|
+
Project-URL: Documentation, https://python-utils.readthedocs.io/
|
|
36
|
+
Project-URL: Repository, https://github.com/WoLpH/python-utils
|
|
37
|
+
Project-URL: Changelog, https://github.com/WoLpH/python-utils/releases
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Provides-Extra: loguru
|
|
40
|
+
Provides-Extra: tests
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
<div align="center">
|
|
44
|
+
|
|
45
|
+
<img src="https://raw.githubusercontent.com/WoLpH/python-utils/develop/docs/_static/banner.svg" alt="python-utils" width="720">
|
|
46
|
+
|
|
47
|
+
# ⚡ Python Utils
|
|
48
|
+
|
|
49
|
+
**The fast, fully-typed stdlib helpers you keep rewriting — in one tiny, dependency-light package.**
|
|
50
|
+
|
|
51
|
+
[](https://pypi.python.org/pypi/python-utils)
|
|
52
|
+
[](https://pypi.python.org/pypi/python-utils)
|
|
53
|
+
[](https://github.com/WoLpH/python-utils/actions/workflows/ci.yml)
|
|
54
|
+
[](https://coveralls.io/github/WoLpH/python-utils?branch=develop)
|
|
55
|
+
[](https://github.com/WoLpH/python-utils)
|
|
56
|
+
[](https://github.com/astral-sh/ruff)
|
|
57
|
+
[](https://github.com/WoLpH/python-utils/blob/develop/LICENSE)
|
|
58
|
+
[](https://pypi.python.org/pypi/python-utils)
|
|
59
|
+
|
|
60
|
+
[**Documentation**](https://python-utils.readthedocs.io/en/latest/) ·
|
|
61
|
+
[**PyPI**](https://pypi.python.org/pypi/python-utils) ·
|
|
62
|
+
[**Source**](https://github.com/WoLpH/python-utils) ·
|
|
63
|
+
[**Issues**](https://github.com/WoLpH/python-utils/issues)
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
Python Utils is a collection of small, battle-tested functions and classes that
|
|
70
|
+
make everyday Python patterns shorter, safer and faster. No sprawling framework,
|
|
71
|
+
no heavy dependencies — just the helpers you find yourself re-writing in project
|
|
72
|
+
after project, packaged once and typed to the hilt.
|
|
73
|
+
|
|
74
|
+
It has powered production code for years (and is used by libraries such as
|
|
75
|
+
[Django Utils](https://pypi.python.org/pypi/django-utils2) and
|
|
76
|
+
[progressbar2](https://pypi.python.org/pypi/progressbar2)).
|
|
77
|
+
|
|
78
|
+
## ✨ Highlights
|
|
79
|
+
|
|
80
|
+
- 🪶 **Zero-cost imports** — thanks to [PEP 562][pep562] lazy loading, `import
|
|
81
|
+
python_utils` pulls in *nothing* until you actually touch a helper. No
|
|
82
|
+
`asyncio`, no `typing_extensions`, until you ask for them.
|
|
83
|
+
- ⚡ **Async-native** — `acount`, `abatcher`, and timeout/stall detectors bring
|
|
84
|
+
`itertools`-style ergonomics to `async for`.
|
|
85
|
+
- 📦 **Smart containers** — self-casting dicts, duplicate-proof lists and a
|
|
86
|
+
sliceable deque.
|
|
87
|
+
- 🔢 **Forgiving converters** — pull an `int`/`float` out of *any* messy string,
|
|
88
|
+
scale bytes to KiB/MiB, remap values between ranges (with `Decimal` precision).
|
|
89
|
+
- ⏱️ **Time & retries** — human-readable durations plus timeout generators for
|
|
90
|
+
sampling slow APIs without hanging.
|
|
91
|
+
- 🎯 **Fully typed & 100% covered** — ships `py.typed`, passes **mypy**,
|
|
92
|
+
**basedpyright** *and* **pyrefly** in strict mode, with 100% test coverage.
|
|
93
|
+
- 🐍 **Modern & tiny** — Python 3.10+, a single runtime dependency
|
|
94
|
+
(`typing_extensions`), BSD-3 licensed.
|
|
95
|
+
|
|
96
|
+
## 🗺️ What's inside
|
|
97
|
+
|
|
98
|
+
| Module | What you get |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| [`converters`](https://python-utils.readthedocs.io/en/latest/) | `to_int` · `to_float` · `to_str` · `to_unicode` · `scale_1024` · `remap` |
|
|
101
|
+
| [`formatters`](https://python-utils.readthedocs.io/en/latest/) | `camel_to_underscore` · `apply_recursive` · `timesince` |
|
|
102
|
+
| [`time`](https://python-utils.readthedocs.io/en/latest/) | `format_time` · `timeout_generator` · `aio_timeout_generator` · `aio_generator_timeout_detector` |
|
|
103
|
+
| [`generators`](https://python-utils.readthedocs.io/en/latest/) | `batcher` · `abatcher` (batch by size **or** time interval) |
|
|
104
|
+
| [`aio`](https://python-utils.readthedocs.io/en/latest/) | `acount` · `acontainer` — async `itertools` |
|
|
105
|
+
| [`containers`](https://python-utils.readthedocs.io/en/latest/) | `CastedDict` · `LazyCastedDict` · `UniqueList` · `SliceableDeque` |
|
|
106
|
+
| [`decorators`](https://python-utils.readthedocs.io/en/latest/) | `listify` · `set_attributes` · `sample` · `wraps_classmethod` |
|
|
107
|
+
| [`logger`](https://python-utils.readthedocs.io/en/latest/) | `Logged` · `LoggerBase` (+ `Logurud` via the `loguru` extra) |
|
|
108
|
+
| [`import_`](https://python-utils.readthedocs.io/en/latest/) | `import_global` — programmatic `from x import *` |
|
|
109
|
+
| [`exceptions`](https://python-utils.readthedocs.io/en/latest/) | `raise_exception` · `reraise` |
|
|
110
|
+
| [`terminal`](https://python-utils.readthedocs.io/en/latest/) | `get_terminal_size` — works in shells, IPython & Jupyter |
|
|
111
|
+
| [`types`](https://python-utils.readthedocs.io/en/latest/) | handy type aliases (`Number`, `Scope`, `StringTypes`, …) |
|
|
112
|
+
|
|
113
|
+
## 📦 Installation
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install python-utils
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Optional extras:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install 'python-utils[loguru]' # loguru-backed logging mixin
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Python **3.10+** is required. The only runtime dependency is
|
|
126
|
+
`typing_extensions` (and it's imported lazily).
|
|
127
|
+
|
|
128
|
+
## 🚀 Quickstart
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import python_utils
|
|
132
|
+
|
|
133
|
+
# Pull a number out of any messy string
|
|
134
|
+
python_utils.to_int('listening on port=8080', regexp=True) # 8080
|
|
135
|
+
|
|
136
|
+
# Human-readable sizes: (value, power-of-1024)
|
|
137
|
+
python_utils.scale_1024(1536, 2) # (1.5, 1) -> 1.5 KiB
|
|
138
|
+
|
|
139
|
+
# Remap a value between ranges (46% volume -> dB on an AVR)
|
|
140
|
+
python_utils.remap(46.0, 0.0, 100.0, -80.0, 10.0) # -38.6
|
|
141
|
+
|
|
142
|
+
# "time ago" formatting, Django-style
|
|
143
|
+
import datetime
|
|
144
|
+
python_utils.timesince(datetime.datetime.now() - datetime.timedelta(seconds=61))
|
|
145
|
+
# '1 minute and 1 second ago'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Everything is reachable straight off the top-level package (`python_utils.<name>`)
|
|
149
|
+
or from its submodule (`python_utils.converters.to_int`) — pick whichever reads
|
|
150
|
+
better. Either way, only the modules you touch get imported.
|
|
151
|
+
|
|
152
|
+
## 🧰 Examples
|
|
153
|
+
|
|
154
|
+
<details open>
|
|
155
|
+
<summary><b>🔢 Converters — numbers out of anything</b></summary>
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from python_utils import converters
|
|
159
|
+
|
|
160
|
+
# Extract digits with a built-in or custom regexp
|
|
161
|
+
converters.to_int('spam15eggs', regexp=True) # 15
|
|
162
|
+
converters.to_int('nope', default=-1) # -1
|
|
163
|
+
converters.to_float('pi is 3.14', regexp=True) # 3.14
|
|
164
|
+
|
|
165
|
+
# Scale bytes to a sensible unit (value, power) -> 2.0 KiB
|
|
166
|
+
converters.scale_1024(2048, 3) # (2.0, 1)
|
|
167
|
+
|
|
168
|
+
# Linear remap; pass a Decimal anywhere to keep full precision
|
|
169
|
+
converters.remap(500, 0, 1000, 0, 100) # 50
|
|
170
|
+
import decimal
|
|
171
|
+
converters.remap(decimal.Decimal('250.0'), 0.0, 1000.0, 0.0, 100.0)
|
|
172
|
+
# Decimal('25.0')
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
</details>
|
|
176
|
+
|
|
177
|
+
<details>
|
|
178
|
+
<summary><b>📦 Containers — dicts & lists with super-powers</b></summary>
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from python_utils import containers
|
|
182
|
+
|
|
183
|
+
# Keys and values are cast on the way in
|
|
184
|
+
d = containers.CastedDict(int, int)
|
|
185
|
+
d['3'] = '4'
|
|
186
|
+
d.update({'5': '6'})
|
|
187
|
+
d # {3: 4, 5: 6}
|
|
188
|
+
|
|
189
|
+
# A list that silently drops duplicates (or raises, if you prefer)
|
|
190
|
+
u = containers.UniqueList(1, 2, 3)
|
|
191
|
+
u.append(2) # ignored
|
|
192
|
+
u # [1, 2, 3]
|
|
193
|
+
|
|
194
|
+
# A deque you can actually slice
|
|
195
|
+
s = containers.SliceableDeque([1, 2, 3, 4, 5])
|
|
196
|
+
s[1:4] # SliceableDeque([2, 3, 4])
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
</details>
|
|
200
|
+
|
|
201
|
+
<details>
|
|
202
|
+
<summary><b>⚡ Async helpers — <code>itertools</code> for <code>async for</code></b></summary>
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from python_utils import aio, generators
|
|
206
|
+
|
|
207
|
+
# Async counter (optionally with a delay and a stop value)
|
|
208
|
+
async def demo():
|
|
209
|
+
async for i in aio.acount(stop=3):
|
|
210
|
+
print(i) # 0, 1, 2
|
|
211
|
+
|
|
212
|
+
# Batch an async stream by size OR time interval — whichever comes first.
|
|
213
|
+
# Great for chunking bursty producers without ever stalling a slow loop.
|
|
214
|
+
async def batched():
|
|
215
|
+
async for batch in generators.abatcher(aio.acount(stop=10), batch_size=3):
|
|
216
|
+
print(batch) # [0, 1, 2], [3, 4, 5], [6, 7, 8], [9]
|
|
217
|
+
|
|
218
|
+
# Sync batching too:
|
|
219
|
+
list(generators.batcher(range(9), 3)) # [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
</details>
|
|
223
|
+
|
|
224
|
+
<details>
|
|
225
|
+
<summary><b>⏱️ Time & retries — sample slow APIs, format durations</b></summary>
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
import datetime
|
|
229
|
+
from python_utils import time
|
|
230
|
+
|
|
231
|
+
# Loop over a slow operation, but give up after `timeout` seconds
|
|
232
|
+
for i in time.timeout_generator(0.1, interval=0.06):
|
|
233
|
+
... # yields 0, 1, 2 then stops
|
|
234
|
+
|
|
235
|
+
# Format timedeltas, datetimes and raw seconds uniformly
|
|
236
|
+
time.format_time(1) # '0:00:01'
|
|
237
|
+
time.format_time(datetime.timedelta(seconds=3661)) # '1:01:01'
|
|
238
|
+
time.format_time(datetime.datetime(2000, 1, 2, 3, 4, 5)) # '2000-01-02 03:04:05'
|
|
239
|
+
time.format_time(None) # '--:--:--'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
There's also `aio_timeout_generator` (the `async for` twin) and
|
|
243
|
+
`aio_generator_timeout_detector`, which fails fast when an async generator
|
|
244
|
+
stalls instead of hanging forever.
|
|
245
|
+
|
|
246
|
+
</details>
|
|
247
|
+
|
|
248
|
+
<details>
|
|
249
|
+
<summary><b>🔤 Formatters — case conversion & friendly timestamps</b></summary>
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from python_utils import formatters
|
|
253
|
+
|
|
254
|
+
formatters.camel_to_underscore('SpamEggsAndBacon') # 'spam_eggs_and_bacon'
|
|
255
|
+
|
|
256
|
+
# Recursively rewrite every key in a nested dict
|
|
257
|
+
formatters.apply_recursive(
|
|
258
|
+
formatters.camel_to_underscore,
|
|
259
|
+
{'SpamEggs': {'FooBar': 1}},
|
|
260
|
+
) # {'spam_eggs': {'foo_bar': 1}}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
</details>
|
|
264
|
+
|
|
265
|
+
<details>
|
|
266
|
+
<summary><b>🎀 Decorators — collect generators, tag functions, sample calls</b></summary>
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from python_utils import decorators
|
|
270
|
+
|
|
271
|
+
# Turn a generator into a concrete collection automatically
|
|
272
|
+
@decorators.listify()
|
|
273
|
+
def numbers():
|
|
274
|
+
yield 1
|
|
275
|
+
yield 2
|
|
276
|
+
yield 3
|
|
277
|
+
|
|
278
|
+
numbers() # [1, 2, 3]
|
|
279
|
+
|
|
280
|
+
@decorators.listify(collection=dict)
|
|
281
|
+
def pairs():
|
|
282
|
+
yield 'a', 1
|
|
283
|
+
yield 'b', 2
|
|
284
|
+
|
|
285
|
+
pairs() # {'a': 1, 'b': 2}
|
|
286
|
+
|
|
287
|
+
# Attach metadata to a function (handy for the Django admin)
|
|
288
|
+
@decorators.set_attributes(short_description='Name')
|
|
289
|
+
def upper_case_name(self, obj):
|
|
290
|
+
return f'{obj.first_name} {obj.last_name}'.upper()
|
|
291
|
+
|
|
292
|
+
# Only actually run ~10% of the calls
|
|
293
|
+
@decorators.sample(0.1)
|
|
294
|
+
def maybe_log(msg): ...
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
</details>
|
|
298
|
+
|
|
299
|
+
<details>
|
|
300
|
+
<summary><b>📝 Logging — a correctly-named logger on every class</b></summary>
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from python_utils.logger import Logged
|
|
304
|
+
|
|
305
|
+
class MyClass(Logged):
|
|
306
|
+
def do_work(self):
|
|
307
|
+
self.info('starting %s', 'work') # stdlib %-style logging args
|
|
308
|
+
self.error('something went wrong')
|
|
309
|
+
|
|
310
|
+
MyClass().do_work()
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Prefer [loguru](https://github.com/Delgan/loguru)? Install the extra
|
|
314
|
+
(`pip install 'python-utils[loguru]'`) and subclass `Logurud` instead — the same
|
|
315
|
+
`self.info(...)` / `self.error(...)` API, backed by loguru so you keep all its
|
|
316
|
+
configuration and per-instance context.
|
|
317
|
+
|
|
318
|
+
</details>
|
|
319
|
+
|
|
320
|
+
<details>
|
|
321
|
+
<summary><b>🖥️ Terminal & 🧩 misc</b></summary>
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from python_utils import terminal, import_
|
|
325
|
+
from python_utils.exceptions import raise_exception, reraise
|
|
326
|
+
|
|
327
|
+
# Robust terminal size (tries IPython/Jupyter, shutil, blessings, ioctl, tput…)
|
|
328
|
+
terminal.get_terminal_size() # e.g. (80, 24)
|
|
329
|
+
|
|
330
|
+
# Programmatic `from some_module import *`
|
|
331
|
+
import_.import_global('os')
|
|
332
|
+
|
|
333
|
+
# Build a callable that raises — useful as a default/callback
|
|
334
|
+
on_error = raise_exception(ValueError, 'boom')
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
</details>
|
|
338
|
+
|
|
339
|
+
## ⚡ Performance: lazy by default
|
|
340
|
+
|
|
341
|
+
`import python_utils` is intentionally *cheap*. Every submodule and every export
|
|
342
|
+
is wired through a [PEP 562][pep562] `__getattr__`, so nothing is imported until
|
|
343
|
+
first access — and then it's cached. In particular:
|
|
344
|
+
|
|
345
|
+
- Need only the synchronous helpers? `asyncio` is never imported.
|
|
346
|
+
- Even `typing_extensions` is deferred, so the import graph stays minimal.
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
import sys
|
|
350
|
+
import python_utils # imports basically nothing extra
|
|
351
|
+
|
|
352
|
+
'asyncio' in sys.modules # False
|
|
353
|
+
python_utils.acount # now `aio` (and asyncio) load, on demand
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
See the [performance guide](https://python-utils.readthedocs.io/en/latest/) for
|
|
357
|
+
the full story.
|
|
358
|
+
|
|
359
|
+
## 📚 Documentation
|
|
360
|
+
|
|
361
|
+
Full API reference and guides live at
|
|
362
|
+
**<https://python-utils.readthedocs.io/en/latest/>**.
|
|
363
|
+
|
|
364
|
+
## 🔗 Links
|
|
365
|
+
|
|
366
|
+
- 📖 Documentation: <https://python-utils.readthedocs.io/en/latest/>
|
|
367
|
+
- 🐙 Source: <https://github.com/WoLpH/python-utils>
|
|
368
|
+
- 📦 PyPI: <https://pypi.python.org/pypi/python-utils>
|
|
369
|
+
- 🐛 Issues: <https://github.com/WoLpH/python-utils/issues>
|
|
370
|
+
- ✍️ Author's blog: <https://wol.ph/>
|
|
371
|
+
|
|
372
|
+
## 🔒 Security
|
|
373
|
+
|
|
374
|
+
To report a security vulnerability, please use the
|
|
375
|
+
[Tidelift security contact](https://tidelift.com/security). Tidelift will
|
|
376
|
+
coordinate the fix and disclosure.
|
|
377
|
+
|
|
378
|
+
## 🤝 Contributing
|
|
379
|
+
|
|
380
|
+
Contributions are very welcome! We keep a strict **100% coverage** bar and run
|
|
381
|
+
`ruff`, three type checkers and the full test matrix in CI. See
|
|
382
|
+
[CONTRIBUTING.md](https://github.com/WoLpH/python-utils/blob/develop/CONTRIBUTING.md)
|
|
383
|
+
to get set up.
|
|
384
|
+
|
|
385
|
+
## 📄 License
|
|
386
|
+
|
|
387
|
+
BSD-3-Clause — see [LICENSE](https://github.com/WoLpH/python-utils/blob/develop/LICENSE).
|
|
388
|
+
|
|
389
|
+
[pep562]: https://peps.python.org/pep-0562/
|