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.
@@ -1,18 +1,17 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyutilkit
3
- Version: 0.1.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.8,<4.0
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
- Long project description and tldr goes here
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
- Long project description and tldr goes here
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
- "py38",
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 = "py38"
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.1.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.8"
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"
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime, tzinfo
4
-
5
4
  from zoneinfo import ZoneInfo, available_timezones
6
5
 
7
6
  from pyutilkit.data.timezones import UNAVAILABLE_TIMEZONES
@@ -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