pyutilkit 0.5.0__tar.gz → 0.9.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.9.0/LICENSE.md +11 -0
- PKG-INFO → pyutilkit-0.9.0/PKG-INFO +12 -4
- {docs → pyutilkit-0.9.0/docs}/README.md +8 -2
- pyproject.toml → pyutilkit-0.9.0/pyproject.toml +43 -78
- {src → pyutilkit-0.9.0/src}/pyutilkit/classes.py +3 -3
- {src → pyutilkit-0.9.0/src}/pyutilkit/date_utils.py +5 -11
- {src → pyutilkit-0.9.0/src}/pyutilkit/subprocess.py +12 -10
- pyutilkit-0.9.0/src/pyutilkit/term.py +292 -0
- {src → pyutilkit-0.9.0/src}/pyutilkit/timing.py +31 -25
- src/pyutilkit/term.py +0 -117
- {src → pyutilkit-0.9.0/src}/pyutilkit/__init__.py +0 -0
- {src → pyutilkit-0.9.0/src}/pyutilkit/data/__init__.py +0 -0
- {src → pyutilkit-0.9.0/src}/pyutilkit/data/timezones.py +0 -0
- {src → pyutilkit-0.9.0/src}/pyutilkit/files.py +0 -0
- {src → pyutilkit-0.9.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.9.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.9.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 = false # many builtins are Any
|
|
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,23 @@ 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
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"TD003",
|
|
152
|
-
"TRY003",
|
|
105
|
+
"C901", # Adding a limit to complexity is too arbitrary
|
|
106
|
+
"COM812", # Avoid magic trailing commas
|
|
107
|
+
"D10", # Not everything needs a docstring
|
|
108
|
+
"D203", # Prefer `no-blank-line-before-class` (D211)
|
|
109
|
+
"D213", # Prefer `multi-line-summary-first-line` (D212)
|
|
110
|
+
"E501", # Avoid clashes with black
|
|
111
|
+
"PLR09", # Adding a limit to complexity is too arbitrary
|
|
153
112
|
]
|
|
154
113
|
|
|
155
114
|
[tool.ruff.lint.per-file-ignores]
|
|
156
115
|
"tests/**" = [
|
|
157
|
-
"FBT001",
|
|
158
|
-
"
|
|
159
|
-
"S101",
|
|
116
|
+
"FBT001", # Test arguments are handled by pytest
|
|
117
|
+
"PLR2004", # Tests should contain magic number comparisons
|
|
118
|
+
"S101", # Pytest needs assert statements
|
|
160
119
|
]
|
|
161
120
|
|
|
162
121
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
@@ -174,16 +133,22 @@ forced-separate = [
|
|
|
174
133
|
split-on-trailing-comma = false
|
|
175
134
|
|
|
176
135
|
[tool.pytest.ini_options]
|
|
177
|
-
addopts = "-
|
|
136
|
+
addopts = "-ra -v"
|
|
178
137
|
testpaths = "tests"
|
|
179
138
|
|
|
180
139
|
[tool.coverage.run]
|
|
140
|
+
branch = true
|
|
181
141
|
source = [
|
|
182
142
|
"src/",
|
|
183
143
|
]
|
|
184
144
|
data_file = ".cov_cache/coverage.dat"
|
|
185
145
|
|
|
186
146
|
[tool.coverage.report]
|
|
147
|
+
exclude_also = [
|
|
148
|
+
"if TYPE_CHECKING:",
|
|
149
|
+
]
|
|
150
|
+
fail_under = 90
|
|
151
|
+
precision = 2
|
|
187
152
|
show_missing = true
|
|
188
153
|
skip_covered = true
|
|
189
154
|
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,19 +20,18 @@ 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
37
|
sys.stdout.buffer.write(line)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import IntEnum, unique
|
|
7
|
+
from math import ceil, floor
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
|
|
13
|
+
from typing_extensions import Self # py3.10: import from typing
|
|
14
|
+
|
|
15
|
+
TRUE_VAR = {"0", "false", "no"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@unique
|
|
19
|
+
class SGRCodes(IntEnum):
|
|
20
|
+
RESET = 0
|
|
21
|
+
|
|
22
|
+
BOLD = 1
|
|
23
|
+
ITALIC = 3
|
|
24
|
+
UNDERLINE = 4
|
|
25
|
+
BLINK = 5
|
|
26
|
+
REVERSE = 7
|
|
27
|
+
CONCEAL = 8
|
|
28
|
+
|
|
29
|
+
BLACK = 30
|
|
30
|
+
RED = 31
|
|
31
|
+
GREEN = 32
|
|
32
|
+
YELLOW = 33
|
|
33
|
+
BLUE = 34
|
|
34
|
+
MAGENTA = 35
|
|
35
|
+
CYAN = 36
|
|
36
|
+
GREY = 37
|
|
37
|
+
|
|
38
|
+
BG_BLACK = 40
|
|
39
|
+
BG_RED = 41
|
|
40
|
+
BG_GREEN = 42
|
|
41
|
+
BG_YELLOW = 43
|
|
42
|
+
BG_BLUE = 44
|
|
43
|
+
BG_MAGENTA = 45
|
|
44
|
+
BG_CYAN = 46
|
|
45
|
+
BG_GREY = 47
|
|
46
|
+
|
|
47
|
+
BLACK_BRIGHT = 90
|
|
48
|
+
RED_BRIGHT = 91
|
|
49
|
+
GREEN_BRIGHT = 92
|
|
50
|
+
YELLOW_BRIGHT = 93
|
|
51
|
+
BLUE_BRIGHT = 94
|
|
52
|
+
MAGENTA_BRIGHT = 95
|
|
53
|
+
CYAN_BRIGHT = 96
|
|
54
|
+
WHITE_BRIGHT = 97
|
|
55
|
+
|
|
56
|
+
BG_BLACK_BRIGHT = 100
|
|
57
|
+
BG_RED_BRIGHT = 101
|
|
58
|
+
BG_GREEN_BRIGHT = 102
|
|
59
|
+
BG_YELLOW_BRIGHT = 103
|
|
60
|
+
BG_BLUE_BRIGHT = 104
|
|
61
|
+
BG_MAGENTA_BRIGHT = 105
|
|
62
|
+
BG_CYAN_BRIGHT = 106
|
|
63
|
+
BG_WHITE_BRIGHT = 107
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def sequence(self) -> str:
|
|
67
|
+
return f"\033[{self.value}m"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, order=True)
|
|
71
|
+
class SGRString:
|
|
72
|
+
__slots__ = ( # py3.9: remove this line
|
|
73
|
+
"_force_prefix",
|
|
74
|
+
"_force_sgr",
|
|
75
|
+
"_is_error",
|
|
76
|
+
"_prefix",
|
|
77
|
+
"_sgr",
|
|
78
|
+
"_string",
|
|
79
|
+
"_suffix",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
_string: str
|
|
83
|
+
_sgr: tuple[SGRCodes, ...]
|
|
84
|
+
_prefix: str
|
|
85
|
+
_suffix: str
|
|
86
|
+
_force_prefix: bool
|
|
87
|
+
_force_sgr: bool
|
|
88
|
+
_is_error: bool
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
obj: object,
|
|
93
|
+
*,
|
|
94
|
+
prefix: str = "",
|
|
95
|
+
suffix: str = "",
|
|
96
|
+
params: Iterable[SGRCodes] = (),
|
|
97
|
+
force_prefix: bool = False,
|
|
98
|
+
force_sgr: bool = False,
|
|
99
|
+
is_error: bool = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
params = tuple(params)
|
|
102
|
+
force_prefix = (
|
|
103
|
+
force_prefix or os.getenv("PY_UTIL_FORCE_PREFIX", "").lower() in TRUE_VAR
|
|
104
|
+
)
|
|
105
|
+
force_sgr = force_sgr or os.getenv("PY_UTIL_FORCE_SGR", "").lower() in TRUE_VAR
|
|
106
|
+
|
|
107
|
+
object.__setattr__(self, "_prefix", prefix)
|
|
108
|
+
object.__setattr__(self, "_string", str(obj))
|
|
109
|
+
object.__setattr__(self, "_sgr", params)
|
|
110
|
+
object.__setattr__(self, "_suffix", suffix)
|
|
111
|
+
object.__setattr__(self, "_force_prefix", force_prefix)
|
|
112
|
+
object.__setattr__(self, "_force_sgr", force_sgr)
|
|
113
|
+
object.__setattr__(self, "_is_error", is_error)
|
|
114
|
+
|
|
115
|
+
def __str__(self) -> str:
|
|
116
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
117
|
+
sgr_suffix = SGRCodes.RESET.sequence if self._sgr else ""
|
|
118
|
+
return f"{self._prefix}{sgr_prefix}{self._string}{sgr_suffix}{self._suffix}"
|
|
119
|
+
|
|
120
|
+
def __len__(self) -> int:
|
|
121
|
+
return len(self._prefix) + len(self._string) + len(self._suffix)
|
|
122
|
+
|
|
123
|
+
def __mul__(self, other: object) -> Self:
|
|
124
|
+
if not isinstance(other, int):
|
|
125
|
+
return NotImplemented
|
|
126
|
+
return type(self)(
|
|
127
|
+
self._string * other,
|
|
128
|
+
prefix=self._prefix,
|
|
129
|
+
suffix=self._suffix,
|
|
130
|
+
params=self._sgr,
|
|
131
|
+
force_prefix=self._force_prefix,
|
|
132
|
+
force_sgr=self._force_sgr,
|
|
133
|
+
is_error=self._is_error,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def __rmul__(self, other: object) -> Self:
|
|
137
|
+
if not isinstance(other, int):
|
|
138
|
+
return NotImplemented
|
|
139
|
+
return type(self)(
|
|
140
|
+
self._string * other,
|
|
141
|
+
prefix=self._prefix,
|
|
142
|
+
suffix=self._suffix,
|
|
143
|
+
params=self._sgr,
|
|
144
|
+
force_prefix=self._force_prefix,
|
|
145
|
+
force_sgr=self._force_sgr,
|
|
146
|
+
is_error=self._is_error,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def print(self, end: str = "\n", *, full_color: bool = False) -> None:
|
|
150
|
+
"""Print the command output.
|
|
151
|
+
|
|
152
|
+
The command will be printed to stdout if it's not the output of an error,
|
|
153
|
+
otherwise to stderr.
|
|
154
|
+
|
|
155
|
+
If the output stream isn't a tty, it will strip the SGR codes and the prefix,
|
|
156
|
+
unless forced to keep them.
|
|
157
|
+
"""
|
|
158
|
+
file = sys.stderr if self._is_error else sys.stdout
|
|
159
|
+
if file.isatty():
|
|
160
|
+
prefix = self._prefix
|
|
161
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
162
|
+
sgr_suffix = SGRCodes.RESET.sequence
|
|
163
|
+
suffix = self._suffix
|
|
164
|
+
else:
|
|
165
|
+
prefix = ""
|
|
166
|
+
sgr_prefix = ""
|
|
167
|
+
sgr_suffix = ""
|
|
168
|
+
suffix = ""
|
|
169
|
+
if self._force_sgr:
|
|
170
|
+
sgr_prefix = "".join(code.sequence for code in self._sgr)
|
|
171
|
+
sgr_suffix = SGRCodes.RESET.sequence if self._sgr else ""
|
|
172
|
+
if self._force_prefix:
|
|
173
|
+
prefix = self._prefix
|
|
174
|
+
suffix = self._suffix
|
|
175
|
+
|
|
176
|
+
if full_color:
|
|
177
|
+
prefix, sgr_prefix = sgr_prefix, prefix
|
|
178
|
+
suffix, sgr_suffix = sgr_suffix, suffix
|
|
179
|
+
|
|
180
|
+
print(
|
|
181
|
+
prefix,
|
|
182
|
+
sgr_prefix,
|
|
183
|
+
self._string,
|
|
184
|
+
sgr_suffix,
|
|
185
|
+
suffix,
|
|
186
|
+
sep="",
|
|
187
|
+
end=end,
|
|
188
|
+
file=file,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def header(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
padding: str = " ",
|
|
195
|
+
left_spaces: int = 1,
|
|
196
|
+
right_spaces: int = 1,
|
|
197
|
+
space: str = " ",
|
|
198
|
+
) -> None:
|
|
199
|
+
try:
|
|
200
|
+
terminal_size = os.get_terminal_size()
|
|
201
|
+
except OSError:
|
|
202
|
+
# in pseudo-terminals an OSError is thrown
|
|
203
|
+
self.print()
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
columns = terminal_size.columns
|
|
207
|
+
title_length = left_spaces + len(self) + right_spaces
|
|
208
|
+
if title_length >= columns:
|
|
209
|
+
self.print()
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
half = (columns - title_length) / 2
|
|
213
|
+
prefix = f"{padding * ceil(half)}{space * left_spaces}{self._prefix}"
|
|
214
|
+
suffix = f"{self._suffix}{space * right_spaces}{padding * floor(half)}"
|
|
215
|
+
type(self)(self._string, prefix=prefix, suffix=suffix, params=self._sgr).print()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True, order=True)
|
|
219
|
+
class SGROutput:
|
|
220
|
+
__slots__ = ("_strings",) # py3.9: remove this line
|
|
221
|
+
_strings: tuple[SGRString, ...]
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
strings: Iterable[SGRString],
|
|
226
|
+
force_prefix: bool | None = None,
|
|
227
|
+
force_sgr: bool | None = None,
|
|
228
|
+
is_error: bool | None = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
strings = tuple(
|
|
231
|
+
self._clean_string(
|
|
232
|
+
string,
|
|
233
|
+
force_prefix=force_prefix,
|
|
234
|
+
force_sgr=force_sgr,
|
|
235
|
+
is_error=is_error,
|
|
236
|
+
)
|
|
237
|
+
for string in strings
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
object.__setattr__(self, "_strings", strings)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _clean_string(
|
|
244
|
+
string: SGRString,
|
|
245
|
+
force_prefix: bool | None,
|
|
246
|
+
force_sgr: bool | None,
|
|
247
|
+
is_error: bool | None,
|
|
248
|
+
) -> SGRString:
|
|
249
|
+
force_prefix = (
|
|
250
|
+
string._force_prefix # noqa: SLF001
|
|
251
|
+
if force_prefix is None
|
|
252
|
+
else force_prefix
|
|
253
|
+
)
|
|
254
|
+
force_sgr = (
|
|
255
|
+
string._force_sgr if force_sgr is None else force_sgr # noqa: SLF001
|
|
256
|
+
)
|
|
257
|
+
is_error = string._is_error if is_error is None else is_error # noqa: SLF001
|
|
258
|
+
return SGRString(
|
|
259
|
+
string._string, # noqa: SLF001
|
|
260
|
+
prefix=string._prefix, # noqa: SLF001
|
|
261
|
+
suffix=string._suffix, # noqa: SLF001
|
|
262
|
+
params=string._sgr, # noqa: SLF001
|
|
263
|
+
force_prefix=force_prefix,
|
|
264
|
+
force_sgr=force_sgr,
|
|
265
|
+
is_error=is_error,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def print(self, sep: str = "", end: str = "\n") -> None:
|
|
269
|
+
n = len(self._strings)
|
|
270
|
+
for index, string in enumerate(self._strings, start=1):
|
|
271
|
+
current_end = end if index == n else sep
|
|
272
|
+
string.print(end=current_end)
|
|
273
|
+
|
|
274
|
+
def header(
|
|
275
|
+
self,
|
|
276
|
+
*,
|
|
277
|
+
padding: str = " ",
|
|
278
|
+
left_spaces: int = 1,
|
|
279
|
+
right_spaces: int = 1,
|
|
280
|
+
space: str = " ",
|
|
281
|
+
) -> None:
|
|
282
|
+
n = len(self._strings)
|
|
283
|
+
if n > 1:
|
|
284
|
+
msg = "Only one string is allowed for the header"
|
|
285
|
+
raise ValueError(msg)
|
|
286
|
+
|
|
287
|
+
self._strings[0].header(
|
|
288
|
+
padding=padding,
|
|
289
|
+
left_spaces=left_spaces,
|
|
290
|
+
right_spaces=right_spaces,
|
|
291
|
+
space=space,
|
|
292
|
+
)
|
|
@@ -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,65 +33,65 @@ 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
|
-
return Timing(nanoseconds=self.nanoseconds
|
|
94
|
+
return Timing(nanoseconds=round(self.nanoseconds / other))
|
|
89
95
|
|
|
90
96
|
|
|
91
97
|
class Stopwatch:
|
|
@@ -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
|