bear-utils 0.0.1__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 +51 -0
- bear_utils/__main__.py +14 -0
- bear_utils/_internal/__init__.py +0 -0
- bear_utils/_internal/_version.py +1 -0
- bear_utils/_internal/cli.py +119 -0
- bear_utils/_internal/debug.py +174 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +136 -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 +194 -0
- bear_utils/ai/ai_helpers/_types.py +15 -0
- bear_utils/cache/__init__.py +131 -0
- bear_utils/cli/__init__.py +22 -0
- bear_utils/cli/_args.py +12 -0
- bear_utils/cli/_get_version.py +207 -0
- bear_utils/cli/commands.py +105 -0
- bear_utils/cli/prompt_helpers.py +186 -0
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +81 -0
- bear_utils/cli/shell/_base_shell.py +430 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/cli/typer_bridge.py +90 -0
- bear_utils/config/__init__.py +13 -0
- bear_utils/config/config_manager.py +229 -0
- bear_utils/config/dir_manager.py +69 -0
- bear_utils/config/settings_manager.py +179 -0
- bear_utils/constants/__init__.py +90 -0
- bear_utils/constants/_exceptions.py +8 -0
- bear_utils/constants/_exit_code.py +60 -0
- bear_utils/constants/_http_status_code.py +37 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/_meta.py +196 -0
- bear_utils/constants/date_related.py +25 -0
- bear_utils/constants/time_related.py +24 -0
- bear_utils/database/__init__.py +8 -0
- bear_utils/database/_db_manager.py +98 -0
- bear_utils/events/__init__.py +18 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +74 -0
- bear_utils/extras/__init__.py +28 -0
- bear_utils/extras/_async_helpers.py +67 -0
- bear_utils/extras/_tools.py +185 -0
- bear_utils/extras/_zapper.py +399 -0
- bear_utils/extras/platform_utils.py +57 -0
- bear_utils/extras/responses/__init__.py +5 -0
- bear_utils/extras/responses/function_response.py +451 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +100 -0
- bear_utils/extras/wrappers/string_io.py +46 -0
- bear_utils/files/__init__.py +6 -0
- bear_utils/files/file_handlers/__init__.py +5 -0
- bear_utils/files/file_handlers/_base_file_handler.py +107 -0
- bear_utils/files/file_handlers/file_handler_factory.py +280 -0
- bear_utils/files/file_handlers/json_file_handler.py +71 -0
- bear_utils/files/file_handlers/log_file_handler.py +40 -0
- bear_utils/files/file_handlers/toml_file_handler.py +76 -0
- bear_utils/files/file_handlers/txt_file_handler.py +76 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
- bear_utils/files/ignore_parser.py +293 -0
- bear_utils/graphics/__init__.py +6 -0
- bear_utils/graphics/bear_gradient.py +145 -0
- bear_utils/graphics/font/__init__.py +13 -0
- bear_utils/graphics/font/_raw_block_letters.py +463 -0
- bear_utils/graphics/font/_theme.py +31 -0
- bear_utils/graphics/font/_utils.py +220 -0
- bear_utils/graphics/font/block_font.py +192 -0
- bear_utils/graphics/font/glitch_font.py +63 -0
- bear_utils/graphics/image_helpers.py +45 -0
- bear_utils/gui/__init__.py +8 -0
- bear_utils/gui/gui_tools/__init__.py +10 -0
- bear_utils/gui/gui_tools/_settings.py +36 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +150 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
- bear_utils/logger_manager/__init__.py +109 -0
- bear_utils/logger_manager/_common.py +63 -0
- bear_utils/logger_manager/_console_junk.py +135 -0
- bear_utils/logger_manager/_log_level.py +50 -0
- bear_utils/logger_manager/_styles.py +95 -0
- bear_utils/logger_manager/logger_protocol.py +42 -0
- bear_utils/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logger_manager/loggers/_console.py +223 -0
- bear_utils/logger_manager/loggers/_level_sin.py +61 -0
- bear_utils/logger_manager/loggers/_logger.py +19 -0
- bear_utils/logger_manager/loggers/base_logger.py +244 -0
- bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
- bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
- bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
- bear_utils/logger_manager/loggers/console_logger.py +278 -0
- bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
- bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
- bear_utils/logger_manager/loggers/file_logger.py +151 -0
- bear_utils/logger_manager/loggers/simple_logger.py +98 -0
- bear_utils/logger_manager/loggers/sub_logger.py +105 -0
- bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
- bear_utils/monitoring/__init__.py +13 -0
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +346 -0
- bear_utils/time/__init__.py +59 -0
- bear_utils-0.0.1.dist-info/METADATA +305 -0
- bear_utils-0.0.1.dist-info/RECORD +107 -0
- bear_utils-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
"""YamlFileHandler class for handling YAML files."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, ClassVar
|
5
|
+
|
6
|
+
import yaml
|
7
|
+
|
8
|
+
from ._base_file_handler import FileHandler
|
9
|
+
|
10
|
+
|
11
|
+
class YamlFileHandler(FileHandler):
|
12
|
+
"""Class for handling .yaml/.yml files with read, write, and present methods"""
|
13
|
+
|
14
|
+
valid_extensions: ClassVar[list[str]] = ["yaml", "yml"]
|
15
|
+
valid_types: ClassVar[tuple[type, ...]] = (dict, str)
|
16
|
+
|
17
|
+
@FileHandler.ValidateFileType
|
18
|
+
def unsafe_read_file(self, file_path: Path, **kwargs) -> dict[str, Any]:
|
19
|
+
"""Read YAML file with potentially unsafe loader.
|
20
|
+
|
21
|
+
WARNING: This method can execute arbitrary code and should only be used
|
22
|
+
with trusted files.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
file_path: Path to the YAML file
|
26
|
+
**kwargs: Additional arguments passed to yaml.load
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Dictionary containing the parsed YAML data
|
30
|
+
"""
|
31
|
+
try:
|
32
|
+
super().read_file(file_path=file_path)
|
33
|
+
with open(file=file_path, encoding="utf-8") as file:
|
34
|
+
return yaml.load(stream=file, **kwargs) # noqa: S506 # Using unsafe loader intentionally
|
35
|
+
except Exception as e:
|
36
|
+
raise ValueError(f"Error reading file: {e}") from e
|
37
|
+
|
38
|
+
@FileHandler.ValidateFileType
|
39
|
+
def read_file(self, file_path: Path) -> dict[str, Any]:
|
40
|
+
"""Read YAML file safely."""
|
41
|
+
try:
|
42
|
+
super().read_file(file_path=file_path)
|
43
|
+
with open(file=file_path, encoding="utf-8") as file:
|
44
|
+
return yaml.safe_load(stream=file)
|
45
|
+
except Exception as e:
|
46
|
+
raise ValueError(f"Error reading file: {e}") from e
|
47
|
+
|
48
|
+
@FileHandler.ValidateFileType
|
49
|
+
def write_file(self, file_path: Path, data: dict[str, Any] | str, **kwargs) -> None:
|
50
|
+
"""Write data to a YAML file."""
|
51
|
+
try:
|
52
|
+
super().write_file(file_path=file_path, data=data)
|
53
|
+
self.check_data_type(data=data, valid_types=self.valid_types)
|
54
|
+
with open(file=file_path, mode="w", encoding="utf-8") as file:
|
55
|
+
yaml.dump(data, stream=file, default_flow_style=False, sort_keys=False, **kwargs)
|
56
|
+
except Exception as e:
|
57
|
+
raise ValueError(f"Error writing file: {e}") from e
|
58
|
+
|
59
|
+
def present_file(self, data: dict[str, Any] | str, **kwargs) -> str:
|
60
|
+
"""Present data as a YAML string."""
|
61
|
+
try:
|
62
|
+
return yaml.dump(data, default_flow_style=False, sort_keys=False, **kwargs)
|
63
|
+
except Exception as e:
|
64
|
+
raise ValueError(f"Error presenting file: {e}") from e
|
@@ -0,0 +1,293 @@
|
|
1
|
+
"""A module for handling file ignore patterns and directories in Bear Utils."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from pathspec import PathSpec
|
7
|
+
|
8
|
+
from bear_utils.cli.prompt_helpers import ask_yes_no
|
9
|
+
from bear_utils.logger_manager import ConsoleLogger
|
10
|
+
|
11
|
+
logger: ConsoleLogger = ConsoleLogger.get_instance(init=True)
|
12
|
+
|
13
|
+
IGNORE_PATTERNS: list[str] = [
|
14
|
+
"__pycache__",
|
15
|
+
".git",
|
16
|
+
".venv",
|
17
|
+
".env",
|
18
|
+
".vscode",
|
19
|
+
".idea",
|
20
|
+
"*.DS_Store*",
|
21
|
+
"__pypackages__",
|
22
|
+
".pytest_cache",
|
23
|
+
".coverage",
|
24
|
+
".*.swp",
|
25
|
+
".*.swo",
|
26
|
+
"*.lock",
|
27
|
+
"dist/",
|
28
|
+
]
|
29
|
+
|
30
|
+
IGNORE_COUNT = 100
|
31
|
+
|
32
|
+
|
33
|
+
class IgnoreHandler:
|
34
|
+
"""Basic ignore handler for manually checking if a file should be ignored based on set patterns."""
|
35
|
+
|
36
|
+
def __init__(self, ignore_file: Path | None = None, combine: bool = False) -> None:
|
37
|
+
"""Initialize the IgnoreHandler with an optional ignore file."""
|
38
|
+
self.ignore_file = ignore_file
|
39
|
+
self.patterns: list[str] = self.load_patterns(ignore_file, combine) if ignore_file else IGNORE_PATTERNS
|
40
|
+
self.spec: PathSpec = self._create_spec(self.patterns)
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
def _create_spec(patterns: list[str]) -> PathSpec:
|
44
|
+
"""Create a pathspec from the given patterns.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
patterns: List of ignore patterns
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
A pathspec object
|
51
|
+
"""
|
52
|
+
return PathSpec.from_lines("gitwildmatch", patterns)
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def load_patterns(ignore_file: Path, combine: bool) -> list[str]:
|
56
|
+
"""Load patterns from a specific ignore file.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
ignore_file: Path to the ignore file
|
60
|
+
"""
|
61
|
+
if not ignore_file.exists():
|
62
|
+
logger.warning(f"Ignore file {ignore_file} does not exist")
|
63
|
+
return []
|
64
|
+
try:
|
65
|
+
lines = ignore_file.read_text().splitlines()
|
66
|
+
patterns: list[str] = [
|
67
|
+
line.strip()
|
68
|
+
for line in lines
|
69
|
+
if line.strip() and not line.strip().startswith("#") and not line.strip().startswith("!")
|
70
|
+
]
|
71
|
+
if patterns:
|
72
|
+
logger.verbose(f"Loaded {len(patterns)} patterns from {ignore_file}")
|
73
|
+
if combine:
|
74
|
+
patterns.extend(IGNORE_PATTERNS) # Combine with default patterns
|
75
|
+
return patterns
|
76
|
+
except Exception as e:
|
77
|
+
logger.error(f"Error loading ignore file {ignore_file}: {e}")
|
78
|
+
return []
|
79
|
+
|
80
|
+
def should_ignore(self, path: Path | str) -> bool:
|
81
|
+
"""Check if a given path should be ignored based on the ignore patterns.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
path (Path): The path to check
|
85
|
+
Returns:
|
86
|
+
bool: True if the path should be ignored, False otherwise
|
87
|
+
"""
|
88
|
+
path = Path(path).expanduser()
|
89
|
+
if path.is_dir() and not str(path).endswith("/"):
|
90
|
+
return self.spec.match_file(str(path) + "/")
|
91
|
+
return self.spec.match_file(str(path))
|
92
|
+
|
93
|
+
def ignore_print(self, path: Path | str, rel: bool = True) -> bool:
|
94
|
+
"""Print whether a given path is ignored based on the ignore patterns.
|
95
|
+
|
96
|
+
Will print the path as relative to the current working directory if rel is True,
|
97
|
+
which it is by default.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
path (Path): The path to check
|
101
|
+
rel (bool): Whether to print the path as relative to the current working directory
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
bool: True if the path is ignored, False otherwise
|
105
|
+
"""
|
106
|
+
path = Path(path).expanduser()
|
107
|
+
if rel:
|
108
|
+
path = path.relative_to(Path.cwd())
|
109
|
+
|
110
|
+
should_ignore = self.should_ignore(path)
|
111
|
+
msg_ignore = f"[red]{path}[/] is ignored"
|
112
|
+
msg_not_ignore = f"[green]{path}[/] is not ignored"
|
113
|
+
(logger.print(msg_ignore) if should_ignore else logger.print(msg_not_ignore))
|
114
|
+
return should_ignore
|
115
|
+
|
116
|
+
def add_patterns(self, patterns: list[str]) -> None:
|
117
|
+
"""Add additional ignore patterns to the existing spec.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
patterns: List of additional patterns to add
|
121
|
+
"""
|
122
|
+
new_patterns = []
|
123
|
+
default_patterns = IGNORE_PATTERNS.copy()
|
124
|
+
if self.spec:
|
125
|
+
for pattern in patterns:
|
126
|
+
if pattern not in default_patterns:
|
127
|
+
new_patterns.append(pattern)
|
128
|
+
self.spec = PathSpec(new_patterns + default_patterns)
|
129
|
+
logger.verbose(f"Added {len(patterns)} ignore patterns")
|
130
|
+
|
131
|
+
|
132
|
+
class IgnoreDirectoryHandler(IgnoreHandler):
|
133
|
+
"""Handles the logic for ignoring files and directories based on .gitignore-style rules."""
|
134
|
+
|
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."""
|
137
|
+
super().__init__(ignore_file)
|
138
|
+
self.directory_to_search: Path = Path(directory_to_search).resolve()
|
139
|
+
self.ignored_files: list[Path] = []
|
140
|
+
self.non_ignored_files: list[Path] = []
|
141
|
+
self.rel: bool = rel
|
142
|
+
|
143
|
+
def _scan_directory(self, directory: Path | str) -> tuple[list[str], list[str]]:
|
144
|
+
"""Scan a directory and separate files and directories into ignored and non-ignored lists.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
directory: The directory to scan
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Tuple of (non_ignored_files, ignored_files) as relative path strings
|
151
|
+
"""
|
152
|
+
directory = Path(directory)
|
153
|
+
all_paths = []
|
154
|
+
|
155
|
+
for root, _, files in os.walk(directory):
|
156
|
+
root_path = Path(root)
|
157
|
+
rel_root: Path = root_path.relative_to(directory)
|
158
|
+
|
159
|
+
if str(rel_root) == ".":
|
160
|
+
rel_root = Path("")
|
161
|
+
else:
|
162
|
+
dir_path = str(rel_root) + "/"
|
163
|
+
all_paths.append(dir_path)
|
164
|
+
|
165
|
+
for file in files:
|
166
|
+
rel_path: Path = rel_root / file
|
167
|
+
all_paths.append(str(rel_path))
|
168
|
+
|
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]
|
172
|
+
return non_ignored_paths, ignored_paths
|
173
|
+
|
174
|
+
@property
|
175
|
+
def ignored_files_count(self) -> int:
|
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
|
+
"""Get the count of non-ignored files.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
int: The number of non-ignored files
|
189
|
+
"""
|
190
|
+
return len(self.non_ignored_files)
|
191
|
+
|
192
|
+
def ignore_report_full_codebase(self):
|
193
|
+
"""Generate a report of ignored and non-ignored files in the directory.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
Tuple of (non_ignored_files, ignored_files) as Path objects
|
197
|
+
"""
|
198
|
+
non_ignored_paths, ignored_paths = self._scan_directory(self.directory_to_search)
|
199
|
+
self.non_ignored_files = [self.directory_to_search / p for p in non_ignored_paths]
|
200
|
+
self.ignored_files = [self.directory_to_search / p for p in ignored_paths]
|
201
|
+
|
202
|
+
@staticmethod
|
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.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
data_structure_to_print: The data structure to print
|
208
|
+
rel: Whether to print the paths as relative to the current working directory
|
209
|
+
"""
|
210
|
+
try:
|
211
|
+
for path in data_structure_to_print:
|
212
|
+
p = Path(path)
|
213
|
+
if rel:
|
214
|
+
p = p.relative_to(Path.cwd())
|
215
|
+
logger.print(str(f"[{color}]{p}[/]"))
|
216
|
+
except KeyboardInterrupt:
|
217
|
+
logger.warning("Printing interrupted by user.")
|
218
|
+
|
219
|
+
def _print_ignored_files(self) -> None:
|
220
|
+
"""Print the ignored files in the directory."""
|
221
|
+
if self.ignored_files_count == 0:
|
222
|
+
logger.print("No ignored files found.")
|
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
|
+
):
|
228
|
+
self._print(self.ignored_files, self.rel, "red")
|
229
|
+
else:
|
230
|
+
self._print(self.ignored_files, self.rel, "red")
|
231
|
+
|
232
|
+
def _print_non_ignored_files(self) -> None:
|
233
|
+
"""Print the non-ignored files in the directory."""
|
234
|
+
if self.non_ignored_files_count == 0:
|
235
|
+
logger.print("No non-ignored files found.")
|
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
|
+
):
|
241
|
+
self._print(self.non_ignored_files, self.rel)
|
242
|
+
else:
|
243
|
+
self._print(self.non_ignored_files, self.rel)
|
244
|
+
|
245
|
+
def print_report(self, what_to_print: str):
|
246
|
+
"""Print the report of ignored or non-ignored files or both
|
247
|
+
|
248
|
+
Args:
|
249
|
+
what_to_print: "ignored", "non_ignored", or "both"
|
250
|
+
"""
|
251
|
+
match what_to_print:
|
252
|
+
case "ignored":
|
253
|
+
self._print_ignored_files()
|
254
|
+
case "non_ignored":
|
255
|
+
self._print_non_ignored_files()
|
256
|
+
case "both":
|
257
|
+
self._print_ignored_files()
|
258
|
+
self._print_non_ignored_files()
|
259
|
+
case _:
|
260
|
+
logger.error("Invalid option. Use 'ignored', 'non_ignored', or 'both'.")
|
261
|
+
|
262
|
+
|
263
|
+
# if __name__ == "__main__":
|
264
|
+
# # Example usage
|
265
|
+
# ignore_handler = IgnoreHandler()
|
266
|
+
# files_to_check = [
|
267
|
+
# Path.cwd() / "src/bear_utils/__pycache__",
|
268
|
+
# Path.cwd() / "src/bear_utils/.git",
|
269
|
+
# Path.cwd() / "src/bear_utils/.venv",
|
270
|
+
# Path.cwd() / "src/bear_utils/uv.lock",
|
271
|
+
# Path.cwd() / "src/bear_utils/ignore_parser.py",
|
272
|
+
# Path.cwd() / "src/bear_utils/extras/wrappers/add_methods.py",
|
273
|
+
# Path.cwd() / "src/bear_utils/chronobear/_time_class.py",
|
274
|
+
# ]
|
275
|
+
# for file in files_to_check:
|
276
|
+
# ignore_handler.ignore_print(file)
|
277
|
+
# logger.set_verbose(True)
|
278
|
+
# directory_handler = IgnoreDirectoryHandler(".")
|
279
|
+
# directory_handler.ignore_print(Path.cwd() / "src/bear_utils/__pycache__")
|
280
|
+
# directory_handler.ignore_report_full_codebase()
|
281
|
+
# logger.print(f"Ignored files: {directory_handler.ignored_files_count}")
|
282
|
+
# logger.print(f"Non-ignored files: {directory_handler.non_ignored_files_count}")
|
283
|
+
# answer = ask_question(
|
284
|
+
# "Do you want to print the ignored or non-ignored files? (ignored/non_ignored/both)",
|
285
|
+
# expected_type="str",
|
286
|
+
# default="both",
|
287
|
+
# )
|
288
|
+
# if answer == "both":
|
289
|
+
# directory_handler.print_report("both")
|
290
|
+
# elif answer == "non_ignored":
|
291
|
+
# directory_handler.print_report("non_ignored")
|
292
|
+
# else:
|
293
|
+
# directory_handler.print_report("ignored")
|
@@ -0,0 +1,6 @@
|
|
1
|
+
"""Graphics related utilities for Bear Utils."""
|
2
|
+
|
3
|
+
from .bear_gradient import ColorGradient, DefaultColors
|
4
|
+
from .image_helpers import encode_image_to_jpeg, encode_image_to_png
|
5
|
+
|
6
|
+
__all__ = ["ColorGradient", "DefaultColors", "encode_image_to_jpeg", "encode_image_to_png"]
|
@@ -0,0 +1,145 @@
|
|
1
|
+
"""A color gradient utility for generating RGB colors based on thresholds."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
from pyglm.glm import clamp, lerp, quat
|
6
|
+
from rich.color import Color as RichColor
|
7
|
+
from rich.color_triplet import ColorTriplet
|
8
|
+
|
9
|
+
RED: RichColor = RichColor.from_rgb(255, 0, 0)
|
10
|
+
YELLOW: RichColor = RichColor.from_rgb(255, 255, 0)
|
11
|
+
GREEN: RichColor = RichColor.from_rgb(0, 255, 0)
|
12
|
+
|
13
|
+
|
14
|
+
def inv_lerp(a: float, b: float, t: float) -> float:
|
15
|
+
"""Inverse linear interpolation."""
|
16
|
+
return clamp((t - a) / (b - a), 0.0, 1.0)
|
17
|
+
|
18
|
+
|
19
|
+
def rgb_int(v: float | quat) -> int:
|
20
|
+
"""Convert a float to an int, clamping to 0-255."""
|
21
|
+
return int(clamp(v, 0.0, 255.0))
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class DefaultColors:
|
26
|
+
"""The default colors for the gradient."""
|
27
|
+
|
28
|
+
start: RichColor = RED # Default Threshold: 0.0
|
29
|
+
mid: RichColor = YELLOW # Default Threshold: 0.7
|
30
|
+
end: RichColor = GREEN # Default Threshold: 1.0
|
31
|
+
|
32
|
+
def output_rgb(self) -> tuple[ColorTriplet, ColorTriplet, ColorTriplet]:
|
33
|
+
"""Get the RGB values of the default colors."""
|
34
|
+
return self.start.get_truecolor(), self.mid.get_truecolor(), self.end.get_truecolor()
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class DefaultThresholds:
|
39
|
+
"""The default thresholds for the gradient."""
|
40
|
+
|
41
|
+
start: float = 0.0 # Default Color: RED
|
42
|
+
mid: float = 0.7 # Default Color: YELLOW
|
43
|
+
end: float = 1.0 # Default Color: GREEN
|
44
|
+
|
45
|
+
def __post_init__(self) -> None:
|
46
|
+
if not (0.0 <= self.start < self.mid < self.end <= 1.0):
|
47
|
+
raise ValueError("thresholds must be strictly increasing and between 0 and 1.")
|
48
|
+
|
49
|
+
def unpack(self) -> tuple[float, float, float]:
|
50
|
+
"""Unpack the thresholds into a tuple."""
|
51
|
+
return self.start, self.mid, self.end
|
52
|
+
|
53
|
+
|
54
|
+
@dataclass
|
55
|
+
class DefaultColorConfig:
|
56
|
+
"""Configuration for the default color gradient."""
|
57
|
+
|
58
|
+
colors: DefaultColors = field(default_factory=DefaultColors)
|
59
|
+
thresholds: DefaultThresholds = field(default_factory=DefaultThresholds)
|
60
|
+
|
61
|
+
|
62
|
+
class ColorGradient:
|
63
|
+
"""Simple 3-color gradient interpolator.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
colors (DefaultColors): Default colors for the gradient.
|
67
|
+
thresholds (Thresholds): Thresholds for the gradient.
|
68
|
+
reverse (bool): If True, reverses the gradient direction.
|
69
|
+
"""
|
70
|
+
|
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
|
76
|
+
self.reverse: bool = reverse
|
77
|
+
self.c0, self.c1, self.c2 = self.colors.output_rgb()
|
78
|
+
self.p0, self.p1, self.p2 = self.thresholds.unpack()
|
79
|
+
|
80
|
+
if not (0.0 <= self.p0 < self.p1 < self.p2 <= 1.0):
|
81
|
+
raise ValueError("thresholds must be strictly increasing and between 0 and 1.")
|
82
|
+
|
83
|
+
def flip(self) -> None:
|
84
|
+
"""Toggle the reverse flag."""
|
85
|
+
self.reverse = not self.reverse
|
86
|
+
|
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.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
_min (float): Minimum of input range.
|
92
|
+
_max (float): Maximum of input range.
|
93
|
+
v (float): Value to map.
|
94
|
+
reverse (bool | None): If True, reverses the gradient direction.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
str: RGB color string.
|
98
|
+
"""
|
99
|
+
return self.map_to_color(_min, _max, v, reverse).rgb
|
100
|
+
|
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.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
_min (float): Minimum of input range.
|
106
|
+
_max (float): Maximum of input range.
|
107
|
+
v (float): Value to map.
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
ColorTriplet: RGB color triplet.
|
111
|
+
"""
|
112
|
+
reverse = reverse if reverse is not None else self.reverse
|
113
|
+
|
114
|
+
t: float = inv_lerp(_min, _max, v) if not reverse else 1.0 - inv_lerp(_min, _max, v)
|
115
|
+
src, dst = (self.c0, self.c1) if t <= self.p1 else (self.c1, self.c2)
|
116
|
+
seg: float = inv_lerp(self.p0, self.p1, t) if t <= self.p1 else inv_lerp(self.p1, self.p2, t)
|
117
|
+
|
118
|
+
r = rgb_int(lerp(src.red, dst.red, seg))
|
119
|
+
g = rgb_int(lerp(src.green, dst.green, seg))
|
120
|
+
b = rgb_int(lerp(src.blue, dst.blue, seg))
|
121
|
+
|
122
|
+
return ColorTriplet(red=r, green=g, blue=b)
|
123
|
+
|
124
|
+
|
125
|
+
# if __name__ == "__main__":
|
126
|
+
# # example usage
|
127
|
+
# from rich.console import Console
|
128
|
+
|
129
|
+
# console = Console()
|
130
|
+
# gradient = ColorGradient()
|
131
|
+
|
132
|
+
# console.print(f"RED: {RED.get_truecolor().rgb}")
|
133
|
+
# console.print(f"YELLOW: {YELLOW.get_truecolor().rgb}")
|
134
|
+
# console.print(f"GREEN: {GREEN.get_truecolor().rgb}")
|
135
|
+
|
136
|
+
# # RED -> YELLOW -> GREEN
|
137
|
+
# for i in range(0, 101, 10):
|
138
|
+
# color: ColorTriplet = gradient.map_to_color(0, 100, i)
|
139
|
+
# console.print(f"Value: {i} => Color: {color.rgb}", style=color.rgb)
|
140
|
+
|
141
|
+
# # GREEN -> YELLOW -> RED
|
142
|
+
# gradient.reverse = True
|
143
|
+
# for i in range(0, 101, 10):
|
144
|
+
# color: ColorTriplet = gradient.map_to_color(0, 100, i)
|
145
|
+
# console.print(f"Value: {i} => Color: {color.rgb}", style=color.rgb)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""A set of command-line interface (CLI) utilities for creating font outputs."""
|
2
|
+
|
3
|
+
from ._theme import CyberTheme, FontStyle
|
4
|
+
from .block_font import BLOCK_LETTERS, char_to_block, print_block_font, word_to_block
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"BLOCK_LETTERS",
|
8
|
+
"CyberTheme",
|
9
|
+
"FontStyle",
|
10
|
+
"char_to_block",
|
11
|
+
"print_block_font",
|
12
|
+
"word_to_block",
|
13
|
+
]
|