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,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
|
+
```
|