pyutilkit 0.1.0__tar.gz → 0.3.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.1.0 → pyutilkit-0.3.0}/PKG-INFO +7 -4
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/docs/README.md +5 -1
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/pyproject.toml +5 -4
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/src/pyutilkit/date_utils.py +0 -1
- pyutilkit-0.3.0/src/pyutilkit/files.py +54 -0
- pyutilkit-0.3.0/src/pyutilkit/py.typed +0 -0
- pyutilkit-0.3.0/src/pyutilkit/term.py +116 -0
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/src/pyutilkit/timing.py +30 -0
- pyutilkit-0.1.0/src/pyutilkit/__init__.py +0 -1
- pyutilkit-0.1.0/src/pyutilkit/term.py +0 -96
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/LICENSE.md +0 -0
- {pyutilkit-0.1.0/src/pyutilkit/data → pyutilkit-0.3.0/src/pyutilkit}/__init__.py +0 -0
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/src/pyutilkit/classes.py +0 -0
- /pyutilkit-0.1.0/src/pyutilkit/py.typed → /pyutilkit-0.3.0/src/pyutilkit/data/__init__.py +0 -0
- {pyutilkit-0.1.0 → pyutilkit-0.3.0}/src/pyutilkit/data/timezones.py +0 -0
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyutilkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: python's missing batteries
|
|
5
5
|
Home-page: https://pyutilkit.readthedocs.io/en/stable/
|
|
6
6
|
License: LGPL-3.0+
|
|
7
7
|
Keywords: utils
|
|
8
8
|
Author: Stephanos Kuma
|
|
9
9
|
Author-email: stephanos@kuma.ai
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4.0
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -31,7 +30,11 @@ Description-Content-Type: text/markdown
|
|
|
31
30
|
[![build automation: yam][yam_badge]][yam_url]
|
|
32
31
|
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
33
32
|
|
|
34
|
-
|
|
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.
|
|
35
38
|
|
|
36
39
|
## Links
|
|
37
40
|
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
[![build automation: yam][yam_badge]][yam_url]
|
|
9
9
|
[![Lint: ruff][ruff_badge]][ruff_url]
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The Python has long maintained the philosophy of "batteries included", giving the user
|
|
12
|
+
a rich standard library, avoiding the need for third party tools for most work. Some packages
|
|
13
|
+
are so common, that the have a similar status to the standard library. Still, some code seems
|
|
14
|
+
to be written time and again, with every project. This small library, with minimal requirements,
|
|
15
|
+
hopes to stop this repetition.
|
|
12
16
|
|
|
13
17
|
## Links
|
|
14
18
|
|
|
@@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
6
6
|
|
|
7
7
|
[tool.black]
|
|
8
8
|
target-version = [
|
|
9
|
-
"
|
|
9
|
+
"py39",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[tool.mypy]
|
|
@@ -32,7 +32,7 @@ warn_unused_configs = true
|
|
|
32
32
|
src = [
|
|
33
33
|
"src",
|
|
34
34
|
]
|
|
35
|
-
target-version = "
|
|
35
|
+
target-version = "py39"
|
|
36
36
|
|
|
37
37
|
[tool.ruff.lint]
|
|
38
38
|
select = [
|
|
@@ -126,7 +126,7 @@ skip_empty = true
|
|
|
126
126
|
|
|
127
127
|
[tool.poetry]
|
|
128
128
|
name = "pyutilkit"
|
|
129
|
-
version = "0.
|
|
129
|
+
version = "0.3.0"
|
|
130
130
|
description = "python's missing batteries"
|
|
131
131
|
authors = [
|
|
132
132
|
"Stephanos Kuma <stephanos@kuma.ai>",
|
|
@@ -149,7 +149,7 @@ classifiers = [
|
|
|
149
149
|
|
|
150
150
|
[tool.poetry.dependencies]
|
|
151
151
|
# python version
|
|
152
|
-
python = "^3.
|
|
152
|
+
python = "^3.9"
|
|
153
153
|
|
|
154
154
|
[tool.poetry.group.dev.dependencies]
|
|
155
155
|
ipdb = { version = "^0.13", python = "^3.10" }
|
|
@@ -161,6 +161,7 @@ pipdeptree = "^2.13"
|
|
|
161
161
|
black = "^24.1"
|
|
162
162
|
mypy = "^1.8"
|
|
163
163
|
ruff = "^0.4"
|
|
164
|
+
typing_extensions = "^4.11"
|
|
164
165
|
|
|
165
166
|
[tool.poetry.group.test.dependencies]
|
|
166
167
|
freezegun = "^1.5"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing_extensions import ParamSpec # py3.9: import from typing
|
|
12
|
+
|
|
13
|
+
P = ParamSpec("P")
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
INGEST_ERROR = "Function `%s` threw `%s` when called with args=%s and kwargs=%s"
|
|
17
|
+
R_co = TypeVar("R_co", covariant=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_exceptions(
|
|
21
|
+
*,
|
|
22
|
+
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
23
|
+
default: R_co | None = None,
|
|
24
|
+
log_level: str = "info",
|
|
25
|
+
) -> Callable[[Callable[P, R_co]], Callable[P, R_co | None]]:
|
|
26
|
+
def decorator(func: Callable[P, R_co]) -> Callable[P, R_co | None]:
|
|
27
|
+
@wraps(func)
|
|
28
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R_co | None:
|
|
29
|
+
try:
|
|
30
|
+
return func(*args, **kwargs)
|
|
31
|
+
except exceptions as exc:
|
|
32
|
+
getattr(logger, log_level)(
|
|
33
|
+
INGEST_ERROR,
|
|
34
|
+
func.__name__,
|
|
35
|
+
exc.__class__.__name__,
|
|
36
|
+
args,
|
|
37
|
+
kwargs,
|
|
38
|
+
exc_info=True,
|
|
39
|
+
)
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
return decorator
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hash_file(path: Path, buffer_size: int = 2**16) -> str:
|
|
48
|
+
sha256 = hashlib.sha256()
|
|
49
|
+
|
|
50
|
+
with path.open("rb") as f:
|
|
51
|
+
while data := f.read(buffer_size):
|
|
52
|
+
sha256.update(data)
|
|
53
|
+
|
|
54
|
+
return sha256.hexdigest()
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from enum import IntEnum, unique
|
|
6
|
+
from math import ceil, floor
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing_extensions import Self # py3.10: import from typing
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@unique
|
|
14
|
+
class SGRCodes(IntEnum):
|
|
15
|
+
RESET = 0
|
|
16
|
+
|
|
17
|
+
BOLD = 1
|
|
18
|
+
ITALIC = 3
|
|
19
|
+
UNDERLINE = 4
|
|
20
|
+
BLINK = 5
|
|
21
|
+
REVERSE = 7
|
|
22
|
+
CONCEAL = 8
|
|
23
|
+
|
|
24
|
+
BLACK = 30
|
|
25
|
+
RED = 31
|
|
26
|
+
GREEN = 32
|
|
27
|
+
YELLOW = 33
|
|
28
|
+
BLUE = 34
|
|
29
|
+
MAGENTA = 35
|
|
30
|
+
CYAN = 36
|
|
31
|
+
GREY = 37
|
|
32
|
+
|
|
33
|
+
BG_BLACK = 40
|
|
34
|
+
BG_RED = 41
|
|
35
|
+
BG_GREEN = 42
|
|
36
|
+
BG_YELLOW = 43
|
|
37
|
+
BG_BLUE = 44
|
|
38
|
+
BG_MAGENTA = 45
|
|
39
|
+
BG_CYAN = 46
|
|
40
|
+
BG_GREY = 47
|
|
41
|
+
|
|
42
|
+
BLACK_BRIGHT = 90
|
|
43
|
+
RED_BRIGHT = 91
|
|
44
|
+
GREEN_BRIGHT = 92
|
|
45
|
+
YELLOW_BRIGHT = 93
|
|
46
|
+
BLUE_BRIGHT = 94
|
|
47
|
+
MAGENTA_BRIGHT = 95
|
|
48
|
+
CYAN_BRIGHT = 96
|
|
49
|
+
WHITE_BRIGHT = 97
|
|
50
|
+
|
|
51
|
+
BG_BLACK_BRIGHT = 100
|
|
52
|
+
BG_RED_BRIGHT = 101
|
|
53
|
+
BG_GREEN_BRIGHT = 102
|
|
54
|
+
BG_YELLOW_BRIGHT = 103
|
|
55
|
+
BG_BLUE_BRIGHT = 104
|
|
56
|
+
BG_MAGENTA_BRIGHT = 105
|
|
57
|
+
BG_CYAN_BRIGHT = 106
|
|
58
|
+
BG_WHITE_BRIGHT = 107
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def sequence(self) -> str:
|
|
62
|
+
return f"\033[{self.value}m"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SGRString(str):
|
|
66
|
+
_sgr: tuple[SGRCodes, ...]
|
|
67
|
+
_string: str
|
|
68
|
+
__slots__ = ("_sgr", "_string")
|
|
69
|
+
|
|
70
|
+
def __new__(cls, obj: Any, *, params: Iterable[SGRCodes] = ()) -> Self:
|
|
71
|
+
string = super().__new__(cls, obj)
|
|
72
|
+
object.__setattr__(string, "_string", str(obj))
|
|
73
|
+
object.__setattr__(string, "_sgr", tuple(params))
|
|
74
|
+
return string
|
|
75
|
+
|
|
76
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
77
|
+
msg = "SGRString is immutable"
|
|
78
|
+
raise AttributeError(msg)
|
|
79
|
+
|
|
80
|
+
def __delattr__(self, name: str) -> None:
|
|
81
|
+
msg = "SGRString is immutable"
|
|
82
|
+
raise AttributeError(msg)
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
if not self._sgr:
|
|
86
|
+
return self._string
|
|
87
|
+
prefix = "".join(code.sequence for code in self._sgr)
|
|
88
|
+
return f"{prefix}{self._string}{SGRCodes.RESET.sequence}"
|
|
89
|
+
|
|
90
|
+
def __mul__(self, other: Any) -> Self:
|
|
91
|
+
if not isinstance(other, int):
|
|
92
|
+
return NotImplemented
|
|
93
|
+
return type(self)(self._string * other, params=self._sgr)
|
|
94
|
+
|
|
95
|
+
def __rmul__(self, other: Any) -> Self:
|
|
96
|
+
if not isinstance(other, int):
|
|
97
|
+
return NotImplemented
|
|
98
|
+
return type(self)(self._string * other, params=self._sgr)
|
|
99
|
+
|
|
100
|
+
def header(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
padding: str = " ",
|
|
104
|
+
left_spaces: int = 1,
|
|
105
|
+
right_spaces: int = 1,
|
|
106
|
+
space: str = " ",
|
|
107
|
+
) -> None:
|
|
108
|
+
columns = os.get_terminal_size().columns
|
|
109
|
+
text = f"{space * left_spaces}{self}{space * right_spaces}"
|
|
110
|
+
title_length = left_spaces + len(self) + right_spaces
|
|
111
|
+
if title_length >= columns:
|
|
112
|
+
print(text.strip())
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
half = (columns - title_length) / 2
|
|
116
|
+
print(f"{padding * ceil(half)}{text}{padding * floor(half)}")
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations # py3.9: remove this line
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass
|
|
4
|
+
from time import perf_counter_ns
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing_extensions import Self # py3.10: import Self from typing
|
|
2
10
|
|
|
3
11
|
|
|
4
12
|
@dataclass(frozen=True, order=True)
|
|
@@ -36,3 +44,25 @@ class Timing:
|
|
|
36
44
|
return f"{milliseconds:.1f}ms"
|
|
37
45
|
seconds = milliseconds / 1000
|
|
38
46
|
return f"{seconds:,.2f}s"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Stopwatch:
|
|
50
|
+
_start: int
|
|
51
|
+
timing: Timing
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._start = 0
|
|
55
|
+
self.timing = Timing(nanoseconds=0)
|
|
56
|
+
|
|
57
|
+
def __enter__(self) -> Self:
|
|
58
|
+
self._start = perf_counter_ns()
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(
|
|
62
|
+
self,
|
|
63
|
+
exc_type: type[Exception] | None,
|
|
64
|
+
exc_value: Exception | None,
|
|
65
|
+
traceback: TracebackType | None,
|
|
66
|
+
) -> None:
|
|
67
|
+
_end = perf_counter_ns()
|
|
68
|
+
self.timing = Timing(nanoseconds=_end - self._start)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from collections.abc import Iterable
|
|
5
|
-
from enum import IntEnum, unique
|
|
6
|
-
from math import ceil, floor
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@unique
|
|
11
|
-
class SGRCodes(IntEnum):
|
|
12
|
-
RESET = 0
|
|
13
|
-
|
|
14
|
-
BOLD = 1
|
|
15
|
-
ITALIC = 3
|
|
16
|
-
UNDERLINE = 4
|
|
17
|
-
BLINK = 5
|
|
18
|
-
REVERSE = 7
|
|
19
|
-
CONCEAL = 8
|
|
20
|
-
|
|
21
|
-
BLACK = 30
|
|
22
|
-
RED = 31
|
|
23
|
-
GREEN = 32
|
|
24
|
-
YELLOW = 33
|
|
25
|
-
BLUE = 34
|
|
26
|
-
MAGENTA = 35
|
|
27
|
-
CYAN = 36
|
|
28
|
-
GREY = 37
|
|
29
|
-
|
|
30
|
-
BG_BLACK = 40
|
|
31
|
-
BG_RED = 41
|
|
32
|
-
BG_GREEN = 42
|
|
33
|
-
BG_YELLOW = 43
|
|
34
|
-
BG_BLUE = 44
|
|
35
|
-
BG_MAGENTA = 45
|
|
36
|
-
BG_CYAN = 46
|
|
37
|
-
BG_GREY = 47
|
|
38
|
-
|
|
39
|
-
BLACK_BRIGHT = 90
|
|
40
|
-
RED_BRIGHT = 91
|
|
41
|
-
GREEN_BRIGHT = 92
|
|
42
|
-
YELLOW_BRIGHT = 93
|
|
43
|
-
BLUE_BRIGHT = 94
|
|
44
|
-
MAGENTA_BRIGHT = 95
|
|
45
|
-
CYAN_BRIGHT = 96
|
|
46
|
-
WHITE_BRIGHT = 97
|
|
47
|
-
|
|
48
|
-
BG_BLACK_BRIGHT = 100
|
|
49
|
-
BG_RED_BRIGHT = 101
|
|
50
|
-
BG_GREEN_BRIGHT = 102
|
|
51
|
-
BG_YELLOW_BRIGHT = 103
|
|
52
|
-
BG_BLUE_BRIGHT = 104
|
|
53
|
-
BG_MAGENTA_BRIGHT = 105
|
|
54
|
-
BG_CYAN_BRIGHT = 106
|
|
55
|
-
BG_WHITE_BRIGHT = 107
|
|
56
|
-
|
|
57
|
-
@property
|
|
58
|
-
def sequence(self) -> str:
|
|
59
|
-
return f"\033[{self.value}m"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class SGRString(str):
|
|
63
|
-
_sgr: str
|
|
64
|
-
__slots__ = ("_sgr",)
|
|
65
|
-
|
|
66
|
-
def __new__(cls, value: Any, *, params: Iterable[SGRCodes] = ()) -> SGRString:
|
|
67
|
-
string = super().__new__(cls, value)
|
|
68
|
-
suffix = SGRCodes.RESET.sequence
|
|
69
|
-
prefix = "".join(param.sequence for param in params) or SGRCodes.RESET.sequence
|
|
70
|
-
object.__setattr__(string, "_sgr", f"{prefix}{value}{suffix}")
|
|
71
|
-
return string
|
|
72
|
-
|
|
73
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
74
|
-
msg = "SGRString is immutable"
|
|
75
|
-
raise AttributeError(msg)
|
|
76
|
-
|
|
77
|
-
def __delattr__(self, name: str) -> None:
|
|
78
|
-
msg = "SGRString is immutable"
|
|
79
|
-
raise AttributeError(msg)
|
|
80
|
-
|
|
81
|
-
def __str__(self) -> str:
|
|
82
|
-
return self._sgr
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def header(
|
|
86
|
-
text: str, *, padding: str = " ", left_spaces: int = 1, right_spaces: int = 1
|
|
87
|
-
) -> None:
|
|
88
|
-
columns = os.get_terminal_size().columns
|
|
89
|
-
text = f"{' ' * left_spaces}{text.strip()}{' ' * right_spaces}"
|
|
90
|
-
title_length = len(text)
|
|
91
|
-
if title_length >= columns:
|
|
92
|
-
print(text.strip())
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
half = (columns - len(text)) / 2
|
|
96
|
-
print(f"{padding * ceil(half)}{text}{padding * floor(half)}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|