pyutilkit 0.11.0__tar.gz → 0.12.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.
- pyutilkit-0.12.0/PKG-INFO +137 -0
- pyutilkit-0.12.0/docs/README.md +118 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/pyproject.toml +23 -20
- pyutilkit-0.12.0/src/pyutilkit/__version__.py +3 -0
- pyutilkit-0.12.0/src/pyutilkit/classes.py +26 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/files.py +16 -4
- pyutilkit-0.12.0/src/pyutilkit/subprocess.py +103 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/term.py +19 -8
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/timing.py +23 -5
- pyutilkit-0.11.0/LICENSE.md +0 -11
- pyutilkit-0.11.0/PKG-INFO +0 -65
- pyutilkit-0.11.0/docs/README.md +0 -46
- pyutilkit-0.11.0/src/pyutilkit/__version__.py +0 -1
- pyutilkit-0.11.0/src/pyutilkit/classes.py +0 -21
- pyutilkit-0.11.0/src/pyutilkit/subprocess.py +0 -55
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/__init__.py +0 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/data/__init__.py +0 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/data/timezones.py +0 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/date_utils.py +0 -0
- {pyutilkit-0.11.0 → pyutilkit-0.12.0}/src/pyutilkit/py.typed +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyutilkit
|
|
3
|
+
Version: 0.12.0
|
|
4
|
+
Summary: python's missing batteries
|
|
5
|
+
Keywords: utils
|
|
6
|
+
Author: Stephanos Kuma
|
|
7
|
+
Author-email: Stephanos Kuma <stephanos@kuma.ai>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Dist: tzdata ; os_name == 'nt'
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Project-URL: homepage, https://pyutilkit.readthedocs.io/en/stable/
|
|
16
|
+
Project-URL: repository, https://github.com/spapanik/pyutilkit
|
|
17
|
+
Project-URL: documentation, https://pyutilkit.readthedocs.io/en/stable/
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# pyutilkit: python's missing batteries
|
|
21
|
+
|
|
22
|
+
[![build][build_badge]][build_url]
|
|
23
|
+
[![lint][lint_badge]][lint_url]
|
|
24
|
+
[![tests][tests_badge]][tests_url]
|
|
25
|
+
[![license][licence_badge]][licence_url]
|
|
26
|
+
[![codecov][codecov_badge]][codecov_url]
|
|
27
|
+
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
28
|
+
[![pypi][pypi_badge]][pypi_url]
|
|
29
|
+
[![downloads][pepy_badge]][pepy_url]
|
|
30
|
+
[![build automation: yam][yam_badge]][yam_url]
|
|
31
|
+
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
32
|
+
|
|
33
|
+
Python has long maintained the philosophy of "batteries included", providing users with a rich
|
|
34
|
+
standard library that avoids the need for third-party tools for most tasks. Some packages are so
|
|
35
|
+
common they have achieved similar status to the standard library. Yet, certain utilities are
|
|
36
|
+
reimplemented across countless projects. This lightweight library, with minimal dependencies,
|
|
37
|
+
aims to eliminate that repetition.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
Install pyutilkit:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install pyutilkit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or with uv (recommended for speed):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv pip install pyutilkit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Basic usage example:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from pyutilkit.date_utils import now
|
|
57
|
+
from pyutilkit.timing import Stopwatch
|
|
58
|
+
from pyutilkit.term import SGRString, SGRCodes
|
|
59
|
+
|
|
60
|
+
# Get current time in any timezone
|
|
61
|
+
from zoneinfo import ZoneInfo
|
|
62
|
+
|
|
63
|
+
tokyo_time = now(ZoneInfo("Asia/Tokyo"))
|
|
64
|
+
print(f"Current time in Tokyo: {tokyo_time}")
|
|
65
|
+
|
|
66
|
+
# Measure execution time
|
|
67
|
+
stopwatch = Stopwatch()
|
|
68
|
+
with stopwatch:
|
|
69
|
+
# Your code here
|
|
70
|
+
result = sum(range(1000000))
|
|
71
|
+
print(f"Computation took: {stopwatch.elapsed}")
|
|
72
|
+
|
|
73
|
+
# Colorful terminal output
|
|
74
|
+
success = SGRString("✓ Task completed", params=[SGRCodes.GREEN, SGRCodes.BOLD])
|
|
75
|
+
success.print()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key Features
|
|
79
|
+
|
|
80
|
+
- **🕐 Timezone Utilities**: Robust datetime handling with cross-platform timezone support, ISO parsing (including Zulu timezone), and seamless timezone conversion
|
|
81
|
+
- **⏱️ High-Precision Timing**: Nanosecond-precision timing with human-readable formatting, lap tracking, and stopwatch functionality
|
|
82
|
+
- **🎨 Terminal Formatting**: ANSI color codes with smart TTY detection, automatic style stripping for piped output, and convenient header formatting
|
|
83
|
+
- **🛡️ Error Handling**: Elegant exception handling decorator that logs errors and returns defaults instead of raising exceptions
|
|
84
|
+
- **📁 File Utilities**: Efficient SHA-256 file hashing with buffered reading for large files
|
|
85
|
+
- **🚀 Subprocess Enhancement**: Run external commands with real-time output streaming, automatic timing, and structured results
|
|
86
|
+
- **🔧 Design Patterns**: Thread-safe Singleton metaclass implementation
|
|
87
|
+
- **✨ Zero Dependencies**: Pure Python using only standard library (except optional tzdata on Windows)
|
|
88
|
+
- **🧪 Fully Tested**: 100% test coverage with comprehensive type annotations
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
- **[Installation Guide](installation.md)** - Setup instructions and requirements
|
|
93
|
+
- **[Usage Guide](usage/index.md)** - Comprehensive examples and tutorials for all modules
|
|
94
|
+
- [Classes](usage/classes.md) - Singleton pattern implementation
|
|
95
|
+
- [Date Utilities](usage/date_utils.md) - Timezone-aware datetime operations
|
|
96
|
+
- [File Utilities](usage/files.md) - Exception handling and file hashing
|
|
97
|
+
- [Subprocess](usage/subprocess.md) - Enhanced command execution
|
|
98
|
+
- [Terminal](usage/term.md) - Terminal formatting and colors
|
|
99
|
+
- [Timing](usage/timing.md) - Performance measurement and benchmarking
|
|
100
|
+
- **[Changelog](CHANGELOG.md)** - Version history and changes
|
|
101
|
+
- **[Code of Conduct](CODE_OF_CONDUCT.md)** - Community guidelines
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- Python 3.10 or higher
|
|
106
|
+
- No external dependencies (tzdata is automatically installed on Windows)
|
|
107
|
+
|
|
108
|
+
## Links
|
|
109
|
+
|
|
110
|
+
- [Full Documentation](https://pyutilkit.readthedocs.io/en/stable/)
|
|
111
|
+
- [PyPI Package](https://pypi.org/project/pyutilkit)
|
|
112
|
+
- [GitHub Repository](https://github.com/spapanik/pyutilkit)
|
|
113
|
+
- [Changelog](CHANGELOG.md)
|
|
114
|
+
- [Report Issues](https://github.com/spapanik/pyutilkit/issues)
|
|
115
|
+
|
|
116
|
+
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
117
|
+
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
118
|
+
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
119
|
+
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
120
|
+
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
121
|
+
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
122
|
+
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
123
|
+
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
124
|
+
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
125
|
+
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
126
|
+
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
127
|
+
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
128
|
+
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
129
|
+
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
130
|
+
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
131
|
+
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
132
|
+
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
133
|
+
[yam_url]: https://github.com/spapanik/yamk
|
|
134
|
+
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
135
|
+
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
136
|
+
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
137
|
+
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pyutilkit: python's missing batteries
|
|
2
|
+
|
|
3
|
+
[![build][build_badge]][build_url]
|
|
4
|
+
[![lint][lint_badge]][lint_url]
|
|
5
|
+
[![tests][tests_badge]][tests_url]
|
|
6
|
+
[![license][licence_badge]][licence_url]
|
|
7
|
+
[![codecov][codecov_badge]][codecov_url]
|
|
8
|
+
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
9
|
+
[![pypi][pypi_badge]][pypi_url]
|
|
10
|
+
[![downloads][pepy_badge]][pepy_url]
|
|
11
|
+
[![build automation: yam][yam_badge]][yam_url]
|
|
12
|
+
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
13
|
+
|
|
14
|
+
Python has long maintained the philosophy of "batteries included", providing users with a rich
|
|
15
|
+
standard library that avoids the need for third-party tools for most tasks. Some packages are so
|
|
16
|
+
common they have achieved similar status to the standard library. Yet, certain utilities are
|
|
17
|
+
reimplemented across countless projects. This lightweight library, with minimal dependencies,
|
|
18
|
+
aims to eliminate that repetition.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
Install pyutilkit:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install pyutilkit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or with uv (recommended for speed):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv pip install pyutilkit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Basic usage example:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from pyutilkit.date_utils import now
|
|
38
|
+
from pyutilkit.timing import Stopwatch
|
|
39
|
+
from pyutilkit.term import SGRString, SGRCodes
|
|
40
|
+
|
|
41
|
+
# Get current time in any timezone
|
|
42
|
+
from zoneinfo import ZoneInfo
|
|
43
|
+
|
|
44
|
+
tokyo_time = now(ZoneInfo("Asia/Tokyo"))
|
|
45
|
+
print(f"Current time in Tokyo: {tokyo_time}")
|
|
46
|
+
|
|
47
|
+
# Measure execution time
|
|
48
|
+
stopwatch = Stopwatch()
|
|
49
|
+
with stopwatch:
|
|
50
|
+
# Your code here
|
|
51
|
+
result = sum(range(1000000))
|
|
52
|
+
print(f"Computation took: {stopwatch.elapsed}")
|
|
53
|
+
|
|
54
|
+
# Colorful terminal output
|
|
55
|
+
success = SGRString("✓ Task completed", params=[SGRCodes.GREEN, SGRCodes.BOLD])
|
|
56
|
+
success.print()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Key Features
|
|
60
|
+
|
|
61
|
+
- **🕐 Timezone Utilities**: Robust datetime handling with cross-platform timezone support, ISO parsing (including Zulu timezone), and seamless timezone conversion
|
|
62
|
+
- **⏱️ High-Precision Timing**: Nanosecond-precision timing with human-readable formatting, lap tracking, and stopwatch functionality
|
|
63
|
+
- **🎨 Terminal Formatting**: ANSI color codes with smart TTY detection, automatic style stripping for piped output, and convenient header formatting
|
|
64
|
+
- **🛡️ Error Handling**: Elegant exception handling decorator that logs errors and returns defaults instead of raising exceptions
|
|
65
|
+
- **📁 File Utilities**: Efficient SHA-256 file hashing with buffered reading for large files
|
|
66
|
+
- **🚀 Subprocess Enhancement**: Run external commands with real-time output streaming, automatic timing, and structured results
|
|
67
|
+
- **🔧 Design Patterns**: Thread-safe Singleton metaclass implementation
|
|
68
|
+
- **✨ Zero Dependencies**: Pure Python using only standard library (except optional tzdata on Windows)
|
|
69
|
+
- **🧪 Fully Tested**: 100% test coverage with comprehensive type annotations
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
- **[Installation Guide](installation.md)** - Setup instructions and requirements
|
|
74
|
+
- **[Usage Guide](usage/index.md)** - Comprehensive examples and tutorials for all modules
|
|
75
|
+
- [Classes](usage/classes.md) - Singleton pattern implementation
|
|
76
|
+
- [Date Utilities](usage/date_utils.md) - Timezone-aware datetime operations
|
|
77
|
+
- [File Utilities](usage/files.md) - Exception handling and file hashing
|
|
78
|
+
- [Subprocess](usage/subprocess.md) - Enhanced command execution
|
|
79
|
+
- [Terminal](usage/term.md) - Terminal formatting and colors
|
|
80
|
+
- [Timing](usage/timing.md) - Performance measurement and benchmarking
|
|
81
|
+
- **[Changelog](CHANGELOG.md)** - Version history and changes
|
|
82
|
+
- **[Code of Conduct](CODE_OF_CONDUCT.md)** - Community guidelines
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
- Python 3.10 or higher
|
|
87
|
+
- No external dependencies (tzdata is automatically installed on Windows)
|
|
88
|
+
|
|
89
|
+
## Links
|
|
90
|
+
|
|
91
|
+
- [Full Documentation](https://pyutilkit.readthedocs.io/en/stable/)
|
|
92
|
+
- [PyPI Package](https://pypi.org/project/pyutilkit)
|
|
93
|
+
- [GitHub Repository](https://github.com/spapanik/pyutilkit)
|
|
94
|
+
- [Changelog](CHANGELOG.md)
|
|
95
|
+
- [Report Issues](https://github.com/spapanik/pyutilkit/issues)
|
|
96
|
+
|
|
97
|
+
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
98
|
+
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
99
|
+
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
100
|
+
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
101
|
+
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
102
|
+
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
103
|
+
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
104
|
+
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
105
|
+
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
106
|
+
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
107
|
+
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
108
|
+
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
109
|
+
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
110
|
+
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
111
|
+
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
112
|
+
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
113
|
+
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
114
|
+
[yam_url]: https://github.com/spapanik/yamk
|
|
115
|
+
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
116
|
+
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
117
|
+
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
118
|
+
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"
|
|
3
|
+
"uv_build>=0.11.0,<0.12.0",
|
|
4
4
|
]
|
|
5
|
-
build-backend = "
|
|
5
|
+
build-backend = "uv_build"
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyutilkit"
|
|
9
|
-
|
|
10
|
-
"version",
|
|
11
|
-
]
|
|
9
|
+
version = "0.12.0"
|
|
12
10
|
|
|
13
11
|
authors = [
|
|
14
12
|
{ name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
|
|
@@ -24,7 +22,7 @@ classifiers = [
|
|
|
24
22
|
"Development Status :: 4 - Beta",
|
|
25
23
|
"Operating System :: OS Independent",
|
|
26
24
|
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
-
"
|
|
25
|
+
"Intended Audience :: Developers",
|
|
28
26
|
]
|
|
29
27
|
|
|
30
28
|
requires-python = ">=3.10"
|
|
@@ -40,37 +38,37 @@ documentation = "https://pyutilkit.readthedocs.io/en/stable/"
|
|
|
40
38
|
[dependency-groups]
|
|
41
39
|
dev = [
|
|
42
40
|
"ipdb~=0.13",
|
|
43
|
-
"ipython~=8.
|
|
41
|
+
"ipython~=8.39",
|
|
44
42
|
"ptpython~=3.0",
|
|
45
43
|
{ include-group = "lint" },
|
|
46
44
|
{ include-group = "test" },
|
|
47
45
|
{ include-group = "docs" },
|
|
48
46
|
]
|
|
49
47
|
lint = [
|
|
50
|
-
"mypy~=1
|
|
51
|
-
"ruff~=0.
|
|
48
|
+
"mypy~=2.1", # Keep mypy for CI/CD
|
|
49
|
+
"ruff~=0.16",
|
|
50
|
+
"ty~=0.0.63",
|
|
51
|
+
"typing-extensions~=4.16", # upgrade: py3.10: use typing module
|
|
52
52
|
{ include-group = "test_core" },
|
|
53
53
|
]
|
|
54
54
|
test = [
|
|
55
|
-
"pytest-cov~=7.
|
|
55
|
+
"pytest-cov~=7.1",
|
|
56
56
|
{ include-group = "test_core" },
|
|
57
57
|
]
|
|
58
58
|
test_core = [
|
|
59
59
|
"freezegun~=1.5",
|
|
60
|
-
"pytest~=9.
|
|
60
|
+
"pytest~=9.1",
|
|
61
61
|
]
|
|
62
62
|
docs = [
|
|
63
63
|
"mkdocs~=1.6",
|
|
64
|
-
"mkdocs-material~=9.
|
|
64
|
+
"mkdocs-material~=9.7",
|
|
65
65
|
"mkdocs-material-extensions~=1.3",
|
|
66
|
-
"pygments~=2.
|
|
67
|
-
"pymdown-extensions~=
|
|
66
|
+
"pygments~=2.20",
|
|
67
|
+
"pymdown-extensions~=11.0",
|
|
68
68
|
]
|
|
69
69
|
|
|
70
|
-
[tool.phosphorus.dynamic]
|
|
71
|
-
version = { file = "src/pyutilkit/__version__.py" }
|
|
72
|
-
|
|
73
70
|
[tool.mypy]
|
|
71
|
+
# Keep mypy for CI/CD
|
|
74
72
|
check_untyped_defs = true
|
|
75
73
|
disallow_any_decorated = true
|
|
76
74
|
disallow_any_explicit = true
|
|
@@ -95,6 +93,7 @@ warn_unused_ignores = true
|
|
|
95
93
|
warn_unreachable = true
|
|
96
94
|
|
|
97
95
|
[[tool.mypy.overrides]]
|
|
96
|
+
# Keep mypy for CI/CD
|
|
98
97
|
module = "tests.*"
|
|
99
98
|
disallow_any_decorated = false # mock.MagicMock is Any
|
|
100
99
|
|
|
@@ -110,6 +109,7 @@ select = [
|
|
|
110
109
|
]
|
|
111
110
|
ignore = [
|
|
112
111
|
"C901", # Adding a limit to complexity is too arbitrary
|
|
112
|
+
"CPY001", # Use only a project copyright
|
|
113
113
|
"COM812", # Avoid conflicts with the formatter
|
|
114
114
|
"D10", # Not everything needs a docstring
|
|
115
115
|
"D203", # Prefer `no-blank-line-before-class` (D211)
|
|
@@ -122,6 +122,7 @@ ignore = [
|
|
|
122
122
|
"FBT001", # Test arguments are handled by pytest
|
|
123
123
|
"PLR2004", # Tests should contain magic number comparisons
|
|
124
124
|
"S101", # Pytest needs assert statements
|
|
125
|
+
"SLF001", # Tests need to access private members for coverage
|
|
125
126
|
]
|
|
126
127
|
|
|
127
128
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
@@ -138,13 +139,15 @@ forced-separate = [
|
|
|
138
139
|
]
|
|
139
140
|
split-on-trailing-comma = false
|
|
140
141
|
|
|
141
|
-
[tool.pytest
|
|
142
|
+
[tool.pytest]
|
|
142
143
|
minversion = "9.0"
|
|
143
144
|
strict = true
|
|
144
|
-
addopts = [
|
|
145
|
+
addopts = [
|
|
146
|
+
"-ra",
|
|
147
|
+
"-v",
|
|
148
|
+
]
|
|
145
149
|
testpaths = [
|
|
146
150
|
"tests",
|
|
147
|
-
"integration",
|
|
148
151
|
]
|
|
149
152
|
|
|
150
153
|
[tool.coverage.run]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import TypeVar, cast
|
|
5
|
+
|
|
6
|
+
_T = TypeVar("_T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Singleton(type):
|
|
10
|
+
instance: object
|
|
11
|
+
_lock: threading.Lock
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(name, bases, namespace)
|
|
17
|
+
cls.instance = None
|
|
18
|
+
cls._lock = threading.Lock()
|
|
19
|
+
|
|
20
|
+
def __call__(cls: type[_T]) -> _T:
|
|
21
|
+
mcs = cast("Singleton", cls)
|
|
22
|
+
if mcs.instance is None:
|
|
23
|
+
with mcs._lock:
|
|
24
|
+
if mcs.instance is None:
|
|
25
|
+
mcs.instance = super(Singleton, mcs).__call__()
|
|
26
|
+
return cast("_T", mcs.instance)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import logging
|
|
5
5
|
from functools import wraps
|
|
6
|
-
from typing import TYPE_CHECKING, ParamSpec, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, ParamSpec, TypeVar, cast
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from collections.abc import Callable
|
|
@@ -13,23 +13,35 @@ logger = logging.getLogger(__name__)
|
|
|
13
13
|
INGEST_ERROR = "Function `%s` threw `%s` when called with args=%s and kwargs=%s"
|
|
14
14
|
R_co = TypeVar("R_co", covariant=True)
|
|
15
15
|
P = ParamSpec("P")
|
|
16
|
+
LogLevel = Literal["debug", "info", "warning", "error", "critical", "exception"]
|
|
17
|
+
LOG_LEVELS = frozenset({"debug", "info", "warning", "error", "critical", "exception"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_log_level(log_level: object) -> LogLevel:
|
|
21
|
+
if not isinstance(log_level, str) or log_level not in LOG_LEVELS:
|
|
22
|
+
supported = ", ".join(sorted(LOG_LEVELS))
|
|
23
|
+
msg = f"Unsupported log level {log_level!r}; expected one of: {supported}"
|
|
24
|
+
raise ValueError(msg)
|
|
25
|
+
return cast("LogLevel", log_level)
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
def handle_exceptions(
|
|
19
29
|
*,
|
|
20
30
|
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
21
31
|
default: R_co | None = None,
|
|
22
|
-
log_level:
|
|
32
|
+
log_level: LogLevel = "info",
|
|
23
33
|
) -> Callable[[Callable[P, R_co]], Callable[P, R_co | None]]:
|
|
34
|
+
log = getattr(logger, _validate_log_level(log_level))
|
|
35
|
+
|
|
24
36
|
def decorator(func: Callable[P, R_co]) -> Callable[P, R_co | None]:
|
|
25
37
|
@wraps(func)
|
|
26
38
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R_co | None:
|
|
27
39
|
try:
|
|
28
40
|
return func(*args, **kwargs)
|
|
29
41
|
except exceptions as exc:
|
|
30
|
-
|
|
42
|
+
log(
|
|
31
43
|
INGEST_ERROR,
|
|
32
|
-
func.__name__,
|
|
44
|
+
func.__name__, # ty: ignore[unresolved-attribute]
|
|
33
45
|
exc.__class__.__name__,
|
|
34
46
|
args,
|
|
35
47
|
kwargs,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from queue import SimpleQueue
|
|
6
|
+
from subprocess import PIPE, Popen
|
|
7
|
+
from threading import Thread
|
|
8
|
+
from typing import TYPE_CHECKING, cast
|
|
9
|
+
|
|
10
|
+
from pyutilkit.timing import Stopwatch, Timing
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import BinaryIO
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProcessOutput:
|
|
19
|
+
stdout: bytes
|
|
20
|
+
stderr: bytes
|
|
21
|
+
pid: int
|
|
22
|
+
returncode: int
|
|
23
|
+
elapsed: Timing
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_output(stream: BinaryIO, line: bytes) -> None:
|
|
27
|
+
stream.write(line)
|
|
28
|
+
stream.flush()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _drain_pipe(
|
|
32
|
+
pipe: BinaryIO,
|
|
33
|
+
output: BinaryIO,
|
|
34
|
+
captured: list[bytes],
|
|
35
|
+
errors: SimpleQueue[Exception],
|
|
36
|
+
) -> None:
|
|
37
|
+
echo = True
|
|
38
|
+
for line in pipe:
|
|
39
|
+
captured.append(line)
|
|
40
|
+
if not echo:
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
_write_output(output, line)
|
|
44
|
+
except Exception as exc: # noqa: BLE001
|
|
45
|
+
errors.put(exc)
|
|
46
|
+
echo = False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _validate_command(command: object) -> list[str]:
|
|
50
|
+
if not isinstance(command, list):
|
|
51
|
+
msg = "command must be a list of argument strings"
|
|
52
|
+
raise TypeError(msg)
|
|
53
|
+
return cast("list[str]", command)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_command(
|
|
57
|
+
command: list[str],
|
|
58
|
+
cwd: str | Path | None = None,
|
|
59
|
+
env: dict[str, str] | None = None,
|
|
60
|
+
) -> ProcessOutput:
|
|
61
|
+
command = _validate_command(command)
|
|
62
|
+
|
|
63
|
+
stdout: list[bytes] = []
|
|
64
|
+
stderr: list[bytes] = []
|
|
65
|
+
errors: SimpleQueue[Exception] = SimpleQueue()
|
|
66
|
+
stopwatch = Stopwatch()
|
|
67
|
+
with (
|
|
68
|
+
stopwatch,
|
|
69
|
+
Popen( # noqa: S603
|
|
70
|
+
command, stdout=PIPE, stderr=PIPE, cwd=cwd, env=env
|
|
71
|
+
) as process,
|
|
72
|
+
):
|
|
73
|
+
stdout_pipe = cast("BinaryIO", process.stdout)
|
|
74
|
+
stderr_pipe = cast("BinaryIO", process.stderr)
|
|
75
|
+
readers = (
|
|
76
|
+
Thread(
|
|
77
|
+
target=_drain_pipe,
|
|
78
|
+
args=(stdout_pipe, sys.stdout.buffer, stdout, errors),
|
|
79
|
+
name=f"pyutilkit-stdout-{process.pid}",
|
|
80
|
+
),
|
|
81
|
+
Thread(
|
|
82
|
+
target=_drain_pipe,
|
|
83
|
+
args=(stderr_pipe, sys.stderr.buffer, stderr, errors),
|
|
84
|
+
name=f"pyutilkit-stderr-{process.pid}",
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
for reader in readers:
|
|
88
|
+
reader.start()
|
|
89
|
+
|
|
90
|
+
process.wait()
|
|
91
|
+
for reader in readers:
|
|
92
|
+
reader.join()
|
|
93
|
+
|
|
94
|
+
if not errors.empty():
|
|
95
|
+
raise errors.get()
|
|
96
|
+
|
|
97
|
+
return ProcessOutput(
|
|
98
|
+
stdout=b"".join(stdout),
|
|
99
|
+
stderr=b"".join(stderr),
|
|
100
|
+
pid=process.pid,
|
|
101
|
+
returncode=process.returncode,
|
|
102
|
+
elapsed=stopwatch.elapsed,
|
|
103
|
+
)
|
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|
|
12
12
|
|
|
13
13
|
from typing_extensions import Self # upgrade: py3.10: import from typing
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
TRUTHY_VALUES = {"1", "true", "yes"}
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@unique
|
|
@@ -90,9 +90,12 @@ class SGRString:
|
|
|
90
90
|
) -> None:
|
|
91
91
|
params = tuple(params)
|
|
92
92
|
force_prefix = (
|
|
93
|
-
force_prefix
|
|
93
|
+
force_prefix
|
|
94
|
+
or os.getenv("PY_UTIL_FORCE_PREFIX", "").lower() in TRUTHY_VALUES
|
|
95
|
+
)
|
|
96
|
+
force_sgr = (
|
|
97
|
+
force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUTHY_VALUES
|
|
94
98
|
)
|
|
95
|
-
force_sgr = force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUE_VAR
|
|
96
99
|
|
|
97
100
|
object.__setattr__(self, "_prefix", prefix)
|
|
98
101
|
object.__setattr__(self, "_string", str(obj))
|
|
@@ -136,7 +139,7 @@ class SGRString:
|
|
|
136
139
|
is_error=self._is_error,
|
|
137
140
|
)
|
|
138
141
|
|
|
139
|
-
def print(self, end: str =
|
|
142
|
+
def print(self, end: str = "\n", *, full_color: bool = False) -> None:
|
|
140
143
|
"""Print the command output.
|
|
141
144
|
|
|
142
145
|
The command will be printed to stdout if it's not the output of an error,
|
|
@@ -202,7 +205,15 @@ class SGRString:
|
|
|
202
205
|
half = (columns - title_length) / 2
|
|
203
206
|
prefix = f"{padding * ceil(half)}{space * left_spaces}{self._prefix}"
|
|
204
207
|
suffix = f"{self._suffix}{space * right_spaces}{padding * floor(half)}"
|
|
205
|
-
type(self)(
|
|
208
|
+
type(self)(
|
|
209
|
+
self._string,
|
|
210
|
+
prefix=prefix,
|
|
211
|
+
suffix=suffix,
|
|
212
|
+
params=self._sgr,
|
|
213
|
+
force_prefix=self._force_prefix,
|
|
214
|
+
force_sgr=self._force_sgr,
|
|
215
|
+
is_error=self._is_error,
|
|
216
|
+
).print()
|
|
206
217
|
|
|
207
218
|
|
|
208
219
|
@dataclass(frozen=True, order=True, slots=True)
|
|
@@ -258,7 +269,7 @@ class SGROutput:
|
|
|
258
269
|
is_error=is_error,
|
|
259
270
|
)
|
|
260
271
|
|
|
261
|
-
def print(self, sep: str = "", end: str =
|
|
272
|
+
def print(self, sep: str = "", end: str = "\n") -> None:
|
|
262
273
|
n = len(self._strings)
|
|
263
274
|
for index, string in enumerate(self._strings, start=1):
|
|
264
275
|
current_end = end if index == n else sep
|
|
@@ -273,8 +284,8 @@ class SGROutput:
|
|
|
273
284
|
space: str = " ",
|
|
274
285
|
) -> None:
|
|
275
286
|
n = len(self._strings)
|
|
276
|
-
if n
|
|
277
|
-
msg = "
|
|
287
|
+
if n != 1:
|
|
288
|
+
msg = "Exactly one string is required for the header"
|
|
278
289
|
raise ValueError(msg)
|
|
279
290
|
|
|
280
291
|
self._strings[0].header(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from time import perf_counter_ns
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
@@ -14,7 +15,8 @@ METRIC_MULTIPLIER = 1_000
|
|
|
14
15
|
SECONDS_PER_MINUTE = 60
|
|
15
16
|
MINUTES_PER_HOUR = 60
|
|
16
17
|
HOURS_PER_DAY = 24
|
|
17
|
-
|
|
18
|
+
SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
|
|
19
|
+
SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
@dataclass(frozen=True, order=True, slots=True)
|
|
@@ -25,6 +27,8 @@ class Timing:
|
|
|
25
27
|
self,
|
|
26
28
|
*,
|
|
27
29
|
days: int = 0,
|
|
30
|
+
hours: int = 0,
|
|
31
|
+
minutes: int = 0,
|
|
28
32
|
seconds: int = 0,
|
|
29
33
|
milliseconds: int = 0,
|
|
30
34
|
microseconds: int = 0,
|
|
@@ -35,9 +39,18 @@ class Timing:
|
|
|
35
39
|
+ METRIC_MULTIPLIER * microseconds
|
|
36
40
|
+ METRIC_MULTIPLIER**2 * milliseconds
|
|
37
41
|
+ METRIC_MULTIPLIER**3 * seconds
|
|
42
|
+
+ SECONDS_PER_MINUTE * METRIC_MULTIPLIER**3 * minutes
|
|
43
|
+
+ SECONDS_PER_HOUR * METRIC_MULTIPLIER**3 * hours
|
|
38
44
|
+ SECONDS_PER_DAY * METRIC_MULTIPLIER**3 * days
|
|
39
45
|
)
|
|
40
|
-
|
|
46
|
+
rounded_nanoseconds = round(total_nanoseconds)
|
|
47
|
+
if rounded_nanoseconds != total_nanoseconds:
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"Timing received a fractional duration; "
|
|
50
|
+
"rounding to the nearest nanosecond.",
|
|
51
|
+
stacklevel=2,
|
|
52
|
+
)
|
|
53
|
+
object.__setattr__(self, "nanoseconds", rounded_nanoseconds)
|
|
41
54
|
|
|
42
55
|
def __str__(self) -> str:
|
|
43
56
|
if self.nanoseconds == 0:
|
|
@@ -149,17 +162,22 @@ class Stopwatch:
|
|
|
149
162
|
def elapsed(self) -> Timing:
|
|
150
163
|
return sum(self.laps, self._zero)
|
|
151
164
|
|
|
152
|
-
|
|
153
|
-
def average(self) -> Timing:
|
|
165
|
+
def _require_laps(self) -> None:
|
|
154
166
|
if not self.laps:
|
|
155
167
|
msg = "No laps recorded"
|
|
156
|
-
raise
|
|
168
|
+
raise ValueError(msg)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def average(self) -> Timing:
|
|
172
|
+
self._require_laps()
|
|
157
173
|
return self.elapsed // len(self)
|
|
158
174
|
|
|
159
175
|
@property
|
|
160
176
|
def min(self) -> Timing:
|
|
177
|
+
self._require_laps()
|
|
161
178
|
return min(self.laps)
|
|
162
179
|
|
|
163
180
|
@property
|
|
164
181
|
def max(self) -> Timing:
|
|
182
|
+
self._require_laps()
|
|
165
183
|
return max(self.laps)
|
pyutilkit-0.11.0/LICENSE.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# BSD 3-Clause License
|
|
2
|
-
|
|
3
|
-
Copyright © 2025 Stephanos Kuma.
|
|
4
|
-
|
|
5
|
-
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
6
|
-
|
|
7
|
-
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
8
|
-
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
9
|
-
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
-
|
|
11
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
pyutilkit-0.11.0/PKG-INFO
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: pyutilkit
|
|
3
|
-
Version: 0.11.0
|
|
4
|
-
Summary: python's missing batteries
|
|
5
|
-
Home-page: https://pyutilkit.readthedocs.io/en/stable/
|
|
6
|
-
License: BSD-3-Clause
|
|
7
|
-
Keywords: utils
|
|
8
|
-
Author: Stephanos Kuma
|
|
9
|
-
Author-email: "Stephanos Kuma" <stephanos@kuma.ai>
|
|
10
|
-
Requires-Python: >=3.10
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Requires-Dist: tzdata ; os_name == 'nt'
|
|
16
|
-
Project-URL: Documentation, https://pyutilkit.readthedocs.io/en/stable/
|
|
17
|
-
Project-URL: Repository, https://github.com/spapanik/pyutilkit
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
|
-
# pyutilkit: python's missing batteries
|
|
21
|
-
|
|
22
|
-
[![build][build_badge]][build_url]
|
|
23
|
-
[![lint][lint_badge]][lint_url]
|
|
24
|
-
[![tests][tests_badge]][tests_url]
|
|
25
|
-
[![license][licence_badge]][licence_url]
|
|
26
|
-
[![codecov][codecov_badge]][codecov_url]
|
|
27
|
-
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
28
|
-
[![pypi][pypi_badge]][pypi_url]
|
|
29
|
-
[![downloads][pepy_badge]][pepy_url]
|
|
30
|
-
[![build automation: yam][yam_badge]][yam_url]
|
|
31
|
-
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
32
|
-
|
|
33
|
-
The Python has long maintained the philosophy of "batteries included", giving the user
|
|
34
|
-
a rich standard library, avoiding the need for third party tools for most work. Some packages
|
|
35
|
-
are so common, that the have a similar status to the standard library. Still, some code seems
|
|
36
|
-
to be written time and again, with every project. This small library, with minimal requirements,
|
|
37
|
-
hopes to stop this repetition.
|
|
38
|
-
|
|
39
|
-
## Links
|
|
40
|
-
|
|
41
|
-
- [Documentation]
|
|
42
|
-
- [Changelog]
|
|
43
|
-
|
|
44
|
-
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
45
|
-
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
46
|
-
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
47
|
-
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
48
|
-
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
49
|
-
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
50
|
-
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
51
|
-
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
52
|
-
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
53
|
-
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
54
|
-
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
55
|
-
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
56
|
-
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
57
|
-
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
58
|
-
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
59
|
-
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
60
|
-
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
61
|
-
[yam_url]: https://github.com/spapanik/yamk
|
|
62
|
-
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
63
|
-
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
64
|
-
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
65
|
-
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
pyutilkit-0.11.0/docs/README.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# pyutilkit: python's missing batteries
|
|
2
|
-
|
|
3
|
-
[![build][build_badge]][build_url]
|
|
4
|
-
[![lint][lint_badge]][lint_url]
|
|
5
|
-
[![tests][tests_badge]][tests_url]
|
|
6
|
-
[![license][licence_badge]][licence_url]
|
|
7
|
-
[![codecov][codecov_badge]][codecov_url]
|
|
8
|
-
[![readthedocs][readthedocs_badge]][readthedocs_url]
|
|
9
|
-
[![pypi][pypi_badge]][pypi_url]
|
|
10
|
-
[![downloads][pepy_badge]][pepy_url]
|
|
11
|
-
[![build automation: yam][yam_badge]][yam_url]
|
|
12
|
-
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
13
|
-
|
|
14
|
-
The Python has long maintained the philosophy of "batteries included", giving the user
|
|
15
|
-
a rich standard library, avoiding the need for third party tools for most work. Some packages
|
|
16
|
-
are so common, that the have a similar status to the standard library. Still, some code seems
|
|
17
|
-
to be written time and again, with every project. This small library, with minimal requirements,
|
|
18
|
-
hopes to stop this repetition.
|
|
19
|
-
|
|
20
|
-
## Links
|
|
21
|
-
|
|
22
|
-
- [Documentation]
|
|
23
|
-
- [Changelog]
|
|
24
|
-
|
|
25
|
-
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
26
|
-
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
27
|
-
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
28
|
-
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
29
|
-
[tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
30
|
-
[tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
31
|
-
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
32
|
-
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
33
|
-
[codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
|
|
34
|
-
[codecov_url]: https://codecov.io/github/spapanik/pyutilkit
|
|
35
|
-
[readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
|
|
36
|
-
[readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
|
|
37
|
-
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
38
|
-
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
39
|
-
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
40
|
-
[pepy_url]: https://pepy.tech/project/pyutilkit
|
|
41
|
-
[yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
|
|
42
|
-
[yam_url]: https://github.com/spapanik/yamk
|
|
43
|
-
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
44
|
-
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
45
|
-
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
46
|
-
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.11.0"
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import threading
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Singleton(type):
|
|
7
|
-
instance: type | None
|
|
8
|
-
|
|
9
|
-
def __init__(
|
|
10
|
-
cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
|
|
11
|
-
) -> None:
|
|
12
|
-
super().__init__(name, bases, namespace)
|
|
13
|
-
cls.instance = None
|
|
14
|
-
cls._lock = threading.Lock()
|
|
15
|
-
|
|
16
|
-
def __call__(cls) -> type:
|
|
17
|
-
if cls.instance is None:
|
|
18
|
-
with cls._lock:
|
|
19
|
-
if cls.instance is None:
|
|
20
|
-
cls.instance = super().__call__()
|
|
21
|
-
return cls.instance
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from subprocess import PIPE, Popen
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
from pyutilkit.timing import Stopwatch, Timing
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class ProcessOutput:
|
|
16
|
-
stdout: bytes
|
|
17
|
-
stderr: bytes
|
|
18
|
-
pid: int
|
|
19
|
-
returncode: int
|
|
20
|
-
elapsed: Timing
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def run_command(
|
|
24
|
-
command: str | list[str],
|
|
25
|
-
cwd: str | Path | None = None,
|
|
26
|
-
env: dict[str, str] | None = None,
|
|
27
|
-
) -> ProcessOutput:
|
|
28
|
-
stdout = []
|
|
29
|
-
stderr = []
|
|
30
|
-
stopwatch = Stopwatch()
|
|
31
|
-
with stopwatch:
|
|
32
|
-
process = Popen( # noqa: S603
|
|
33
|
-
command, stdout=PIPE, stderr=PIPE, cwd=cwd, env=env
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
for line in process.stdout or []:
|
|
37
|
-
sys.stdout.buffer.write(line)
|
|
38
|
-
sys.stdout.flush()
|
|
39
|
-
stdout.append(line)
|
|
40
|
-
|
|
41
|
-
for line in process.stderr or []:
|
|
42
|
-
sys.stderr.buffer.write(line)
|
|
43
|
-
sys.stderr.flush()
|
|
44
|
-
stderr.append(line)
|
|
45
|
-
|
|
46
|
-
with stopwatch:
|
|
47
|
-
process.wait()
|
|
48
|
-
|
|
49
|
-
return ProcessOutput(
|
|
50
|
-
stdout=b"".join(stdout),
|
|
51
|
-
stderr=b"".join(stderr),
|
|
52
|
-
pid=process.pid,
|
|
53
|
-
returncode=process.returncode,
|
|
54
|
-
elapsed=stopwatch.elapsed,
|
|
55
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|