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,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,4 @@
1
+ from .file_handlers import FileHandlerFactory
2
+ from .ignore_parser import IGNORE_PATTERNS
3
+
4
+ __all__ = ["FileHandlerFactory", "IGNORE_PATTERNS"]
@@ -0,0 +1,3 @@
1
+ from .file_handler_factory import FileHandlerFactory
2
+
3
+ __all__ = ["FileHandlerFactory"]
@@ -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
+ }