python-utils 3.9.0__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.
Files changed (56) hide show
  1. python_utils-4.0.0/PKG-INFO +389 -0
  2. python_utils-4.0.0/README.md +347 -0
  3. python_utils-4.0.0/_python_utils_tests/__init__.py +1 -0
  4. python_utils-4.0.0/_python_utils_tests/test_aio.py +79 -0
  5. python_utils-4.0.0/_python_utils_tests/test_aliases.py +76 -0
  6. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_containers.py +7 -0
  7. python_utils-4.0.0/_python_utils_tests/test_decorators.py +82 -0
  8. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_generators.py +8 -0
  9. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_import.py +9 -0
  10. python_utils-4.0.0/_python_utils_tests/test_import_footprint.py +106 -0
  11. python_utils-4.0.0/_python_utils_tests/test_lazy_imports.py +113 -0
  12. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_logger.py +7 -3
  13. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_python_utils.py +3 -0
  14. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/test_time.py +32 -16
  15. python_utils-4.0.0/pyproject.toml +165 -0
  16. python_utils-4.0.0/python_utils/__about__.py +37 -0
  17. python_utils-4.0.0/python_utils/__init__.py +241 -0
  18. python_utils-4.0.0/python_utils/_aliases.py +53 -0
  19. python_utils-4.0.0/python_utils/aio.py +133 -0
  20. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/containers.py +92 -78
  21. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/converters.py +58 -47
  22. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/decorators.py +56 -36
  23. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/exceptions.py +11 -8
  24. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/formatters.py +19 -23
  25. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/generators.py +25 -25
  26. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/import_.py +12 -10
  27. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/logger.py +77 -42
  28. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/loguru.py +3 -1
  29. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/terminal.py +41 -12
  30. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/time.py +140 -98
  31. {python_utils-3.9.0 → python_utils-4.0.0}/python_utils/types.py +108 -110
  32. python_utils-4.0.0/tox.ini +87 -0
  33. python_utils-3.9.0/MANIFEST.in +0 -12
  34. python_utils-3.9.0/PKG-INFO +0 -309
  35. python_utils-3.9.0/README.rst +0 -277
  36. python_utils-3.9.0/_python_utils_tests/__init__.py +0 -0
  37. python_utils-3.9.0/_python_utils_tests/test_aio.py +0 -68
  38. python_utils-3.9.0/_python_utils_tests/test_decorators.py +0 -71
  39. python_utils-3.9.0/coverage.rc +0 -3
  40. python_utils-3.9.0/pyproject.toml +0 -19
  41. python_utils-3.9.0/pytest.ini +0 -16
  42. python_utils-3.9.0/python_utils/__about__.py +0 -22
  43. python_utils-3.9.0/python_utils/__init__.py +0 -126
  44. python_utils-3.9.0/python_utils/aio.py +0 -117
  45. python_utils-3.9.0/python_utils.egg-info/PKG-INFO +0 -309
  46. python_utils-3.9.0/python_utils.egg-info/SOURCES.txt +0 -41
  47. python_utils-3.9.0/python_utils.egg-info/dependency_links.txt +0 -1
  48. python_utils-3.9.0/python_utils.egg-info/requires.txt +0 -23
  49. python_utils-3.9.0/python_utils.egg-info/top_level.txt +0 -1
  50. python_utils-3.9.0/requirements.txt +0 -1
  51. python_utils-3.9.0/setup.cfg +0 -46
  52. python_utils-3.9.0/setup.py +0 -70
  53. python_utils-3.9.0/tox.ini +0 -63
  54. {python_utils-3.9.0 → python_utils-4.0.0}/LICENSE +0 -0
  55. {python_utils-3.9.0 → python_utils-4.0.0}/_python_utils_tests/requirements.txt +0 -0
  56. {python_utils-3.9.0 → 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
+ [![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)
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/