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.
Files changed (79) hide show
  1. bear_utils/__init__.py +24 -1
  2. bear_utils/ai/__init__.py +5 -5
  3. bear_utils/ai/ai_helpers/__init__.py +24 -18
  4. bear_utils/ai/ai_helpers/_parsers.py +27 -21
  5. bear_utils/ai/ai_helpers/_types.py +2 -7
  6. bear_utils/cache/__init__.py +35 -23
  7. bear_utils/cli/__init__.py +13 -0
  8. bear_utils/cli/commands.py +14 -8
  9. bear_utils/cli/prompt_helpers.py +40 -34
  10. bear_utils/cli/shell/__init__.py +1 -0
  11. bear_utils/cli/shell/_base_command.py +18 -18
  12. bear_utils/cli/shell/_base_shell.py +37 -34
  13. bear_utils/config/__init__.py +4 -2
  14. bear_utils/config/config_manager.py +193 -56
  15. bear_utils/config/dir_manager.py +8 -3
  16. bear_utils/config/settings_manager.py +94 -171
  17. bear_utils/constants/__init__.py +2 -1
  18. bear_utils/constants/_exceptions.py +6 -1
  19. bear_utils/constants/date_related.py +2 -0
  20. bear_utils/constants/logger_protocol.py +28 -0
  21. bear_utils/constants/time_related.py +2 -0
  22. bear_utils/database/__init__.py +2 -0
  23. bear_utils/database/_db_manager.py +10 -11
  24. bear_utils/events/__init__.py +3 -1
  25. bear_utils/events/events_class.py +11 -11
  26. bear_utils/events/events_module.py +17 -8
  27. bear_utils/extras/__init__.py +8 -6
  28. bear_utils/extras/_async_helpers.py +2 -3
  29. bear_utils/extras/_tools.py +62 -52
  30. bear_utils/extras/platform_utils.py +5 -1
  31. bear_utils/extras/responses/__init__.py +1 -0
  32. bear_utils/extras/responses/function_response.py +301 -0
  33. bear_utils/extras/wrappers/__init__.py +1 -0
  34. bear_utils/extras/wrappers/add_methods.py +17 -15
  35. bear_utils/files/__init__.py +3 -1
  36. bear_utils/files/file_handlers/__init__.py +2 -0
  37. bear_utils/files/file_handlers/_base_file_handler.py +23 -3
  38. bear_utils/files/file_handlers/file_handler_factory.py +38 -38
  39. bear_utils/files/file_handlers/json_file_handler.py +49 -22
  40. bear_utils/files/file_handlers/log_file_handler.py +19 -12
  41. bear_utils/files/file_handlers/toml_file_handler.py +13 -5
  42. bear_utils/files/file_handlers/txt_file_handler.py +56 -14
  43. bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
  44. bear_utils/files/ignore_parser.py +52 -57
  45. bear_utils/graphics/__init__.py +3 -1
  46. bear_utils/graphics/bear_gradient.py +17 -12
  47. bear_utils/graphics/image_helpers.py +11 -5
  48. bear_utils/gui/__init__.py +3 -1
  49. bear_utils/gui/gui_tools/__init__.py +3 -1
  50. bear_utils/gui/gui_tools/_settings.py +0 -1
  51. bear_utils/gui/gui_tools/qt_app.py +16 -11
  52. bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
  53. bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
  54. bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
  55. bear_utils/logging/__init__.py +6 -4
  56. bear_utils/logging/logger_manager/__init__.py +1 -0
  57. bear_utils/logging/logger_manager/_common.py +0 -1
  58. bear_utils/logging/logger_manager/_console_junk.py +14 -10
  59. bear_utils/logging/logger_manager/_styles.py +1 -2
  60. bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
  61. bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -36
  62. bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
  63. bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
  64. bear_utils/logging/logger_manager/loggers/_console_logger.py +52 -26
  65. bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
  66. bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
  67. bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
  68. bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
  69. bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
  70. bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
  71. bear_utils/logging/loggers.py +9 -13
  72. bear_utils/monitoring/__init__.py +7 -4
  73. bear_utils/monitoring/_common.py +28 -0
  74. bear_utils/monitoring/host_monitor.py +44 -48
  75. bear_utils/time/__init__.py +13 -6
  76. {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/METADATA +50 -6
  77. bear_utils-0.7.23.dist-info/RECORD +83 -0
  78. bear_utils-0.7.21.dist-info/RECORD +0 -79
  79. {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/WHEEL +0 -0
@@ -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
- from subprocess import CompletedProcess
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
- from ..cli.shell._base_command import BaseShellCommand as ShellCommand
9
- from ..cli.shell._base_shell import AsyncShellSession
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 ..logging.loggers import BaseLogger
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(self, title: str, sep="#", len=60, s1="bold red", s2="bold blue", return_txt: bool = False) -> str:
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 * len
26
- title = f" {title} ".center(len, sep).replace(title, f"[{s1}]{title}[/{s1}]")
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
- A class to manage clipboard operations such as copying, pasting, and clearing.
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
- self.clipboard_history = deque(maxlen=maxlen)
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: # pragma: no cover - safety net for unforeseen shell errors
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
- import platform
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
- 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)
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
 
@@ -1,4 +1,6 @@
1
+ """A module for file handling utilities in Bear Utils."""
2
+
1
3
  from .file_handlers import FileHandlerFactory
2
4
  from .ignore_parser import IGNORE_PATTERNS
3
5
 
4
- __all__ = ["FileHandlerFactory", "IGNORE_PATTERNS"]
6
+ __all__ = ["IGNORE_PATTERNS", "FileHandlerFactory"]