bear-utils 0.7.21__py3-none-any.whl → 0.7.23__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 +24 -1
- bear_utils/ai/__init__.py +5 -5
- bear_utils/ai/ai_helpers/__init__.py +24 -18
- bear_utils/ai/ai_helpers/_parsers.py +27 -21
- bear_utils/ai/ai_helpers/_types.py +2 -7
- bear_utils/cache/__init__.py +35 -23
- bear_utils/cli/__init__.py +13 -0
- bear_utils/cli/commands.py +14 -8
- bear_utils/cli/prompt_helpers.py +40 -34
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +18 -18
- bear_utils/cli/shell/_base_shell.py +37 -34
- bear_utils/config/__init__.py +4 -2
- bear_utils/config/config_manager.py +193 -56
- bear_utils/config/dir_manager.py +8 -3
- bear_utils/config/settings_manager.py +94 -171
- bear_utils/constants/__init__.py +2 -1
- bear_utils/constants/_exceptions.py +6 -1
- bear_utils/constants/date_related.py +2 -0
- bear_utils/constants/logger_protocol.py +28 -0
- bear_utils/constants/time_related.py +2 -0
- bear_utils/database/__init__.py +2 -0
- bear_utils/database/_db_manager.py +10 -11
- bear_utils/events/__init__.py +3 -1
- bear_utils/events/events_class.py +11 -11
- bear_utils/events/events_module.py +17 -8
- bear_utils/extras/__init__.py +8 -6
- bear_utils/extras/_async_helpers.py +2 -3
- bear_utils/extras/_tools.py +62 -52
- bear_utils/extras/platform_utils.py +5 -1
- bear_utils/extras/responses/__init__.py +1 -0
- bear_utils/extras/responses/function_response.py +301 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +17 -15
- bear_utils/files/__init__.py +3 -1
- bear_utils/files/file_handlers/__init__.py +2 -0
- bear_utils/files/file_handlers/_base_file_handler.py +23 -3
- bear_utils/files/file_handlers/file_handler_factory.py +38 -38
- bear_utils/files/file_handlers/json_file_handler.py +49 -22
- bear_utils/files/file_handlers/log_file_handler.py +19 -12
- bear_utils/files/file_handlers/toml_file_handler.py +13 -5
- bear_utils/files/file_handlers/txt_file_handler.py +56 -14
- bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
- bear_utils/files/ignore_parser.py +52 -57
- bear_utils/graphics/__init__.py +3 -1
- bear_utils/graphics/bear_gradient.py +17 -12
- bear_utils/graphics/image_helpers.py +11 -5
- bear_utils/gui/__init__.py +3 -1
- bear_utils/gui/gui_tools/__init__.py +3 -1
- bear_utils/gui/gui_tools/_settings.py +0 -1
- bear_utils/gui/gui_tools/qt_app.py +16 -11
- bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
- bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
- bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
- bear_utils/logging/__init__.py +6 -4
- bear_utils/logging/logger_manager/__init__.py +1 -0
- bear_utils/logging/logger_manager/_common.py +0 -1
- bear_utils/logging/logger_manager/_console_junk.py +14 -10
- bear_utils/logging/logger_manager/_styles.py +1 -2
- bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -36
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
- bear_utils/logging/logger_manager/loggers/_console_logger.py +52 -26
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
- bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
- bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
- bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
- bear_utils/logging/loggers.py +9 -13
- bear_utils/monitoring/__init__.py +7 -4
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +44 -48
- bear_utils/time/__init__.py +13 -6
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/METADATA +50 -6
- bear_utils-0.7.23.dist-info/RECORD +83 -0
- bear_utils-0.7.21.dist-info/RECORD +0 -79
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/WHEEL +0 -0
bear_utils/extras/_tools.py
CHANGED
@@ -1,29 +1,40 @@
|
|
1
1
|
import asyncio
|
2
|
-
import shutil
|
3
2
|
from asyncio.subprocess import PIPE
|
4
3
|
from collections import deque
|
5
4
|
from functools import cached_property
|
6
|
-
|
5
|
+
import shutil
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
from bear_utils.cli.shell._base_command import BaseShellCommand as ShellCommand
|
9
|
+
from bear_utils.cli.shell._base_shell import AsyncShellSession
|
10
|
+
from bear_utils.extras.platform_utils import OS, get_platform
|
11
|
+
from bear_utils.logging.logger_manager.loggers._base_logger import BaseLogger
|
7
12
|
|
8
|
-
|
9
|
-
from
|
10
|
-
from ..logging.logger_manager.loggers._base_logger import BaseLogger
|
11
|
-
from .platform_utils import OS, get_platform
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from subprocess import CompletedProcess
|
12
15
|
|
13
16
|
|
14
17
|
class TextHelper:
|
15
18
|
@cached_property
|
16
19
|
def local_console(self) -> BaseLogger:
|
17
|
-
from
|
20
|
+
from bear_utils.logging.loggers import BaseLogger # noqa: PLC0415
|
18
21
|
|
19
22
|
init: bool = not BaseLogger.has_instance()
|
20
23
|
return BaseLogger.get_instance(init=init)
|
21
24
|
|
22
|
-
def print_header(
|
25
|
+
def print_header(
|
26
|
+
self,
|
27
|
+
title: str,
|
28
|
+
sep: str = "#",
|
29
|
+
length: int = 60,
|
30
|
+
s1: str = "bold red",
|
31
|
+
s2: str = "bold blue",
|
32
|
+
return_txt: bool = False,
|
33
|
+
) -> str:
|
23
34
|
"""Generate a header string"""
|
24
35
|
# FIXME: There are probably better ways to do this, but this is OK.
|
25
|
-
fill: str = sep *
|
26
|
-
title = f" {title} ".center(
|
36
|
+
fill: str = sep * length
|
37
|
+
title = f" {title} ".center(length, sep).replace(title, f"[{s1}]{title}[/{s1}]")
|
27
38
|
output_text: str = f"\n{fill}\n{title}\n{fill}\n"
|
28
39
|
if not return_txt:
|
29
40
|
self.local_console.print(output_text, style=s2)
|
@@ -31,13 +42,14 @@ class TextHelper:
|
|
31
42
|
|
32
43
|
|
33
44
|
class ClipboardManager:
|
34
|
-
"""
|
35
|
-
|
45
|
+
"""A class to manage clipboard operations such as copying, pasting, and clearing.
|
46
|
+
|
36
47
|
This class provides methods to interact with the system clipboard.
|
37
48
|
"""
|
38
49
|
|
39
50
|
def __init__(self, maxlen: int = 10) -> None:
|
40
|
-
|
51
|
+
"""Initialize the ClipboardManager with a maximum history length."""
|
52
|
+
self.clipboard_history: deque[str] = deque(maxlen=maxlen)
|
41
53
|
self.shell = AsyncShellSession(env={"LANG": "en_US.UTF-8"}, verbose=False)
|
42
54
|
self._copy: ShellCommand[str]
|
43
55
|
self._paste: ShellCommand[str]
|
@@ -45,23 +57,31 @@ class ClipboardManager:
|
|
45
57
|
platform: OS = get_platform()
|
46
58
|
match platform:
|
47
59
|
case OS.DARWIN:
|
48
|
-
self._copy = ShellCommand.adhoc("pbcopy")
|
49
|
-
self._paste = ShellCommand.adhoc("pbpaste")
|
60
|
+
self._copy = ShellCommand.adhoc(name="pbcopy")
|
61
|
+
self._paste = ShellCommand.adhoc(name="pbpaste")
|
50
62
|
case OS.LINUX:
|
51
|
-
if shutil.which("wl-copy") and shutil.which("wl-paste"):
|
52
|
-
self._copy = ShellCommand.adhoc("wl-copy")
|
53
|
-
self._paste = ShellCommand.adhoc("wl-paste")
|
54
|
-
elif shutil.which("xclip"):
|
55
|
-
self._copy = ShellCommand.adhoc("xclip").sub("-selection", "clipboard")
|
56
|
-
self._paste = ShellCommand.adhoc("xclip").sub("-selection", "clipboard").value("-o")
|
63
|
+
if shutil.which(cmd="wl-copy") and shutil.which(cmd="wl-paste"):
|
64
|
+
self._copy = ShellCommand.adhoc(name="wl-copy")
|
65
|
+
self._paste = ShellCommand.adhoc(name="wl-paste")
|
66
|
+
elif shutil.which(cmd="xclip"):
|
67
|
+
self._copy = ShellCommand.adhoc(name="xclip").sub("-selection", "clipboard")
|
68
|
+
self._paste = ShellCommand.adhoc(name="xclip").sub("-selection", "clipboard").value("-o")
|
57
69
|
else:
|
58
70
|
raise RuntimeError("No clipboard command found on Linux")
|
59
71
|
case OS.WINDOWS:
|
60
|
-
self._copy = ShellCommand.adhoc("clip")
|
61
|
-
self._paste = ShellCommand.adhoc("powershell").sub("Get-Clipboard")
|
72
|
+
self._copy = ShellCommand.adhoc(name="clip")
|
73
|
+
self._paste = ShellCommand.adhoc(name="powershell").sub("Get-Clipboard")
|
62
74
|
case _:
|
63
75
|
raise RuntimeError(f"Unsupported platform: {platform}")
|
64
76
|
|
77
|
+
def _copy_cmd(self) -> ShellCommand[str]:
|
78
|
+
"""Get the copy command based on the platform."""
|
79
|
+
return self._copy
|
80
|
+
|
81
|
+
def _paste_cmd(self) -> ShellCommand[str]:
|
82
|
+
"""Get the paste command based on the platform."""
|
83
|
+
return self._paste
|
84
|
+
|
65
85
|
def get_history(self) -> deque:
|
66
86
|
"""Get the clipboard history.
|
67
87
|
|
@@ -71,8 +91,7 @@ class ClipboardManager:
|
|
71
91
|
return self.clipboard_history
|
72
92
|
|
73
93
|
async def copy(self, output: str) -> int:
|
74
|
-
"""
|
75
|
-
A function that copies the output to the clipboard.
|
94
|
+
"""A function that copies the output to the clipboard.
|
76
95
|
|
77
96
|
Args:
|
78
97
|
output (str): The output to copy to the clipboard.
|
@@ -80,15 +99,14 @@ class ClipboardManager:
|
|
80
99
|
Returns:
|
81
100
|
int: The return code of the command.
|
82
101
|
"""
|
83
|
-
await self.shell.run(self._copy, stdin=PIPE)
|
102
|
+
await self.shell.run(cmd=self._copy, stdin=PIPE)
|
84
103
|
result: CompletedProcess[str] = await self.shell.communicate(stdin=output)
|
85
104
|
if result.returncode == 0:
|
86
105
|
self.clipboard_history.append(output) # Only append to history if the copy was successful
|
87
106
|
return result.returncode
|
88
107
|
|
89
108
|
async def paste(self) -> str:
|
90
|
-
"""
|
91
|
-
Paste the output from the clipboard.
|
109
|
+
"""Paste the output from the clipboard.
|
92
110
|
|
93
111
|
Returns:
|
94
112
|
str: The content of the clipboard.
|
@@ -97,27 +115,25 @@ class ClipboardManager:
|
|
97
115
|
RuntimeError: If the paste command fails.
|
98
116
|
"""
|
99
117
|
try:
|
100
|
-
await self.shell.run(self._paste)
|
118
|
+
await self.shell.run(cmd=self._paste)
|
101
119
|
result: CompletedProcess[str] = await self.shell.communicate()
|
102
|
-
except Exception as e:
|
120
|
+
except Exception as e:
|
103
121
|
raise RuntimeError(f"Error pasting from clipboard: {e}") from e
|
104
122
|
if result.returncode != 0:
|
105
123
|
raise RuntimeError(f"{self._paste.cmd} failed with return code {result.returncode}")
|
106
124
|
return result.stdout
|
107
125
|
|
108
126
|
async def clear(self) -> int:
|
109
|
-
"""
|
110
|
-
A function that clears the clipboard.
|
127
|
+
"""A function that clears the clipboard.
|
111
128
|
|
112
129
|
Returns:
|
113
130
|
int: The return code of the command.
|
114
131
|
"""
|
115
|
-
return await self.copy("")
|
132
|
+
return await self.copy(output="")
|
116
133
|
|
117
134
|
|
118
135
|
def copy_to_clipboard(output: str) -> int:
|
119
|
-
"""
|
120
|
-
Copy the output to the clipboard.
|
136
|
+
"""Copy the output to the clipboard.
|
121
137
|
|
122
138
|
Args:
|
123
139
|
output (str): The output to copy to the clipboard.
|
@@ -127,12 +143,11 @@ def copy_to_clipboard(output: str) -> int:
|
|
127
143
|
"""
|
128
144
|
clipboard_manager = ClipboardManager()
|
129
145
|
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
130
|
-
return loop.run_until_complete(clipboard_manager.copy(output))
|
146
|
+
return loop.run_until_complete(future=clipboard_manager.copy(output))
|
131
147
|
|
132
148
|
|
133
149
|
async def copy_to_clipboard_async(output: str) -> int:
|
134
|
-
"""
|
135
|
-
Asynchronously copy the output to the clipboard.
|
150
|
+
"""Asynchronously copy the output to the clipboard.
|
136
151
|
|
137
152
|
Args:
|
138
153
|
output (str): The output to copy to the clipboard.
|
@@ -141,24 +156,22 @@ async def copy_to_clipboard_async(output: str) -> int:
|
|
141
156
|
int: The return code of the command.
|
142
157
|
"""
|
143
158
|
clipboard_manager = ClipboardManager()
|
144
|
-
return await clipboard_manager.copy(output)
|
159
|
+
return await clipboard_manager.copy(output=output)
|
145
160
|
|
146
161
|
|
147
162
|
def paste_from_clipboard() -> str:
|
148
|
-
"""
|
149
|
-
Paste the output from the clipboard.
|
163
|
+
"""Paste the output from the clipboard.
|
150
164
|
|
151
165
|
Returns:
|
152
166
|
str: The content of the clipboard.
|
153
167
|
"""
|
154
168
|
clipboard_manager = ClipboardManager()
|
155
169
|
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
156
|
-
return loop.run_until_complete(clipboard_manager.paste())
|
170
|
+
return loop.run_until_complete(future=clipboard_manager.paste())
|
157
171
|
|
158
172
|
|
159
173
|
async def paste_from_clipboard_async() -> str:
|
160
|
-
"""
|
161
|
-
Asynchronously paste the output from the clipboard.
|
174
|
+
"""Asynchronously paste the output from the clipboard.
|
162
175
|
|
163
176
|
Returns:
|
164
177
|
str: The content of the clipboard.
|
@@ -168,8 +181,7 @@ async def paste_from_clipboard_async() -> str:
|
|
168
181
|
|
169
182
|
|
170
183
|
def clear_clipboard() -> int:
|
171
|
-
"""
|
172
|
-
Clear the clipboard.
|
184
|
+
"""Clear the clipboard.
|
173
185
|
|
174
186
|
Returns:
|
175
187
|
int: The return code of the command.
|
@@ -180,8 +192,7 @@ def clear_clipboard() -> int:
|
|
180
192
|
|
181
193
|
|
182
194
|
async def clear_clipboard_async() -> int:
|
183
|
-
"""
|
184
|
-
Asynchronously clear the clipboard.
|
195
|
+
"""Asynchronously clear the clipboard.
|
185
196
|
|
186
197
|
Returns:
|
187
198
|
int: The return code of the command.
|
@@ -198,8 +209,7 @@ def fmt_header(
|
|
198
209
|
style2: str = "bold blue",
|
199
210
|
print_out: bool = True,
|
200
211
|
) -> str:
|
201
|
-
"""
|
202
|
-
Generate a header string for visual tests.
|
212
|
+
"""Generate a header string for visual tests.
|
203
213
|
|
204
214
|
Args:
|
205
215
|
title (str): The title to display in the header.
|
@@ -210,6 +220,6 @@ def fmt_header(
|
|
210
220
|
"""
|
211
221
|
text_helper = TextHelper()
|
212
222
|
if print_out:
|
213
|
-
text_helper.print_header(title, sep, length, style1, style2, return_txt=False)
|
223
|
+
text_helper.print_header(title=title, sep=sep, length=length, s1=style1, s2=style2, return_txt=False)
|
214
224
|
return ""
|
215
|
-
return text_helper.print_header(title, sep, length, style1, style2, return_txt=True)
|
225
|
+
return text_helper.print_header(title=title, sep=sep, length=length, s1=style1, s2=style2, return_txt=True)
|
@@ -1,8 +1,12 @@
|
|
1
|
-
|
1
|
+
"""A module for detecting the current operating system."""
|
2
|
+
|
2
3
|
from enum import StrEnum
|
4
|
+
import platform
|
3
5
|
|
4
6
|
|
5
7
|
class OS(StrEnum):
|
8
|
+
"""Enumeration of operating systems."""
|
9
|
+
|
6
10
|
DARWIN = "Darwin"
|
7
11
|
LINUX = "Linux"
|
8
12
|
WINDOWS = "Windows"
|
@@ -0,0 +1 @@
|
|
1
|
+
"""A module for handling responses for functions, methods, and classes in Bear Utils."""
|
@@ -0,0 +1,301 @@
|
|
1
|
+
"""Function Response Class for handling function call results."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from io import StringIO
|
6
|
+
import json
|
7
|
+
from subprocess import CompletedProcess
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal, Self, overload
|
9
|
+
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from bear_utils.constants.logger_protocol import LoggerProtocol
|
14
|
+
|
15
|
+
SUCCESS: list[str] = ["name", "success"]
|
16
|
+
FAILURE: list[str] = ["name"]
|
17
|
+
|
18
|
+
|
19
|
+
class FunctionResponse(BaseModel):
|
20
|
+
"""A class to represent the response of a function call, including success status, content, and error messages."""
|
21
|
+
|
22
|
+
name: str = Field(default="", description="Name of the function that was called.")
|
23
|
+
returncode: int = Field(default=0, description="Return code of the function, 0 for success, !=0 for failure.")
|
24
|
+
extra: dict = Field(default_factory=dict, description="Additional metadata or information related to the response.")
|
25
|
+
content: list[str] = Field(default=[], description="Content returned by the function call")
|
26
|
+
error: list[str] = Field(default=[], description="Error message if the function call failed")
|
27
|
+
number_of_tasks: int = Field(default=0, description="Number of tasks processed in this response.")
|
28
|
+
logger: LoggerProtocol | None = Field(default=None, description="Logger instance for logging messages.")
|
29
|
+
|
30
|
+
model_config = {
|
31
|
+
"arbitrary_types_allowed": True,
|
32
|
+
}
|
33
|
+
|
34
|
+
def __repr__(self) -> str:
|
35
|
+
"""Return a string representation of Response."""
|
36
|
+
result = StringIO()
|
37
|
+
result.write("Response(")
|
38
|
+
if self.name:
|
39
|
+
result.write(f"name={self.name!r}, ")
|
40
|
+
if self.returncode:
|
41
|
+
result.write(f"success={self.success!r}, ")
|
42
|
+
if self.content:
|
43
|
+
content: str = ", ".join(self.content)
|
44
|
+
result.write(f"content={content!r}, ")
|
45
|
+
if self.error:
|
46
|
+
error: str = ", ".join(self.error)
|
47
|
+
result.write(f"error={error!r}, ")
|
48
|
+
if self.extra:
|
49
|
+
result.write(f"extra={json.dumps(self.extra)!r}, ")
|
50
|
+
if self.number_of_tasks > 0:
|
51
|
+
result.write(f"number_of_tasks={self.number_of_tasks!r}, ")
|
52
|
+
result.write(")")
|
53
|
+
returned_result: str = result.getvalue().replace(", )", ")")
|
54
|
+
result.close()
|
55
|
+
return returned_result
|
56
|
+
|
57
|
+
def __str__(self) -> str:
|
58
|
+
"""Return a string representation of Response."""
|
59
|
+
return self.__repr__()
|
60
|
+
|
61
|
+
@field_validator("name", mode="before")
|
62
|
+
@classmethod
|
63
|
+
def validate_name(cls, value: str | Any) -> str:
|
64
|
+
"""Ensure name is a string, lowercased, and without spaces."""
|
65
|
+
if value is None:
|
66
|
+
return ""
|
67
|
+
if not isinstance(value, str):
|
68
|
+
try:
|
69
|
+
value = str(value)
|
70
|
+
except Exception as e:
|
71
|
+
raise TypeError(f"Name must be a string, got {type(value).__name__}.") from e
|
72
|
+
return value.lower().replace(" ", "_")
|
73
|
+
|
74
|
+
@field_validator("returncode")
|
75
|
+
@classmethod
|
76
|
+
def validate_returncode(cls, value: int) -> int:
|
77
|
+
"""Ensure returncode is an integer above or equal to zero."""
|
78
|
+
if not isinstance(value, int) or value < 0:
|
79
|
+
raise ValueError("Return code must be a non-negative integer.")
|
80
|
+
return value
|
81
|
+
|
82
|
+
@field_validator("extra", mode="before")
|
83
|
+
@classmethod
|
84
|
+
def validate_extra(cls, value: dict | Any) -> dict:
|
85
|
+
"""Ensure extra is always a dictionary."""
|
86
|
+
if value is None:
|
87
|
+
return {}
|
88
|
+
if not isinstance(value, dict):
|
89
|
+
raise TypeError("Extra must be a dictionary.")
|
90
|
+
return value
|
91
|
+
|
92
|
+
@field_validator("content", mode="before")
|
93
|
+
@classmethod
|
94
|
+
def validate_content(cls, value: str | list[str] | Any) -> list[str]:
|
95
|
+
"""Ensure content is always a list of strings."""
|
96
|
+
if isinstance(value, str):
|
97
|
+
return [value]
|
98
|
+
if isinstance(value, list):
|
99
|
+
if not all(isinstance(item, str) for item in value):
|
100
|
+
raise TypeError("Content must be a list of strings.")
|
101
|
+
return value
|
102
|
+
raise TypeError("Content must be a string or a list of strings.")
|
103
|
+
|
104
|
+
@field_validator("error", mode="before")
|
105
|
+
@classmethod
|
106
|
+
def validate_error(cls, value: str | list[str] | Any) -> list[str]:
|
107
|
+
"""Ensure error is always a list of strings."""
|
108
|
+
if isinstance(value, str):
|
109
|
+
return [value]
|
110
|
+
if isinstance(value, list):
|
111
|
+
if not all(isinstance(item, str) for item in value):
|
112
|
+
raise TypeError("Error must be a list of strings.")
|
113
|
+
return value
|
114
|
+
raise TypeError("Error must be a string or a list of strings.")
|
115
|
+
|
116
|
+
@classmethod
|
117
|
+
def from_process(cls, process: CompletedProcess[str], **kwargs) -> Self:
|
118
|
+
"""Create a FunctionResponse from a CompletedProcess object."""
|
119
|
+
returncode: int = process.returncode if process.returncode is not None else 0
|
120
|
+
content: str = process.stdout.strip() if process.stdout else ""
|
121
|
+
error: str = process.stderr.strip() if process.stderr else ""
|
122
|
+
|
123
|
+
if returncode == 0 and not content and error:
|
124
|
+
error, content = content, error
|
125
|
+
|
126
|
+
return cls().add(returncode=returncode, content=content, error=error, **kwargs)
|
127
|
+
|
128
|
+
@property
|
129
|
+
def success(self) -> bool:
|
130
|
+
"""Check if the response indicates success."""
|
131
|
+
return self.returncode == 0
|
132
|
+
|
133
|
+
def successful(
|
134
|
+
self,
|
135
|
+
content: str | list[str] | CompletedProcess,
|
136
|
+
error: str | list[str] = "",
|
137
|
+
returncode: int | None = None,
|
138
|
+
**kwargs,
|
139
|
+
) -> Self:
|
140
|
+
"""Set the response to a success state with optional content."""
|
141
|
+
self.add(content=content, error=error, returncode=returncode or 0, **kwargs)
|
142
|
+
return self
|
143
|
+
|
144
|
+
def fail(
|
145
|
+
self,
|
146
|
+
content: list[str] | str | CompletedProcess = "",
|
147
|
+
error: str | list[str] = "",
|
148
|
+
returncode: int | None = None,
|
149
|
+
**kwargs,
|
150
|
+
) -> Self:
|
151
|
+
"""Set the response to a failure state with an error message."""
|
152
|
+
self.add(content=content, error=error, returncode=returncode or 1, **kwargs)
|
153
|
+
return self
|
154
|
+
|
155
|
+
def _add_error(self, error: str) -> None:
|
156
|
+
"""Append an error message to the existing error."""
|
157
|
+
if error != "":
|
158
|
+
self.error.append(error)
|
159
|
+
|
160
|
+
def _add_to_error(self, error: str | list[str], name: str | None = None) -> None:
|
161
|
+
"""Append additional error messages to the existing error."""
|
162
|
+
try:
|
163
|
+
if isinstance(error, list):
|
164
|
+
for err in error:
|
165
|
+
self._add_error(error=f"{name}: {err}" if name else err)
|
166
|
+
elif isinstance(error, str):
|
167
|
+
self._add_error(error=f"{name}: {error}" if name else error)
|
168
|
+
except Exception as e:
|
169
|
+
raise ValueError(f"Failed to add error: {e!s}") from e
|
170
|
+
|
171
|
+
def _add_content(self, content: str) -> None:
|
172
|
+
"""Append content to the existing content."""
|
173
|
+
if content != "":
|
174
|
+
self.content.append(content)
|
175
|
+
|
176
|
+
def _add_to_content(self, content: str | list[str], name: str | None = None) -> None:
|
177
|
+
"""Append additional content to the existing content."""
|
178
|
+
try:
|
179
|
+
if isinstance(content, list):
|
180
|
+
for item in content:
|
181
|
+
self._add_content(content=f"{name}: {item}" if name else item)
|
182
|
+
elif isinstance(content, str):
|
183
|
+
self._add_content(content=f"{name}: {content}" if name else content)
|
184
|
+
except Exception as e:
|
185
|
+
raise ValueError(f"Failed to add content: {e!s}") from e
|
186
|
+
|
187
|
+
def add(
|
188
|
+
self,
|
189
|
+
content: list[str] | str | FunctionResponse | CompletedProcess | None = None,
|
190
|
+
error: str | list[str] | None = None,
|
191
|
+
returncode: int | None = None,
|
192
|
+
log_output: bool = False,
|
193
|
+
extra: dict[str, Any] | None = None,
|
194
|
+
) -> Self:
|
195
|
+
"""Append additional content to the existing content.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
content (list[str] | str | FunctionResponse | CompletedProcess): The content to add.
|
199
|
+
error (str | list[str] | None): The error message(s) to add.
|
200
|
+
returncode (int | None): The return code of the function call.
|
201
|
+
log_output (bool): Whether to log the output using the logger.
|
202
|
+
**kwargs: Additional metadata to include in the response.
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Self: The updated FunctionResponse instance.
|
206
|
+
"""
|
207
|
+
try:
|
208
|
+
if isinstance(content, FunctionResponse):
|
209
|
+
if content.extra:
|
210
|
+
self.extra.update(content.extra)
|
211
|
+
self._add_to_error(error=content.error, name=content.name)
|
212
|
+
self._add_to_content(content=content.content, name=content.name)
|
213
|
+
self.number_of_tasks += 1
|
214
|
+
elif isinstance(content, CompletedProcess):
|
215
|
+
result: CompletedProcess[str] = content
|
216
|
+
self._add_to_content(content=result.stdout.strip() if result.stdout else "")
|
217
|
+
self._add_to_error(error=result.stderr.strip() if result.stderr else "")
|
218
|
+
self.returncode = result.returncode
|
219
|
+
self.number_of_tasks += 1
|
220
|
+
elif isinstance(content, str | list):
|
221
|
+
self._add_to_content(content=content)
|
222
|
+
self.number_of_tasks += 1
|
223
|
+
if isinstance(error, str | list):
|
224
|
+
self._add_to_error(error=error)
|
225
|
+
if returncode is not None:
|
226
|
+
self.returncode = returncode
|
227
|
+
if extra is not None and isinstance(extra, dict):
|
228
|
+
self.extra.update(extra)
|
229
|
+
if log_output and self.logger is not None:
|
230
|
+
if content is not None and error is None:
|
231
|
+
if isinstance(content, list):
|
232
|
+
for item in content:
|
233
|
+
self.logger.info(message=f"{self.name}: {item}" if self.name else item)
|
234
|
+
elif isinstance(content, str):
|
235
|
+
self.logger.info(message=f"{self.name}: {content}" if self.name else content)
|
236
|
+
elif error is not None and content is None:
|
237
|
+
if isinstance(error, list):
|
238
|
+
for err in error:
|
239
|
+
self.logger.error(message=f"{self.name}: {err}" if self.name else err)
|
240
|
+
elif isinstance(error, str):
|
241
|
+
self.logger.error(message=f"{self.name}: {error}" if self.name else error)
|
242
|
+
|
243
|
+
except Exception as e:
|
244
|
+
raise ValueError(f"Failed to add content: {e!s}") from e
|
245
|
+
return self
|
246
|
+
|
247
|
+
@overload
|
248
|
+
def done(self, to_dict: Literal[True], suppress: list[str] | None = None) -> dict[str, Any]: ...
|
249
|
+
|
250
|
+
@overload
|
251
|
+
def done(self, to_dict: Literal[False], suppress: list[str] | None = None) -> Self: ...
|
252
|
+
|
253
|
+
def done(self, to_dict: bool = False, suppress: list[str] | None = None) -> dict[str, Any] | Self:
|
254
|
+
"""Convert the FunctionResponse to a dictionary or return the instance itself.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
to_dict (bool): If True, return a dictionary representation.
|
258
|
+
If False, return the FunctionResponse instance.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
dict[str, Any] | Self: The dictionary representation or the FunctionResponse instance.
|
262
|
+
"""
|
263
|
+
if suppress is None:
|
264
|
+
suppress = []
|
265
|
+
if to_dict:
|
266
|
+
result: dict[str, Any] = {}
|
267
|
+
if self.name and "name" not in suppress:
|
268
|
+
result["name"] = self.name
|
269
|
+
if "success" not in suppress:
|
270
|
+
result.update({"success": self.success})
|
271
|
+
if self.returncode > 0 and "returncode" not in suppress:
|
272
|
+
result["returncode"] = self.returncode
|
273
|
+
if self.number_of_tasks > 0 and "number_of_tasks" not in suppress:
|
274
|
+
result["number_of_tasks"] = self.number_of_tasks
|
275
|
+
if self.content and "content" not in suppress:
|
276
|
+
result["content"] = self.content
|
277
|
+
if self.error and "error" not in suppress:
|
278
|
+
result["error"] = self.error
|
279
|
+
if self.extra:
|
280
|
+
result.update(self.extra)
|
281
|
+
return result
|
282
|
+
return self
|
283
|
+
|
284
|
+
|
285
|
+
def success(
|
286
|
+
content: str | list[str] | CompletedProcess[str] | FunctionResponse,
|
287
|
+
error: str = "",
|
288
|
+
**kwargs,
|
289
|
+
) -> FunctionResponse:
|
290
|
+
"""Create a successful FunctionResponse."""
|
291
|
+
return FunctionResponse().add(content=content, error=error, **kwargs)
|
292
|
+
|
293
|
+
|
294
|
+
def fail(
|
295
|
+
content: str | list[str] | CompletedProcess[str] = "",
|
296
|
+
error: str | list[str] = "",
|
297
|
+
returncode: int | None = None,
|
298
|
+
**kwargs,
|
299
|
+
) -> FunctionResponse:
|
300
|
+
"""Create a failed FunctionResponse."""
|
301
|
+
return FunctionResponse().fail(content=content, error=error, returncode=returncode, **kwargs)
|
@@ -0,0 +1 @@
|
|
1
|
+
"""A module for wrappers in Bear Utils."""
|
@@ -1,3 +1,5 @@
|
|
1
|
+
"""A module for adding rich comparison methods to classes based on an attribute."""
|
2
|
+
|
1
3
|
from collections.abc import Callable
|
2
4
|
from types import NotImplementedType
|
3
5
|
from typing import Any, TypeVar
|
@@ -29,7 +31,7 @@ def add_comparison_methods(attribute: str) -> Callable[[type[T]], type[T]]:
|
|
29
31
|
"""
|
30
32
|
|
31
33
|
def decorator(cls: type[T]) -> type[T]:
|
32
|
-
def extract_comparable_value(self, other: Any) -> NotImplementedType | Any:
|
34
|
+
def extract_comparable_value(self: object, other: Any) -> NotImplementedType | Any: # noqa: ARG001
|
33
35
|
"""Helper to extract the comparable value from the other object."""
|
34
36
|
if isinstance(other, PRIMITIVE_TYPES):
|
35
37
|
return other
|
@@ -39,59 +41,59 @@ def add_comparison_methods(attribute: str) -> Callable[[type[T]], type[T]]:
|
|
39
41
|
|
40
42
|
return NotImplemented
|
41
43
|
|
42
|
-
def equals_method(self, other: Any) -> NotImplementedType | bool:
|
44
|
+
def equals_method(self: object, other: Any) -> NotImplementedType | bool:
|
43
45
|
"""Equal comparison method (__eq__)."""
|
44
46
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
45
47
|
if other_val is NotImplemented:
|
46
48
|
return NotImplemented
|
47
49
|
return getattr(self, attribute) == other_val
|
48
50
|
|
49
|
-
def not_equals_method(self, other: Any) -> NotImplementedType | bool:
|
51
|
+
def not_equals_method(self: object, other: Any) -> NotImplementedType | bool:
|
50
52
|
"""Not equal comparison method (__ne__)."""
|
51
53
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
52
54
|
if other_val is NotImplemented:
|
53
55
|
return NotImplemented
|
54
56
|
return getattr(self, attribute) != other_val
|
55
57
|
|
56
|
-
def less_than_method(self, other: Any) -> NotImplementedType | bool:
|
58
|
+
def less_than_method(self: object, other: Any) -> NotImplementedType | bool:
|
57
59
|
"""Less than comparison method (__lt__)."""
|
58
60
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
59
61
|
if other_val is NotImplemented:
|
60
62
|
return NotImplemented
|
61
63
|
return getattr(self, attribute) < other_val
|
62
64
|
|
63
|
-
def greater_than_method(self, other: Any) -> NotImplementedType | bool:
|
65
|
+
def greater_than_method(self: object, other: Any) -> NotImplementedType | bool:
|
64
66
|
"""Greater than comparison method (__gt__)."""
|
65
67
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
66
68
|
if other_val is NotImplemented:
|
67
69
|
return NotImplemented
|
68
70
|
return getattr(self, attribute) > other_val
|
69
71
|
|
70
|
-
def less_than_or_equal_method(self, other: Any) -> NotImplementedType | bool:
|
72
|
+
def less_than_or_equal_method(self: object, other: Any) -> NotImplementedType | bool:
|
71
73
|
"""Less than or equal comparison method (__le__)."""
|
72
74
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
73
75
|
if other_val is NotImplemented:
|
74
76
|
return NotImplemented
|
75
77
|
return getattr(self, attribute) <= other_val
|
76
78
|
|
77
|
-
def greater_than_or_equal_method(self, other: Any) -> NotImplementedType | bool:
|
79
|
+
def greater_than_or_equal_method(self: object, other: Any) -> NotImplementedType | bool:
|
78
80
|
"""Greater than or equal comparison method (__ge__)."""
|
79
81
|
other_val: NotImplementedType | Any = extract_comparable_value(self, other)
|
80
82
|
if other_val is NotImplemented:
|
81
83
|
return NotImplemented
|
82
84
|
return getattr(self, attribute) >= other_val
|
83
85
|
|
84
|
-
def hash_method(self) -> int:
|
86
|
+
def hash_method(self: object) -> int:
|
85
87
|
"""Generate hash based on the attribute used for equality."""
|
86
88
|
return hash(getattr(self, attribute))
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
90
|
+
cls.__eq__ = equals_method
|
91
|
+
cls.__ne__ = not_equals_method
|
92
|
+
cls.__lt__ = less_than_method # type: ignore[assignment]
|
93
|
+
cls.__gt__ = greater_than_method # type: ignore[assignment]
|
94
|
+
cls.__le__ = less_than_or_equal_method # type: ignore[assignment]
|
95
|
+
cls.__ge__ = greater_than_or_equal_method # type: ignore[assignment]
|
96
|
+
cls.__hash__ = hash_method
|
95
97
|
|
96
98
|
return cls
|
97
99
|
|
bear_utils/files/__init__.py
CHANGED