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,346 @@
1
+ """Host Monitor Module."""
2
+
3
+ import asyncio
4
+ from asyncio import Task
5
+ from collections import deque
6
+ from collections.abc import Awaitable, Callable
7
+ from contextlib import suppress
8
+ from dataclasses import dataclass
9
+ import subprocess
10
+ from typing import TYPE_CHECKING, Literal, Self, TypedDict, cast, overload
11
+
12
+ from bear_utils.logger_manager import get_console
13
+ from bear_utils.monitoring._common import CPU, CPU_MEM, DISK, GPU, MEM, TaskChoice
14
+
15
+ ROLLING_AVERAGE_TIME = 300
16
+
17
+ if TYPE_CHECKING:
18
+ from subprocess import CompletedProcess
19
+
20
+ _base_logger, console = get_console("HostMonitor")
21
+
22
+
23
+ def has_nvidia_gpu() -> bool:
24
+ """Check if the system has an NVIDIA GPU."""
25
+ try:
26
+ result = subprocess.run(
27
+ check=False,
28
+ args=["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
29
+ capture_output=True,
30
+ text=True,
31
+ )
32
+ return result.returncode == 0
33
+ except Exception as e:
34
+ console.error(f"Error checking for NVIDIA GPU: {e}", exc_info=True)
35
+ return False
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class GPUSamples:
40
+ """GPU samples for monitoring."""
41
+
42
+ gpu_usage: float = 0.0
43
+ gpu_mem_usage: float = 0.0
44
+ gpu_mem_total: float = 0.0
45
+ gpu_mem_free: float = 0.0
46
+
47
+ def __post_init__(self) -> None:
48
+ if self.gpu_mem_total == 0:
49
+ self.gpu_mem_total = self.gpu_mem_usage + self.gpu_mem_free
50
+
51
+ @classmethod
52
+ async def get_gpu_samples(cls) -> Self:
53
+ """Get GPU samples using nvidia-smi."""
54
+ try:
55
+ result: CompletedProcess[str] = subprocess.run(
56
+ check=False,
57
+ args=[
58
+ "nvidia-smi",
59
+ "--query-gpu=utilization.gpu,memory.used,memory.total,memory.free",
60
+ "--format=csv,noheader",
61
+ ],
62
+ capture_output=True,
63
+ text=True,
64
+ )
65
+ if result.returncode != 0:
66
+ console.error(f"nvidia-smi command failed with return code {result.returncode}")
67
+ return cls()
68
+ gpu_usage, gpu_mem_used, gpu_mem_total, gpu_mem_free = map(float, result.stdout.split(","))
69
+ return cls(
70
+ gpu_usage=gpu_usage,
71
+ gpu_mem_usage=gpu_mem_used,
72
+ gpu_mem_total=gpu_mem_total,
73
+ gpu_mem_free=gpu_mem_free,
74
+ )
75
+ except Exception as e:
76
+ console.error(f"Error getting GPU samples: {e}", exc_info=True)
77
+ return cls()
78
+
79
+
80
+ @dataclass(slots=True)
81
+ class HostMonitorResult:
82
+ """Result class for host monitoring."""
83
+
84
+ # CPU
85
+ cpu_usage_avg: float = 0.0
86
+ cpu_max: float = 0.0
87
+ cpu_min: float = 0.0
88
+
89
+ mem_usage_avg: float = 0.0
90
+ mem_max: float = 0.0
91
+ mem_min: float = 0.0
92
+
93
+ disk_usage_avg: float = 0.0
94
+
95
+ gpu_usage_avg: float = 0.0
96
+ gpu_usage_max: float = 0.0
97
+ gpu_usage_min: float = 0.0
98
+
99
+ gpu_mem_usage_avg: float = 0.0
100
+ gpu_mem_usage_max: float = 0.0
101
+ gpu_mem_usage_min: float = 0.0
102
+
103
+ @property
104
+ def empty(self) -> bool:
105
+ """Check if the result is empty."""
106
+ return not self.has_cpu_data and not self.has_mem_data and not self.has_disk_data and not self.has_gpu_data
107
+
108
+ @property
109
+ def has_cpu_data(self) -> bool:
110
+ """Check if CPU data is available."""
111
+ return self.cpu_usage_avg != 0.0 or self.cpu_max != 0.0 or self.cpu_min != 0.0
112
+
113
+ @property
114
+ def has_mem_data(self) -> bool:
115
+ """Check if memory data is available."""
116
+ return self.mem_usage_avg != 0.0 or self.mem_max != 0.0 or self.mem_min != 0.0
117
+
118
+ @property
119
+ def has_disk_data(self) -> bool:
120
+ """Check if disk data is available."""
121
+ return self.disk_usage_avg != 0.0
122
+
123
+ @property
124
+ def has_gpu_data(self) -> bool:
125
+ """Check if GPU data is available."""
126
+ return (
127
+ self.gpu_usage_avg != 0.0
128
+ or self.gpu_usage_max != 0.0
129
+ or self.gpu_usage_min != 0.0
130
+ or self.gpu_mem_usage_avg != 0.0
131
+ )
132
+
133
+
134
+ class SampleStore(TypedDict):
135
+ """TypedDict for storing samples and getter function."""
136
+
137
+ samples: deque[float | GPUSamples]
138
+ getter: Callable[[], Awaitable[float | GPUSamples]]
139
+
140
+
141
+ class HostMonitor:
142
+ """Host Monitor for collecting and averaging system metrics."""
143
+
144
+ def __init__(self, sample_interval: float = 1, tasks: tuple = CPU_MEM):
145
+ """Initialize the HostMonitor with specified tasks and sample interval."""
146
+ self.console = console
147
+ self.disk_path = "/"
148
+ self.sample_stores: dict[TaskChoice, SampleStore] = {}
149
+ self.tasks: list[TaskChoice] = list(tasks)
150
+
151
+ if self.is_task_enabled(TaskChoice.GPU) and not has_nvidia_gpu():
152
+ self.tasks.remove(TaskChoice.GPU)
153
+ self.console.warning("No NVIDIA GPU detected, removing GPU task from monitoring.")
154
+
155
+ self.sample_stores = {
156
+ task: {
157
+ "samples": deque(maxlen=int(ROLLING_AVERAGE_TIME // sample_interval)),
158
+ "getter": getattr(self, f"get_{task.value}"),
159
+ }
160
+ for task in tasks
161
+ }
162
+
163
+ self.sampling_task: Task | None = None
164
+ self.is_monitoring: bool = False
165
+ self.sample_interval: float = sample_interval
166
+ self.last_result = HostMonitorResult()
167
+
168
+ self.console.verbose("HostMonitor initialized")
169
+
170
+ def is_task_enabled(self, task: TaskChoice) -> bool:
171
+ """Check if a specific task is enabled."""
172
+ return task in self.tasks
173
+
174
+ async def start(self) -> None:
175
+ """Start the host monitoring."""
176
+ self.is_monitoring = True
177
+ self.sampling_task = asyncio.create_task(self._collect_samples())
178
+
179
+ async def stop(self) -> None:
180
+ """Stop the host monitoring."""
181
+ self.is_monitoring = False
182
+ if self.sampling_task:
183
+ self.sampling_task.cancel()
184
+ with suppress(asyncio.CancelledError):
185
+ await self.sampling_task
186
+ self.sampling_task = None
187
+
188
+ async def clear(self) -> None:
189
+ """Clear all samples from the sample stores."""
190
+ for store in self.sample_stores.values():
191
+ store["samples"].clear()
192
+
193
+ @overload
194
+ async def get(self, task: Literal[TaskChoice.CPU, TaskChoice.MEM, TaskChoice.DISK]) -> float: ...
195
+
196
+ @overload
197
+ async def get(self, task: Literal[TaskChoice.GPU]) -> GPUSamples: ...
198
+
199
+ async def get(self, task: TaskChoice) -> float | GPUSamples:
200
+ """Manually get a sample for the specified task."""
201
+ getter_func = self.sample_stores[task]["getter"]
202
+ if getter_func is None:
203
+ self.console.error(f"Getter method for task {task} is None.")
204
+ return 0.0
205
+ result: float | GPUSamples | None = await getter_func()
206
+ if result is not None:
207
+ if isinstance(result, GPUSamples):
208
+ return result
209
+ return float(result)
210
+ return 0.0
211
+
212
+ async def _collect_samples(self) -> None:
213
+ await self.clear()
214
+ while self.is_monitoring:
215
+ await self._record_data()
216
+ await asyncio.sleep(self.sample_interval)
217
+
218
+ async def _record_data(self) -> None:
219
+ for task in self.tasks:
220
+ getter_func = self.sample_stores[task]["getter"]
221
+ if getter_func is None:
222
+ self.console.error(f"Getter method for task {task} is None.")
223
+ continue
224
+ result: float | GPUSamples | None = await getter_func()
225
+ if result is not None:
226
+ self.sample_stores[task]["samples"].append(result)
227
+
228
+ @overload
229
+ async def _get_samples(self, task: Literal[TaskChoice.CPU, TaskChoice.MEM, TaskChoice.DISK]) -> list[float]: ...
230
+
231
+ @overload
232
+ async def _get_samples(self, task: Literal[TaskChoice.GPU]) -> list[GPUSamples]: ...
233
+
234
+ async def _get_samples(self, task: TaskChoice) -> list[float] | list[GPUSamples]:
235
+ """Get collected samples for the specified task."""
236
+ if not self.is_monitoring or not self.sample_stores.get(task):
237
+ return [0.0]
238
+ try:
239
+ sample = list(self.sample_stores[task]["samples"])
240
+ return cast("list[GPUSamples]", sample) if task == GPU else cast("list[float]", sample)
241
+ except Exception as e:
242
+ self.console.error(f"Error getting {task} samples: {e}", exc_info=True)
243
+ return [0.0]
244
+
245
+ async def get_sample(self, task: TaskChoice) -> float:
246
+ """Get a single sample for the specified task."""
247
+ if not self.is_monitoring or not self.sample_stores.get(task):
248
+ return 0.0
249
+ try:
250
+ result: list[float] | list[GPUSamples] = await self._get_samples(task)
251
+ if not result:
252
+ return 0.0
253
+ if task == GPU and isinstance(result[0], GPUSamples):
254
+ first_result: GPUSamples = result[0]
255
+ return first_result.gpu_usage if isinstance(first_result, GPUSamples) else 0.0
256
+ return result[0] if isinstance(result[0], float) else 0.0
257
+ except Exception as e:
258
+ self.console.error(f"Error getting single {task} sample: {e}", exc_info=True)
259
+ return 0.0
260
+
261
+ @property
262
+ def is_running(self) -> bool:
263
+ """Check if the monitor is running."""
264
+ return self.is_monitoring and self.sampling_task is not None and bool(self.sample_stores)
265
+
266
+ async def get_current_samples(self) -> HostMonitorResult:
267
+ """Get the current samples for all tasks."""
268
+ result = HostMonitorResult()
269
+ if not self.is_running:
270
+ return result
271
+ try:
272
+ if self.is_task_enabled(CPU):
273
+ cpu_samples: list[float] = await self._get_samples(TaskChoice.CPU)
274
+ if cpu_samples:
275
+ result.cpu_usage_avg = round(sum(cpu_samples) / len(cpu_samples), 2)
276
+ result.cpu_max = max(cpu_samples)
277
+ result.cpu_min = min(cpu_samples)
278
+ if self.is_task_enabled(MEM):
279
+ mem_samples: list[float] = await self._get_samples(TaskChoice.MEM)
280
+ if mem_samples:
281
+ result.mem_usage_avg = round(sum(mem_samples) / len(mem_samples), 2)
282
+ result.mem_max = max(mem_samples)
283
+ result.mem_min = min(mem_samples)
284
+ if self.is_task_enabled(DISK):
285
+ disk_samples: list[float] = await self._get_samples(TaskChoice.DISK)
286
+ if disk_samples:
287
+ result.disk_usage_avg = round(sum(disk_samples) / len(disk_samples), 2)
288
+ if self.is_task_enabled(GPU):
289
+ gpu_samples: list[GPUSamples] = await self._get_samples(TaskChoice.GPU)
290
+ if gpu_samples:
291
+ gpu_usage: list[float] = [sample.gpu_usage for sample in gpu_samples]
292
+ result.gpu_usage_avg = round(sum(gpu_usage) / len(gpu_usage), 2)
293
+ result.gpu_usage_max = max(gpu_usage)
294
+ result.gpu_usage_min = min(gpu_usage)
295
+ self.last_result: HostMonitorResult = result
296
+ except Exception as e:
297
+ self.console.error(f"Error getting current samples: {e}", exc_info=True)
298
+ return result
299
+
300
+ async def get_avg_cpu_temp(self) -> float:
301
+ """Get the average CPU temperature."""
302
+ if not self.is_monitoring or not self.sample_stores.get(CPU):
303
+ return 0.0
304
+ try:
305
+ current_cpu_samples: list[float] = await self._get_samples(TaskChoice.CPU)
306
+ if current_cpu_samples:
307
+ return round(sum(current_cpu_samples) / len(current_cpu_samples), 2)
308
+ except Exception as e:
309
+ self.console.error(f"Error getting CPU temperature: {e}", exc_info=True)
310
+ return 0.0
311
+
312
+ async def get_avg_mem_usage(self) -> float:
313
+ """Get the average memory usage."""
314
+ if not self.is_monitoring or not self.sample_stores.get(MEM):
315
+ return 0.0
316
+ try:
317
+ current_mem_samples: list[float] = await self._get_samples(TaskChoice.MEM)
318
+ if current_mem_samples:
319
+ return round(sum(current_mem_samples) / len(current_mem_samples), 2)
320
+ except Exception as e:
321
+ print(f"Error getting memory usage: {e}")
322
+ return 0.0
323
+
324
+ async def get_disk_usage(self) -> float:
325
+ """Get the average disk usage."""
326
+ if not self.is_monitoring or not self.sample_stores.get(DISK):
327
+ return 0.0
328
+ try:
329
+ current_disk_samples: list[float] = await self._get_samples(TaskChoice.DISK)
330
+ if current_disk_samples:
331
+ return round(sum(current_disk_samples) / len(current_disk_samples), 2)
332
+ except Exception as e:
333
+ print(f"Error getting disk usage: {e}")
334
+ return 0.0
335
+
336
+ async def get_avg_gpu_usage(self) -> float:
337
+ """Get the average GPU usage."""
338
+ if not self.is_monitoring or not self.sample_stores.get(GPU):
339
+ return 0.0
340
+ try:
341
+ current_gpu_samples: list[GPUSamples] = await self._get_samples(TaskChoice.GPU)
342
+ if current_gpu_samples:
343
+ return round(sum(sample.gpu_usage for sample in current_gpu_samples) / len(current_gpu_samples), 2)
344
+ except Exception as e:
345
+ print(f"Error getting GPU usage: {e}")
346
+ return 0.0
@@ -0,0 +1,59 @@
1
+ """A module for Bear Epoch Time utilities centering around the EpochTimestamp class."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ from bear_epoch_time import EpochTimestamp, TimerData, TimeTools, add_ord_suffix, create_timer, timer
6
+ from bear_epoch_time._helpers import (
7
+ convert_to_milliseconds,
8
+ convert_to_seconds,
9
+ seconds_to_time,
10
+ seconds_to_timedelta,
11
+ timedelta_to_seconds,
12
+ )
13
+ from bear_epoch_time.constants.date_related import (
14
+ DATE_FORMAT,
15
+ DATE_TIME_FORMAT,
16
+ DT_FORMAT_WITH_SECONDS,
17
+ DT_FORMAT_WITH_TZ,
18
+ DT_FORMAT_WITH_TZ_AND_SECONDS,
19
+ ET_TIME_ZONE,
20
+ PT_TIME_ZONE,
21
+ TIME_FORMAT_WITH_SECONDS,
22
+ UTC_TIME_ZONE,
23
+ )
24
+
25
+
26
+ def get_bear_epoch_time_version() -> str:
27
+ """Get the version of the bear_epoch_time package."""
28
+ try:
29
+ return version("bear_epoch_time")
30
+ except Exception:
31
+ return "unknown"
32
+
33
+
34
+ __version__: str = get_bear_epoch_time_version()
35
+
36
+
37
+ __all__ = [
38
+ "DATE_FORMAT",
39
+ "DATE_TIME_FORMAT",
40
+ "DT_FORMAT_WITH_SECONDS",
41
+ "DT_FORMAT_WITH_TZ",
42
+ "DT_FORMAT_WITH_TZ_AND_SECONDS",
43
+ "ET_TIME_ZONE",
44
+ "PT_TIME_ZONE",
45
+ "TIME_FORMAT_WITH_SECONDS",
46
+ "UTC_TIME_ZONE",
47
+ "EpochTimestamp",
48
+ "TimeTools",
49
+ "TimerData",
50
+ "__version__",
51
+ "add_ord_suffix",
52
+ "convert_to_milliseconds",
53
+ "convert_to_seconds",
54
+ "create_timer",
55
+ "seconds_to_time",
56
+ "seconds_to_timedelta",
57
+ "timedelta_to_seconds",
58
+ "timer",
59
+ ]
@@ -0,0 +1,305 @@
1
+ Metadata-Version: 2.4
2
+ Name: bear-utils
3
+ Version: 0.0.1
4
+ Summary: Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things.
5
+ Author-email: chaz <bright.lid5647@fastmail.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: bear-epoch-time>=1.1.4
8
+ Requires-Dist: diskcache<6.0.0,>=5.6.3
9
+ Requires-Dist: fastapi>=0.116.0
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: pathspec>=0.12.1
12
+ Requires-Dist: pillow<12.0.0,>=11.2.1
13
+ Requires-Dist: prompt-toolkit<4.0.0,>=3.0.51
14
+ Requires-Dist: pydantic>=2.11.5
15
+ Requires-Dist: pyfiglet>=1.0.3
16
+ Requires-Dist: pyglm<3.0.0,>=2.8.2
17
+ Requires-Dist: pyyaml>=6.0.2
18
+ Requires-Dist: rich<15.0.0,>=14.0.0
19
+ Requires-Dist: singleton-base>=1.0.5
20
+ Requires-Dist: sqlalchemy<3.0.0,>=2.0.40
21
+ Requires-Dist: tinydb>=4.8.2
22
+ Requires-Dist: toml>=0.10.2
23
+ Requires-Dist: typer>=0.16.0
24
+ Requires-Dist: uvicorn>=0.35.0
25
+ Provides-Extra: gui
26
+ Requires-Dist: pyqt6>=6.9.0; extra == 'gui'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Bear Utils
30
+
31
+ Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
32
+
33
+ ## Overview
34
+
35
+ Bear Utils is a collection of utility modules I've created for common tasks across my Python projects. The library is designed to be modular and easy to use, with each component focusing on a specific functionality.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install bear-util. # Core package (recommended for most users)
41
+ pip install bear-utils[gui] # With optional GUI functionality
42
+
43
+ # Using UV
44
+ uv add bear-utils # Core only
45
+ uv add bear-utils --group gui # With GUI functionality
46
+ ```
47
+
48
+ ## Key Components
49
+
50
+ ### Cache Management
51
+
52
+ The `cache` package provides disk cache functionality via `diskcache`:
53
+
54
+ ```python
55
+ from bear_utils.cache import CacheWrapper, cache_factory
56
+
57
+ # Create a cache instance
58
+ cache = Cache(directory="~/.cache/my_app")
59
+
60
+ # Use the cache factory to create a decorated function
61
+ @cache_factory(directory="~/.cache/my_app")
62
+ def expensive_function(arg1, arg2):
63
+ # Function will be cached based on arguments
64
+ pass
65
+ ```
66
+
67
+ ### Database Management
68
+
69
+ The `database` package provides SQLAlchemy integration with helpful patterns:
70
+
71
+ ```python
72
+ from bear_utils.database import DatabaseManager
73
+ from sqlalchemy import Column, Integer, String
74
+
75
+ # Get declarative base
76
+ Base = DatabaseManager.get_base()
77
+
78
+ # Define models
79
+ class User(Base):
80
+ __tablename__ = "users"
81
+ id = Column(Integer, primary_key=True)
82
+ name = Column(String)
83
+
84
+ # Create and use database
85
+ db = DatabaseManager("sqlite:///app.db")
86
+ with db.session() as session:
87
+ session.add(User(name="test"))
88
+ session.commit()
89
+ ```
90
+
91
+ ### Logging
92
+
93
+ The `logging` package provides an enhanced console logger using rich:
94
+
95
+ ```python
96
+ from bear_utils.logger_manager import ConsoleLogger
97
+
98
+ logger = ConsoleLogger()
99
+ logger.info("Information message")
100
+ logger.error("Error message")
101
+ logger.warning("Warning message")
102
+ ```
103
+
104
+ ### Time Management
105
+
106
+ The `time` package contains tools for time handling and measurement:
107
+
108
+ ```python
109
+ from bear_utils.time import TimeTools, EpochTimestamp
110
+
111
+ # Get current timestamp
112
+ now = EpochTimestamp()
113
+
114
+ # Time function execution
115
+ from bear_utils.time.time_manager import timer
116
+
117
+ with timer(name="my_operation"):
118
+ # Code to measure
119
+ pass
120
+ ```
121
+
122
+ ### File Handling
123
+
124
+ The `files` package provides abstractions for working with different file types:
125
+
126
+ ```python
127
+ from pathlib import Path
128
+ from bear_utils.files.file_handlers import FileHandlerFactory
129
+
130
+ factory = FileHandlerFactory()
131
+ handler = factory.get_handler(Path("config.json"))
132
+ data = handler.read_file()
133
+ ```
134
+
135
+ ### Prompt Helpers
136
+
137
+ Utilities for creating interactive command-line prompts:
138
+
139
+ ```python
140
+ from bear_utils.cli.prompt_helpers import restricted_prompt
141
+
142
+ choice = restricted_prompt(
143
+ message="Select an option:",
144
+ valid_options=["option1", "option2"]
145
+ )
146
+ ```
147
+
148
+ ### GUI Utilities (Optional)
149
+
150
+ **Note: GUI functionality requires the optional PyQt6 dependency. Install with `pip install bear-utils[gui]`**
151
+
152
+ The `gui` package provides PyQt6-based dialog utilities for desktop applications:
153
+
154
+ ```python
155
+ # Color picker dialog
156
+ from bear_utils.gui import select_color
157
+
158
+ color_info = select_color(
159
+ initial_color="#FF5733",
160
+ title="Choose a Color"
161
+ )
162
+ if color_info:
163
+ print(f"Selected: {color_info.hex}") # e.g., "#FF5733"
164
+ print(f"RGB: {color_info.rgb}") # ColorTriplet(255, 87, 51)
165
+ print(f"RGBA: {color_info.rgba}") # (255, 87, 51, 255)
166
+
167
+ # Text input dialog
168
+ from bear_utils.gui import get_text
169
+
170
+ user_input = get_text(
171
+ title="Input Required",
172
+ label="Enter your name:",
173
+ default="Default text"
174
+ )
175
+
176
+ # Qt Application wrapper
177
+ from bear_utils.gui import QTApplication
178
+
179
+ app = QTApplication(
180
+ app_name="My App",
181
+ org_name="My Organization"
182
+ )
183
+ ```
184
+
185
+ **Error handling**: If PyQt6 is not installed, importing GUI components will raise a helpful error:
186
+ ```
187
+ ImportError: PyQt6 is required for GUI functionality. Install it with: pip install bear-utils[gui]
188
+ ```
189
+
190
+ ### Image Helpers
191
+
192
+ Utilities for working with images are located in the `graphics` package:
193
+
194
+ ```python
195
+ from pathlib import Path
196
+ from bear_utils.graphics import encode_image_to_jpeg, encode_image_to_png
197
+
198
+ # Encode image to base64 string
199
+ jpeg_data = encode_image_to_jpeg(Path("image.jpg"), max_size=800)
200
+ png_data = encode_image_to_png(Path("image.png"), max_size=800)
201
+ ```
202
+
203
+ ### Clipboard Helpers
204
+
205
+ `bear_utils.extras._tools` includes simple helpers for interacting with the system clipboard:
206
+
207
+ ```python
208
+ from bear_utils.extras._tools import copy_to_clipboard, paste_from_clipboard
209
+
210
+ copy_to_clipboard("hello world")
211
+ text = paste_from_clipboard()
212
+ ```
213
+
214
+ Supported platforms include macOS (`pbcopy/pbpaste`), Windows (`clip`/`powershell Get-Clipboard`),
215
+ and Linux with either Wayland (`wl-copy`/`wl-paste`) or X11 (`xclip`).
216
+
217
+ ### Gradient Utilities
218
+
219
+ The `graphics.bear_gradient` module provides color gradient functionality for the purposes of visualizing data or creating color transitions:
220
+
221
+ ```python
222
+ from bear_utils.graphics import ColorGradient, RichColor
223
+ from rich.console import Console
224
+ from rich.color_triplet import ColorTriplet
225
+
226
+ console = Console()
227
+
228
+ # Health meter goes from red (low health) to green (full health)
229
+ health_gradient = ColorGradient()
230
+
231
+ console.print("🏥 [bold]Health Meter Demonstration[/bold] 🏥\n")
232
+
233
+ # Normal health: Red (low) -> Green (high)
234
+ console.print("[bold green]Normal Health Levels (0% = Critical, 100% = Perfect):[/bold green]")
235
+ for health in range(0, 101, 10):
236
+ color: ColorTriplet = health_gradient.map_to_color(0, 100, health)
237
+ health_bar = "█" * (health // 5)
238
+ console.print(f"HP: {health:3d}/100 {health_bar:<20}", style=color.rgb)
239
+
240
+ console.print("\n" + "="*50 + "\n")
241
+
242
+ # Reversed: Infection/Damage meter (Green = good, Red = bad)
243
+ console.print("[bold red]Infection Level (0% = Healthy, 100% = Critical):[/bold red]")
244
+ health_gradient.reverse = True
245
+ for infection in range(0, 101, 10):
246
+ color: ColorTriplet = health_gradient.map_to_color(0, 100, infection)
247
+ infection_bar = "█" * (infection // 5)
248
+ status = "🦠" if infection > 70 else "⚠️" if infection > 30 else "✅"
249
+ console.print(f"Infection: {infection:3d}% {infection_bar:<20} {status}", style=color.rgb)
250
+
251
+ health_scenarios = [
252
+ (5, "💀 Nearly Dead"),
253
+ (25, "🩸 Critical Condition"),
254
+ (50, "⚠️ Wounded"),
255
+ (75, "😐 Recovering"),
256
+ (95, "💪 Almost Full Health"),
257
+ (100, "✨ Perfect Health")
258
+ ]
259
+
260
+ console.print("[bold green]Health Status Examples:[/bold green]")
261
+ for hp, status in health_scenarios:
262
+ color: ColorTriplet = health_gradient.map_to_color(0, 100, hp)
263
+ console.print(f"{status}: {hp}/100 HP", style=color.rgb)
264
+ ```
265
+
266
+ ### Event System
267
+
268
+ The `events` module provides a pub/sub event system that supports both synchronous and asynchronous handlers:
269
+
270
+ ```python
271
+ from bear_utils.events.events_module import subscribe, publish
272
+
273
+ # Subscribe to events
274
+ @subscribe("user_logged_in")
275
+ def handle_login(user_id):
276
+ print(f"User {user_id} logged in")
277
+
278
+ # Publish events
279
+ publish("user_logged_in", 12345)
280
+
281
+ # Async support
282
+ @subscribe("data_processed")
283
+ async def handle_process(data):
284
+ await process_async(data)
285
+
286
+ # Clear handlers
287
+ from bear_utils.events.events_module import clear_handlers_for_event
288
+ clear_handlers_for_event("user_logged_in")
289
+ ```
290
+
291
+ ### Async Helpers
292
+
293
+ The `extras/_async_helpers.py` module provides utility functions for working with async code:
294
+
295
+ ```python
296
+ from bear_utils.extras._async_helpers import is_async_function
297
+
298
+ def handle_function(func):
299
+ if is_async_function(func):
300
+ # Handle async function
301
+ pass
302
+ else:
303
+ # Handle sync function
304
+ pass
305
+ ```