bear-utils 0.7.11__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 (83) hide show
  1. bear_utils/__init__.py +13 -0
  2. bear_utils/ai/__init__.py +30 -0
  3. bear_utils/ai/ai_helpers/__init__.py +130 -0
  4. bear_utils/ai/ai_helpers/_common.py +19 -0
  5. bear_utils/ai/ai_helpers/_config.py +24 -0
  6. bear_utils/ai/ai_helpers/_parsers.py +188 -0
  7. bear_utils/ai/ai_helpers/_types.py +20 -0
  8. bear_utils/cache/__init__.py +119 -0
  9. bear_utils/cli/__init__.py +4 -0
  10. bear_utils/cli/commands.py +59 -0
  11. bear_utils/cli/prompt_helpers.py +166 -0
  12. bear_utils/cli/shell/__init__.py +0 -0
  13. bear_utils/cli/shell/_base_command.py +74 -0
  14. bear_utils/cli/shell/_base_shell.py +390 -0
  15. bear_utils/cli/shell/_common.py +19 -0
  16. bear_utils/config/__init__.py +11 -0
  17. bear_utils/config/config_manager.py +92 -0
  18. bear_utils/config/dir_manager.py +64 -0
  19. bear_utils/config/settings_manager.py +232 -0
  20. bear_utils/constants/__init__.py +16 -0
  21. bear_utils/constants/_exceptions.py +3 -0
  22. bear_utils/constants/_lazy_typing.py +15 -0
  23. bear_utils/constants/date_related.py +36 -0
  24. bear_utils/constants/time_related.py +22 -0
  25. bear_utils/database/__init__.py +6 -0
  26. bear_utils/database/_db_manager.py +104 -0
  27. bear_utils/events/__init__.py +16 -0
  28. bear_utils/events/events_class.py +52 -0
  29. bear_utils/events/events_module.py +65 -0
  30. bear_utils/extras/__init__.py +17 -0
  31. bear_utils/extras/_async_helpers.py +15 -0
  32. bear_utils/extras/_tools.py +178 -0
  33. bear_utils/extras/platform_utils.py +53 -0
  34. bear_utils/extras/wrappers/__init__.py +0 -0
  35. bear_utils/extras/wrappers/add_methods.py +98 -0
  36. bear_utils/files/__init__.py +4 -0
  37. bear_utils/files/file_handlers/__init__.py +3 -0
  38. bear_utils/files/file_handlers/_base_file_handler.py +93 -0
  39. bear_utils/files/file_handlers/file_handler_factory.py +278 -0
  40. bear_utils/files/file_handlers/json_file_handler.py +44 -0
  41. bear_utils/files/file_handlers/log_file_handler.py +33 -0
  42. bear_utils/files/file_handlers/txt_file_handler.py +34 -0
  43. bear_utils/files/file_handlers/yaml_file_handler.py +57 -0
  44. bear_utils/files/ignore_parser.py +298 -0
  45. bear_utils/graphics/__init__.py +4 -0
  46. bear_utils/graphics/bear_gradient.py +140 -0
  47. bear_utils/graphics/image_helpers.py +39 -0
  48. bear_utils/gui/__init__.py +3 -0
  49. bear_utils/gui/gui_tools/__init__.py +5 -0
  50. bear_utils/gui/gui_tools/_settings.py +37 -0
  51. bear_utils/gui/gui_tools/_types.py +12 -0
  52. bear_utils/gui/gui_tools/qt_app.py +145 -0
  53. bear_utils/gui/gui_tools/qt_color_picker.py +119 -0
  54. bear_utils/gui/gui_tools/qt_file_handler.py +138 -0
  55. bear_utils/gui/gui_tools/qt_input_dialog.py +306 -0
  56. bear_utils/logging/__init__.py +25 -0
  57. bear_utils/logging/logger_manager/__init__.py +0 -0
  58. bear_utils/logging/logger_manager/_common.py +47 -0
  59. bear_utils/logging/logger_manager/_console_junk.py +131 -0
  60. bear_utils/logging/logger_manager/_styles.py +91 -0
  61. bear_utils/logging/logger_manager/loggers/__init__.py +0 -0
  62. bear_utils/logging/logger_manager/loggers/_base_logger.py +238 -0
  63. bear_utils/logging/logger_manager/loggers/_base_logger.pyi +50 -0
  64. bear_utils/logging/logger_manager/loggers/_buffer_logger.py +55 -0
  65. bear_utils/logging/logger_manager/loggers/_console_logger.py +249 -0
  66. bear_utils/logging/logger_manager/loggers/_console_logger.pyi +64 -0
  67. bear_utils/logging/logger_manager/loggers/_file_logger.py +141 -0
  68. bear_utils/logging/logger_manager/loggers/_level_sin.py +58 -0
  69. bear_utils/logging/logger_manager/loggers/_logger.py +18 -0
  70. bear_utils/logging/logger_manager/loggers/_sub_logger.py +110 -0
  71. bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +38 -0
  72. bear_utils/logging/loggers.py +76 -0
  73. bear_utils/monitoring/__init__.py +10 -0
  74. bear_utils/monitoring/host_monitor.py +350 -0
  75. bear_utils/time/__init__.py +16 -0
  76. bear_utils/time/_helpers.py +91 -0
  77. bear_utils/time/_time_class.py +316 -0
  78. bear_utils/time/_timer.py +80 -0
  79. bear_utils/time/_tools.py +17 -0
  80. bear_utils/time/time_manager.py +218 -0
  81. bear_utils-0.7.11.dist-info/METADATA +260 -0
  82. bear_utils-0.7.11.dist-info/RECORD +83 -0
  83. bear_utils-0.7.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,76 @@
1
+ from ..constants.date_related import DATE_TIME_FORMAT
2
+ from .logger_manager._common import FIVE_MEGABYTES, SIMPLE_FORMAT, VERBOSE_CONSOLE_FORMAT, VERBOSE_FORMAT, ExecValues
3
+ from .logger_manager._console_junk import ConsoleBuffering, ConsoleFormatter, ConsoleHandler
4
+ from .logger_manager._styles import DEFAULT_THEME, LOGGER_METHODS, VERBOSE, LoggerExtraInfo, get_method
5
+ from .logger_manager.loggers._base_logger import BaseLogger
6
+ from .logger_manager.loggers._buffer_logger import BufferLogger
7
+ from .logger_manager.loggers._console_logger import ConsoleLogger
8
+ from .logger_manager.loggers._file_logger import FileLogger
9
+ from .logger_manager.loggers._sub_logger import SubConsoleLogger
10
+
11
+ AllLoggers = BaseLogger | ConsoleLogger | SubConsoleLogger
12
+ Loggers = BaseLogger | ConsoleLogger
13
+
14
+
15
+ def get_logger(
16
+ console: bool = True,
17
+ file: bool = False,
18
+ queue_handler: bool = False,
19
+ buffering: bool = False,
20
+ **kwargs,
21
+ ) -> BaseLogger | ConsoleLogger | BufferError | FileLogger:
22
+ """Get a logger instance based on the specified parameters.
23
+
24
+ Args:
25
+ name (str): The name of the logger.
26
+ level (int): The logging level.
27
+ console (bool): Whether to enable console logging.
28
+ file (bool): Whether to enable file logging.
29
+ queue_handler (bool): Whether to use a queue handler.
30
+ buffering (bool): Whether to enable buffering.
31
+ style_disabled (bool): Whether to disable styling.
32
+ logger_mode (bool): Whether the logger is in logger mode.
33
+ **kwargs: Additional keyword arguments for customization.
34
+ Returns:
35
+ BaseLogger | ConsoleLogger | BufferLogger| FileLogger: An instance of the appropriate logger.
36
+ """
37
+ if (not console and not file) and buffering:
38
+ return BufferLogger(queue_handler=queue_handler, **kwargs)
39
+ elif (console and file) or (console and buffering):
40
+ return ConsoleLogger(queue_handler=queue_handler, buffering=buffering, **kwargs)
41
+ elif not console and not buffering and file:
42
+ return FileLogger(queue_handler=queue_handler, **kwargs)
43
+ else:
44
+ return BaseLogger(**kwargs)
45
+
46
+
47
+ def get_console(namespace: str) -> tuple[BaseLogger, SubConsoleLogger]:
48
+ """
49
+ Get a console logger and a sub-logger for a specific namespace.
50
+
51
+ Args:
52
+ namespace (str): The namespace for the sub-logger.
53
+
54
+ Returns:
55
+ tuple[BaseLogger, SubConsoleLogger]: A tuple containing the base logger and the sub-logger.
56
+ """
57
+ base_logger = BaseLogger.get_instance(init=True)
58
+ sub_logger = SubConsoleLogger(logger=base_logger, namespace=namespace)
59
+ return base_logger, sub_logger
60
+
61
+
62
+ def get_sub_logger(logger: BaseLogger | ConsoleLogger, namespace: str) -> SubConsoleLogger[BaseLogger | ConsoleLogger]:
63
+ """
64
+ Get a sub-logger for a specific namespace.
65
+
66
+ Args:
67
+ logger (BaseLogger): The parent logger.
68
+ namespace (str): The namespace for the sub-logger.
69
+
70
+ Returns:
71
+ SubConsoleLogger: A sub-logger instance.
72
+ """
73
+ if not isinstance(logger, (BaseLogger, ConsoleLogger)):
74
+ raise TypeError("Expected logger to be an instance of BaseLogger or ConsoleLogger")
75
+
76
+ return SubConsoleLogger(logger=logger, namespace=namespace)
@@ -0,0 +1,10 @@
1
+ from .host_monitor import CPU, DISK, GPU, MEM, HostMonitor, TaskChoice
2
+
3
+ __all__ = [
4
+ "TaskChoice",
5
+ "HostMonitor",
6
+ "CPU",
7
+ "MEM",
8
+ "DISK",
9
+ "GPU",
10
+ ]
@@ -0,0 +1,350 @@
1
+ import asyncio
2
+ import subprocess
3
+ from asyncio import Task
4
+ from collections import deque
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+ from typing import Literal, Self, TypedDict, cast, overload
9
+
10
+ from ..logging.loggers import BaseLogger, SubConsoleLogger, get_console
11
+
12
+ ROLLING_AVERAGE_TIME = 300
13
+
14
+ _base_logger, console = get_console("HostMonitor")
15
+
16
+
17
+ class TaskChoice(StrEnum):
18
+ """Enum for task choices."""
19
+
20
+ CPU = "cpu"
21
+ MEM = "mem"
22
+ DISK = "disk"
23
+ GPU = "gpu"
24
+
25
+
26
+ CPU = TaskChoice.CPU
27
+ MEM = TaskChoice.MEM
28
+ DISK = TaskChoice.DISK
29
+ GPU = TaskChoice.GPU
30
+
31
+ CPU_MEM: list[TaskChoice] = [CPU, MEM]
32
+ CPU_MEM_GPU: list[TaskChoice] = [CPU, MEM, GPU]
33
+ ALL_TASKS: list[TaskChoice] = [CPU, MEM, DISK, GPU]
34
+
35
+
36
+ def has_nvidia_gpu() -> bool:
37
+ """Check if the system has an NVIDIA GPU."""
38
+ try:
39
+ result = subprocess.run(
40
+ args=["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
41
+ capture_output=True,
42
+ text=True,
43
+ )
44
+ return result.returncode == 0
45
+ except Exception as e:
46
+ console.error(f"Error checking for NVIDIA GPU: {e}", exc_info=True)
47
+ return False
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class GPUSamples:
52
+ """GPU samples for monitoring."""
53
+
54
+ gpu_usage: float = 0.0
55
+ gpu_mem_usage: float = 0.0
56
+ gpu_mem_total: float = 0.0
57
+ gpu_mem_free: float = 0.0
58
+
59
+ def __post_init__(self) -> None:
60
+ if self.gpu_mem_total == 0:
61
+ self.gpu_mem_total = self.gpu_mem_usage + self.gpu_mem_free
62
+
63
+ @classmethod
64
+ async def get_gpu_samples(cls) -> Self:
65
+ """Get GPU samples using nvidia-smi."""
66
+ try:
67
+ result = subprocess.run(
68
+ args=[
69
+ "nvidia-smi",
70
+ "--query-gpu=utilization.gpu,memory.used,memory.total,memory.free",
71
+ "--format=csv,noheader",
72
+ ],
73
+ capture_output=True,
74
+ text=True,
75
+ )
76
+ if result.returncode != 0:
77
+ raise RuntimeError("nvidia-smi command failed")
78
+ gpu_usage, gpu_mem_used, gpu_mem_total, gpu_mem_free = map(float, result.stdout.split(","))
79
+ return cls(
80
+ gpu_usage=gpu_usage,
81
+ gpu_mem_usage=gpu_mem_used,
82
+ gpu_mem_total=gpu_mem_total,
83
+ gpu_mem_free=gpu_mem_free,
84
+ )
85
+ except Exception as e:
86
+ console.error(f"Error getting GPU samples: {e}", exc_info=True)
87
+ return cls()
88
+
89
+
90
+ @dataclass(slots=True)
91
+ class HostMonitorResult:
92
+ # CPU
93
+ cpu_usage_avg: float = 0.0
94
+ cpu_max: float = 0.0
95
+ cpu_min: float = 0.0
96
+
97
+ mem_usage_avg: float = 0.0
98
+ mem_max: float = 0.0
99
+ mem_min: float = 0.0
100
+
101
+ disk_usage_avg: float = 0.0
102
+
103
+ gpu_usage_avg: float = 0.0
104
+ gpu_usage_max: float = 0.0
105
+ gpu_usage_min: float = 0.0
106
+
107
+ gpu_mem_usage_avg: float = 0.0
108
+ gpu_mem_usage_max: float = 0.0
109
+ gpu_mem_usage_min: float = 0.0
110
+
111
+ @property
112
+ def empty(self) -> bool:
113
+ """Check if the result is empty."""
114
+ return not self.has_cpu_data and not self.has_mem_data and not self.has_disk_data and not self.has_gpu_data
115
+
116
+ @property
117
+ def has_cpu_data(self) -> bool:
118
+ """Check if CPU data is available."""
119
+ return self.cpu_usage_avg != 0.0 or self.cpu_max != 0.0 or self.cpu_min != 0.0
120
+
121
+ @property
122
+ def has_mem_data(self) -> bool:
123
+ """Check if memory data is available."""
124
+ return self.mem_usage_avg != 0.0 or self.mem_max != 0.0 or self.mem_min != 0.0
125
+
126
+ @property
127
+ def has_disk_data(self) -> bool:
128
+ """Check if disk data is available."""
129
+ return self.disk_usage_avg != 0.0
130
+
131
+ @property
132
+ def has_gpu_data(self) -> bool:
133
+ """Check if GPU data is available."""
134
+ return (
135
+ self.gpu_usage_avg != 0.0
136
+ or self.gpu_usage_max != 0.0
137
+ or self.gpu_usage_min != 0.0
138
+ or self.gpu_mem_usage_avg != 0.0
139
+ )
140
+
141
+
142
+ class SampleStore(TypedDict):
143
+ samples: deque[float | GPUSamples]
144
+ getter: Callable[[], Awaitable[float | GPUSamples]]
145
+
146
+
147
+ class HostMonitor:
148
+ def __init__(self, sample_interval: float = 1, tasks: list[TaskChoice] = [CPU, MEM]):
149
+ self.console = console
150
+ self.disk_path = "/"
151
+ self.sample_stores: dict[TaskChoice, SampleStore] = {}
152
+ self.tasks: list[TaskChoice] = tasks
153
+
154
+ if self.is_task_enabled(GPU):
155
+ if not has_nvidia_gpu():
156
+ self.tasks.remove(TaskChoice.GPU)
157
+ self.console.warning("No NVIDIA GPU detected, removing GPU task from monitoring.")
158
+
159
+ self.sample_stores = {
160
+ task: {
161
+ "samples": deque(maxlen=int(ROLLING_AVERAGE_TIME // sample_interval)),
162
+ "getter": getattr(self, f"get_{task.value}"),
163
+ }
164
+ for task in tasks
165
+ }
166
+
167
+ self.sampling_task: Task | None = None
168
+ self.is_monitoring: bool = False
169
+ self.sample_interval = sample_interval
170
+ self.last_result = HostMonitorResult()
171
+
172
+ self.console.verbose("HostMonitor initialized")
173
+
174
+ def is_task_enabled(self, task: TaskChoice) -> bool:
175
+ """Check if a specific task is enabled."""
176
+ return task in self.tasks
177
+
178
+ async def start(self) -> None:
179
+ self.is_monitoring = True
180
+ self.sampling_task = asyncio.create_task(self._collect_samples())
181
+
182
+ async def stop(self) -> None:
183
+ self.is_monitoring = False
184
+ if self.sampling_task:
185
+ self.sampling_task.cancel()
186
+ try:
187
+ await self.sampling_task
188
+ except asyncio.CancelledError:
189
+ pass
190
+ self.sampling_task = None
191
+
192
+ async def clear(self) -> None:
193
+ for store in self.sample_stores.values():
194
+ store["samples"].clear()
195
+
196
+ @overload
197
+ async def get(self, task: Literal[TaskChoice.CPU, TaskChoice.MEM, TaskChoice.DISK]) -> float: ...
198
+
199
+ @overload
200
+ async def get(self, task: Literal[TaskChoice.GPU]) -> GPUSamples: ...
201
+
202
+ async def get(self, task: TaskChoice) -> float | GPUSamples:
203
+ """Manually get a sample for the specified task."""
204
+ getter_func = self.sample_stores[task]["getter"]
205
+ if getter_func is None:
206
+ self.console.error(f"Getter method for task {task} is None.")
207
+ return 0.0
208
+ result: float | GPUSamples | None = await getter_func()
209
+ if result is not None:
210
+ if isinstance(result, GPUSamples):
211
+ return result
212
+ return float(result)
213
+
214
+ async def _collect_samples(self) -> None:
215
+ await self.clear()
216
+ while self.is_monitoring:
217
+ await self._record_data()
218
+ await asyncio.sleep(self.sample_interval)
219
+
220
+ async def _record_data(self) -> None:
221
+ for task in self.tasks:
222
+ getter_func = self.sample_stores[task]["getter"]
223
+ if getter_func is None:
224
+ self.console.error(f"Getter method for task {task} is None.")
225
+ continue
226
+ result: float | GPUSamples | None = await getter_func()
227
+ if result is not None:
228
+ self.sample_stores[task]["samples"].append(result)
229
+
230
+ @overload
231
+ async def _get_samples(self, task: Literal[TaskChoice.CPU, TaskChoice.MEM, TaskChoice.DISK]) -> list[float]: ...
232
+
233
+ @overload
234
+ async def _get_samples(self, task: Literal[TaskChoice.GPU]) -> list[GPUSamples]: ...
235
+
236
+ async def _get_samples(self, task: TaskChoice) -> list[float] | list[GPUSamples]:
237
+ """Get collected samples for the specified task."""
238
+ if not self.is_monitoring or not self.sample_stores.get(task):
239
+ return [0.0]
240
+ try:
241
+ sample = list(self.sample_stores[task]["samples"])
242
+ return cast(list[GPUSamples], sample) if task == GPU else cast(list[float], sample)
243
+ except Exception as e:
244
+ self.console.error(f"Error getting {task} samples: {e}", exc_info=True)
245
+ return [0.0]
246
+
247
+ async def get_sample(self, task: TaskChoice) -> float:
248
+ """Get a single sample for the specified task."""
249
+ if not self.is_monitoring or not self.sample_stores.get(task):
250
+ return 0.0
251
+ try:
252
+ result: list[float] | list[GPUSamples] = await self._get_samples(task)
253
+ if not result:
254
+ return 0.0
255
+ if task == GPU and isinstance(result[0], GPUSamples):
256
+ first_result: GPUSamples = result[0]
257
+ return first_result.gpu_usage if isinstance(first_result, GPUSamples) else 0.0
258
+ else:
259
+ return result[0] if isinstance(result[0], float) else 0.0
260
+ except Exception as e:
261
+ self.console.error(f"Error getting single {task} sample: {e}", exc_info=True)
262
+ return 0.0
263
+
264
+ @property
265
+ def is_running(self) -> bool:
266
+ """Check if the monitor is running."""
267
+ return self.is_monitoring and self.sampling_task is not None and bool(self.sample_stores)
268
+
269
+ async def get_current_samples(self) -> HostMonitorResult:
270
+ result = HostMonitorResult()
271
+ if not self.is_running:
272
+ return result
273
+ try:
274
+ if self.is_task_enabled(CPU):
275
+ cpu_samples: list[float] = await self._get_samples(TaskChoice.CPU)
276
+ if cpu_samples:
277
+ result.cpu_usage_avg = round(sum(cpu_samples) / len(cpu_samples), 2)
278
+ result.cpu_max = max(cpu_samples)
279
+ result.cpu_min = min(cpu_samples)
280
+ if self.is_task_enabled(MEM):
281
+ mem_samples: list[float] = await self._get_samples(TaskChoice.MEM)
282
+ if mem_samples:
283
+ result.mem_usage_avg = round(sum(mem_samples) / len(mem_samples), 2)
284
+ result.mem_max = max(mem_samples)
285
+ result.mem_min = min(mem_samples)
286
+ if self.is_task_enabled(DISK):
287
+ disk_samples: list[float] = await self._get_samples(TaskChoice.DISK)
288
+ if disk_samples:
289
+ result.disk_usage_avg = round(sum(disk_samples) / len(disk_samples), 2)
290
+ if self.is_task_enabled(GPU):
291
+ gpu_samples: list[GPUSamples] = await self._get_samples(TaskChoice.GPU)
292
+ if gpu_samples:
293
+ gpu_usage: list[float] = [sample.gpu_usage for sample in gpu_samples]
294
+ result.gpu_usage_avg = round(sum(gpu_usage) / len(gpu_usage), 2)
295
+ result.gpu_usage_max = max(gpu_usage)
296
+ result.gpu_usage_min = min(gpu_usage)
297
+ self.last_result: HostMonitorResult = result
298
+ except Exception as e:
299
+ self.console.error(f"Error getting current samples: {e}", exc_info=True)
300
+ return result
301
+
302
+ async def get_avg_cpu_temp(self) -> float:
303
+ if not self.is_monitoring or not self.sample_stores.get(CPU):
304
+ return 0.0
305
+ try:
306
+ current_cpu_samples: list[float] = await self._get_samples(TaskChoice.CPU)
307
+ if current_cpu_samples:
308
+ average_cpu = round(sum(current_cpu_samples) / len(current_cpu_samples), 2)
309
+ return average_cpu
310
+ except Exception as e:
311
+ print(f"Error getting CPU temperature: {e}")
312
+ return 0.0
313
+
314
+ async def get_avg_mem_usage(self) -> float:
315
+ if not self.is_monitoring or not self.sample_stores.get(MEM):
316
+ return 0.0
317
+ try:
318
+ current_mem_samples: list[float] = await self._get_samples(TaskChoice.MEM)
319
+ if current_mem_samples:
320
+ average_mem: float = round(sum(current_mem_samples) / len(current_mem_samples), 2)
321
+ return average_mem
322
+ except Exception as e:
323
+ print(f"Error getting memory usage: {e}")
324
+ return 0.0
325
+
326
+ async def get_disk_usage(self) -> float:
327
+ if not self.is_monitoring or not self.sample_stores.get(DISK):
328
+ return 0.0
329
+ try:
330
+ current_disk_samples: list[float] = await self._get_samples(TaskChoice.DISK)
331
+ if current_disk_samples:
332
+ average_disk: float = round(sum(current_disk_samples) / len(current_disk_samples), 2)
333
+ return average_disk
334
+ except Exception as e:
335
+ print(f"Error getting disk usage: {e}")
336
+ return 0.0
337
+
338
+ async def get_avg_gpu_usage(self) -> float:
339
+ if not self.is_monitoring or not self.sample_stores.get(GPU):
340
+ return 0.0
341
+ try:
342
+ current_gpu_samples: list[GPUSamples] = await self._get_samples(TaskChoice.GPU)
343
+ if current_gpu_samples:
344
+ average_gpu: float = round(
345
+ sum(sample.gpu_usage for sample in current_gpu_samples) / len(current_gpu_samples), 2
346
+ )
347
+ return average_gpu
348
+ except Exception as e:
349
+ print(f"Error getting GPU usage: {e}")
350
+ return 0.0
@@ -0,0 +1,16 @@
1
+ from ..constants.date_related import DATE_FORMAT, DATE_TIME_FORMAT
2
+ from ._time_class import EpochTimestamp
3
+ from ._timer import TimerData, create_timer, timer
4
+ from ._tools import add_ord_suffix
5
+ from .time_manager import TimeTools
6
+
7
+ __all__ = [
8
+ "EpochTimestamp",
9
+ "TimerData",
10
+ "create_timer",
11
+ "timer",
12
+ "TimeTools",
13
+ "add_ord_suffix",
14
+ "DATE_FORMAT",
15
+ "DATE_TIME_FORMAT",
16
+ ]
@@ -0,0 +1,91 @@
1
+ import re
2
+ from datetime import timedelta
3
+
4
+ from ..constants.time_related import SECONDS_IN_DAY, SECONDS_IN_HOUR, SECONDS_IN_MINUTE, SECONDS_IN_MONTH
5
+
6
+
7
+ def convert_to_seconds(time_str: str) -> int:
8
+ """Convert a time string to seconds.
9
+
10
+ Examples
11
+ --------
12
+ >>> convert_to_seconds("1M 30m")
13
+ 2610000
14
+
15
+ Notes
16
+ -----
17
+ * ``M`` or ``mo`` denotes **months**.
18
+ * ``m`` denotes **minutes**.
19
+ """
20
+
21
+ time_parts: list[tuple[str, str]] = re.findall(r"(\d+)\s*(M|mo|[dhms])", time_str)
22
+ total_seconds = 0
23
+ for value, unit in time_parts:
24
+ if not value.isdigit():
25
+ raise ValueError(f"Invalid time value: {value}")
26
+ value = int(value)
27
+
28
+ if unit == "M" or unit.lower() == "mo":
29
+ total_seconds += value * SECONDS_IN_MONTH
30
+ elif unit == "d":
31
+ total_seconds += value * SECONDS_IN_DAY
32
+ elif unit == "h":
33
+ total_seconds += value * SECONDS_IN_HOUR
34
+ elif unit == "m":
35
+ total_seconds += value * SECONDS_IN_MINUTE
36
+ elif unit == "s":
37
+ total_seconds += value
38
+ else:
39
+ raise ValueError(f"Invalid time unit: {unit}")
40
+ return total_seconds
41
+
42
+
43
+ def timedelta_to_seconds(td: timedelta) -> int:
44
+ """Convert a timedelta object to seconds."""
45
+ if not isinstance(td, timedelta):
46
+ raise ValueError("Input must be a timedelta object")
47
+ return int(td.total_seconds())
48
+
49
+
50
+ def convert_to_milliseconds(time_str: str) -> int:
51
+ return convert_to_seconds(time_str) * 1000
52
+
53
+
54
+ def milliseconds_to_time(milliseconds: int) -> str:
55
+ """Convert milliseconds to a human-readable time string."""
56
+ if milliseconds < 0:
57
+ raise ValueError("Milliseconds cannot be negative")
58
+ seconds = milliseconds // 1000
59
+ return seconds_to_time(seconds)
60
+
61
+
62
+ def seconds_to_timedelta(seconds: int) -> timedelta:
63
+ """Convert seconds to a timedelta object."""
64
+ if seconds < 0:
65
+ raise ValueError("Seconds cannot be negative")
66
+ return timedelta(seconds=seconds)
67
+
68
+
69
+ def seconds_to_time(seconds: int) -> str:
70
+ """Convert seconds to a human-readable time string.
71
+
72
+ Months are represented with ``M`` while minutes use ``m``.
73
+ """
74
+ if seconds < 0:
75
+ raise ValueError("Seconds cannot be negative")
76
+ months, reminder = divmod(seconds, SECONDS_IN_MONTH)
77
+ days, reminder = divmod(reminder, SECONDS_IN_DAY)
78
+ hours, reminder = divmod(reminder, SECONDS_IN_HOUR)
79
+ minutes, seconds = divmod(reminder, SECONDS_IN_MINUTE)
80
+ time_parts = []
81
+ if months > 0:
82
+ time_parts.append(f"{months}M")
83
+ if days > 0:
84
+ time_parts.append(f"{days}d")
85
+ if hours > 0:
86
+ time_parts.append(f"{hours}h")
87
+ if minutes > 0:
88
+ time_parts.append(f"{minutes}m")
89
+ if seconds > 0:
90
+ time_parts.append(f"{seconds}s")
91
+ return " ".join(time_parts)