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.
- bear_utils/__init__.py +13 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +130 -0
- bear_utils/ai/ai_helpers/_common.py +19 -0
- bear_utils/ai/ai_helpers/_config.py +24 -0
- bear_utils/ai/ai_helpers/_parsers.py +188 -0
- bear_utils/ai/ai_helpers/_types.py +20 -0
- bear_utils/cache/__init__.py +119 -0
- bear_utils/cli/__init__.py +4 -0
- bear_utils/cli/commands.py +59 -0
- bear_utils/cli/prompt_helpers.py +166 -0
- bear_utils/cli/shell/__init__.py +0 -0
- bear_utils/cli/shell/_base_command.py +74 -0
- bear_utils/cli/shell/_base_shell.py +390 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/config/__init__.py +11 -0
- bear_utils/config/config_manager.py +92 -0
- bear_utils/config/dir_manager.py +64 -0
- bear_utils/config/settings_manager.py +232 -0
- bear_utils/constants/__init__.py +16 -0
- bear_utils/constants/_exceptions.py +3 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/date_related.py +36 -0
- bear_utils/constants/time_related.py +22 -0
- bear_utils/database/__init__.py +6 -0
- bear_utils/database/_db_manager.py +104 -0
- bear_utils/events/__init__.py +16 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +65 -0
- bear_utils/extras/__init__.py +17 -0
- bear_utils/extras/_async_helpers.py +15 -0
- bear_utils/extras/_tools.py +178 -0
- bear_utils/extras/platform_utils.py +53 -0
- bear_utils/extras/wrappers/__init__.py +0 -0
- bear_utils/extras/wrappers/add_methods.py +98 -0
- bear_utils/files/__init__.py +4 -0
- bear_utils/files/file_handlers/__init__.py +3 -0
- bear_utils/files/file_handlers/_base_file_handler.py +93 -0
- bear_utils/files/file_handlers/file_handler_factory.py +278 -0
- bear_utils/files/file_handlers/json_file_handler.py +44 -0
- bear_utils/files/file_handlers/log_file_handler.py +33 -0
- bear_utils/files/file_handlers/txt_file_handler.py +34 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +57 -0
- bear_utils/files/ignore_parser.py +298 -0
- bear_utils/graphics/__init__.py +4 -0
- bear_utils/graphics/bear_gradient.py +140 -0
- bear_utils/graphics/image_helpers.py +39 -0
- bear_utils/gui/__init__.py +3 -0
- bear_utils/gui/gui_tools/__init__.py +5 -0
- bear_utils/gui/gui_tools/_settings.py +37 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +145 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +119 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +138 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +306 -0
- bear_utils/logging/__init__.py +25 -0
- bear_utils/logging/logger_manager/__init__.py +0 -0
- bear_utils/logging/logger_manager/_common.py +47 -0
- bear_utils/logging/logger_manager/_console_junk.py +131 -0
- bear_utils/logging/logger_manager/_styles.py +91 -0
- bear_utils/logging/logger_manager/loggers/__init__.py +0 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +238 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +50 -0
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +55 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.py +249 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +64 -0
- bear_utils/logging/logger_manager/loggers/_file_logger.py +141 -0
- bear_utils/logging/logger_manager/loggers/_level_sin.py +58 -0
- bear_utils/logging/logger_manager/loggers/_logger.py +18 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +110 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +38 -0
- bear_utils/logging/loggers.py +76 -0
- bear_utils/monitoring/__init__.py +10 -0
- bear_utils/monitoring/host_monitor.py +350 -0
- bear_utils/time/__init__.py +16 -0
- bear_utils/time/_helpers.py +91 -0
- bear_utils/time/_time_class.py +316 -0
- bear_utils/time/_timer.py +80 -0
- bear_utils/time/_tools.py +17 -0
- bear_utils/time/time_manager.py +218 -0
- bear_utils-0.7.11.dist-info/METADATA +260 -0
- bear_utils-0.7.11.dist-info/RECORD +83 -0
- 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,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,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,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()
|