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.
- bear_utils/__init__.py +13 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +130 -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 +188 -0
- bear_utils/ai/ai_helpers/_types.py +20 -0
- bear_utils/cache/__init__.py +119 -0
- bear_utils/cli/__init__.py +4 -0
- bear_utils/cli/commands.py +59 -0
- bear_utils/cli/prompt_helpers.py +166 -0
- bear_utils/cli/shell/__init__.py +0 -0
- bear_utils/cli/shell/_base_command.py +74 -0
- bear_utils/cli/shell/_base_shell.py +390 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/config/__init__.py +11 -0
- bear_utils/config/config_manager.py +92 -0
- bear_utils/config/dir_manager.py +64 -0
- bear_utils/config/settings_manager.py +232 -0
- bear_utils/constants/__init__.py +16 -0
- bear_utils/constants/_exceptions.py +3 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/date_related.py +36 -0
- bear_utils/constants/time_related.py +22 -0
- bear_utils/database/__init__.py +6 -0
- bear_utils/database/_db_manager.py +104 -0
- bear_utils/events/__init__.py +16 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +65 -0
- bear_utils/extras/__init__.py +17 -0
- bear_utils/extras/_async_helpers.py +15 -0
- bear_utils/extras/_tools.py +178 -0
- bear_utils/extras/platform_utils.py +53 -0
- bear_utils/extras/wrappers/__init__.py +0 -0
- bear_utils/extras/wrappers/add_methods.py +98 -0
- bear_utils/files/__init__.py +4 -0
- bear_utils/files/file_handlers/__init__.py +3 -0
- bear_utils/files/file_handlers/_base_file_handler.py +93 -0
- bear_utils/files/file_handlers/file_handler_factory.py +278 -0
- bear_utils/files/file_handlers/json_file_handler.py +44 -0
- bear_utils/files/file_handlers/log_file_handler.py +33 -0
- bear_utils/files/file_handlers/txt_file_handler.py +34 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +57 -0
- bear_utils/files/ignore_parser.py +298 -0
- bear_utils/graphics/__init__.py +4 -0
- bear_utils/graphics/bear_gradient.py +140 -0
- bear_utils/graphics/image_helpers.py +39 -0
- bear_utils/gui/__init__.py +3 -0
- bear_utils/gui/gui_tools/__init__.py +5 -0
- bear_utils/gui/gui_tools/_settings.py +37 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +145 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +119 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +138 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +306 -0
- bear_utils/logging/__init__.py +25 -0
- bear_utils/logging/logger_manager/__init__.py +0 -0
- bear_utils/logging/logger_manager/_common.py +47 -0
- bear_utils/logging/logger_manager/_console_junk.py +131 -0
- bear_utils/logging/logger_manager/_styles.py +91 -0
- bear_utils/logging/logger_manager/loggers/__init__.py +0 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +238 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +50 -0
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +55 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.py +249 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +64 -0
- bear_utils/logging/logger_manager/loggers/_file_logger.py +141 -0
- bear_utils/logging/logger_manager/loggers/_level_sin.py +58 -0
- bear_utils/logging/logger_manager/loggers/_logger.py +18 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +110 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +38 -0
- bear_utils/logging/loggers.py +76 -0
- bear_utils/monitoring/__init__.py +10 -0
- bear_utils/monitoring/host_monitor.py +350 -0
- bear_utils/time/__init__.py +16 -0
- bear_utils/time/_helpers.py +91 -0
- bear_utils/time/_time_class.py +316 -0
- bear_utils/time/_timer.py +80 -0
- bear_utils/time/_tools.py +17 -0
- bear_utils/time/time_manager.py +218 -0
- bear_utils-0.7.11.dist-info/METADATA +260 -0
- bear_utils-0.7.11.dist-info/RECORD +83 -0
- bear_utils-0.7.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
import asyncio
|
2
|
+
import weakref
|
3
|
+
from collections import defaultdict
|
4
|
+
from collections.abc import Callable
|
5
|
+
from functools import wraps
|
6
|
+
from types import MethodType
|
7
|
+
from typing import Any, TypeAlias
|
8
|
+
from weakref import WeakMethod, ref
|
9
|
+
|
10
|
+
from ..extras._async_helpers import is_async_function
|
11
|
+
|
12
|
+
Callback: TypeAlias = Callable[..., Any]
|
13
|
+
|
14
|
+
_event_registry: dict[str, weakref.WeakSet[Callback]] = defaultdict(weakref.WeakSet)
|
15
|
+
|
16
|
+
|
17
|
+
def clear_handlers_for_event(event_name: str) -> None:
|
18
|
+
_event_registry.pop(event_name, None)
|
19
|
+
|
20
|
+
|
21
|
+
def clear_all() -> None:
|
22
|
+
_event_registry.clear()
|
23
|
+
|
24
|
+
|
25
|
+
def _make_callback(name: str) -> Callable[[Any], None]:
|
26
|
+
"""Create an internal callback to remove dead handlers."""
|
27
|
+
|
28
|
+
def callback(weak_method: Any) -> None:
|
29
|
+
_event_registry[name].remove(weak_method)
|
30
|
+
if not _event_registry[name]:
|
31
|
+
del _event_registry[name]
|
32
|
+
|
33
|
+
return callback
|
34
|
+
|
35
|
+
|
36
|
+
def set_handler(name: str, func: Callback) -> None:
|
37
|
+
if isinstance(func, MethodType):
|
38
|
+
_event_registry[name].add(WeakMethod(func, _make_callback(name)))
|
39
|
+
else:
|
40
|
+
_event_registry[name].add(ref(func, _make_callback(name)))
|
41
|
+
|
42
|
+
|
43
|
+
def dispatch_event(name: str, *args, **kwargs) -> Any | None:
|
44
|
+
results = list()
|
45
|
+
for func in _event_registry.get(name, []):
|
46
|
+
if is_async_function(func):
|
47
|
+
result = asyncio.run(func(*args, **kwargs))
|
48
|
+
else:
|
49
|
+
result = func(*args, **kwargs)
|
50
|
+
results.append(result)
|
51
|
+
if not results:
|
52
|
+
return None
|
53
|
+
return results[0] if len(results) == 1 else results
|
54
|
+
|
55
|
+
|
56
|
+
def event_handler(event_name: str) -> Callable[[Callback], Callback]:
|
57
|
+
def decorator(callback: Callback) -> Callback:
|
58
|
+
@wraps(callback)
|
59
|
+
def wrapper(*args, **kwargs):
|
60
|
+
return callback(*args, **kwargs)
|
61
|
+
|
62
|
+
set_handler(event_name, wrapper)
|
63
|
+
return wrapper
|
64
|
+
|
65
|
+
return decorator
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from ._tools import ClipboardManager, clear_clipboard, copy_to_clipboard, fmt_header, paste_from_clipboard
|
2
|
+
from .platform_utils import OS, get_platform, is_linux, is_macos, is_windows
|
3
|
+
from .wrappers.add_methods import add_comparison_methods
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
"OS",
|
7
|
+
"get_platform",
|
8
|
+
"is_linux",
|
9
|
+
"is_macos",
|
10
|
+
"is_windows",
|
11
|
+
"ClipboardManager",
|
12
|
+
"copy_to_clipboard",
|
13
|
+
"paste_from_clipboard",
|
14
|
+
"clear_clipboard",
|
15
|
+
"fmt_header",
|
16
|
+
"add_comparison_methods",
|
17
|
+
]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import inspect
|
2
|
+
from collections.abc import Callable
|
3
|
+
|
4
|
+
|
5
|
+
def is_async_function(func: Callable) -> bool:
|
6
|
+
"""
|
7
|
+
Check if a function is asynchronous.
|
8
|
+
|
9
|
+
Args:
|
10
|
+
func (Callable): The function/method to check.
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
bool: True if the function is asynchronous, False otherwise.
|
14
|
+
"""
|
15
|
+
return inspect.iscoroutinefunction(func) or inspect.isasyncgenfunction(func) or inspect.isasyncgen(func)
|
@@ -0,0 +1,178 @@
|
|
1
|
+
import asyncio
|
2
|
+
import shutil
|
3
|
+
from collections import deque
|
4
|
+
from functools import cached_property
|
5
|
+
from subprocess import CompletedProcess
|
6
|
+
|
7
|
+
from ..cli.shell._base_command import BaseShellCommand as ShellCommand
|
8
|
+
from ..cli.shell._base_shell import AsyncShellSession
|
9
|
+
from ..logging.logger_manager.loggers._base_logger import BaseLogger
|
10
|
+
from .platform_utils import OS, get_platform
|
11
|
+
|
12
|
+
|
13
|
+
class TextHelper:
|
14
|
+
@cached_property
|
15
|
+
def local_console(self) -> BaseLogger:
|
16
|
+
from ..logging.loggers import BaseLogger
|
17
|
+
|
18
|
+
init: bool = not BaseLogger.has_instance()
|
19
|
+
return BaseLogger.get_instance(init=init)
|
20
|
+
|
21
|
+
def print_header(self, title: str, sep="#", len=60, s1="bold red", s2="bold blue", return_txt: bool = False) -> str:
|
22
|
+
"""Generate a header string"""
|
23
|
+
# FIXME: There are probably better ways to do this, but this is OK.
|
24
|
+
fill: str = sep * len
|
25
|
+
title = f" {title} ".center(len, sep).replace(title, f"[{s1}]{title}[/{s1}]")
|
26
|
+
output_text: str = f"\n{fill}\n{title}\n{fill}\n"
|
27
|
+
if not return_txt:
|
28
|
+
self.local_console.print(output_text, style=s2)
|
29
|
+
return output_text
|
30
|
+
|
31
|
+
|
32
|
+
class ClipboardManager:
|
33
|
+
"""
|
34
|
+
A class to manage clipboard operations such as copying, pasting, and clearing.
|
35
|
+
This class provides methods to interact with the system clipboard.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, maxlen: int = 10) -> None:
|
39
|
+
self.clipboard_history = deque(maxlen=maxlen)
|
40
|
+
self.shell = AsyncShellSession(env={"LANG": "en_US.UTF-8"}, verbose=False)
|
41
|
+
self._copy: ShellCommand[str]
|
42
|
+
self._paste: ShellCommand[str]
|
43
|
+
|
44
|
+
platform: OS = get_platform()
|
45
|
+
match platform:
|
46
|
+
case OS.DARWIN:
|
47
|
+
self._copy = ShellCommand.adhoc("pbcopy")
|
48
|
+
self._paste = ShellCommand.adhoc("pbpaste")
|
49
|
+
case OS.LINUX:
|
50
|
+
if shutil.which("wl-copy") and shutil.which("wl-paste"):
|
51
|
+
self._copy = ShellCommand.adhoc("wl-copy")
|
52
|
+
self._paste = ShellCommand.adhoc("wl-paste")
|
53
|
+
elif shutil.which("xclip"):
|
54
|
+
self._copy = ShellCommand.adhoc("xclip").sub("-selection", "clipboard")
|
55
|
+
self._paste = ShellCommand.adhoc("xclip").sub("-selection", "clipboard").value("-o")
|
56
|
+
else:
|
57
|
+
raise RuntimeError("No clipboard command found on Linux")
|
58
|
+
case OS.WINDOWS:
|
59
|
+
self._copy = ShellCommand.adhoc("clip")
|
60
|
+
self._paste = ShellCommand.adhoc("powershell").sub("Get-Clipboard")
|
61
|
+
case _:
|
62
|
+
raise RuntimeError(f"Unsupported platform: {platform}")
|
63
|
+
|
64
|
+
def get_history(self) -> deque:
|
65
|
+
"""Get the clipboard history.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
deque: The history of clipboard entries.
|
69
|
+
"""
|
70
|
+
return self.clipboard_history
|
71
|
+
|
72
|
+
async def copy(self, output: str) -> int:
|
73
|
+
"""
|
74
|
+
A function that copies the output to the clipboard.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
output (str): The output to copy to the clipboard.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
int: The return code of the command.
|
81
|
+
"""
|
82
|
+
await self.shell.run(self._copy)
|
83
|
+
result: CompletedProcess[str] = await self.shell.communicate(stdin=output)
|
84
|
+
if result.returncode == 0:
|
85
|
+
self.clipboard_history.append(output) # Only append to history if the copy was successful
|
86
|
+
return result.returncode
|
87
|
+
|
88
|
+
async def paste(self) -> str:
|
89
|
+
"""
|
90
|
+
Paste the output from the clipboard.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
str: The content of the clipboard.
|
94
|
+
|
95
|
+
Raises:
|
96
|
+
RuntimeError: If the paste command fails.
|
97
|
+
"""
|
98
|
+
try:
|
99
|
+
await self.shell.run(self._paste)
|
100
|
+
result: CompletedProcess[str] = await self.shell.communicate()
|
101
|
+
except Exception as e: # pragma: no cover - safety net for unforeseen shell errors
|
102
|
+
raise RuntimeError(f"Error pasting from clipboard: {e}") from e
|
103
|
+
if result.returncode != 0:
|
104
|
+
raise RuntimeError(f"{self._paste.cmd} failed with return code {result.returncode}")
|
105
|
+
return result.stdout
|
106
|
+
|
107
|
+
async def clear(self) -> int:
|
108
|
+
"""
|
109
|
+
A function that clears the clipboard.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
int: The return code of the command.
|
113
|
+
"""
|
114
|
+
return await self.copy("")
|
115
|
+
|
116
|
+
|
117
|
+
def copy_to_clipboard(output: str) -> int:
|
118
|
+
"""
|
119
|
+
Copy the output to the clipboard.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
output (str): The output to copy to the clipboard.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
int: The return code of the command.
|
126
|
+
"""
|
127
|
+
clipboard_manager = ClipboardManager()
|
128
|
+
loop = asyncio.get_event_loop()
|
129
|
+
return loop.run_until_complete(clipboard_manager.copy(output))
|
130
|
+
|
131
|
+
|
132
|
+
def paste_from_clipboard() -> str:
|
133
|
+
"""
|
134
|
+
Paste the output from the clipboard.
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
str: The content of the clipboard.
|
138
|
+
"""
|
139
|
+
clipboard_manager = ClipboardManager()
|
140
|
+
loop = asyncio.get_event_loop()
|
141
|
+
return loop.run_until_complete(clipboard_manager.paste())
|
142
|
+
|
143
|
+
|
144
|
+
def clear_clipboard() -> int:
|
145
|
+
"""
|
146
|
+
Clear the clipboard.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
int: The return code of the command.
|
150
|
+
"""
|
151
|
+
clipboard_manager = ClipboardManager()
|
152
|
+
loop = asyncio.get_event_loop()
|
153
|
+
return loop.run_until_complete(clipboard_manager.clear())
|
154
|
+
|
155
|
+
|
156
|
+
def fmt_header(
|
157
|
+
title: str,
|
158
|
+
sep: str = "#",
|
159
|
+
length: int = 60,
|
160
|
+
style1: str = "bold red",
|
161
|
+
style2: str = "bold blue",
|
162
|
+
print_out: bool = True,
|
163
|
+
) -> str:
|
164
|
+
"""
|
165
|
+
Generate a header string for visual tests.
|
166
|
+
|
167
|
+
Args:
|
168
|
+
title (str): The title to display in the header.
|
169
|
+
sep (str): The character to use for the separator. Defaults to '#'.
|
170
|
+
length (int): The total length of the header line. Defaults to 60.
|
171
|
+
style1 (str): The style for the title text. Defaults to 'bold red'.
|
172
|
+
style2 (str): The style for the separator text. Defaults to 'bold blue'.
|
173
|
+
"""
|
174
|
+
text_helper = TextHelper()
|
175
|
+
if print_out:
|
176
|
+
text_helper.print_header(title, sep, length, style1, style2, return_txt=False)
|
177
|
+
return ""
|
178
|
+
return text_helper.print_header(title, sep, length, style1, style2, return_txt=True)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import platform
|
2
|
+
from enum import StrEnum
|
3
|
+
|
4
|
+
|
5
|
+
class OS(StrEnum):
|
6
|
+
DARWIN = "Darwin"
|
7
|
+
LINUX = "Linux"
|
8
|
+
WINDOWS = "Windows"
|
9
|
+
OTHER = "Other"
|
10
|
+
|
11
|
+
|
12
|
+
DARWIN = OS.DARWIN
|
13
|
+
LINUX = OS.LINUX
|
14
|
+
WINDOWS = OS.WINDOWS
|
15
|
+
OTHER = OS.OTHER
|
16
|
+
|
17
|
+
|
18
|
+
def get_platform() -> OS:
|
19
|
+
"""Return the current operating system as an :class:`OS` enum.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
OS: The current operating system as an enum member, or `OS.OTHER` if the platform is not recognized.
|
23
|
+
"""
|
24
|
+
system = platform.system()
|
25
|
+
return OS(system) if system in OS.__members__.values() else OS.OTHER
|
26
|
+
|
27
|
+
|
28
|
+
def is_macos() -> bool:
|
29
|
+
"""Return ``True`` if running on macOS."""
|
30
|
+
return get_platform() == DARWIN
|
31
|
+
|
32
|
+
|
33
|
+
def is_windows() -> bool:
|
34
|
+
"""Return ``True`` if running on Windows."""
|
35
|
+
return get_platform() == WINDOWS
|
36
|
+
|
37
|
+
|
38
|
+
def is_linux() -> bool:
|
39
|
+
"""Return ``True`` if running on Linux."""
|
40
|
+
return get_platform() == LINUX
|
41
|
+
|
42
|
+
|
43
|
+
if __name__ == "__main__":
|
44
|
+
detected_platform: OS = get_platform()
|
45
|
+
match detected_platform:
|
46
|
+
case OS.DARWIN:
|
47
|
+
print("Detected macOS")
|
48
|
+
case OS.LINUX:
|
49
|
+
print("Detected Linux")
|
50
|
+
case OS.WINDOWS:
|
51
|
+
print("Detected Windows")
|
52
|
+
case _:
|
53
|
+
print(f"Detected unsupported platform: {detected_platform}")
|
File without changes
|
@@ -0,0 +1,98 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from types import NotImplementedType
|
3
|
+
from typing import Any, TypeVar
|
4
|
+
|
5
|
+
T = TypeVar("T")
|
6
|
+
|
7
|
+
PRIMITIVE_TYPES: tuple[type[str], type[int], type[float], type[bool]] = (str, int, float, bool)
|
8
|
+
|
9
|
+
|
10
|
+
def add_comparison_methods(attribute: str) -> Callable[[type[T]], type[T]]:
|
11
|
+
"""Class decorator that adds rich comparison methods based on a specific attribute.
|
12
|
+
|
13
|
+
This decorator adds __eq__, __ne__, __lt__, __gt__, __le__, __ge__, and __hash__ methods
|
14
|
+
to a class, all of which delegate to the specified attribute. This allows instances
|
15
|
+
of the decorated class to be compared with each other, as well as with primitive values
|
16
|
+
that the attribute can be compared with.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
attribute: Name of the instance attribute to use for comparisons
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Class decorator function that adds comparison methods to a class
|
23
|
+
|
24
|
+
Example:
|
25
|
+
@add_comparison_methods('name')
|
26
|
+
class Person:
|
27
|
+
def __init__(self, name):
|
28
|
+
self.name = name
|
29
|
+
"""
|
30
|
+
|
31
|
+
def decorator(cls: type[T]) -> type[T]:
|
32
|
+
def extract_comparable_value(self, other: Any) -> NotImplementedType | Any:
|
33
|
+
"""Helper to extract the comparable value from the other object."""
|
34
|
+
if isinstance(other, PRIMITIVE_TYPES):
|
35
|
+
return other
|
36
|
+
|
37
|
+
if hasattr(other, attribute):
|
38
|
+
return getattr(other, attribute)
|
39
|
+
|
40
|
+
return NotImplemented
|
41
|
+
|
42
|
+
def equals_method(self, other: Any) -> NotImplementedType | bool:
|
43
|
+
"""Equal comparison method (__eq__)."""
|
44
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
45
|
+
if other_val is NotImplemented:
|
46
|
+
return NotImplemented
|
47
|
+
return getattr(self, attribute) == other_val
|
48
|
+
|
49
|
+
def not_equals_method(self, other: Any) -> NotImplementedType | bool:
|
50
|
+
"""Not equal comparison method (__ne__)."""
|
51
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
52
|
+
if other_val is NotImplemented:
|
53
|
+
return NotImplemented
|
54
|
+
return getattr(self, attribute) != other_val
|
55
|
+
|
56
|
+
def less_than_method(self, other: Any) -> NotImplementedType | bool:
|
57
|
+
"""Less than comparison method (__lt__)."""
|
58
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
59
|
+
if other_val is NotImplemented:
|
60
|
+
return NotImplemented
|
61
|
+
return getattr(self, attribute) < other_val
|
62
|
+
|
63
|
+
def greater_than_method(self, other: Any) -> NotImplementedType | bool:
|
64
|
+
"""Greater than comparison method (__gt__)."""
|
65
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
66
|
+
if other_val is NotImplemented:
|
67
|
+
return NotImplemented
|
68
|
+
return getattr(self, attribute) > other_val
|
69
|
+
|
70
|
+
def less_than_or_equal_method(self, other: Any) -> NotImplementedType | bool:
|
71
|
+
"""Less than or equal comparison method (__le__)."""
|
72
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
73
|
+
if other_val is NotImplemented:
|
74
|
+
return NotImplemented
|
75
|
+
return getattr(self, attribute) <= other_val
|
76
|
+
|
77
|
+
def greater_than_or_equal_method(self, other: Any) -> NotImplementedType | bool:
|
78
|
+
"""Greater than or equal comparison method (__ge__)."""
|
79
|
+
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
80
|
+
if other_val is NotImplemented:
|
81
|
+
return NotImplemented
|
82
|
+
return getattr(self, attribute) >= other_val
|
83
|
+
|
84
|
+
def hash_method(self) -> int:
|
85
|
+
"""Generate hash based on the attribute used for equality."""
|
86
|
+
return hash(getattr(self, attribute))
|
87
|
+
|
88
|
+
setattr(cls, "__eq__", equals_method)
|
89
|
+
setattr(cls, "__ne__", not_equals_method)
|
90
|
+
setattr(cls, "__lt__", less_than_method)
|
91
|
+
setattr(cls, "__gt__", greater_than_method)
|
92
|
+
setattr(cls, "__le__", less_than_or_equal_method)
|
93
|
+
setattr(cls, "__ge__", greater_than_or_equal_method)
|
94
|
+
setattr(cls, "__hash__", hash_method)
|
95
|
+
|
96
|
+
return cls
|
97
|
+
|
98
|
+
return decorator
|
@@ -0,0 +1,93 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from collections.abc import Callable
|
3
|
+
from functools import wraps
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, ClassVar, ParamSpec, TypeVar, cast
|
6
|
+
|
7
|
+
P = ParamSpec("P")
|
8
|
+
R = TypeVar("R")
|
9
|
+
|
10
|
+
|
11
|
+
class FileHandler(ABC):
|
12
|
+
"""Abstract class for file handling with read, write, and present methods
|
13
|
+
|
14
|
+
:attr ext str: File extension to check for.
|
15
|
+
:method file_checker: Class method to check if file is of correct type.
|
16
|
+
:method read_file: Read file method.
|
17
|
+
:method write_file: Write file method.
|
18
|
+
:method present_file: Present file method.
|
19
|
+
"""
|
20
|
+
|
21
|
+
valid_extensions: ClassVar[list[str]] = []
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def file_checker(cls, file_path: Path) -> bool:
|
25
|
+
"""Check if the file is of the correct type.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
file_path: Path to the file
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
bool: True if the file is of the correct type, False otherwise
|
32
|
+
"""
|
33
|
+
return file_path.suffix.lstrip(".") in cls.valid_extensions
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def ValidateFileType(cls, method: Callable[P, R]) -> Callable[P, R]:
|
37
|
+
"""Decorator to validate file type before executing a method.
|
38
|
+
|
39
|
+
This decorator checks if the file is of the correct type before
|
40
|
+
executing the method. If not, it raises a ValueError.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
method: Method to decorate
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Decorated method
|
47
|
+
"""
|
48
|
+
|
49
|
+
@wraps(method)
|
50
|
+
def wrapper(self: "FileHandler", file_path: Path, *args: Any, **kwargs: Any) -> R:
|
51
|
+
if not self.file_checker(file_path):
|
52
|
+
raise ValueError(f"Invalid file type. Expected {self.valid_extensions}")
|
53
|
+
return method(self, file_path, *args, **kwargs) # type: ignore
|
54
|
+
|
55
|
+
return cast(Callable[P, R], wrapper)
|
56
|
+
|
57
|
+
@abstractmethod
|
58
|
+
def read_file(self, file_path: Path) -> dict[str, Any] | str:
|
59
|
+
if not file_path.exists():
|
60
|
+
raise ValueError(f"File does not exist: {file_path}")
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
def write_file(self, file_path: Path, data: dict[str, Any] | str, **kwargs) -> None:
|
64
|
+
if not file_path.parent.exists():
|
65
|
+
if kwargs.get("mkdir", False):
|
66
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
67
|
+
else:
|
68
|
+
raise ValueError(f"Directory does not exist: {file_path.parent}. Set mkdir=True to create it.")
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
def present_file(self, data: dict[str, Any] | str) -> str: ...
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
def get_file_info(file_path: Path) -> dict[str, Any]:
|
75
|
+
"""Get information about a file.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
file_path: Path to the file
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Dictionary with file information
|
82
|
+
"""
|
83
|
+
if not file_path.exists():
|
84
|
+
raise ValueError(f"File does not exist: {file_path}")
|
85
|
+
|
86
|
+
return {
|
87
|
+
"path": file_path,
|
88
|
+
"name": file_path.name,
|
89
|
+
"extension": file_path.suffix,
|
90
|
+
"size": file_path.stat().st_size if file_path.exists() else 0,
|
91
|
+
"is_file": file_path.is_file() if file_path.exists() else False,
|
92
|
+
"modified": file_path.stat().st_mtime if file_path.exists() else None,
|
93
|
+
}
|