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.
Files changed (79) hide show
  1. bear_utils/__init__.py +24 -1
  2. bear_utils/ai/__init__.py +5 -5
  3. bear_utils/ai/ai_helpers/__init__.py +24 -18
  4. bear_utils/ai/ai_helpers/_parsers.py +27 -21
  5. bear_utils/ai/ai_helpers/_types.py +2 -7
  6. bear_utils/cache/__init__.py +35 -23
  7. bear_utils/cli/__init__.py +13 -0
  8. bear_utils/cli/commands.py +14 -8
  9. bear_utils/cli/prompt_helpers.py +40 -34
  10. bear_utils/cli/shell/__init__.py +1 -0
  11. bear_utils/cli/shell/_base_command.py +17 -18
  12. bear_utils/cli/shell/_base_shell.py +37 -34
  13. bear_utils/config/__init__.py +4 -2
  14. bear_utils/config/config_manager.py +193 -56
  15. bear_utils/config/dir_manager.py +8 -3
  16. bear_utils/config/settings_manager.py +94 -171
  17. bear_utils/constants/__init__.py +2 -1
  18. bear_utils/constants/_exceptions.py +6 -1
  19. bear_utils/constants/date_related.py +2 -0
  20. bear_utils/constants/logger_protocol.py +28 -0
  21. bear_utils/constants/time_related.py +2 -0
  22. bear_utils/database/__init__.py +2 -0
  23. bear_utils/database/_db_manager.py +10 -11
  24. bear_utils/events/__init__.py +3 -1
  25. bear_utils/events/events_class.py +11 -11
  26. bear_utils/events/events_module.py +17 -8
  27. bear_utils/extras/__init__.py +8 -6
  28. bear_utils/extras/_async_helpers.py +2 -3
  29. bear_utils/extras/_tools.py +54 -52
  30. bear_utils/extras/platform_utils.py +5 -1
  31. bear_utils/extras/responses/__init__.py +1 -0
  32. bear_utils/extras/responses/function_response.py +301 -0
  33. bear_utils/extras/wrappers/__init__.py +1 -0
  34. bear_utils/extras/wrappers/add_methods.py +17 -15
  35. bear_utils/files/__init__.py +3 -1
  36. bear_utils/files/file_handlers/__init__.py +2 -0
  37. bear_utils/files/file_handlers/_base_file_handler.py +23 -3
  38. bear_utils/files/file_handlers/file_handler_factory.py +38 -38
  39. bear_utils/files/file_handlers/json_file_handler.py +49 -22
  40. bear_utils/files/file_handlers/log_file_handler.py +19 -12
  41. bear_utils/files/file_handlers/toml_file_handler.py +13 -5
  42. bear_utils/files/file_handlers/txt_file_handler.py +56 -14
  43. bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
  44. bear_utils/files/ignore_parser.py +52 -57
  45. bear_utils/graphics/__init__.py +3 -1
  46. bear_utils/graphics/bear_gradient.py +17 -12
  47. bear_utils/graphics/image_helpers.py +11 -5
  48. bear_utils/gui/__init__.py +3 -1
  49. bear_utils/gui/gui_tools/__init__.py +3 -1
  50. bear_utils/gui/gui_tools/_settings.py +0 -1
  51. bear_utils/gui/gui_tools/qt_app.py +16 -11
  52. bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
  53. bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
  54. bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
  55. bear_utils/logging/__init__.py +6 -4
  56. bear_utils/logging/logger_manager/__init__.py +1 -0
  57. bear_utils/logging/logger_manager/_common.py +0 -1
  58. bear_utils/logging/logger_manager/_console_junk.py +15 -11
  59. bear_utils/logging/logger_manager/_styles.py +1 -2
  60. bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
  61. bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -33
  62. bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
  63. bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
  64. bear_utils/logging/logger_manager/loggers/_console_logger.py +54 -26
  65. bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
  66. bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
  67. bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
  68. bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
  69. bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
  70. bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
  71. bear_utils/logging/loggers.py +9 -13
  72. bear_utils/monitoring/__init__.py +7 -4
  73. bear_utils/monitoring/_common.py +28 -0
  74. bear_utils/monitoring/host_monitor.py +44 -48
  75. bear_utils/time/__init__.py +13 -6
  76. {bear_utils-0.7.21.dist-info → bear_utils-0.7.22.dist-info}/METADATA +50 -6
  77. bear_utils-0.7.22.dist-info/RECORD +83 -0
  78. bear_utils-0.7.21.dist-info/RECORD +0 -79
  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 ..cli.prompt_helpers import ask_yes_no
7
- from ..logging import ConsoleLogger
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
- l.strip()
65
- for l in lines
66
- if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("!")
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
- Print whether a given path is ignored based on the ignore patterns.
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
- path = path.relative_to(Path.cwd())
217
- logger.print(str(f"[{color}]{path}[/]"))
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
- 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:
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
- 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:
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"
@@ -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__ = ["DefaultColors", "ColorGradient", "encode_image_to_jpeg", "encode_image_to_png"]
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=DefaultColorConfig(), reverse: bool = False) -> None:
67
- self.config: DefaultColorConfig = config
68
- self.colors: DefaultColors = config.colors
69
- self.thresholds: DefaultThresholds = config.thresholds
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")
@@ -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", "select_color", "get_text"]
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", "select_color", "get_text"]
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 ...logging import VERBOSE, ConsoleLogger
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
- Singleton class to manage the QApplication instance.
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(self, initial_color=None, title="Select Color", options=None) -> ColorInfo | None:
23
- """
24
- Shows a color selection dialog and returns the selected color.
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) >= 3:
58
- r, g, b = initial_color[:3]
59
- a = initial_color[3] if len(initial_color) > 3 else 255
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
- else:
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(initial_color=None, title="Select Color", options=None) -> ColorInfo | None:
88
- """
89
- Select a color using the QTColorPicker singleton instance.
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: