bear-utils 0.7.11__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.
Files changed (83) hide show
  1. bear_utils/__init__.py +13 -0
  2. bear_utils/ai/__init__.py +30 -0
  3. bear_utils/ai/ai_helpers/__init__.py +130 -0
  4. bear_utils/ai/ai_helpers/_common.py +19 -0
  5. bear_utils/ai/ai_helpers/_config.py +24 -0
  6. bear_utils/ai/ai_helpers/_parsers.py +188 -0
  7. bear_utils/ai/ai_helpers/_types.py +20 -0
  8. bear_utils/cache/__init__.py +119 -0
  9. bear_utils/cli/__init__.py +4 -0
  10. bear_utils/cli/commands.py +59 -0
  11. bear_utils/cli/prompt_helpers.py +166 -0
  12. bear_utils/cli/shell/__init__.py +0 -0
  13. bear_utils/cli/shell/_base_command.py +74 -0
  14. bear_utils/cli/shell/_base_shell.py +390 -0
  15. bear_utils/cli/shell/_common.py +19 -0
  16. bear_utils/config/__init__.py +11 -0
  17. bear_utils/config/config_manager.py +92 -0
  18. bear_utils/config/dir_manager.py +64 -0
  19. bear_utils/config/settings_manager.py +232 -0
  20. bear_utils/constants/__init__.py +16 -0
  21. bear_utils/constants/_exceptions.py +3 -0
  22. bear_utils/constants/_lazy_typing.py +15 -0
  23. bear_utils/constants/date_related.py +36 -0
  24. bear_utils/constants/time_related.py +22 -0
  25. bear_utils/database/__init__.py +6 -0
  26. bear_utils/database/_db_manager.py +104 -0
  27. bear_utils/events/__init__.py +16 -0
  28. bear_utils/events/events_class.py +52 -0
  29. bear_utils/events/events_module.py +65 -0
  30. bear_utils/extras/__init__.py +17 -0
  31. bear_utils/extras/_async_helpers.py +15 -0
  32. bear_utils/extras/_tools.py +178 -0
  33. bear_utils/extras/platform_utils.py +53 -0
  34. bear_utils/extras/wrappers/__init__.py +0 -0
  35. bear_utils/extras/wrappers/add_methods.py +98 -0
  36. bear_utils/files/__init__.py +4 -0
  37. bear_utils/files/file_handlers/__init__.py +3 -0
  38. bear_utils/files/file_handlers/_base_file_handler.py +93 -0
  39. bear_utils/files/file_handlers/file_handler_factory.py +278 -0
  40. bear_utils/files/file_handlers/json_file_handler.py +44 -0
  41. bear_utils/files/file_handlers/log_file_handler.py +33 -0
  42. bear_utils/files/file_handlers/txt_file_handler.py +34 -0
  43. bear_utils/files/file_handlers/yaml_file_handler.py +57 -0
  44. bear_utils/files/ignore_parser.py +298 -0
  45. bear_utils/graphics/__init__.py +4 -0
  46. bear_utils/graphics/bear_gradient.py +140 -0
  47. bear_utils/graphics/image_helpers.py +39 -0
  48. bear_utils/gui/__init__.py +3 -0
  49. bear_utils/gui/gui_tools/__init__.py +5 -0
  50. bear_utils/gui/gui_tools/_settings.py +37 -0
  51. bear_utils/gui/gui_tools/_types.py +12 -0
  52. bear_utils/gui/gui_tools/qt_app.py +145 -0
  53. bear_utils/gui/gui_tools/qt_color_picker.py +119 -0
  54. bear_utils/gui/gui_tools/qt_file_handler.py +138 -0
  55. bear_utils/gui/gui_tools/qt_input_dialog.py +306 -0
  56. bear_utils/logging/__init__.py +25 -0
  57. bear_utils/logging/logger_manager/__init__.py +0 -0
  58. bear_utils/logging/logger_manager/_common.py +47 -0
  59. bear_utils/logging/logger_manager/_console_junk.py +131 -0
  60. bear_utils/logging/logger_manager/_styles.py +91 -0
  61. bear_utils/logging/logger_manager/loggers/__init__.py +0 -0
  62. bear_utils/logging/logger_manager/loggers/_base_logger.py +238 -0
  63. bear_utils/logging/logger_manager/loggers/_base_logger.pyi +50 -0
  64. bear_utils/logging/logger_manager/loggers/_buffer_logger.py +55 -0
  65. bear_utils/logging/logger_manager/loggers/_console_logger.py +249 -0
  66. bear_utils/logging/logger_manager/loggers/_console_logger.pyi +64 -0
  67. bear_utils/logging/logger_manager/loggers/_file_logger.py +141 -0
  68. bear_utils/logging/logger_manager/loggers/_level_sin.py +58 -0
  69. bear_utils/logging/logger_manager/loggers/_logger.py +18 -0
  70. bear_utils/logging/logger_manager/loggers/_sub_logger.py +110 -0
  71. bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +38 -0
  72. bear_utils/logging/loggers.py +76 -0
  73. bear_utils/monitoring/__init__.py +10 -0
  74. bear_utils/monitoring/host_monitor.py +350 -0
  75. bear_utils/time/__init__.py +16 -0
  76. bear_utils/time/_helpers.py +91 -0
  77. bear_utils/time/_time_class.py +316 -0
  78. bear_utils/time/_timer.py +80 -0
  79. bear_utils/time/_tools.py +17 -0
  80. bear_utils/time/time_manager.py +218 -0
  81. bear_utils-0.7.11.dist-info/METADATA +260 -0
  82. bear_utils-0.7.11.dist-info/RECORD +83 -0
  83. bear_utils-0.7.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,298 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from pathspec import PathSpec
5
+
6
+ from ..cli.prompt_helpers import ask_yes_no
7
+ from ..logging import ConsoleLogger
8
+
9
+ logger: ConsoleLogger = ConsoleLogger.get_instance(init=True)
10
+
11
+ IGNORE_PATTERNS: list[str] = [
12
+ "__pycache__",
13
+ ".git",
14
+ ".venv",
15
+ ".env",
16
+ ".vscode",
17
+ ".idea",
18
+ "*.DS_Store*",
19
+ "__pypackages__",
20
+ ".pytest_cache",
21
+ ".coverage",
22
+ ".*.swp",
23
+ ".*.swo",
24
+ "*.lock",
25
+ "dist/",
26
+ ]
27
+
28
+
29
+ class IgnoreHandler:
30
+ """Basic ignore handler for manually checking if a file should be ignored based on set patterns."""
31
+
32
+ def __init__(self, ignore_file: Path | None = None, combine: bool = False):
33
+ self.ignore_file = ignore_file
34
+ self.patterns: list[str] = self.load_patterns(ignore_file, combine) if ignore_file else IGNORE_PATTERNS
35
+ self.spec: PathSpec = self._create_spec(self.patterns)
36
+
37
+ @staticmethod
38
+ def _create_spec(patterns: list[str]) -> PathSpec:
39
+ """
40
+ Create a pathspec from the given patterns.
41
+
42
+ Args:
43
+ patterns: List of ignore patterns
44
+
45
+ Returns:
46
+ A pathspec object
47
+ """
48
+ return PathSpec.from_lines("gitwildmatch", patterns)
49
+
50
+ @staticmethod
51
+ def load_patterns(ignore_file: Path, combine: bool) -> list[str]:
52
+ """
53
+ Load patterns from a specific ignore file.
54
+
55
+ Args:
56
+ ignore_file: Path to the ignore file
57
+ """
58
+ if not ignore_file.exists():
59
+ logger.warning(f"Ignore file {ignore_file} does not exist")
60
+ return []
61
+ try:
62
+ lines = ignore_file.read_text().splitlines()
63
+ patterns: list[str] = [
64
+ l.strip()
65
+ for l in lines
66
+ if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("!")
67
+ ]
68
+ if patterns:
69
+ logger.verbose(f"Loaded {len(patterns)} patterns from {ignore_file}")
70
+ if combine:
71
+ patterns.extend(IGNORE_PATTERNS) # Combine with default patterns
72
+ return patterns
73
+ except Exception as e:
74
+ logger.error(f"Error loading ignore file {ignore_file}: {e}")
75
+ return []
76
+
77
+ def should_ignore(self, path: Path | str) -> bool:
78
+ """
79
+ Check if a given path should be ignored based on the ignore patterns.
80
+
81
+ Args:
82
+ path (Path): The path to check
83
+ Returns:
84
+ bool: True if the path should be ignored, False otherwise
85
+ """
86
+ path = Path(path).expanduser()
87
+ if path.is_dir() and not str(path).endswith("/"):
88
+ return self.spec.match_file(str(path) + "/")
89
+ return self.spec.match_file(str(path))
90
+
91
+ def ignore_print(self, path: Path | str, rel: bool = True) -> bool:
92
+ """
93
+ Print whether a given path is ignored based on the ignore patterns.
94
+ Will print the path as relative to the current working directory if rel is True,
95
+ which it is by default.
96
+
97
+ Args:
98
+ path (Path): The path to check
99
+ rel (bool): Whether to print the path as relative to the current working directory
100
+
101
+ Returns:
102
+ bool: True if the path is ignored, False otherwise
103
+ """
104
+ path = Path(path).expanduser()
105
+ if rel:
106
+ path = path.relative_to(Path.cwd())
107
+
108
+ should_ignore = self.should_ignore(path)
109
+ msg_ignore = f"[red]{path}[/] is ignored"
110
+ msg_not_ignore = f"[green]{path}[/] is not ignored"
111
+ (logger.print(msg_ignore) if should_ignore else logger.print(msg_not_ignore))
112
+ return should_ignore
113
+
114
+ def add_patterns(self, patterns: list[str]) -> None:
115
+ """
116
+ Add additional ignore patterns to the existing spec.
117
+
118
+ Args:
119
+ patterns: List of additional patterns to add
120
+ """
121
+ new_patterns = []
122
+ default_patterns = IGNORE_PATTERNS.copy()
123
+ if self.spec:
124
+ for pattern in patterns:
125
+ if pattern not in default_patterns:
126
+ new_patterns.append(pattern)
127
+ self.spec = PathSpec(new_patterns + default_patterns)
128
+ logger.verbose(f"Added {len(patterns)} ignore patterns")
129
+
130
+
131
+ class IgnoreDirectoryHandler(IgnoreHandler):
132
+ """Handles the logic for ignoring files and directories based on .gitignore-style rules."""
133
+
134
+ def __init__(self, directory_to_search: Path | str, ignore_file: Path | None = None, rel: bool = True):
135
+ super().__init__(ignore_file)
136
+ self.directory_to_search = Path(directory_to_search).resolve()
137
+ self.ignored_files: list[Path] = []
138
+ self.non_ignored_files: list[Path] = []
139
+ self.rel = rel
140
+
141
+ 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
+
145
+ Args:
146
+ directory: The directory to scan
147
+
148
+ Returns:
149
+ Tuple of (non_ignored_files, ignored_files) as relative path strings
150
+ """
151
+ directory = Path(directory)
152
+ all_paths = []
153
+
154
+ for root, _, files in os.walk(directory):
155
+ root_path = Path(root)
156
+ rel_root = root_path.relative_to(directory)
157
+
158
+ if str(rel_root) == ".":
159
+ rel_root = Path("")
160
+ else:
161
+ dir_path = str(rel_root) + "/"
162
+ all_paths.append(dir_path)
163
+
164
+ for file in files:
165
+ rel_path = rel_root / file
166
+ all_paths.append(str(rel_path))
167
+
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]
171
+ return non_ignored_paths, ignored_paths
172
+
173
+ @property
174
+ def ignored_files_count(self) -> int:
175
+ """
176
+ Get the count of ignored files.
177
+
178
+ Returns:
179
+ int: The number of ignored files
180
+ """
181
+ return len(self.ignored_files)
182
+
183
+ @property
184
+ def non_ignored_files_count(self) -> int:
185
+ """
186
+ Get the count of non-ignored files.
187
+
188
+ Returns:
189
+ int: The number of non-ignored files
190
+ """
191
+ return len(self.non_ignored_files)
192
+
193
+ def ignore_report_full_codebase(self):
194
+ """
195
+ Generate a report of ignored and non-ignored files in the directory.
196
+
197
+ Returns:
198
+ Tuple of (non_ignored_files, ignored_files) as Path objects
199
+ """
200
+ non_ignored_paths, ignored_paths = self._scan_directory(self.directory_to_search)
201
+ self.non_ignored_files = [self.directory_to_search / p for p in non_ignored_paths]
202
+ self.ignored_files = [self.directory_to_search / p for p in ignored_paths]
203
+
204
+ @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.
208
+
209
+ Args:
210
+ data_structure_to_print: The data structure to print
211
+ rel: Whether to print the paths as relative to the current working directory
212
+ """
213
+ try:
214
+ for path in data_structure_to_print:
215
+ if rel:
216
+ path = path.relative_to(Path.cwd())
217
+ logger.print(str(f"[{color}]{path}[/]"))
218
+ except KeyboardInterrupt:
219
+ logger.warning("Printing interrupted by user.")
220
+
221
+ def _print_ignored_files(self):
222
+ """Print the ignored files in the directory."""
223
+ if self.ignored_files_count == 0:
224
+ logger.print("No ignored files found.")
225
+ else:
226
+ if self.ignored_files_count > 100:
227
+ if ask_yes_no(
228
+ "There are a lot of ignored files. Do you want to print them all? (y/n)",
229
+ default="n",
230
+ ):
231
+ self._print(self.ignored_files, self.rel, "red")
232
+ else:
233
+ self._print(self.ignored_files, self.rel, "red")
234
+
235
+ def _print_non_ignored_files(self):
236
+ """Print the non-ignored files in the directory."""
237
+ if self.non_ignored_files_count == 0:
238
+ logger.print("No non-ignored files found.")
239
+ else:
240
+ if self.non_ignored_files_count > 100:
241
+ if ask_yes_no(
242
+ "There are a lot of non-ignored files. Do you want to print them all? (y/n)",
243
+ default="n",
244
+ ):
245
+ self._print(self.non_ignored_files, self.rel)
246
+ else:
247
+ self._print(self.non_ignored_files, self.rel)
248
+
249
+ def print_report(self, what_to_print: str):
250
+ """
251
+ Print the report of ignored or non-ignored files or both
252
+
253
+ Args:
254
+ what_to_print: "ignored", "non_ignored", or "both"
255
+ """
256
+ match what_to_print:
257
+ case "ignored":
258
+ self._print_ignored_files()
259
+ case "non_ignored":
260
+ self._print_non_ignored_files()
261
+ case "both":
262
+ self._print_ignored_files()
263
+ self._print_non_ignored_files()
264
+ case _:
265
+ logger.error("Invalid option. Use 'ignored', 'non_ignored', or 'both'.")
266
+
267
+
268
+ # if __name__ == "__main__":
269
+ # # Example usage
270
+ # ignore_handler = IgnoreHandler()
271
+ # files_to_check = [
272
+ # Path.cwd() / "src/bear_utils/__pycache__",
273
+ # Path.cwd() / "src/bear_utils/.git",
274
+ # Path.cwd() / "src/bear_utils/.venv",
275
+ # Path.cwd() / "src/bear_utils/uv.lock",
276
+ # Path.cwd() / "src/bear_utils/ignore_parser.py",
277
+ # Path.cwd() / "src/bear_utils/extras/wrappers/add_methods.py",
278
+ # Path.cwd() / "src/bear_utils/chronobear/_time_class.py",
279
+ # ]
280
+ # for file in files_to_check:
281
+ # ignore_handler.ignore_print(file)
282
+ # logger.set_verbose(True)
283
+ # directory_handler = IgnoreDirectoryHandler(".")
284
+ # directory_handler.ignore_print(Path.cwd() / "src/bear_utils/__pycache__")
285
+ # directory_handler.ignore_report_full_codebase()
286
+ # logger.print(f"Ignored files: {directory_handler.ignored_files_count}")
287
+ # logger.print(f"Non-ignored files: {directory_handler.non_ignored_files_count}")
288
+ # answer = ask_question(
289
+ # "Do you want to print the ignored or non-ignored files? (ignored/non_ignored/both)",
290
+ # expected_type="str",
291
+ # default="both",
292
+ # )
293
+ # if answer == "both":
294
+ # directory_handler.print_report("both")
295
+ # elif answer == "non_ignored":
296
+ # directory_handler.print_report("non_ignored")
297
+ # else:
298
+ # directory_handler.print_report("ignored")
@@ -0,0 +1,4 @@
1
+ from .bear_gradient import ColorGradient, DefaultColors
2
+ from .image_helpers import encode_image_to_jpeg, encode_image_to_png
3
+
4
+ __all__ = ["DefaultColors", "ColorGradient", "encode_image_to_jpeg", "encode_image_to_png"]
@@ -0,0 +1,140 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from pyglm.glm import clamp, lerp, quat
4
+ from rich.color import Color as RichColor
5
+ from rich.color_triplet import ColorTriplet
6
+
7
+ RED: RichColor = RichColor.from_rgb(255, 0, 0)
8
+ YELLOW: RichColor = RichColor.from_rgb(255, 255, 0)
9
+ GREEN: RichColor = RichColor.from_rgb(0, 255, 0)
10
+
11
+
12
+ def inv_lerp(a: float, b: float, t: float) -> float:
13
+ """Inverse linear interpolation."""
14
+ return clamp((t - a) / (b - a), 0.0, 1.0)
15
+
16
+
17
+ def rgb_int(v: float | quat) -> int:
18
+ """Convert a float to an int, clamping to 0-255."""
19
+ return int(clamp(v, 0.0, 255.0))
20
+
21
+
22
+ @dataclass
23
+ class DefaultColors:
24
+ """The default colors for the gradient."""
25
+
26
+ start: RichColor = RED # Default Threshold: 0.0
27
+ mid: RichColor = YELLOW # Default Threshold: 0.7
28
+ end: RichColor = GREEN # Default Threshold: 1.0
29
+
30
+ def output_rgb(self) -> tuple[ColorTriplet, ColorTriplet, ColorTriplet]:
31
+ return self.start.get_truecolor(), self.mid.get_truecolor(), self.end.get_truecolor()
32
+
33
+
34
+ @dataclass
35
+ class DefaultThresholds:
36
+ """The default thresholds for the gradient."""
37
+
38
+ start: float = 0.0 # Default Color: RED
39
+ mid: float = 0.7 # Default Color: YELLOW
40
+ end: float = 1.0 # Default Color: GREEN
41
+
42
+ def __post_init__(self) -> None:
43
+ if not (0.0 <= self.start < self.mid < self.end <= 1.0):
44
+ raise ValueError("thresholds must be strictly increasing and between 0 and 1.")
45
+
46
+ def unpack(self) -> tuple[float, float, float]:
47
+ return self.start, self.mid, self.end
48
+
49
+
50
+ @dataclass
51
+ class DefaultColorConfig:
52
+ colors: DefaultColors = field(default_factory=DefaultColors)
53
+ thresholds: DefaultThresholds = field(default_factory=DefaultThresholds)
54
+
55
+
56
+ class ColorGradient:
57
+ """
58
+ Simple 3-color gradient interpolator.
59
+
60
+ Args:
61
+ colors (DefaultColors): Default colors for the gradient.
62
+ thresholds (Thresholds): Thresholds for the gradient.
63
+ reverse (bool): If True, reverses the gradient direction.
64
+ """
65
+
66
+ def __init__(self, config=DefaultColorConfig(), reverse: bool = False) -> None:
67
+ self.config: DefaultColorConfig = config
68
+ self.colors: DefaultColors = config.colors
69
+ self.thresholds: DefaultThresholds = config.thresholds
70
+ self.reverse: bool = reverse
71
+ self.c0, self.c1, self.c2 = self.colors.output_rgb()
72
+ self.p0, self.p1, self.p2 = self.thresholds.unpack()
73
+
74
+ if not (0.0 <= self.p0 < self.p1 < self.p2 <= 1.0):
75
+ raise ValueError("thresholds must be strictly increasing and between 0 and 1.")
76
+
77
+ def flip(self) -> None:
78
+ """Toggle the reverse flag."""
79
+ self.reverse = not self.reverse
80
+
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.
84
+
85
+ Args:
86
+ _min (float): Minimum of input range.
87
+ _max (float): Maximum of input range.
88
+ v (float): Value to map.
89
+
90
+ Returns:
91
+ str: RGB color string.
92
+ """
93
+ return self.map_to_color(_min, _max, v, reverse).rgb
94
+
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.
98
+
99
+ Args:
100
+ _min (float): Minimum of input range.
101
+ _max (float): Maximum of input range.
102
+ v (float): Value to map.
103
+
104
+ Returns:
105
+ ColorTriplet: RGB color triplet.
106
+ """
107
+ reverse = reverse if reverse is not None else self.reverse
108
+
109
+ t: float = inv_lerp(_min, _max, v) if not reverse else 1.0 - inv_lerp(_min, _max, v)
110
+ src, dst = (self.c0, self.c1) if t <= self.p1 else (self.c1, self.c2)
111
+ seg: float = inv_lerp(self.p0, self.p1, t) if t <= self.p1 else inv_lerp(self.p1, self.p2, t)
112
+
113
+ r = rgb_int(lerp(src.red, dst.red, seg))
114
+ g = rgb_int(lerp(src.green, dst.green, seg))
115
+ b = rgb_int(lerp(src.blue, dst.blue, seg))
116
+
117
+ return ColorTriplet(red=r, green=g, blue=b)
118
+
119
+
120
+ # if __name__ == "__main__":
121
+ # # example usage
122
+ # from rich.console import Console
123
+
124
+ # console = Console()
125
+ # gradient = ColorGradient()
126
+
127
+ # console.print(f"RED: {RED.get_truecolor().rgb}")
128
+ # console.print(f"YELLOW: {YELLOW.get_truecolor().rgb}")
129
+ # console.print(f"GREEN: {GREEN.get_truecolor().rgb}")
130
+
131
+ # # RED -> YELLOW -> GREEN
132
+ # for i in range(0, 101, 10):
133
+ # color: ColorTriplet = gradient.map_to_color(0, 100, i)
134
+ # console.print(f"Value: {i} => Color: {color.rgb}", style=color.rgb)
135
+
136
+ # # GREEN -> YELLOW -> RED
137
+ # gradient.reverse = True
138
+ # for i in range(0, 101, 10):
139
+ # color: ColorTriplet = gradient.map_to_color(0, 100, i)
140
+ # console.print(f"Value: {i} => Color: {color.rgb}", style=color.rgb)
@@ -0,0 +1,39 @@
1
+ import base64
2
+ from io import BytesIO
3
+ from pathlib import Path
4
+
5
+ from PIL import Image
6
+
7
+
8
+ def encode_image_to_jpeg(image_path: Path, max_size: int = 1024, jpeg_quality=75) -> str:
9
+ """Resize image to optimize for token usage"""
10
+ image = Image.open(image_path)
11
+ if max(image.size) > max_size:
12
+ image.thumbnail((max_size, max_size))
13
+ buffered = BytesIO()
14
+ if image.format != "JPEG":
15
+ image = image.convert("RGB")
16
+ image.save(buffered, format="JPEG", quality=jpeg_quality)
17
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
18
+
19
+
20
+ def encode_image_to_png(image_path: Path, max_size: int = 1024) -> str:
21
+ """Resize image to optimize for token usage"""
22
+ image = Image.open(image_path)
23
+ if max(image.size) > max_size:
24
+ image.thumbnail((max_size, max_size))
25
+ buffered = BytesIO()
26
+ if image.format != "PNG":
27
+ image = image.convert("RGBA")
28
+ image.save(buffered, format="PNG")
29
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
30
+
31
+
32
+ def convert_webp_to_jpeg(image_path: Path, jpeg_quality=95) -> str:
33
+ """Convert a WebP image to JPEG format."""
34
+ image = Image.open(image_path)
35
+ buffered = BytesIO()
36
+ if image.format != "JPEG":
37
+ image = image.convert("RGB")
38
+ image.save(buffered, format="JPEG", quality=jpeg_quality)
39
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
@@ -0,0 +1,3 @@
1
+ from .gui_tools import QTApplication, get_text, select_color
2
+
3
+ __all__ = ["QTApplication", "select_color", "get_text"]
@@ -0,0 +1,5 @@
1
+ from .qt_app import QTApplication
2
+ from .qt_color_picker import select_color
3
+ from .qt_input_dialog import get_text
4
+
5
+ __all__ = ["QTApplication", "select_color", "get_text"]
@@ -0,0 +1,37 @@
1
+ from typing import Any
2
+
3
+ from PyQt6.QtCore import QSettings
4
+
5
+ from .qt_app import QTApplication
6
+
7
+
8
+ class Settings(QTApplication):
9
+ """Settings class that inherits from QTApplication for standalone use."""
10
+
11
+ def __init__(self, app_name: str = "Settings App", org_name: str = "YourOrg", org_domain: str = "org.domain"):
12
+
13
+ super().__init__(app_name, org_name, org_domain)
14
+
15
+ self._settings = QSettings(org_name, app_name)
16
+
17
+ def get(self, key: str, default: Any = None, value_type: type | None = None) -> Any:
18
+ """Get a setting value with optional type conversion."""
19
+ if value_type:
20
+ return self._settings.value(key, default, type=value_type)
21
+ return self._settings.value(key, default)
22
+
23
+ def set(self, key: str, value: Any) -> None:
24
+ """Set a setting value."""
25
+ self._settings.setValue(key, value)
26
+
27
+ def has(self, key: str) -> bool:
28
+ """Check if a setting exists."""
29
+ return self._settings.contains(key)
30
+
31
+ def remove_key(self, key: str) -> None:
32
+ """Remove a setting."""
33
+ self._settings.remove(key)
34
+
35
+ def clear_settings(self) -> None:
36
+ """Clear all settings."""
37
+ self._settings.clear()
@@ -0,0 +1,12 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class ActionHolder:
7
+ """A class to hold the action and its arguments."""
8
+
9
+ # text, shortcut, callback)
10
+ text: str
11
+ shortcut: str
12
+ callback: Callable
@@ -0,0 +1,145 @@
1
+ import atexit
2
+ import sys
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ from PyQt6.QtCore import QCoreApplication, QObject, Qt
7
+ from PyQt6.QtGui import QAction, QIcon, QKeySequence, QShortcut
8
+ from PyQt6.QtWidgets import QApplication, QDialog, QLabel, QMenu, QMenuBar, QMessageBox, QVBoxLayout
9
+
10
+ from ...logging import VERBOSE, ConsoleLogger
11
+ from ._types import ActionHolder
12
+
13
+
14
+ class QTApplication(QObject):
15
+ """
16
+ Singleton class to manage the QApplication instance.
17
+ This ensures that only one instance of QApplication is created.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ app_name: str = "Qt Application",
23
+ org_name: str = "Organization",
24
+ org_domain: str = "org.domain",
25
+ ):
26
+ super().__init__()
27
+ if not QApplication.instance():
28
+ self.app: QCoreApplication | None = QApplication(sys.argv)
29
+ if self.app:
30
+ self.app.setApplicationName(app_name)
31
+ self.app.setOrganizationName(org_name)
32
+ self.app.setOrganizationDomain(org_domain)
33
+ else:
34
+ self.app = QApplication.instance()
35
+ self.console = ConsoleLogger.get_instance(init=True, name=app_name, level=VERBOSE)
36
+ atexit.register(self.cleanup)
37
+
38
+ def _default_exit_shortcuts(self):
39
+ """Set up default exit shortcuts for the application."""
40
+ self._add_shortcut(QKeySequence("Escape"), self.cleanup)
41
+
42
+ def _add_shortcut(self, shortcut: QKeySequence | QKeySequence.StandardKey, callback: Callable) -> None:
43
+ """Add a shortcut to the application."""
44
+ q_shortcut = QShortcut(shortcut, self.dialog)
45
+ q_shortcut.activated.connect(callback)
46
+
47
+ def _add_to_menu(self, menu_name: str, actions: list[ActionHolder]) -> QMenu:
48
+ """Add an action to the menu."""
49
+ menu = QMenu(menu_name, self.dialog)
50
+ for a in actions:
51
+ action: QAction = self._add_action(text=a.text, shortcut=a.shortcut, callback=a.callback)
52
+ menu.addAction(action)
53
+ return menu
54
+
55
+ def _add_action(self, text: str, shortcut: str, callback: Callable) -> QAction:
56
+ """Create and return an action for the menu."""
57
+ action = QAction(text, self.dialog)
58
+ action.setShortcut(shortcut)
59
+ action.triggered.connect(callback)
60
+ return action
61
+
62
+ def _start_menu_bar(self) -> None:
63
+ """Create and setup the menu bar."""
64
+ self.menu_bar = QMenuBar(self.dialog)
65
+
66
+ def _end_menu_bar(self, menus_to_add: list[QMenu]) -> None:
67
+ for menu in menus_to_add:
68
+ self.menu_bar.addMenu(menu)
69
+ self.main_layout.setMenuBar(self.menu_bar)
70
+
71
+ def _setup_initial_window(self, title: str, icon_path: Path, width: int, height: int):
72
+ """Create and show the initial window with loading indicator."""
73
+ self.dialog = QDialog(None)
74
+ self.dialog.setWindowTitle(title)
75
+ self.dialog.setMinimumSize(width, height)
76
+
77
+ if icon_path.exists():
78
+ self.dialog.setWindowIcon(QIcon(str(icon_path)))
79
+
80
+ self.main_layout = QVBoxLayout(self.dialog)
81
+ self.loading_label = QLabel("Loading...")
82
+ self.main_layout.addWidget(self.loading_label)
83
+
84
+ def get_app(self):
85
+ if not self.app and not QApplication.instance():
86
+ self.app = QApplication(sys.argv)
87
+ elif not self.app:
88
+ self.app: QCoreApplication | None = QApplication.instance()
89
+ return self.app
90
+
91
+ def show_message(
92
+ self,
93
+ message: str,
94
+ title: str = "Message",
95
+ icon: QMessageBox.Icon = QMessageBox.Icon.Information,
96
+ on_ok_action: Callable[[], None] | None = None,
97
+ ) -> None:
98
+ """
99
+ Show a message dialog with configurable icon and action.
100
+
101
+ Args:
102
+ message: The message to display
103
+ title: Dialog title
104
+ icon: Message box icon (Information, Warning, Critical, Question)
105
+ on_ok_action: Function to call when OK is clicked
106
+ """
107
+ msg_box = QMessageBox()
108
+ msg_box.setIcon(icon)
109
+ msg_box.setWindowTitle(title)
110
+ msg_box.setText(message)
111
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
112
+ msg_box.setWindowModality(Qt.WindowModality.ApplicationModal)
113
+
114
+ result = msg_box.exec()
115
+
116
+ if result == QMessageBox.StandardButton.Ok and on_ok_action:
117
+ on_ok_action()
118
+
119
+ def show_warning(
120
+ self, message: str, on_ok_action: Callable[[], None] | None = None, title: str = "Warning"
121
+ ) -> None:
122
+ """Show a warning dialog."""
123
+ self.show_message(message, title=title, icon=QMessageBox.Icon.Warning, on_ok_action=on_ok_action)
124
+
125
+ def show_error(self, message: str, on_ok_action: Callable[[], None] | None = None, title: str = "Error") -> None:
126
+ """Show an error dialog."""
127
+ self.show_message(message, title=title, icon=QMessageBox.Icon.Critical, on_ok_action=on_ok_action)
128
+
129
+ def show_info(self, message: str, title: str = "Information") -> None:
130
+ """Show an information dialog."""
131
+ self.show_message(message, title=title, icon=QMessageBox.Icon.Information)
132
+
133
+ def cleanup(self):
134
+ if self.app:
135
+ self.console.verbose("Cleaning up QTApplication instance.")
136
+ self.app.quit()
137
+ self.app = None
138
+
139
+
140
+ if __name__ == "__main__":
141
+ qt_app = QTApplication()
142
+ qt_app.show_info("This is an info message.")
143
+ qt_app.show_warning("This is a warning message.")
144
+ qt_app.show_error("This is an error message.")
145
+ qt_app.cleanup()