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.
Files changed (107) hide show
  1. bear_utils/__init__.py +51 -0
  2. bear_utils/__main__.py +14 -0
  3. bear_utils/_internal/__init__.py +0 -0
  4. bear_utils/_internal/_version.py +1 -0
  5. bear_utils/_internal/cli.py +119 -0
  6. bear_utils/_internal/debug.py +174 -0
  7. bear_utils/ai/__init__.py +30 -0
  8. bear_utils/ai/ai_helpers/__init__.py +136 -0
  9. bear_utils/ai/ai_helpers/_common.py +19 -0
  10. bear_utils/ai/ai_helpers/_config.py +24 -0
  11. bear_utils/ai/ai_helpers/_parsers.py +194 -0
  12. bear_utils/ai/ai_helpers/_types.py +15 -0
  13. bear_utils/cache/__init__.py +131 -0
  14. bear_utils/cli/__init__.py +22 -0
  15. bear_utils/cli/_args.py +12 -0
  16. bear_utils/cli/_get_version.py +207 -0
  17. bear_utils/cli/commands.py +105 -0
  18. bear_utils/cli/prompt_helpers.py +186 -0
  19. bear_utils/cli/shell/__init__.py +1 -0
  20. bear_utils/cli/shell/_base_command.py +81 -0
  21. bear_utils/cli/shell/_base_shell.py +430 -0
  22. bear_utils/cli/shell/_common.py +19 -0
  23. bear_utils/cli/typer_bridge.py +90 -0
  24. bear_utils/config/__init__.py +13 -0
  25. bear_utils/config/config_manager.py +229 -0
  26. bear_utils/config/dir_manager.py +69 -0
  27. bear_utils/config/settings_manager.py +179 -0
  28. bear_utils/constants/__init__.py +90 -0
  29. bear_utils/constants/_exceptions.py +8 -0
  30. bear_utils/constants/_exit_code.py +60 -0
  31. bear_utils/constants/_http_status_code.py +37 -0
  32. bear_utils/constants/_lazy_typing.py +15 -0
  33. bear_utils/constants/_meta.py +196 -0
  34. bear_utils/constants/date_related.py +25 -0
  35. bear_utils/constants/time_related.py +24 -0
  36. bear_utils/database/__init__.py +8 -0
  37. bear_utils/database/_db_manager.py +98 -0
  38. bear_utils/events/__init__.py +18 -0
  39. bear_utils/events/events_class.py +52 -0
  40. bear_utils/events/events_module.py +74 -0
  41. bear_utils/extras/__init__.py +28 -0
  42. bear_utils/extras/_async_helpers.py +67 -0
  43. bear_utils/extras/_tools.py +185 -0
  44. bear_utils/extras/_zapper.py +399 -0
  45. bear_utils/extras/platform_utils.py +57 -0
  46. bear_utils/extras/responses/__init__.py +5 -0
  47. bear_utils/extras/responses/function_response.py +451 -0
  48. bear_utils/extras/wrappers/__init__.py +1 -0
  49. bear_utils/extras/wrappers/add_methods.py +100 -0
  50. bear_utils/extras/wrappers/string_io.py +46 -0
  51. bear_utils/files/__init__.py +6 -0
  52. bear_utils/files/file_handlers/__init__.py +5 -0
  53. bear_utils/files/file_handlers/_base_file_handler.py +107 -0
  54. bear_utils/files/file_handlers/file_handler_factory.py +280 -0
  55. bear_utils/files/file_handlers/json_file_handler.py +71 -0
  56. bear_utils/files/file_handlers/log_file_handler.py +40 -0
  57. bear_utils/files/file_handlers/toml_file_handler.py +76 -0
  58. bear_utils/files/file_handlers/txt_file_handler.py +76 -0
  59. bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
  60. bear_utils/files/ignore_parser.py +293 -0
  61. bear_utils/graphics/__init__.py +6 -0
  62. bear_utils/graphics/bear_gradient.py +145 -0
  63. bear_utils/graphics/font/__init__.py +13 -0
  64. bear_utils/graphics/font/_raw_block_letters.py +463 -0
  65. bear_utils/graphics/font/_theme.py +31 -0
  66. bear_utils/graphics/font/_utils.py +220 -0
  67. bear_utils/graphics/font/block_font.py +192 -0
  68. bear_utils/graphics/font/glitch_font.py +63 -0
  69. bear_utils/graphics/image_helpers.py +45 -0
  70. bear_utils/gui/__init__.py +8 -0
  71. bear_utils/gui/gui_tools/__init__.py +10 -0
  72. bear_utils/gui/gui_tools/_settings.py +36 -0
  73. bear_utils/gui/gui_tools/_types.py +12 -0
  74. bear_utils/gui/gui_tools/qt_app.py +150 -0
  75. bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
  76. bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
  77. bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
  78. bear_utils/logger_manager/__init__.py +109 -0
  79. bear_utils/logger_manager/_common.py +63 -0
  80. bear_utils/logger_manager/_console_junk.py +135 -0
  81. bear_utils/logger_manager/_log_level.py +50 -0
  82. bear_utils/logger_manager/_styles.py +95 -0
  83. bear_utils/logger_manager/logger_protocol.py +42 -0
  84. bear_utils/logger_manager/loggers/__init__.py +1 -0
  85. bear_utils/logger_manager/loggers/_console.py +223 -0
  86. bear_utils/logger_manager/loggers/_level_sin.py +61 -0
  87. bear_utils/logger_manager/loggers/_logger.py +19 -0
  88. bear_utils/logger_manager/loggers/base_logger.py +244 -0
  89. bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
  90. bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
  91. bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
  92. bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
  93. bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
  94. bear_utils/logger_manager/loggers/console_logger.py +278 -0
  95. bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
  96. bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
  97. bear_utils/logger_manager/loggers/file_logger.py +151 -0
  98. bear_utils/logger_manager/loggers/simple_logger.py +98 -0
  99. bear_utils/logger_manager/loggers/sub_logger.py +105 -0
  100. bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
  101. bear_utils/monitoring/__init__.py +13 -0
  102. bear_utils/monitoring/_common.py +28 -0
  103. bear_utils/monitoring/host_monitor.py +346 -0
  104. bear_utils/time/__init__.py +59 -0
  105. bear_utils-0.0.1.dist-info/METADATA +305 -0
  106. bear_utils-0.0.1.dist-info/RECORD +107 -0
  107. 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
+ ]