pyutilkit 0.5.0__tar.gz → 0.8.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.
Potentially problematic release.
This version of pyutilkit might be problematic. Click here for more details.
- pyutilkit-0.8.0/LICENSE.md +11 -0
- PKG-INFO → pyutilkit-0.8.0/PKG-INFO +12 -4
- {docs → pyutilkit-0.8.0/docs}/README.md +8 -2
- pyproject.toml → pyutilkit-0.8.0/pyproject.toml +42 -78
- {src → pyutilkit-0.8.0/src}/pyutilkit/classes.py +3 -3
- {src → pyutilkit-0.8.0/src}/pyutilkit/date_utils.py +5 -11
- {src → pyutilkit-0.8.0/src}/pyutilkit/subprocess.py +15 -13
- pyutilkit-0.8.0/src/pyutilkit/term.py +202 -0
- {src → pyutilkit-0.8.0/src}/pyutilkit/timing.py +30 -24
- src/pyutilkit/term.py +0 -117
- {src → pyutilkit-0.8.0/src}/pyutilkit/__init__.py +0 -0
- {src → pyutilkit-0.8.0/src}/pyutilkit/data/__init__.py +0 -0
- {src → pyutilkit-0.8.0/src}/pyutilkit/data/timezones.py +0 -0
- {src → pyutilkit-0.8.0/src}/pyutilkit/files.py +0 -0
- {src → pyutilkit-0.8.0/src}/pyutilkit/py.typed +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright © 2024 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyutilkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: python's missing batteries
|
|
5
5
|
Home-page: https://pyutilkit.readthedocs.io/en/stable/
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -9,14 +9,18 @@ Author: Stephanos Kuma
|
|
|
9
9
|
Author-email: "Stephanos Kuma" <stephanos@kuma.ai>
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
13
|
-
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Requires-Dist: tzdata ; os_name == 'nt'
|
|
14
16
|
Project-URL: Documentation, https://pyutilkit.readthedocs.io/en/stable/
|
|
15
17
|
Project-URL: Repository, https://github.com/spapanik/pyutilkit
|
|
16
18
|
Description-Content-Type: text/markdown
|
|
17
19
|
|
|
18
20
|
# pyutilkit: python's missing batteries
|
|
19
21
|
|
|
22
|
+
[![build][build_badge]][build_url]
|
|
23
|
+
[![lint][lint_badge]][lint_url]
|
|
20
24
|
[![tests][test_badge]][test_url]
|
|
21
25
|
[![license][licence_badge]][licence_url]
|
|
22
26
|
[![pypi][pypi_badge]][pypi_url]
|
|
@@ -36,10 +40,14 @@ hopes to stop this repetition.
|
|
|
36
40
|
- [Documentation]
|
|
37
41
|
- [Changelog]
|
|
38
42
|
|
|
43
|
+
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
44
|
+
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
45
|
+
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
46
|
+
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
39
47
|
[test_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
40
48
|
[test_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
41
49
|
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
42
|
-
[licence_url]: https://
|
|
50
|
+
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
43
51
|
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
44
52
|
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
45
53
|
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
@@ -51,4 +59,4 @@ hopes to stop this repetition.
|
|
|
51
59
|
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
52
60
|
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
53
61
|
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
54
|
-
[Changelog]: https://
|
|
62
|
+
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pyutilkit: python's missing batteries
|
|
2
2
|
|
|
3
|
+
[![build][build_badge]][build_url]
|
|
4
|
+
[![lint][lint_badge]][lint_url]
|
|
3
5
|
[![tests][test_badge]][test_url]
|
|
4
6
|
[![license][licence_badge]][licence_url]
|
|
5
7
|
[![pypi][pypi_badge]][pypi_url]
|
|
@@ -19,10 +21,14 @@ hopes to stop this repetition.
|
|
|
19
21
|
- [Documentation]
|
|
20
22
|
- [Changelog]
|
|
21
23
|
|
|
24
|
+
[build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
|
|
25
|
+
[build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
|
|
26
|
+
[lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
|
|
27
|
+
[lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
|
|
22
28
|
[test_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
|
|
23
29
|
[test_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
|
|
24
30
|
[licence_badge]: https://img.shields.io/pypi/l/pyutilkit
|
|
25
|
-
[licence_url]: https://
|
|
31
|
+
[licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
|
|
26
32
|
[pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
|
|
27
33
|
[pypi_url]: https://pypi.org/project/pyutilkit
|
|
28
34
|
[pepy_badge]: https://pepy.tech/badge/pyutilkit
|
|
@@ -34,4 +40,4 @@ hopes to stop this repetition.
|
|
|
34
40
|
[ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
|
|
35
41
|
[ruff_url]: https://github.com/charliermarsh/ruff
|
|
36
42
|
[Documentation]: https://pyutilkit.readthedocs.io/en/stable/
|
|
37
|
-
[Changelog]: https://
|
|
43
|
+
[Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"phosphorus>=0.
|
|
3
|
+
"phosphorus>=0.8",
|
|
4
4
|
]
|
|
5
5
|
build-backend = "phosphorus.construction.api"
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyutilkit"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.8.0"
|
|
10
10
|
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
|
|
@@ -21,11 +21,13 @@ keywords = [
|
|
|
21
21
|
classifiers = [
|
|
22
22
|
"Development Status :: 4 - Beta",
|
|
23
23
|
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"License :: OSI Approved :: BSD License",
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
requires-python = ">=3.9"
|
|
27
29
|
dependencies = [
|
|
28
|
-
"
|
|
30
|
+
"tzdata; os_name == 'nt'",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
@@ -35,26 +37,25 @@ documentation = "https://pyutilkit.readthedocs.io/en/stable/"
|
|
|
35
37
|
|
|
36
38
|
[tool.phosphorus.dev-dependencies]
|
|
37
39
|
dev = [
|
|
38
|
-
"ipdb~=0.13
|
|
39
|
-
"ipython~=8.
|
|
40
|
+
"ipdb~=0.13",
|
|
41
|
+
"ipython~=8.18",
|
|
40
42
|
]
|
|
41
43
|
lint = [
|
|
42
|
-
"black~=24.
|
|
43
|
-
"mypy~=1.
|
|
44
|
-
"ruff~=0.
|
|
45
|
-
"typing_extensions~=4.11",
|
|
44
|
+
"black~=24.10",
|
|
45
|
+
"mypy~=1.13",
|
|
46
|
+
"ruff~=0.8",
|
|
46
47
|
]
|
|
47
48
|
test = [
|
|
48
49
|
"freezegun~=1.5",
|
|
49
|
-
"pytest~=8.
|
|
50
|
-
"pytest-cov~=
|
|
50
|
+
"pytest~=8.3",
|
|
51
|
+
"pytest-cov~=6.0",
|
|
51
52
|
]
|
|
52
53
|
docs = [
|
|
53
54
|
"mkdocs~=1.6",
|
|
54
55
|
"mkdocs-material~=9.5",
|
|
55
56
|
"mkdocs-material-extensions~=1.3",
|
|
56
|
-
"
|
|
57
|
-
"pymdown-extensions~=10.
|
|
57
|
+
"pygments~=2.18",
|
|
58
|
+
"pymdown-extensions~=10.12",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
[tool.black]
|
|
@@ -64,7 +65,11 @@ target-version = [
|
|
|
64
65
|
|
|
65
66
|
[tool.mypy]
|
|
66
67
|
check_untyped_defs = true
|
|
68
|
+
disallow_any_decorated = true
|
|
69
|
+
disallow_any_explicit = true
|
|
70
|
+
disallow_any_expr = true
|
|
67
71
|
disallow_any_generics = true
|
|
72
|
+
disallow_any_unimported = true
|
|
68
73
|
disallow_incomplete_defs = true
|
|
69
74
|
disallow_subclassing_any = true
|
|
70
75
|
disallow_untyped_calls = true
|
|
@@ -73,13 +78,18 @@ disallow_untyped_defs = true
|
|
|
73
78
|
extra_checks = true
|
|
74
79
|
ignore_missing_imports = true
|
|
75
80
|
no_implicit_reexport = true
|
|
81
|
+
show_column_numbers = true
|
|
76
82
|
show_error_codes = true
|
|
77
83
|
strict_equality = true
|
|
78
|
-
warn_return_any = true
|
|
79
84
|
warn_redundant_casts = true
|
|
85
|
+
warn_return_any = true
|
|
86
|
+
warn_unused_configs = true
|
|
80
87
|
warn_unused_ignores = true
|
|
81
88
|
warn_unreachable = true
|
|
82
|
-
|
|
89
|
+
|
|
90
|
+
[[tool.mypy.overrides]]
|
|
91
|
+
module = "tests.*"
|
|
92
|
+
disallow_any_decorated = false # mock.MagicMock is Any
|
|
83
93
|
|
|
84
94
|
[tool.ruff]
|
|
85
95
|
src = [
|
|
@@ -89,74 +99,22 @@ target-version = "py39"
|
|
|
89
99
|
|
|
90
100
|
[tool.ruff.lint]
|
|
91
101
|
select = [
|
|
92
|
-
"
|
|
93
|
-
"ANN",
|
|
94
|
-
"ARG",
|
|
95
|
-
"ASYNC",
|
|
96
|
-
"B",
|
|
97
|
-
"BLE",
|
|
98
|
-
"C4",
|
|
99
|
-
"COM",
|
|
100
|
-
"DTZ",
|
|
101
|
-
"E",
|
|
102
|
-
"EM",
|
|
103
|
-
"ERA",
|
|
104
|
-
"EXE",
|
|
105
|
-
"F",
|
|
106
|
-
"FA",
|
|
107
|
-
"FBT",
|
|
108
|
-
"FIX",
|
|
109
|
-
"FLY",
|
|
110
|
-
"FURB",
|
|
111
|
-
"G",
|
|
112
|
-
"I",
|
|
113
|
-
"ICN",
|
|
114
|
-
"INP",
|
|
115
|
-
"ISC",
|
|
116
|
-
"LOG",
|
|
117
|
-
"N",
|
|
118
|
-
"PGH",
|
|
119
|
-
"PERF",
|
|
120
|
-
"PIE",
|
|
121
|
-
"PLC",
|
|
122
|
-
"PLE",
|
|
123
|
-
"PLW",
|
|
124
|
-
"PT",
|
|
125
|
-
"PTH",
|
|
126
|
-
"Q",
|
|
127
|
-
"RET",
|
|
128
|
-
"RSE",
|
|
129
|
-
"RUF",
|
|
130
|
-
"S",
|
|
131
|
-
"SIM",
|
|
132
|
-
"SLF",
|
|
133
|
-
"SLOT",
|
|
134
|
-
"T10",
|
|
135
|
-
"TCH",
|
|
136
|
-
"TD",
|
|
137
|
-
"TID",
|
|
138
|
-
"TRY",
|
|
139
|
-
"UP",
|
|
140
|
-
"W",
|
|
141
|
-
"YTT",
|
|
102
|
+
"ALL",
|
|
142
103
|
]
|
|
143
104
|
ignore = [
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"E501",
|
|
149
|
-
"
|
|
150
|
-
"TD002",
|
|
151
|
-
"TD003",
|
|
152
|
-
"TRY003",
|
|
105
|
+
"COM812", # Avoid magic trailing commas
|
|
106
|
+
"D10", # Not everything needs a docstring
|
|
107
|
+
"D203", # Prefer `no-blank-line-before-class` (D211)
|
|
108
|
+
"D213", # Prefer `multi-line-summary-first-line` (D212)
|
|
109
|
+
"E501", # Avoid clashes with black
|
|
110
|
+
"PLR09", # Adding a limit to complexity is too arbitrary
|
|
153
111
|
]
|
|
154
112
|
|
|
155
113
|
[tool.ruff.lint.per-file-ignores]
|
|
156
114
|
"tests/**" = [
|
|
157
|
-
"FBT001",
|
|
158
|
-
"
|
|
159
|
-
"S101",
|
|
115
|
+
"FBT001", # Test arguments are handled by pytest
|
|
116
|
+
"PLR2004", # Tests should contain magic number comparisons
|
|
117
|
+
"S101", # Pytest needs assert statements
|
|
160
118
|
]
|
|
161
119
|
|
|
162
120
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
@@ -174,16 +132,22 @@ forced-separate = [
|
|
|
174
132
|
split-on-trailing-comma = false
|
|
175
133
|
|
|
176
134
|
[tool.pytest.ini_options]
|
|
177
|
-
addopts = "-
|
|
135
|
+
addopts = "-ra -v"
|
|
178
136
|
testpaths = "tests"
|
|
179
137
|
|
|
180
138
|
[tool.coverage.run]
|
|
139
|
+
branch = true
|
|
181
140
|
source = [
|
|
182
141
|
"src/",
|
|
183
142
|
]
|
|
184
143
|
data_file = ".cov_cache/coverage.dat"
|
|
185
144
|
|
|
186
145
|
[tool.coverage.report]
|
|
146
|
+
exclude_also = [
|
|
147
|
+
"if TYPE_CHECKING:",
|
|
148
|
+
]
|
|
149
|
+
fail_under = 75
|
|
150
|
+
precision = 2
|
|
187
151
|
show_missing = true
|
|
188
152
|
skip_covered = true
|
|
189
153
|
skip_empty = true
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import cast
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Singleton(type):
|
|
7
7
|
instance: type[Singleton] | None
|
|
8
8
|
|
|
9
9
|
def __init__(
|
|
10
|
-
cls, name: str, bases: tuple[type[
|
|
10
|
+
cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
|
|
11
11
|
) -> None:
|
|
12
12
|
super().__init__(name, bases, namespace)
|
|
13
13
|
cls.instance = None
|
|
@@ -15,4 +15,4 @@ class Singleton(type):
|
|
|
15
15
|
def __call__(cls) -> type[Singleton]:
|
|
16
16
|
if cls.instance is None:
|
|
17
17
|
cls.instance = super().__call__()
|
|
18
|
-
return cls.instance
|
|
18
|
+
return cast(type[Singleton], cls.instance)
|
|
@@ -9,8 +9,7 @@ UTC = ZoneInfo("UTC")
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def get_timezones() -> set[str]:
|
|
12
|
-
"""
|
|
13
|
-
Get all the available timezones
|
|
12
|
+
"""Get all the available timezones.
|
|
14
13
|
|
|
15
14
|
This takes into accounts timezones that might not be present in
|
|
16
15
|
all systems.
|
|
@@ -23,8 +22,7 @@ def now(tz_info: ZoneInfo = UTC) -> datetime:
|
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def from_iso(date_string: str, tz_info: tzinfo = UTC) -> datetime:
|
|
26
|
-
"""
|
|
27
|
-
Get datetime from an iso string
|
|
25
|
+
"""Get datetime from an iso string.
|
|
28
26
|
|
|
29
27
|
This to allow the Zulu timezone, which is a valid ISO timezone.
|
|
30
28
|
"""
|
|
@@ -37,16 +35,13 @@ def from_iso(date_string: str, tz_info: tzinfo = UTC) -> datetime:
|
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def from_timestamp(timestamp: float, tz_info: tzinfo = UTC) -> datetime:
|
|
40
|
-
"""
|
|
41
|
-
Get a datetime tz-aware time object from a timestamp
|
|
42
|
-
"""
|
|
38
|
+
"""Get a datetime tz-aware time object from a timestamp."""
|
|
43
39
|
utc_dt = datetime.fromtimestamp(timestamp, tz=UTC)
|
|
44
40
|
return convert_timezone(utc_dt, tz_info)
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
def add_timezone(dt: datetime, tz_info: tzinfo = UTC) -> datetime:
|
|
48
|
-
"""
|
|
49
|
-
Add a timezone to a naive datetime
|
|
44
|
+
"""Add a timezone to a naive datetime.
|
|
50
45
|
|
|
51
46
|
Raise an error in case of a tz-aware datetime
|
|
52
47
|
"""
|
|
@@ -57,8 +52,7 @@ def add_timezone(dt: datetime, tz_info: tzinfo = UTC) -> datetime:
|
|
|
57
52
|
|
|
58
53
|
|
|
59
54
|
def convert_timezone(dt: datetime, tz_info: tzinfo = UTC) -> datetime:
|
|
60
|
-
"""
|
|
61
|
-
Change the timezone of a tz-aware datetime
|
|
55
|
+
"""Change the timezone of a tz-aware datetime.
|
|
62
56
|
|
|
63
57
|
Raise an error in case of a naive datetime
|
|
64
58
|
"""
|
|
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import sys
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from subprocess import PIPE, Popen
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from pyutilkit.timing import Stopwatch, Timing
|
|
9
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
@dataclass(frozen=True)
|
|
12
15
|
class ProcessOutput:
|
|
@@ -17,27 +20,26 @@ class ProcessOutput:
|
|
|
17
20
|
elapsed: Timing
|
|
18
21
|
|
|
19
22
|
|
|
20
|
-
def run_command(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
msg = "stderr must be set to PIPE"
|
|
26
|
-
raise ValueError(msg)
|
|
27
|
-
|
|
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
28
|
stdout = []
|
|
29
29
|
stderr = []
|
|
30
30
|
stopwatch = Stopwatch()
|
|
31
31
|
with stopwatch:
|
|
32
|
-
process = Popen(
|
|
32
|
+
process = Popen( # noqa: S603
|
|
33
|
+
command, stdout=PIPE, stderr=PIPE, cwd=cwd, env=env
|
|
34
|
+
)
|
|
33
35
|
|
|
34
36
|
for line in process.stdout or []:
|
|
35
|
-
sys.stdout.buffer.write(line)
|
|
37
|
+
sys.stdout.buffer.write(line) # type: ignore[misc]
|
|
36
38
|
sys.stdout.flush()
|
|
37
39
|
stdout.append(line)
|
|
38
40
|
|
|
39
41
|
for line in process.stderr or []:
|
|
40
|
-
sys.stderr.buffer.write(line)
|
|
42
|
+
sys.stderr.buffer.write(line) # type: ignore[misc]
|
|
41
43
|
sys.stderr.flush()
|
|
42
44
|
stderr.append(line)
|
|
43
45
|
|
|
@@ -48,6 +50,6 @@ def run_command(command: str | list[str], **kwargs: Any) -> ProcessOutput:
|
|
|
48
50
|
stdout=b"".join(stdout),
|
|
49
51
|
stderr=b"".join(stderr),
|
|
50
52
|
pid=process.pid,
|
|
51
|
-
returncode=process.returncode,
|
|
53
|
+
returncode=process.returncode, # type: ignore[misc]
|
|
52
54
|
elapsed=stopwatch.elapsed,
|
|
53
55
|
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from enum import IntEnum, unique
|
|
6
|
+
from math import ceil, floor
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from typing_extensions import Self # py3.10: import from typing
|
|
13
|
+
|
|
14
|
+
TRUE_VAR = {"0", "false", "no"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@unique
|
|
18
|
+
class SGRCodes(IntEnum):
|
|
19
|
+
RESET = 0
|
|
20
|
+
|
|
21
|
+
BOLD = 1
|
|
22
|
+
ITALIC = 3
|
|
23
|
+
UNDERLINE = 4
|
|
24
|
+
BLINK = 5
|
|
25
|
+
REVERSE = 7
|
|
26
|
+
CONCEAL = 8
|
|
27
|
+
|
|
28
|
+
BLACK = 30
|
|
29
|
+
RED = 31
|
|
30
|
+
GREEN = 32
|
|
31
|
+
YELLOW = 33
|
|
32
|
+
BLUE = 34
|
|
33
|
+
MAGENTA = 35
|
|
34
|
+
CYAN = 36
|
|
35
|
+
GREY = 37
|
|
36
|
+
|
|
37
|
+
BG_BLACK = 40
|
|
38
|
+
BG_RED = 41
|
|
39
|
+
BG_GREEN = 42
|
|
40
|
+
BG_YELLOW = 43
|
|
41
|
+
BG_BLUE = 44
|
|
42
|
+
BG_MAGENTA = 45
|
|
43
|
+
BG_CYAN = 46
|
|
44
|
+
BG_GREY = 47
|
|
45
|
+
|
|
46
|
+
BLACK_BRIGHT = 90
|
|
47
|
+
RED_BRIGHT = 91
|
|
48
|
+
GREEN_BRIGHT = 92
|
|
49
|
+
YELLOW_BRIGHT = 93
|
|
50
|
+
BLUE_BRIGHT = 94
|
|
51
|
+
MAGENTA_BRIGHT = 95
|
|
52
|
+
CYAN_BRIGHT = 96
|
|
53
|
+
WHITE_BRIGHT = 97
|
|
54
|
+
|
|
55
|
+
BG_BLACK_BRIGHT = 100
|
|
56
|
+
BG_RED_BRIGHT = 101
|
|
57
|
+
BG_GREEN_BRIGHT = 102
|
|
58
|
+
BG_YELLOW_BRIGHT = 103
|
|
59
|
+
BG_BLUE_BRIGHT = 104
|
|
60
|
+
BG_MAGENTA_BRIGHT = 105
|
|
61
|
+
BG_CYAN_BRIGHT = 106
|
|
62
|
+
BG_WHITE_BRIGHT = 107
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def sequence(self) -> str:
|
|
66
|
+
return f"\033[{self.value}m"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SGRString(str):
|
|
70
|
+
_sgr: tuple[SGRCodes, ...]
|
|
71
|
+
_string: str
|
|
72
|
+
_prefix: str
|
|
73
|
+
_suffix: str
|
|
74
|
+
__slots__ = ("_prefix", "_sgr", "_string", "_suffix")
|
|
75
|
+
|
|
76
|
+
def __new__(
|
|
77
|
+
cls,
|
|
78
|
+
obj: object,
|
|
79
|
+
*,
|
|
80
|
+
prefix: str = "",
|
|
81
|
+
suffix: str = "",
|
|
82
|
+
params: Iterable[SGRCodes] = (),
|
|
83
|
+
) -> Self:
|
|
84
|
+
string = super().__new__(cls, obj)
|
|
85
|
+
params = tuple(params)
|
|
86
|
+
object.__setattr__(string, "_prefix", prefix)
|
|
87
|
+
object.__setattr__(string, "_string", str(obj))
|
|
88
|
+
object.__setattr__(string, "_sgr", params)
|
|
89
|
+
object.__setattr__(string, "_suffix", suffix)
|
|
90
|
+
return string
|
|
91
|
+
|
|
92
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
93
|
+
msg = "SGRString is immutable"
|
|
94
|
+
raise AttributeError(msg)
|
|
95
|
+
|
|
96
|
+
def __delattr__(self, name: str) -> None:
|
|
97
|
+
msg = "SGRString is immutable"
|
|
98
|
+
raise AttributeError(msg)
|
|
99
|
+
|
|
100
|
+
def __str__(self) -> str:
|
|
101
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
102
|
+
sgr_suffix = SGRCodes.RESET.sequence if self._sgr else ""
|
|
103
|
+
return f"{self._prefix}{sgr_prefix}{self._string}{sgr_suffix}{self._suffix}"
|
|
104
|
+
|
|
105
|
+
def __len__(self) -> int:
|
|
106
|
+
return len(self._prefix) + len(self._string) + len(self._suffix)
|
|
107
|
+
|
|
108
|
+
def __mul__(self, other: object) -> Self:
|
|
109
|
+
if not isinstance(other, int):
|
|
110
|
+
return NotImplemented
|
|
111
|
+
return type(self)(
|
|
112
|
+
self._string * other,
|
|
113
|
+
prefix=self._prefix,
|
|
114
|
+
suffix=self._suffix,
|
|
115
|
+
params=self._sgr,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def __rmul__(self, other: object) -> Self:
|
|
119
|
+
if not isinstance(other, int):
|
|
120
|
+
return NotImplemented
|
|
121
|
+
return type(self)(
|
|
122
|
+
self._string * other,
|
|
123
|
+
prefix=self._prefix,
|
|
124
|
+
suffix=self._suffix,
|
|
125
|
+
params=self._sgr,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def print(
|
|
129
|
+
self,
|
|
130
|
+
end: str = "\n",
|
|
131
|
+
*,
|
|
132
|
+
force_prefix: bool = False,
|
|
133
|
+
force_sgr: bool = False,
|
|
134
|
+
is_error: bool = False,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Print the command output.
|
|
137
|
+
|
|
138
|
+
The command will be printed to stdout if it's not the output of an error,
|
|
139
|
+
otherwise to stderr.
|
|
140
|
+
|
|
141
|
+
If the output stream isn't a tty, it will strip the SGR codes and the prefix,
|
|
142
|
+
unless forced to keep them.
|
|
143
|
+
"""
|
|
144
|
+
file = sys.stderr if is_error else sys.stdout
|
|
145
|
+
if file.isatty(): # type: ignore[misc]
|
|
146
|
+
prefix = self._prefix
|
|
147
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
148
|
+
sgr_suffix = SGRCodes.RESET.sequence
|
|
149
|
+
suffix = self._suffix
|
|
150
|
+
else:
|
|
151
|
+
prefix = ""
|
|
152
|
+
sgr_prefix = ""
|
|
153
|
+
sgr_suffix = ""
|
|
154
|
+
suffix = ""
|
|
155
|
+
if force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUE_VAR:
|
|
156
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
157
|
+
sgr_suffix = SGRCodes.RESET.sequence if self._sgr else ""
|
|
158
|
+
if (
|
|
159
|
+
force_prefix
|
|
160
|
+
or os.getenv("PY_UTIL_FORCE_OUTFIX", "").lower() in TRUE_VAR
|
|
161
|
+
):
|
|
162
|
+
prefix = self._prefix
|
|
163
|
+
suffix = self._suffix
|
|
164
|
+
|
|
165
|
+
print(
|
|
166
|
+
prefix,
|
|
167
|
+
sgr_prefix,
|
|
168
|
+
self._string,
|
|
169
|
+
sgr_suffix,
|
|
170
|
+
suffix,
|
|
171
|
+
sep="",
|
|
172
|
+
end=end,
|
|
173
|
+
file=file,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def header(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
padding: str = " ",
|
|
180
|
+
left_spaces: int = 1,
|
|
181
|
+
right_spaces: int = 1,
|
|
182
|
+
space: str = " ",
|
|
183
|
+
force_sgr: bool = False,
|
|
184
|
+
is_error: bool = False,
|
|
185
|
+
) -> None:
|
|
186
|
+
try:
|
|
187
|
+
terminal_size = os.get_terminal_size()
|
|
188
|
+
except OSError:
|
|
189
|
+
# in pseudo-terminals an OSError is thrown
|
|
190
|
+
self.print(force_prefix=True, force_sgr=force_sgr, is_error=is_error)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
columns = terminal_size.columns
|
|
194
|
+
title_length = left_spaces + len(self) + right_spaces
|
|
195
|
+
if title_length >= columns:
|
|
196
|
+
self.print(is_error=is_error)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
half = (columns - title_length) / 2
|
|
200
|
+
prefix = f"{padding * ceil(half)}{space * left_spaces}{self._prefix}"
|
|
201
|
+
suffix = f"{self._suffix}{space * right_spaces}{padding * floor(half)}"
|
|
202
|
+
type(self)(self._string, prefix=prefix, suffix=suffix, params=self._sgr).print()
|
|
@@ -2,13 +2,19 @@ from __future__ import annotations # py3.9: remove this line
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from time import perf_counter_ns
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from types import TracebackType
|
|
9
9
|
|
|
10
10
|
from typing_extensions import Self # py3.10: import Self from typing
|
|
11
11
|
|
|
12
|
+
METRIC_MULTIPLIER = 1000
|
|
13
|
+
SECONDS_PER_MINUTE = 60
|
|
14
|
+
MINUTES_PER_HOUR = 60
|
|
15
|
+
HOURS_PER_DAY = 24
|
|
16
|
+
SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY
|
|
17
|
+
|
|
12
18
|
|
|
13
19
|
@dataclass(frozen=True, order=True)
|
|
14
20
|
class Timing:
|
|
@@ -27,62 +33,62 @@ class Timing:
|
|
|
27
33
|
) -> None:
|
|
28
34
|
total_nanoseconds = (
|
|
29
35
|
nanoseconds
|
|
30
|
-
+
|
|
31
|
-
+
|
|
32
|
-
+
|
|
33
|
-
+
|
|
36
|
+
+ METRIC_MULTIPLIER * microseconds
|
|
37
|
+
+ METRIC_MULTIPLIER**2 * milliseconds
|
|
38
|
+
+ METRIC_MULTIPLIER**3 * seconds
|
|
39
|
+
+ SECONDS_PER_DAY * METRIC_MULTIPLIER**3 * days
|
|
34
40
|
)
|
|
35
41
|
object.__setattr__(self, "nanoseconds", total_nanoseconds)
|
|
36
42
|
|
|
37
43
|
def __str__(self) -> str:
|
|
38
|
-
if self.nanoseconds <
|
|
44
|
+
if self.nanoseconds < METRIC_MULTIPLIER:
|
|
39
45
|
return f"{self.nanoseconds}ns"
|
|
40
|
-
microseconds = self.nanoseconds /
|
|
41
|
-
if microseconds <
|
|
46
|
+
microseconds = self.nanoseconds / METRIC_MULTIPLIER
|
|
47
|
+
if microseconds < METRIC_MULTIPLIER:
|
|
42
48
|
return f"{microseconds:.1f}µs"
|
|
43
|
-
milliseconds = microseconds /
|
|
44
|
-
if milliseconds <
|
|
49
|
+
milliseconds = microseconds / METRIC_MULTIPLIER
|
|
50
|
+
if milliseconds < METRIC_MULTIPLIER:
|
|
45
51
|
return f"{milliseconds:.1f}ms"
|
|
46
|
-
seconds = milliseconds /
|
|
47
|
-
if seconds <
|
|
52
|
+
seconds = milliseconds / METRIC_MULTIPLIER
|
|
53
|
+
if seconds < SECONDS_PER_MINUTE:
|
|
48
54
|
return f"{seconds:.2f}s"
|
|
49
55
|
round_seconds = int(seconds)
|
|
50
|
-
minutes, seconds = divmod(round_seconds,
|
|
51
|
-
hours, minutes = divmod(minutes,
|
|
52
|
-
if hours <
|
|
56
|
+
minutes, seconds = divmod(round_seconds, SECONDS_PER_MINUTE)
|
|
57
|
+
hours, minutes = divmod(minutes, MINUTES_PER_HOUR)
|
|
58
|
+
if hours < HOURS_PER_DAY:
|
|
53
59
|
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
54
|
-
days, hours = divmod(hours,
|
|
60
|
+
days, hours = divmod(hours, HOURS_PER_DAY)
|
|
55
61
|
return f"{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
56
62
|
|
|
57
63
|
def __bool__(self) -> bool:
|
|
58
64
|
return bool(self.nanoseconds)
|
|
59
65
|
|
|
60
|
-
def __add__(self, other:
|
|
66
|
+
def __add__(self, other: object) -> Timing:
|
|
61
67
|
if not isinstance(other, Timing):
|
|
62
68
|
return NotImplemented
|
|
63
69
|
return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
|
|
64
70
|
|
|
65
|
-
def __radd__(self, other:
|
|
71
|
+
def __radd__(self, other: object) -> Timing:
|
|
66
72
|
if not isinstance(other, Timing):
|
|
67
73
|
return NotImplemented
|
|
68
74
|
return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
|
|
69
75
|
|
|
70
|
-
def __mul__(self, other:
|
|
76
|
+
def __mul__(self, other: object) -> Timing:
|
|
71
77
|
if not isinstance(other, int):
|
|
72
78
|
return NotImplemented
|
|
73
79
|
return Timing(nanoseconds=self.nanoseconds * other)
|
|
74
80
|
|
|
75
|
-
def __rmul__(self, other:
|
|
81
|
+
def __rmul__(self, other: object) -> Timing:
|
|
76
82
|
if not isinstance(other, int):
|
|
77
83
|
return NotImplemented
|
|
78
84
|
return Timing(nanoseconds=self.nanoseconds * other)
|
|
79
85
|
|
|
80
|
-
def __floordiv__(self, other:
|
|
86
|
+
def __floordiv__(self, other: object) -> Timing:
|
|
81
87
|
if not isinstance(other, int):
|
|
82
88
|
return NotImplemented
|
|
83
89
|
return Timing(nanoseconds=self.nanoseconds // other)
|
|
84
90
|
|
|
85
|
-
def __truediv__(self, other:
|
|
91
|
+
def __truediv__(self, other: object) -> Timing:
|
|
86
92
|
if not isinstance(other, int):
|
|
87
93
|
return NotImplemented
|
|
88
94
|
return Timing(nanoseconds=self.nanoseconds // other)
|
|
@@ -102,8 +108,8 @@ class Stopwatch:
|
|
|
102
108
|
|
|
103
109
|
def __exit__(
|
|
104
110
|
self,
|
|
105
|
-
exc_type: type[
|
|
106
|
-
exc_value:
|
|
111
|
+
exc_type: type[BaseException] | None,
|
|
112
|
+
exc_value: BaseException | None,
|
|
107
113
|
traceback: TracebackType | None,
|
|
108
114
|
) -> None:
|
|
109
115
|
_end = perf_counter_ns()
|
src/pyutilkit/term.py
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from enum import IntEnum, unique
|
|
5
|
-
from math import ceil, floor
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from collections.abc import Iterable
|
|
10
|
-
|
|
11
|
-
from typing_extensions import Self # py3.10: import from typing
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@unique
|
|
15
|
-
class SGRCodes(IntEnum):
|
|
16
|
-
RESET = 0
|
|
17
|
-
|
|
18
|
-
BOLD = 1
|
|
19
|
-
ITALIC = 3
|
|
20
|
-
UNDERLINE = 4
|
|
21
|
-
BLINK = 5
|
|
22
|
-
REVERSE = 7
|
|
23
|
-
CONCEAL = 8
|
|
24
|
-
|
|
25
|
-
BLACK = 30
|
|
26
|
-
RED = 31
|
|
27
|
-
GREEN = 32
|
|
28
|
-
YELLOW = 33
|
|
29
|
-
BLUE = 34
|
|
30
|
-
MAGENTA = 35
|
|
31
|
-
CYAN = 36
|
|
32
|
-
GREY = 37
|
|
33
|
-
|
|
34
|
-
BG_BLACK = 40
|
|
35
|
-
BG_RED = 41
|
|
36
|
-
BG_GREEN = 42
|
|
37
|
-
BG_YELLOW = 43
|
|
38
|
-
BG_BLUE = 44
|
|
39
|
-
BG_MAGENTA = 45
|
|
40
|
-
BG_CYAN = 46
|
|
41
|
-
BG_GREY = 47
|
|
42
|
-
|
|
43
|
-
BLACK_BRIGHT = 90
|
|
44
|
-
RED_BRIGHT = 91
|
|
45
|
-
GREEN_BRIGHT = 92
|
|
46
|
-
YELLOW_BRIGHT = 93
|
|
47
|
-
BLUE_BRIGHT = 94
|
|
48
|
-
MAGENTA_BRIGHT = 95
|
|
49
|
-
CYAN_BRIGHT = 96
|
|
50
|
-
WHITE_BRIGHT = 97
|
|
51
|
-
|
|
52
|
-
BG_BLACK_BRIGHT = 100
|
|
53
|
-
BG_RED_BRIGHT = 101
|
|
54
|
-
BG_GREEN_BRIGHT = 102
|
|
55
|
-
BG_YELLOW_BRIGHT = 103
|
|
56
|
-
BG_BLUE_BRIGHT = 104
|
|
57
|
-
BG_MAGENTA_BRIGHT = 105
|
|
58
|
-
BG_CYAN_BRIGHT = 106
|
|
59
|
-
BG_WHITE_BRIGHT = 107
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def sequence(self) -> str:
|
|
63
|
-
return f"\033[{self.value}m"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class SGRString(str):
|
|
67
|
-
_sgr: tuple[SGRCodes, ...]
|
|
68
|
-
_string: str
|
|
69
|
-
__slots__ = ("_sgr", "_string")
|
|
70
|
-
|
|
71
|
-
def __new__(cls, obj: Any, *, params: Iterable[SGRCodes] = ()) -> Self:
|
|
72
|
-
string = super().__new__(cls, obj)
|
|
73
|
-
object.__setattr__(string, "_string", str(obj))
|
|
74
|
-
object.__setattr__(string, "_sgr", tuple(params))
|
|
75
|
-
return string
|
|
76
|
-
|
|
77
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
78
|
-
msg = "SGRString is immutable"
|
|
79
|
-
raise AttributeError(msg)
|
|
80
|
-
|
|
81
|
-
def __delattr__(self, name: str) -> None:
|
|
82
|
-
msg = "SGRString is immutable"
|
|
83
|
-
raise AttributeError(msg)
|
|
84
|
-
|
|
85
|
-
def __str__(self) -> str:
|
|
86
|
-
if not self._sgr:
|
|
87
|
-
return self._string
|
|
88
|
-
prefix = "".join(code.sequence for code in self._sgr)
|
|
89
|
-
return f"{prefix}{self._string}{SGRCodes.RESET.sequence}"
|
|
90
|
-
|
|
91
|
-
def __mul__(self, other: Any) -> Self:
|
|
92
|
-
if not isinstance(other, int):
|
|
93
|
-
return NotImplemented
|
|
94
|
-
return type(self)(self._string * other, params=self._sgr)
|
|
95
|
-
|
|
96
|
-
def __rmul__(self, other: Any) -> Self:
|
|
97
|
-
if not isinstance(other, int):
|
|
98
|
-
return NotImplemented
|
|
99
|
-
return type(self)(self._string * other, params=self._sgr)
|
|
100
|
-
|
|
101
|
-
def header(
|
|
102
|
-
self,
|
|
103
|
-
*,
|
|
104
|
-
padding: str = " ",
|
|
105
|
-
left_spaces: int = 1,
|
|
106
|
-
right_spaces: int = 1,
|
|
107
|
-
space: str = " ",
|
|
108
|
-
) -> None:
|
|
109
|
-
columns = os.get_terminal_size().columns
|
|
110
|
-
text = f"{space * left_spaces}{self}{space * right_spaces}"
|
|
111
|
-
title_length = left_spaces + len(self) + right_spaces
|
|
112
|
-
if title_length >= columns:
|
|
113
|
-
print(text.strip())
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
half = (columns - title_length) / 2
|
|
117
|
-
print(f"{padding * ceil(half)}{text}{padding * floor(half)}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|