bear-utils 0.7.21__py3-none-any.whl → 0.7.22__py3-none-any.whl
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.
- bear_utils/__init__.py +24 -1
- bear_utils/ai/__init__.py +5 -5
- bear_utils/ai/ai_helpers/__init__.py +24 -18
- bear_utils/ai/ai_helpers/_parsers.py +27 -21
- bear_utils/ai/ai_helpers/_types.py +2 -7
- bear_utils/cache/__init__.py +35 -23
- bear_utils/cli/__init__.py +13 -0
- bear_utils/cli/commands.py +14 -8
- bear_utils/cli/prompt_helpers.py +40 -34
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +17 -18
- bear_utils/cli/shell/_base_shell.py +37 -34
- bear_utils/config/__init__.py +4 -2
- bear_utils/config/config_manager.py +193 -56
- bear_utils/config/dir_manager.py +8 -3
- bear_utils/config/settings_manager.py +94 -171
- bear_utils/constants/__init__.py +2 -1
- bear_utils/constants/_exceptions.py +6 -1
- bear_utils/constants/date_related.py +2 -0
- bear_utils/constants/logger_protocol.py +28 -0
- bear_utils/constants/time_related.py +2 -0
- bear_utils/database/__init__.py +2 -0
- bear_utils/database/_db_manager.py +10 -11
- bear_utils/events/__init__.py +3 -1
- bear_utils/events/events_class.py +11 -11
- bear_utils/events/events_module.py +17 -8
- bear_utils/extras/__init__.py +8 -6
- bear_utils/extras/_async_helpers.py +2 -3
- bear_utils/extras/_tools.py +54 -52
- bear_utils/extras/platform_utils.py +5 -1
- bear_utils/extras/responses/__init__.py +1 -0
- bear_utils/extras/responses/function_response.py +301 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +17 -15
- bear_utils/files/__init__.py +3 -1
- bear_utils/files/file_handlers/__init__.py +2 -0
- bear_utils/files/file_handlers/_base_file_handler.py +23 -3
- bear_utils/files/file_handlers/file_handler_factory.py +38 -38
- bear_utils/files/file_handlers/json_file_handler.py +49 -22
- bear_utils/files/file_handlers/log_file_handler.py +19 -12
- bear_utils/files/file_handlers/toml_file_handler.py +13 -5
- bear_utils/files/file_handlers/txt_file_handler.py +56 -14
- bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
- bear_utils/files/ignore_parser.py +52 -57
- bear_utils/graphics/__init__.py +3 -1
- bear_utils/graphics/bear_gradient.py +17 -12
- bear_utils/graphics/image_helpers.py +11 -5
- bear_utils/gui/__init__.py +3 -1
- bear_utils/gui/gui_tools/__init__.py +3 -1
- bear_utils/gui/gui_tools/_settings.py +0 -1
- bear_utils/gui/gui_tools/qt_app.py +16 -11
- bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
- bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
- bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
- bear_utils/logging/__init__.py +6 -4
- bear_utils/logging/logger_manager/__init__.py +1 -0
- bear_utils/logging/logger_manager/_common.py +0 -1
- bear_utils/logging/logger_manager/_console_junk.py +15 -11
- bear_utils/logging/logger_manager/_styles.py +1 -2
- bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -33
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
- bear_utils/logging/logger_manager/loggers/_console_logger.py +54 -26
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
- bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
- bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
- bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
- bear_utils/logging/loggers.py +9 -13
- bear_utils/monitoring/__init__.py +7 -4
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +44 -48
- bear_utils/time/__init__.py +13 -6
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.22.dist-info}/METADATA +50 -6
- bear_utils-0.7.22.dist-info/RECORD +83 -0
- bear_utils-0.7.21.dist-info/RECORD +0 -79
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.22.dist-info}/WHEEL +0 -0
@@ -1,10 +1,12 @@
|
|
1
|
+
"""A module for handling file ignore patterns and directories in Bear Utils."""
|
2
|
+
|
1
3
|
import os
|
2
4
|
from pathlib import Path
|
3
5
|
|
4
6
|
from pathspec import PathSpec
|
5
7
|
|
6
|
-
from
|
7
|
-
from
|
8
|
+
from bear_utils.cli.prompt_helpers import ask_yes_no
|
9
|
+
from bear_utils.logging import ConsoleLogger
|
8
10
|
|
9
11
|
logger: ConsoleLogger = ConsoleLogger.get_instance(init=True)
|
10
12
|
|
@@ -25,19 +27,21 @@ IGNORE_PATTERNS: list[str] = [
|
|
25
27
|
"dist/",
|
26
28
|
]
|
27
29
|
|
30
|
+
IGNORE_COUNT = 100
|
31
|
+
|
28
32
|
|
29
33
|
class IgnoreHandler:
|
30
34
|
"""Basic ignore handler for manually checking if a file should be ignored based on set patterns."""
|
31
35
|
|
32
|
-
def __init__(self, ignore_file: Path | None = None, combine: bool = False):
|
36
|
+
def __init__(self, ignore_file: Path | None = None, combine: bool = False) -> None:
|
37
|
+
"""Initialize the IgnoreHandler with an optional ignore file."""
|
33
38
|
self.ignore_file = ignore_file
|
34
39
|
self.patterns: list[str] = self.load_patterns(ignore_file, combine) if ignore_file else IGNORE_PATTERNS
|
35
40
|
self.spec: PathSpec = self._create_spec(self.patterns)
|
36
41
|
|
37
42
|
@staticmethod
|
38
43
|
def _create_spec(patterns: list[str]) -> PathSpec:
|
39
|
-
"""
|
40
|
-
Create a pathspec from the given patterns.
|
44
|
+
"""Create a pathspec from the given patterns.
|
41
45
|
|
42
46
|
Args:
|
43
47
|
patterns: List of ignore patterns
|
@@ -49,8 +53,7 @@ class IgnoreHandler:
|
|
49
53
|
|
50
54
|
@staticmethod
|
51
55
|
def load_patterns(ignore_file: Path, combine: bool) -> list[str]:
|
52
|
-
"""
|
53
|
-
Load patterns from a specific ignore file.
|
56
|
+
"""Load patterns from a specific ignore file.
|
54
57
|
|
55
58
|
Args:
|
56
59
|
ignore_file: Path to the ignore file
|
@@ -61,9 +64,9 @@ class IgnoreHandler:
|
|
61
64
|
try:
|
62
65
|
lines = ignore_file.read_text().splitlines()
|
63
66
|
patterns: list[str] = [
|
64
|
-
|
65
|
-
for
|
66
|
-
if
|
67
|
+
line.strip()
|
68
|
+
for line in lines
|
69
|
+
if line.strip() and not line.strip().startswith("#") and not line.strip().startswith("!")
|
67
70
|
]
|
68
71
|
if patterns:
|
69
72
|
logger.verbose(f"Loaded {len(patterns)} patterns from {ignore_file}")
|
@@ -75,8 +78,7 @@ class IgnoreHandler:
|
|
75
78
|
return []
|
76
79
|
|
77
80
|
def should_ignore(self, path: Path | str) -> bool:
|
78
|
-
"""
|
79
|
-
Check if a given path should be ignored based on the ignore patterns.
|
81
|
+
"""Check if a given path should be ignored based on the ignore patterns.
|
80
82
|
|
81
83
|
Args:
|
82
84
|
path (Path): The path to check
|
@@ -89,8 +91,8 @@ class IgnoreHandler:
|
|
89
91
|
return self.spec.match_file(str(path))
|
90
92
|
|
91
93
|
def ignore_print(self, path: Path | str, rel: bool = True) -> bool:
|
92
|
-
"""
|
93
|
-
|
94
|
+
"""Print whether a given path is ignored based on the ignore patterns.
|
95
|
+
|
94
96
|
Will print the path as relative to the current working directory if rel is True,
|
95
97
|
which it is by default.
|
96
98
|
|
@@ -112,8 +114,7 @@ class IgnoreHandler:
|
|
112
114
|
return should_ignore
|
113
115
|
|
114
116
|
def add_patterns(self, patterns: list[str]) -> None:
|
115
|
-
"""
|
116
|
-
Add additional ignore patterns to the existing spec.
|
117
|
+
"""Add additional ignore patterns to the existing spec.
|
117
118
|
|
118
119
|
Args:
|
119
120
|
patterns: List of additional patterns to add
|
@@ -131,16 +132,16 @@ class IgnoreHandler:
|
|
131
132
|
class IgnoreDirectoryHandler(IgnoreHandler):
|
132
133
|
"""Handles the logic for ignoring files and directories based on .gitignore-style rules."""
|
133
134
|
|
134
|
-
def __init__(self, directory_to_search: Path | str, ignore_file: Path | None = None, rel: bool = True):
|
135
|
+
def __init__(self, directory_to_search: Path | str, ignore_file: Path | None = None, rel: bool = True) -> None:
|
136
|
+
"""Initialize the IgnoreDirectoryHandler with a directory to search and an optional ignore file."""
|
135
137
|
super().__init__(ignore_file)
|
136
|
-
self.directory_to_search = Path(directory_to_search).resolve()
|
138
|
+
self.directory_to_search: Path = Path(directory_to_search).resolve()
|
137
139
|
self.ignored_files: list[Path] = []
|
138
140
|
self.non_ignored_files: list[Path] = []
|
139
|
-
self.rel = rel
|
141
|
+
self.rel: bool = rel
|
140
142
|
|
141
143
|
def _scan_directory(self, directory: Path | str) -> tuple[list[str], list[str]]:
|
142
|
-
"""
|
143
|
-
Scan a directory and separate files and directories into ignored and non-ignored lists.
|
144
|
+
"""Scan a directory and separate files and directories into ignored and non-ignored lists.
|
144
145
|
|
145
146
|
Args:
|
146
147
|
directory: The directory to scan
|
@@ -153,7 +154,7 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
153
154
|
|
154
155
|
for root, _, files in os.walk(directory):
|
155
156
|
root_path = Path(root)
|
156
|
-
rel_root = root_path.relative_to(directory)
|
157
|
+
rel_root: Path = root_path.relative_to(directory)
|
157
158
|
|
158
159
|
if str(rel_root) == ".":
|
159
160
|
rel_root = Path("")
|
@@ -162,18 +163,17 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
162
163
|
all_paths.append(dir_path)
|
163
164
|
|
164
165
|
for file in files:
|
165
|
-
rel_path = rel_root / file
|
166
|
+
rel_path: Path = rel_root / file
|
166
167
|
all_paths.append(str(rel_path))
|
167
168
|
|
168
|
-
ignored_status = [self.spec.match_file(f) for f in all_paths]
|
169
|
-
non_ignored_paths = [f for f, ignored in zip(all_paths, ignored_status) if not ignored]
|
170
|
-
ignored_paths = [f for f, ignored in zip(all_paths, ignored_status) if ignored]
|
169
|
+
ignored_status: list[bool] = [self.spec.match_file(f) for f in all_paths]
|
170
|
+
non_ignored_paths = [f for f, ignored in zip(all_paths, ignored_status, strict=False) if not ignored]
|
171
|
+
ignored_paths = [f for f, ignored in zip(all_paths, ignored_status, strict=False) if ignored]
|
171
172
|
return non_ignored_paths, ignored_paths
|
172
173
|
|
173
174
|
@property
|
174
175
|
def ignored_files_count(self) -> int:
|
175
|
-
"""
|
176
|
-
Get the count of ignored files.
|
176
|
+
"""Get the count of ignored files.
|
177
177
|
|
178
178
|
Returns:
|
179
179
|
int: The number of ignored files
|
@@ -182,8 +182,7 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
182
182
|
|
183
183
|
@property
|
184
184
|
def non_ignored_files_count(self) -> int:
|
185
|
-
"""
|
186
|
-
Get the count of non-ignored files.
|
185
|
+
"""Get the count of non-ignored files.
|
187
186
|
|
188
187
|
Returns:
|
189
188
|
int: The number of non-ignored files
|
@@ -191,8 +190,7 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
191
190
|
return len(self.non_ignored_files)
|
192
191
|
|
193
192
|
def ignore_report_full_codebase(self):
|
194
|
-
"""
|
195
|
-
Generate a report of ignored and non-ignored files in the directory.
|
193
|
+
"""Generate a report of ignored and non-ignored files in the directory.
|
196
194
|
|
197
195
|
Returns:
|
198
196
|
Tuple of (non_ignored_files, ignored_files) as Path objects
|
@@ -202,9 +200,8 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
202
200
|
self.ignored_files = [self.directory_to_search / p for p in ignored_paths]
|
203
201
|
|
204
202
|
@staticmethod
|
205
|
-
def _print(data_structure_to_print: list[Path], rel: bool = True, color: str = "green"):
|
206
|
-
"""
|
207
|
-
Print the contents of a data structure (list of paths) to the console.
|
203
|
+
def _print(data_structure_to_print: list[Path], rel: bool = True, color: str = "green") -> None:
|
204
|
+
"""Print the contents of a data structure (list of paths) to the console.
|
208
205
|
|
209
206
|
Args:
|
210
207
|
data_structure_to_print: The data structure to print
|
@@ -212,43 +209,41 @@ class IgnoreDirectoryHandler(IgnoreHandler):
|
|
212
209
|
"""
|
213
210
|
try:
|
214
211
|
for path in data_structure_to_print:
|
212
|
+
p = Path(path)
|
215
213
|
if rel:
|
216
|
-
|
217
|
-
logger.print(str(f"[{color}]{
|
214
|
+
p = p.relative_to(Path.cwd())
|
215
|
+
logger.print(str(f"[{color}]{p}[/]"))
|
218
216
|
except KeyboardInterrupt:
|
219
217
|
logger.warning("Printing interrupted by user.")
|
220
218
|
|
221
|
-
def _print_ignored_files(self):
|
219
|
+
def _print_ignored_files(self) -> None:
|
222
220
|
"""Print the ignored files in the directory."""
|
223
221
|
if self.ignored_files_count == 0:
|
224
222
|
logger.print("No ignored files found.")
|
225
|
-
|
226
|
-
if
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
):
|
231
|
-
self._print(self.ignored_files, self.rel, "red")
|
232
|
-
else:
|
223
|
+
elif self.ignored_files_count > IGNORE_COUNT:
|
224
|
+
if ask_yes_no(
|
225
|
+
"There are a lot of ignored files. Do you want to print them all? (y/n)",
|
226
|
+
default="n",
|
227
|
+
):
|
233
228
|
self._print(self.ignored_files, self.rel, "red")
|
229
|
+
else:
|
230
|
+
self._print(self.ignored_files, self.rel, "red")
|
234
231
|
|
235
|
-
def _print_non_ignored_files(self):
|
232
|
+
def _print_non_ignored_files(self) -> None:
|
236
233
|
"""Print the non-ignored files in the directory."""
|
237
234
|
if self.non_ignored_files_count == 0:
|
238
235
|
logger.print("No non-ignored files found.")
|
239
|
-
|
240
|
-
if
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
):
|
245
|
-
self._print(self.non_ignored_files, self.rel)
|
246
|
-
else:
|
236
|
+
elif self.non_ignored_files_count > IGNORE_COUNT:
|
237
|
+
if ask_yes_no(
|
238
|
+
"There are a lot of non-ignored files. Do you want to print them all? (y/n)",
|
239
|
+
default="n",
|
240
|
+
):
|
247
241
|
self._print(self.non_ignored_files, self.rel)
|
242
|
+
else:
|
243
|
+
self._print(self.non_ignored_files, self.rel)
|
248
244
|
|
249
245
|
def print_report(self, what_to_print: str):
|
250
|
-
"""
|
251
|
-
Print the report of ignored or non-ignored files or both
|
246
|
+
"""Print the report of ignored or non-ignored files or both
|
252
247
|
|
253
248
|
Args:
|
254
249
|
what_to_print: "ignored", "non_ignored", or "both"
|
bear_utils/graphics/__init__.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
+
"""Graphics related utilities for Bear Utils."""
|
2
|
+
|
1
3
|
from .bear_gradient import ColorGradient, DefaultColors
|
2
4
|
from .image_helpers import encode_image_to_jpeg, encode_image_to_png
|
3
5
|
|
4
|
-
__all__ = ["
|
6
|
+
__all__ = ["ColorGradient", "DefaultColors", "encode_image_to_jpeg", "encode_image_to_png"]
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""A color gradient utility for generating RGB colors based on thresholds."""
|
2
|
+
|
1
3
|
from dataclasses import dataclass, field
|
2
4
|
|
3
5
|
from pyglm.glm import clamp, lerp, quat
|
@@ -28,6 +30,7 @@ class DefaultColors:
|
|
28
30
|
end: RichColor = GREEN # Default Threshold: 1.0
|
29
31
|
|
30
32
|
def output_rgb(self) -> tuple[ColorTriplet, ColorTriplet, ColorTriplet]:
|
33
|
+
"""Get the RGB values of the default colors."""
|
31
34
|
return self.start.get_truecolor(), self.mid.get_truecolor(), self.end.get_truecolor()
|
32
35
|
|
33
36
|
|
@@ -44,18 +47,20 @@ class DefaultThresholds:
|
|
44
47
|
raise ValueError("thresholds must be strictly increasing and between 0 and 1.")
|
45
48
|
|
46
49
|
def unpack(self) -> tuple[float, float, float]:
|
50
|
+
"""Unpack the thresholds into a tuple."""
|
47
51
|
return self.start, self.mid, self.end
|
48
52
|
|
49
53
|
|
50
54
|
@dataclass
|
51
55
|
class DefaultColorConfig:
|
56
|
+
"""Configuration for the default color gradient."""
|
57
|
+
|
52
58
|
colors: DefaultColors = field(default_factory=DefaultColors)
|
53
59
|
thresholds: DefaultThresholds = field(default_factory=DefaultThresholds)
|
54
60
|
|
55
61
|
|
56
62
|
class ColorGradient:
|
57
|
-
"""
|
58
|
-
Simple 3-color gradient interpolator.
|
63
|
+
"""Simple 3-color gradient interpolator.
|
59
64
|
|
60
65
|
Args:
|
61
66
|
colors (DefaultColors): Default colors for the gradient.
|
@@ -63,10 +68,11 @@ class ColorGradient:
|
|
63
68
|
reverse (bool): If True, reverses the gradient direction.
|
64
69
|
"""
|
65
70
|
|
66
|
-
def __init__(self, config=
|
67
|
-
|
68
|
-
self.
|
69
|
-
self.
|
71
|
+
def __init__(self, config: DefaultColorConfig | None = None, reverse: bool = False) -> None:
|
72
|
+
"""Initialize the ColorGradient with a configuration and optional reverse flag."""
|
73
|
+
self.config: DefaultColorConfig = config or DefaultColorConfig()
|
74
|
+
self.colors: DefaultColors = self.config.colors
|
75
|
+
self.thresholds: DefaultThresholds = self.config.thresholds
|
70
76
|
self.reverse: bool = reverse
|
71
77
|
self.c0, self.c1, self.c2 = self.colors.output_rgb()
|
72
78
|
self.p0, self.p1, self.p2 = self.thresholds.unpack()
|
@@ -78,23 +84,22 @@ class ColorGradient:
|
|
78
84
|
"""Toggle the reverse flag."""
|
79
85
|
self.reverse = not self.reverse
|
80
86
|
|
81
|
-
def map_to_rgb(self, _min: float, _max: float, v: float, reverse=None) -> str:
|
82
|
-
"""
|
83
|
-
Get rgb color for a value by linear interpolation.
|
87
|
+
def map_to_rgb(self, _min: float, _max: float, v: float, reverse: bool | None = None) -> str:
|
88
|
+
"""Get rgb color for a value by linear interpolation.
|
84
89
|
|
85
90
|
Args:
|
86
91
|
_min (float): Minimum of input range.
|
87
92
|
_max (float): Maximum of input range.
|
88
93
|
v (float): Value to map.
|
94
|
+
reverse (bool | None): If True, reverses the gradient direction.
|
89
95
|
|
90
96
|
Returns:
|
91
97
|
str: RGB color string.
|
92
98
|
"""
|
93
99
|
return self.map_to_color(_min, _max, v, reverse).rgb
|
94
100
|
|
95
|
-
def map_to_color(self, _min: float, _max: float, v: float, reverse=None) -> ColorTriplet:
|
96
|
-
"""
|
97
|
-
Get rgb color for a value by linear interpolation.
|
101
|
+
def map_to_color(self, _min: float, _max: float, v: float, reverse: bool | None = None) -> ColorTriplet:
|
102
|
+
"""Get rgb color for a value by linear interpolation.
|
98
103
|
|
99
104
|
Args:
|
100
105
|
_min (float): Minimum of input range.
|
@@ -1,18 +1,24 @@
|
|
1
|
+
"""A module for image processing utilities, including encoding images to JPEG and PNG formats, and converting WebP images to JPEG."""
|
2
|
+
|
1
3
|
import base64
|
2
4
|
from io import BytesIO
|
3
5
|
from pathlib import Path
|
6
|
+
from typing import TYPE_CHECKING
|
4
7
|
|
5
8
|
from PIL import Image
|
6
9
|
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from PIL.Image import Image as PILImage
|
12
|
+
|
7
13
|
|
8
|
-
def encode_image_to_jpeg(image_path: Path, max_size: int = 1024, jpeg_quality=75) -> str:
|
14
|
+
def encode_image_to_jpeg(image_path: Path, max_size: int = 1024, jpeg_quality: int = 75) -> str:
|
9
15
|
"""Resize image to optimize for token usage"""
|
10
16
|
image = Image.open(image_path)
|
11
17
|
if max(image.size) > max_size:
|
12
18
|
image.thumbnail((max_size, max_size))
|
13
19
|
buffered = BytesIO()
|
14
20
|
if image.format != "JPEG":
|
15
|
-
image = image.convert("RGB")
|
21
|
+
image: PILImage = image.convert("RGB")
|
16
22
|
image.save(buffered, format="JPEG", quality=jpeg_quality)
|
17
23
|
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
18
24
|
|
@@ -24,16 +30,16 @@ def encode_image_to_png(image_path: Path, max_size: int = 1024) -> str:
|
|
24
30
|
image.thumbnail((max_size, max_size))
|
25
31
|
buffered = BytesIO()
|
26
32
|
if image.format != "PNG":
|
27
|
-
image = image.convert("RGBA")
|
33
|
+
image: PILImage = image.convert("RGBA")
|
28
34
|
image.save(buffered, format="PNG")
|
29
35
|
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
30
36
|
|
31
37
|
|
32
|
-
def convert_webp_to_jpeg(image_path: Path, jpeg_quality=95) -> str:
|
38
|
+
def convert_webp_to_jpeg(image_path: Path, jpeg_quality: int = 95) -> str:
|
33
39
|
"""Convert a WebP image to JPEG format."""
|
34
40
|
image = Image.open(image_path)
|
35
41
|
buffered = BytesIO()
|
36
42
|
if image.format != "JPEG":
|
37
|
-
image = image.convert("RGB")
|
43
|
+
image: PILImage = image.convert("RGB")
|
38
44
|
image.save(buffered, format="JPEG", quality=jpeg_quality)
|
39
45
|
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
bear_utils/gui/__init__.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
"""A module for GUI-related utilities using PyQt6. Optional Module."""
|
2
|
+
|
1
3
|
try:
|
2
4
|
from .gui_tools import QTApplication, get_text, select_color
|
3
5
|
|
4
|
-
__all__ = ["QTApplication", "
|
6
|
+
__all__ = ["QTApplication", "get_text", "select_color"]
|
5
7
|
except ImportError as e:
|
6
8
|
raise ImportError("PyQt6 is required for GUI functionality. Install it with: pip install bear-utils[gui]") from e
|
@@ -1,8 +1,10 @@
|
|
1
|
+
"""A module for managing a PyQt6 application instance and providing utility methods for dialogs and menus."""
|
2
|
+
|
1
3
|
try:
|
2
4
|
from .qt_app import QTApplication
|
3
5
|
from .qt_color_picker import select_color
|
4
6
|
from .qt_input_dialog import get_text
|
5
7
|
|
6
|
-
__all__ = ["QTApplication", "
|
8
|
+
__all__ = ["QTApplication", "get_text", "select_color"]
|
7
9
|
except ImportError as e:
|
8
10
|
raise ImportError("PyQt6 is required for GUI functionality. Install it with: pip install bear-utils[gui]") from e
|
@@ -9,7 +9,6 @@ class Settings(QTApplication):
|
|
9
9
|
"""Settings class that inherits from QTApplication for standalone use."""
|
10
10
|
|
11
11
|
def __init__(self, app_name: str = "Settings App", org_name: str = "YourOrg", org_domain: str = "org.domain"):
|
12
|
-
|
13
12
|
super().__init__(app_name, org_name, org_domain)
|
14
13
|
|
15
14
|
self._settings = QSettings(org_name, app_name)
|
@@ -1,19 +1,22 @@
|
|
1
|
+
"""A module for managing a PyQt6 application instance and providing utility methods for dialogs and menus."""
|
2
|
+
|
1
3
|
import atexit
|
2
|
-
import sys
|
3
4
|
from collections.abc import Callable
|
4
5
|
from pathlib import Path
|
6
|
+
import sys
|
5
7
|
|
6
8
|
from PyQt6.QtCore import QCoreApplication, QObject, Qt
|
7
9
|
from PyQt6.QtGui import QAction, QIcon, QKeySequence, QShortcut
|
8
10
|
from PyQt6.QtWidgets import QApplication, QDialog, QLabel, QMenu, QMenuBar, QMessageBox, QVBoxLayout
|
9
11
|
|
10
|
-
from
|
12
|
+
from bear_utils.logging import VERBOSE, ConsoleLogger
|
13
|
+
|
11
14
|
from ._types import ActionHolder
|
12
15
|
|
13
16
|
|
14
17
|
class QTApplication(QObject):
|
15
|
-
"""
|
16
|
-
|
18
|
+
"""Singleton class to manage the QApplication instance.
|
19
|
+
|
17
20
|
This ensures that only one instance of QApplication is created.
|
18
21
|
"""
|
19
22
|
|
@@ -22,7 +25,8 @@ class QTApplication(QObject):
|
|
22
25
|
app_name: str = "Qt Application",
|
23
26
|
org_name: str = "Organization",
|
24
27
|
org_domain: str = "org.domain",
|
25
|
-
):
|
28
|
+
) -> None:
|
29
|
+
"""Initialize the QTApplication instance."""
|
26
30
|
super().__init__()
|
27
31
|
if not QApplication.instance():
|
28
32
|
self.app: QCoreApplication | None = QApplication(sys.argv)
|
@@ -35,7 +39,7 @@ class QTApplication(QObject):
|
|
35
39
|
self.console = ConsoleLogger.get_instance(init=True, name=app_name, level=VERBOSE)
|
36
40
|
atexit.register(self.cleanup)
|
37
41
|
|
38
|
-
def _default_exit_shortcuts(self):
|
42
|
+
def _default_exit_shortcuts(self) -> None:
|
39
43
|
"""Set up default exit shortcuts for the application."""
|
40
44
|
self._add_shortcut(QKeySequence("Escape"), self.cleanup)
|
41
45
|
|
@@ -68,7 +72,7 @@ class QTApplication(QObject):
|
|
68
72
|
self.menu_bar.addMenu(menu)
|
69
73
|
self.main_layout.setMenuBar(self.menu_bar)
|
70
74
|
|
71
|
-
def _setup_initial_window(self, title: str, icon_path: Path, width: int, height: int):
|
75
|
+
def _setup_initial_window(self, title: str, icon_path: Path, width: int, height: int) -> None:
|
72
76
|
"""Create and show the initial window with loading indicator."""
|
73
77
|
self.dialog = QDialog(None)
|
74
78
|
self.dialog.setWindowTitle(title)
|
@@ -81,7 +85,8 @@ class QTApplication(QObject):
|
|
81
85
|
self.loading_label = QLabel("Loading...")
|
82
86
|
self.main_layout.addWidget(self.loading_label)
|
83
87
|
|
84
|
-
def get_app(self):
|
88
|
+
def get_app(self) -> QApplication | QCoreApplication | None:
|
89
|
+
"""Get the current QApplication instance."""
|
85
90
|
if not self.app and not QApplication.instance():
|
86
91
|
self.app = QApplication(sys.argv)
|
87
92
|
elif not self.app:
|
@@ -95,8 +100,7 @@ class QTApplication(QObject):
|
|
95
100
|
icon: QMessageBox.Icon = QMessageBox.Icon.Information,
|
96
101
|
on_ok_action: Callable[[], None] | None = None,
|
97
102
|
) -> None:
|
98
|
-
"""
|
99
|
-
Show a message dialog with configurable icon and action.
|
103
|
+
"""Show a message dialog with configurable icon and action.
|
100
104
|
|
101
105
|
Args:
|
102
106
|
message: The message to display
|
@@ -130,7 +134,8 @@ class QTApplication(QObject):
|
|
130
134
|
"""Show an information dialog."""
|
131
135
|
self.show_message(message, title=title, icon=QMessageBox.Icon.Information)
|
132
136
|
|
133
|
-
def cleanup(self):
|
137
|
+
def cleanup(self) -> None:
|
138
|
+
"""Clean up the QTApplication instance."""
|
134
139
|
if self.app:
|
135
140
|
self.console.verbose("Cleaning up QTApplication instance.")
|
136
141
|
self.app.quit()
|
@@ -1,4 +1,7 @@
|
|
1
|
+
"""A module for a color picker dialog using PyQt6."""
|
2
|
+
|
1
3
|
from dataclasses import dataclass
|
4
|
+
from typing import Any
|
2
5
|
|
3
6
|
from PyQt6.QtGui import QColor
|
4
7
|
from PyQt6.QtWidgets import QColorDialog
|
@@ -9,6 +12,8 @@ from .qt_app import QTApplication
|
|
9
12
|
|
10
13
|
@dataclass
|
11
14
|
class ColorInfo:
|
15
|
+
"""Data class to hold color information."""
|
16
|
+
|
12
17
|
qcolor: QColor
|
13
18
|
hex: str
|
14
19
|
rgb: ColorTriplet
|
@@ -19,9 +24,13 @@ class ColorInfo:
|
|
19
24
|
class QTColorPicker(QTApplication):
|
20
25
|
"""Singleton class to manage the color picker dialog."""
|
21
26
|
|
22
|
-
def select_color(
|
23
|
-
|
24
|
-
|
27
|
+
def select_color(
|
28
|
+
self,
|
29
|
+
initial_color: str | None = None,
|
30
|
+
title: str = "Select Color",
|
31
|
+
options: Any | None = None,
|
32
|
+
) -> ColorInfo | None:
|
33
|
+
"""Shows a color selection dialog and returns the selected color.
|
25
34
|
|
26
35
|
Args:
|
27
36
|
initial_color: Initial color to show in the dialog. Can be:
|
@@ -42,7 +51,7 @@ class QTColorPicker(QTApplication):
|
|
42
51
|
"""
|
43
52
|
try:
|
44
53
|
dialog = QColorDialog()
|
45
|
-
|
54
|
+
channels_num = 3
|
46
55
|
if title:
|
47
56
|
dialog.setWindowTitle(title)
|
48
57
|
|
@@ -54,10 +63,10 @@ class QTColorPicker(QTApplication):
|
|
54
63
|
dialog.setCurrentColor(initial_color)
|
55
64
|
elif isinstance(initial_color, str) and initial_color.startswith("#"):
|
56
65
|
dialog.setCurrentColor(QColor(initial_color))
|
57
|
-
elif isinstance(initial_color, tuple) and len(initial_color) >=
|
58
|
-
r, g, b = initial_color[:
|
59
|
-
a = initial_color[
|
60
|
-
dialog.setCurrentColor(QColor(r, g, b, a))
|
66
|
+
elif isinstance(initial_color, tuple) and len(initial_color) >= channels_num:
|
67
|
+
r, g, b = initial_color[:channels_num]
|
68
|
+
a = initial_color[channels_num] if len(initial_color) > channels_num else 255
|
69
|
+
dialog.setCurrentColor(QColor(int(r), int(g), int(b), int(a)))
|
61
70
|
|
62
71
|
if dialog.exec() == QColorDialog.DialogCode.Accepted:
|
63
72
|
selected_color = dialog.selectedColor()
|
@@ -77,16 +86,18 @@ class QTColorPicker(QTApplication):
|
|
77
86
|
),
|
78
87
|
hsv=(selected_color.hue(), selected_color.saturation(), selected_color.value()),
|
79
88
|
)
|
80
|
-
|
81
|
-
return None
|
89
|
+
return None
|
82
90
|
except Exception as e:
|
83
91
|
self.console.error(f"Error in color selection dialog: {e}")
|
84
92
|
return None
|
85
93
|
|
86
94
|
|
87
|
-
def select_color(
|
88
|
-
|
89
|
-
|
95
|
+
def select_color(
|
96
|
+
initial_color: str | None = None,
|
97
|
+
title: str = "Select Color",
|
98
|
+
options: Any | None = None,
|
99
|
+
) -> ColorInfo | None:
|
100
|
+
"""Select a color using the QTColorPicker singleton instance.
|
90
101
|
|
91
102
|
Args:
|
92
103
|
initial_color: Initial color to show in the dialog. Can be:
|