bear-utils 0.8.24__tar.gz → 0.8.25__tar.gz

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 (120) hide show
  1. {bear_utils-0.8.24 → bear_utils-0.8.25}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.24 → bear_utils-0.8.25}/PKG-INFO +2 -2
  3. {bear_utils-0.8.24 → bear_utils-0.8.25}/README.md +1 -1
  4. {bear_utils-0.8.24 → bear_utils-0.8.25}/pyproject.toml +1 -1
  5. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/prompt_helpers.py +12 -14
  6. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_base_command.py +1 -4
  7. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/typer_bridge.py +1 -1
  8. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/responses/function_response.py +103 -54
  9. bear_utils-0.8.25/src/bear_utils/extras/wrappers/string_io.py +46 -0
  10. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_function_response.py +1 -12
  11. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_gradient.py +24 -11
  12. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_prompt_helpers.py +8 -8
  13. {bear_utils-0.8.24 → bear_utils-0.8.25}/.gitignore +0 -0
  14. {bear_utils-0.8.24 → bear_utils-0.8.25}/.python-version +0 -0
  15. {bear_utils-0.8.24 → bear_utils-0.8.25}/AGENTS.md +0 -0
  16. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/coverage.ini +0 -0
  17. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/default.toml +0 -0
  18. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/git-changelog.toml +0 -0
  19. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/pytest.ini +0 -0
  20. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/ruff.toml +0 -0
  21. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/vscode/launch.json +0 -0
  22. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/vscode/settings.json +0 -0
  23. {bear_utils-0.8.24 → bear_utils-0.8.25}/config/vscode/tasks.json +0 -0
  24. {bear_utils-0.8.24 → bear_utils-0.8.25}/directory_structure.txt +0 -0
  25. {bear_utils-0.8.24 → bear_utils-0.8.25}/directory_structure.xml +0 -0
  26. {bear_utils-0.8.24 → bear_utils-0.8.25}/maskfile.md +0 -0
  27. {bear_utils-0.8.24 → bear_utils-0.8.25}/noxfile.py +0 -0
  28. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/__init__.py +0 -0
  29. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/__main__.py +0 -0
  30. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/_internal/__init__.py +0 -0
  31. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/_internal/cli.py +0 -0
  32. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/_internal/debug.py +0 -0
  33. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/__init__.py +0 -0
  34. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  35. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  36. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  37. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  38. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  39. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cache/__init__.py +0 -0
  40. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/__init__.py +0 -0
  41. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/commands.py +0 -0
  42. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/shell/__init__.py +0 -0
  43. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_base_shell.py +1 -1
  44. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_common.py +0 -0
  45. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/config/__init__.py +0 -0
  46. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/config/config_manager.py +0 -0
  47. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/config/dir_manager.py +0 -0
  48. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/config/settings_manager.py +0 -0
  49. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/__init__.py +0 -0
  50. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/_exceptions.py +0 -0
  51. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/_lazy_typing.py +0 -0
  52. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/date_related.py +0 -0
  53. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/server.py +0 -0
  54. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/constants/time_related.py +0 -0
  55. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/database/__init__.py +0 -0
  56. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/database/_db_manager.py +0 -0
  57. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/events/__init__.py +0 -0
  58. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/events/events_class.py +0 -0
  59. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/events/events_module.py +0 -0
  60. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/__init__.py +0 -0
  61. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/_async_helpers.py +0 -0
  62. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/_tools.py +0 -0
  63. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/platform_utils.py +0 -0
  64. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/responses/__init__.py +0 -0
  65. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  66. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  67. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/__init__.py +0 -0
  68. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  69. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  70. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  71. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  72. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  73. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  74. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  75. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  76. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/files/ignore_parser.py +0 -0
  77. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/graphics/__init__.py +0 -0
  78. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/graphics/bear_gradient.py +0 -0
  79. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/graphics/image_helpers.py +0 -0
  80. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/__init__.py +0 -0
  81. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  82. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  83. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  84. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_app.py +0 -0
  85. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  86. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  87. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  88. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/__init__.py +0 -0
  89. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_common.py +0 -0
  90. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  91. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_log_level.py +0 -0
  92. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_styles.py +0 -0
  93. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/logger_protocol.py +0 -0
  94. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  95. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/_level_sin.py +0 -0
  96. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/base_logger.py +0 -0
  97. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/base_logger.pyi +0 -0
  98. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/__init__.py +0 -0
  99. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/logger.py +0 -0
  100. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/logger.pyi +0 -0
  101. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/buffer_logger.py +0 -0
  102. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/console_logger.py +0 -0
  103. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/console_logger.pyi +0 -0
  104. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/fastapi_logger.py +0 -0
  105. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/file_logger.py +0 -0
  106. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/simple_logger.py +0 -0
  107. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/sub_logger.py +0 -0
  108. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/sub_logger.pyi +0 -0
  109. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/monitoring/__init__.py +0 -0
  110. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/monitoring/_common.py +0 -0
  111. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/monitoring/host_monitor.py +0 -0
  112. {bear_utils-0.8.24 → bear_utils-0.8.25}/src/bear_utils/time/__init__.py +0 -0
  113. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/__init__.py +0 -0
  114. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_add_ord_suffix.py +0 -0
  115. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_clipboard.py +0 -0
  116. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_database_manager.py +0 -0
  117. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_default_shell.py +0 -0
  118. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_logger.py +0 -0
  119. {bear_utils-0.8.24 → bear_utils-0.8.25}/tests/test_platform_utils.py +0 -0
  120. {bear_utils-0.8.24 → bear_utils-0.8.25}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.24
2
+ current_version = 0.8.25
3
3
 
4
4
  [bumpversion:file:pyproject.toml]
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bear-utils
3
- Version: 0.8.24
3
+ Version: 0.8.25
4
4
  Summary: Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things.
5
5
  Author-email: chaz <bright.lid5647@fastmail.com>
6
6
  Requires-Python: >=3.12
@@ -25,7 +25,7 @@ Provides-Extra: gui
25
25
  Requires-Dist: pyqt6>=6.9.0; extra == 'gui'
26
26
  Description-Content-Type: text/markdown
27
27
 
28
- # Bear Utils v# Bear Utils v0.8.24
28
+ # Bear Utils v# Bear Utils v0.8.25
29
29
 
30
30
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
31
31
 
@@ -1,4 +1,4 @@
1
- # Bear Utils v# Bear Utils v0.8.24
1
+ # Bear Utils v# Bear Utils v0.8.25
2
2
 
3
3
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
4
4
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bear-utils"
3
- version = "0.8.24"
3
+ version = "0.8.25"
4
4
  description = "Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things."
5
5
  authors = [{ name = "chaz", email = "bright.lid5647@fastmail.com" }]
6
6
  readme = "README.md"
@@ -107,26 +107,25 @@ def ask_yes_no(question: str, default: bool | None = None) -> bool | None:
107
107
  Returns:
108
108
  True for yes, False for no, or None if user exits
109
109
  """
110
- console, sub = get_console("prompt_helpers.py")
110
+ console, _ = get_console("prompt_helpers.py")
111
111
 
112
- try:
113
- while True:
114
- console.print(question)
115
- response: str = prompt("> ").strip().lower()
112
+ while True:
113
+ try:
114
+ response: str = prompt(f"{question}\n> ").strip().lower()
116
115
  if not response:
117
116
  if default is not None:
118
117
  return default
119
- sub.error("Please enter 'yes', 'no', or 'exit'.")
118
+ console.print("Please enter 'yes', 'no', or 'exit'.")
120
119
  continue
121
120
  if _parse_exit(response):
122
121
  return None
123
122
  try:
124
123
  return _parse_bool(response)
125
124
  except ValueError:
126
- sub.error("Invalid input. Please enter 'yes', 'no', or 'exit'.")
127
- except KeyboardInterrupt:
128
- sub.warning("KeyboardInterrupt: Exiting the prompt.")
129
- return None
125
+ console.print("Invalid input. Please enter 'yes', 'no', or 'exit'.", style="red")
126
+ except KeyboardInterrupt:
127
+ console.print("KeyboardInterrupt: Exiting the prompt.", style="yellow")
128
+ return None
130
129
 
131
130
 
132
131
  def restricted_prompt(
@@ -165,16 +164,15 @@ def restricted_prompt(
165
164
 
166
165
  try:
167
166
  while True:
168
- console.print(question)
169
167
  response: str = prompt(
170
- "> ",
168
+ f"{question}\n> ",
171
169
  completer=completer,
172
170
  validator=OptionValidator(),
173
171
  complete_while_typing=True,
174
172
  ).strip()
175
173
  comparison_response: str = response if case_sensitive else response.lower()
176
174
  if not response:
177
- console.error("Please enter a valid option or 'exit'.")
175
+ console.print("Please enter a valid option or 'exit'.", style="red")
178
176
  continue
179
177
  if comparison_response == comparison_exit:
180
178
  return None
@@ -184,5 +182,5 @@ def restricted_prompt(
184
182
  return valid_options[idx]
185
183
  return response
186
184
  except KeyboardInterrupt:
187
- console.warning("KeyboardInterrupt: Exiting the prompt.")
185
+ console.print("KeyboardInterrupt: Exiting the prompt.", style="yellow")
188
186
  return None
@@ -1,8 +1,5 @@
1
1
  from subprocess import CompletedProcess
2
- from typing import TYPE_CHECKING, Any, ClassVar, Self
3
-
4
- if TYPE_CHECKING:
5
- from subprocess import CompletedProcess
2
+ from typing import Any, ClassVar, Self
6
3
 
7
4
 
8
5
  class BaseShellCommand[T: str]:
@@ -41,7 +41,7 @@ class TyperBridge(SingletonBase):
41
41
  self.console: AsyncLoggerProtocol | LoggerProtocol | Console = console or Console()
42
42
  self.command_meta: dict[str, CommandMeta] = {}
43
43
 
44
- def metadata(self, *alias_names: str) -> Callable[..., Callable[..., Any]]:
44
+ def alias(self, *alias_names: str) -> Callable[..., Callable[..., Any]]:
45
45
  """Register aliases as hidden Typer commands."""
46
46
 
47
47
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -41,12 +41,24 @@ class FunctionResponse(BaseModel):
41
41
  "arbitrary_types_allowed": True,
42
42
  }
43
43
 
44
+ def _has_attr(self, key: str) -> bool:
45
+ """Check if the attribute exists in the attrs Namespace."""
46
+ return hasattr(self.attrs, key)
47
+
48
+ def _get_attr(self, key: str, default: Any = None) -> Any:
49
+ """Get the attribute from the attrs Namespace, returning default if not found."""
50
+ if self._has_attr(key):
51
+ return getattr(self.attrs, key, default)
52
+ return default
53
+
54
+ def _get_attrs(self) -> dict[str, Any]:
55
+ """Get all attributes from the attrs Namespace as a dictionary."""
56
+ return {k: getattr(self.attrs, k, None) for k in self.attrs.__dict__ if not k.startswith("_")}
57
+
44
58
  def __getattr__(self, key: str, default: Any = None) -> Any:
45
59
  if key in FunctionResponse.model_fields:
46
60
  raise AttributeError(f"This should never be called, {key} is a model field.")
47
- if hasattr(self.attrs, key):
48
- return getattr(self.attrs, key)
49
- return default
61
+ return self._get_attr(key, default)
50
62
 
51
63
  def __setattr__(self, key: str, value: Any) -> None:
52
64
  if key in FunctionResponse.model_fields:
@@ -58,19 +70,23 @@ class FunctionResponse(BaseModel):
58
70
  """Return a string representation of Response."""
59
71
  parts: list[str] = []
60
72
 
61
- def add(k: str, v: Any, _bool: bool = True, formatter: Callable | None = None) -> None:
73
+ def add(k: str, v: Any, _bool: bool = True, fmt_func: Callable | None = None) -> None:
62
74
  if _bool:
63
- formatted_value: str = formatter(v) if formatter else repr(v)
75
+ formatted_value: str = fmt_func(v) if fmt_func else repr(v)
64
76
  parts.append(f"{k}={formatted_value}")
65
77
 
66
78
  add("name", self.name, bool(self.name))
67
- add("returncode", self.returncode, self.returncode != 0)
68
- add("success", self.success, bool(self.returncode))
69
79
  add("content", ", ".join(self.content), bool(self.content))
70
- add("error", ", ".join(self.error), bool(self.error))
71
- add("extra", self.extra, bool(self.extra), json.dumps)
72
- add("number_of_tasks", self.number_of_tasks, self.number_of_tasks > 0)
73
80
 
81
+ # error state depends on returncode or error
82
+ add("error", ", ".join(self.error), self.error_state)
83
+ add("success", self.success, _bool=True)
84
+ add("returncode", self.returncode, self.error_state)
85
+ add("number_of_tasks", self.number_of_tasks, self.error_state)
86
+ add("extra", self.extra, bool(self.extra), json.dumps)
87
+ attrs = self._get_attrs()
88
+ for attr in attrs:
89
+ add(attr, attrs[attr])
74
90
  return f"Response({', '.join(parts)})"
75
91
 
76
92
  def __str__(self) -> str:
@@ -139,28 +155,19 @@ class FunctionResponse(BaseModel):
139
155
  content: str = process.stdout.strip() if process.stdout else ""
140
156
  error: str = process.stderr.strip() if process.stderr else ""
141
157
 
142
- if returncode == 0 and not content and error:
158
+ if returncode == 0 and not content and error: # Some processes return empty stdout on success
143
159
  error, content = content, error
144
-
145
160
  return cls().add(returncode=returncode, content=content, error=error, **kwargs)
146
161
 
147
- def from_response(self, response: FunctionResponse | Any, **kwargs) -> Self:
148
- """Create a FunctionResponse from another FunctionResponse object."""
149
- if not isinstance(response, FunctionResponse):
150
- raise TypeError("Expected a FunctionResponse instance.")
151
- self.sub_tasks.append(response)
152
- return self.add(
153
- content=response.content,
154
- error=response.error,
155
- returncode=response.returncode,
156
- log_output=kwargs.pop("log_output", False),
157
- **kwargs,
158
- )
162
+ @property
163
+ def error_state(self) -> bool:
164
+ """Check if the returncode or error field indicates an error state."""
165
+ return self.returncode != 0 or bool(self.error)
159
166
 
160
167
  @property
161
168
  def success(self) -> bool:
162
169
  """Check if the response indicates success."""
163
- return self.returncode == 0
170
+ return self.returncode == 0 and not bool(self.error)
164
171
 
165
172
  def sub_task(
166
173
  self,
@@ -185,13 +192,15 @@ class FunctionResponse(BaseModel):
185
192
 
186
193
  def successful(
187
194
  self,
188
- content: str | list[str] | CompletedProcess,
195
+ content: str | list[str] | CompletedProcess | FunctionResponse,
189
196
  error: str | list[str] = "",
190
197
  returncode: int | None = None,
198
+ log_output: bool = False,
191
199
  **kwargs,
192
200
  ) -> Self:
193
201
  """Set the response to a success state with optional content."""
194
- self.add(content=content, error=error, returncode=returncode or 0, **kwargs)
202
+ return_code: int = returncode if returncode is not None else 0
203
+ self.add(content=content, error=error, returncode=return_code, log_output=log_output, **kwargs)
195
204
  return self
196
205
 
197
206
  def fail(
@@ -199,16 +208,17 @@ class FunctionResponse(BaseModel):
199
208
  content: list[str] | str | CompletedProcess = "",
200
209
  error: str | list[str] = "",
201
210
  returncode: int | None = None,
211
+ log_output: bool = False,
202
212
  **kwargs,
203
213
  ) -> Self:
204
214
  """Set the response to a failure state with an error message."""
205
- self.add(content=content, error=error, returncode=returncode or 1, **kwargs)
215
+ return_code: int = returncode if returncode is not None else 1
216
+ self.add(content=content, error=error, returncode=return_code, log_output=log_output, **kwargs)
206
217
  return self
207
218
 
208
219
  def _add_item(self, item: str, target_list: list[str]) -> None:
209
220
  """Append an item to the target list if not empty."""
210
- if item != "":
211
- target_list.append(item)
221
+ target_list.append(item) if item != "" else None
212
222
 
213
223
  def _add_to_list(self, items: str | list[str], target_list: list[str], name: str | None = None) -> None:
214
224
  """Append items to the target list with optional name prefix."""
@@ -222,24 +232,24 @@ class FunctionResponse(BaseModel):
222
232
  raise ValueError(f"Failed to add items: {e!s}") from e
223
233
 
224
234
  def _add_content(self, content: str | list[str], name: str | None = None) -> None:
225
- """Append content to the existing content."""
226
- self._add_to_list(content, self.content, name)
235
+ """Add content to the FunctionResponse content list."""
236
+ return self._add_to_list(items=content, target_list=self.content, name=name)
227
237
 
228
238
  def _add_error(self, error: str | list[str], name: str | None = None) -> None:
229
- """Append error to the existing error."""
230
- self._add_to_list(error, self.error, name)
239
+ """Add error messages to the FunctionResponse error list."""
240
+ return self._add_to_list(items=error, target_list=self.error, name=name)
231
241
 
232
242
  def _handle_function_response(self, func_response: FunctionResponse) -> None:
233
243
  """Handle a FunctionResponse object and update the current response."""
234
244
  if func_response.extra:
235
245
  self.extra.update(func_response.extra)
236
- self._add_error(error=func_response.error, name=func_response.name)
237
- self._add_content(content=func_response.content, name=func_response.name)
246
+ self._add_content(func_response.content, name=func_response.name)
247
+ self._add_error(func_response.error, name=func_response.name)
238
248
 
239
249
  def _handle_completed_process(self, result: CompletedProcess[str]) -> None:
240
250
  """Handle a CompletedProcess object and update the FunctionResponse."""
241
- self._add_content(content=result.stdout.strip() if result.stdout else "")
242
- self._add_error(error=result.stderr.strip() if result.stderr else "")
251
+ self._add_content(result.stdout.strip())
252
+ self._add_error(result.stderr.strip())
243
253
  self.returncode = result.returncode
244
254
 
245
255
  def _handle_content(self, content: list[str] | str | FunctionResponse | CompletedProcess | Any) -> None:
@@ -249,7 +259,7 @@ class FunctionResponse(BaseModel):
249
259
  elif isinstance(content, CompletedProcess):
250
260
  self._handle_completed_process(result=content)
251
261
  elif isinstance(content, (str | list)):
252
- self._add_content(content=content)
262
+ self._add_to_list(content, self.content)
253
263
  else:
254
264
  return
255
265
  self.number_of_tasks += 1
@@ -293,7 +303,7 @@ class FunctionResponse(BaseModel):
293
303
  if content is not None:
294
304
  self._handle_content(content=content)
295
305
  if error is not None and isinstance(error, (str | list)):
296
- self._add_error(error=error)
306
+ self._add_to_list(error, target_list=self.error)
297
307
  if isinstance(returncode, int):
298
308
  self.returncode = returncode
299
309
  if isinstance(extra, dict):
@@ -302,9 +312,7 @@ class FunctionResponse(BaseModel):
302
312
  self._log_handling(content=content, error=error, logger=self.logger)
303
313
  except Exception as e:
304
314
  raise ValueError(f"Failed to add content: {e!s}") from e
305
- if to_dict:
306
- return self.done(to_dict=True)
307
- return self
315
+ return self.done(to_dict=True) if to_dict else self
308
316
 
309
317
  def _log_handling(
310
318
  self,
@@ -350,17 +358,34 @@ class FunctionResponse(BaseModel):
350
358
  res.conditional_run()
351
359
 
352
360
  @overload
353
- def done(self, to_dict: Literal[True], suppress: list[str] | None = None) -> dict[str, Any]: ...
361
+ def done(
362
+ self,
363
+ to_dict: Literal[True],
364
+ suppress: list[str] | None = None,
365
+ include: list[str] | None = None,
366
+ ) -> dict[str, Any]: ...
354
367
 
355
368
  @overload
356
- def done(self, to_dict: Literal[False], suppress: list[str] | None = None) -> Self: ...
369
+ def done(
370
+ self,
371
+ to_dict: Literal[False],
372
+ suppress: list[str] | None = None,
373
+ include: list[str] | None = None,
374
+ ) -> Self: ...
357
375
 
358
- def done(self, to_dict: bool = False, suppress: list[str] | None = None) -> dict[str, Any] | Self:
376
+ def done(
377
+ self,
378
+ to_dict: bool = False,
379
+ suppress: list[str] | None = None,
380
+ include: list[str] | None = None,
381
+ ) -> dict[str, Any] | Self:
359
382
  """Convert the FunctionResponse to a dictionary or return the instance itself.
360
383
 
361
384
  Args:
362
385
  to_dict (bool): If True, return a dictionary representation.
363
386
  If False, return the FunctionResponse instance.
387
+ suppress (list[str] | None): List of keys to suppress in the output dictionary.
388
+ include (list[str] | None): List of keys to include in the output dictionary.
364
389
 
365
390
  Returns:
366
391
  dict[str, Any] | Self: The dictionary representation or the FunctionResponse instance.
@@ -371,36 +396,60 @@ class FunctionResponse(BaseModel):
371
396
  if suppress is None:
372
397
  suppress = []
373
398
 
399
+ if include is None:
400
+ include = []
401
+
374
402
  result: dict[str, Any] = {}
375
403
 
376
404
  def add(k: str, v: Any, _bool: bool = True) -> None:
377
405
  if k not in suppress and _bool:
378
406
  result[k] = v
379
407
 
408
+ def dict_include(_bool: bool = True) -> dict[str, Any]:
409
+ result.update(self.extra)
410
+ for k in include:
411
+ if hasattr(self.attrs, k) and _bool:
412
+ result[k] = getattr(self.attrs, k)
413
+ return result
414
+
380
415
  add("name", self.name, bool(self.name))
381
- add("success", self.success)
382
- add("returncode", self.returncode, self.returncode > 0)
383
- add("number_of_tasks", self.number_of_tasks, (self.number_of_tasks > 0 and not self.success))
384
416
  add("content", self.content, bool(self.content))
385
- add("error", self.error, bool(self.error))
386
- result.update(self.extra)
387
- return result
417
+ add("success", self.success, _bool=True)
418
+ # depends on the error state
419
+ add("error", self.error, self.error_state)
420
+ add("returncode", self.returncode, self.error_state)
421
+ add("number_of_tasks", self.number_of_tasks, self.error_state)
422
+
423
+ return dict_include()
388
424
 
389
425
 
390
426
  def success(
391
427
  content: str | list[str] | CompletedProcess[str] | FunctionResponse,
392
428
  error: str = "",
429
+ log_output: bool = False,
393
430
  **kwargs,
394
431
  ) -> FunctionResponse:
395
432
  """Create a successful FunctionResponse."""
396
- return FunctionResponse().add(content=content, error=error, **kwargs)
433
+ res = FunctionResponse()
434
+ return res.successful(content, error, 0, log_output, **kwargs)
397
435
 
398
436
 
399
437
  def fail(
400
438
  content: str | list[str] | CompletedProcess[str] = "",
401
439
  error: str | list[str] = "",
402
440
  returncode: int | None = None,
441
+ log_output: bool = False,
403
442
  **kwargs,
404
443
  ) -> FunctionResponse:
405
444
  """Create a failed FunctionResponse."""
406
- return FunctionResponse().fail(content=content, error=error, returncode=returncode, **kwargs)
445
+ res = FunctionResponse()
446
+ return res.fail(content, error, returncode, log_output, **kwargs)
447
+
448
+
449
+ if __name__ == "__main__":
450
+ # For testing purposes, you can run this module directly.
451
+
452
+ response = FunctionResponse(name="test_function")
453
+ response.add(content=["This is a test content."], error=["No errors."], returncode=1, extra={"smelly": "value"})
454
+ response.poop = "farts"
455
+ print(response.done(to_dict=True))
@@ -0,0 +1,46 @@
1
+ """A Simple wrapper around StringIO to make things easier."""
2
+
3
+ from io import BytesIO, StringIO
4
+ from typing import Any, cast
5
+
6
+
7
+ class BaseIOWrapper[T: StringIO | BytesIO]:
8
+ """A Base wrapper around IO objects to make things easier."""
9
+
10
+ def __init__(self, io_obj: T | Any) -> None:
11
+ """Initialize the IOWrapper with a IO Object object."""
12
+ if not isinstance(io_obj, (StringIO | BytesIO)):
13
+ raise TypeError("io_obj must be an instance of StringIO or BytesIO")
14
+ self._value: T = cast("T", io_obj)
15
+ self._cached_value = None
16
+
17
+ def _reset_io(self) -> None:
18
+ """Reset the current a IO Object."""
19
+ self._value.truncate(0)
20
+ self._value.seek(0)
21
+ self._cached_value = None
22
+
23
+
24
+ class StringIOWrapper(BaseIOWrapper[StringIO]):
25
+ """A Simple wrapper around StringIO to make things easier."""
26
+
27
+ def __init__(self, **kwargs) -> None:
28
+ """Initialize the IOWrapper with a a IO Object object."""
29
+ super().__init__(StringIO(**kwargs))
30
+ self._cached_value: str = ""
31
+
32
+ def _reset_io(self) -> None:
33
+ """Reset the current a IO Object."""
34
+ self._value.truncate(0)
35
+ self._value.seek(0)
36
+ self._cached_value = ""
37
+
38
+ def write(self, *values: str) -> None:
39
+ """Write values to the a IO Object object."""
40
+ for value in values:
41
+ self._value.write(value)
42
+
43
+ def getvalue(self) -> str:
44
+ """Get the string value from the a IO Object object."""
45
+ self._cached_value = self._value.getvalue()
46
+ return self._cached_value
@@ -507,16 +507,6 @@ class TestDoneMethod:
507
507
  assert "name" not in result
508
508
  assert "success" not in result
509
509
 
510
- def test_done_with_suppress_failure(self):
511
- """Test done() with FAILURE suppress list."""
512
- response = FunctionResponse(name="test", content=["output"], returncode=1)
513
- result = response.done(to_dict=True, suppress=FAILURE)
514
-
515
- expected = {"success": False, "returncode": 1, "content": ["output"]}
516
-
517
- assert result == expected
518
- assert "name" not in result
519
-
520
510
  def test_done_with_custom_suppress(self):
521
511
  """Test done() with custom suppress list."""
522
512
  response = FunctionResponse(name="test", content=["output"], number_of_tasks=5)
@@ -565,7 +555,7 @@ class TestStringRepresentation:
565
555
  """Test repr with minimal data."""
566
556
  response = FunctionResponse()
567
557
  result = repr(response)
568
- assert result == "Response()"
558
+ assert "Response(" in result
569
559
 
570
560
  def test_repr_full(self):
571
561
  """Test repr with all fields populated."""
@@ -637,7 +627,6 @@ class TestComplexScenarios:
637
627
 
638
628
  result = response.done(to_dict=True)
639
629
 
640
- # SUCCESS suppresses "name" and "success", so check what actually remains
641
630
  assert "content" in result
642
631
  assert "number_of_tasks" not in result # This will only be here in failures
643
632
  assert "name" in result
@@ -21,17 +21,6 @@ def test_gradients_visual():
21
21
  health_bar = "█" * (health // 5)
22
22
  console.print(f"HP: {health:3d}/100 {health_bar:<20}", style=color.rgb)
23
23
 
24
- console.print("\n" + "=" * 50 + "\n")
25
-
26
- # Reversed: Infection/Damage meter (Green = good, Red = bad)
27
- console.print("[bold red]Infection Level (0% = Healthy, 100% = Critical):[/bold red]")
28
- health_gradient.reverse = True
29
- for infection in range(0, 101, 10):
30
- color: ColorTriplet = health_gradient.map_to_color(0, 100, infection)
31
- infection_bar = "█" * (infection // 5)
32
- status = "🦠" if infection > 70 else "⚠️" if infection > 30 else "✅"
33
- console.print(f"Infection: {infection:3d}% {infection_bar:<20} {status}", style=color.rgb)
34
-
35
24
  health_scenarios = [
36
25
  (5, "💀 Nearly Dead"),
37
26
  (25, "🩸 Critical Condition"),
@@ -45,3 +34,27 @@ def test_gradients_visual():
45
34
  for hp, status in health_scenarios:
46
35
  color: ColorTriplet = health_gradient.map_to_color(0, 100, hp)
47
36
  console.print(f"{status}: {hp}/100 HP", style=color.rgb)
37
+
38
+ console.print("\n" + "=" * 50 + "\n")
39
+
40
+ # Reversed: Infection/Damage meter (Green = good, Red = bad)
41
+ console.print("[bold red]Infection Level (0% = Healthy, 100% = Critical):[/bold red]")
42
+ health_gradient.reverse = True
43
+ for infection in range(0, 101, 10):
44
+ color: ColorTriplet = health_gradient.map_to_color(0, 100, infection)
45
+ infection_bar = "█" * (infection // 5)
46
+ status = "🦠" if infection > 70 else "⚠️" if infection > 30 else "✅"
47
+ console.print(f"Infection: {infection:3d}% {infection_bar:<20} {status}", style=color.rgb)
48
+
49
+ infected_scenarios = [
50
+ (5, "✅ Healthy"),
51
+ (25, "⚠️ Mild Infection"),
52
+ (50, "🦠 Moderate Infection"),
53
+ (75, "🦠 Severe Infection"),
54
+ (100, "💀 Critical Condition"),
55
+ ]
56
+
57
+ console.print("[bold red]Infection Status Examples:[/bold red]")
58
+ for ip, status in infected_scenarios:
59
+ color: ColorTriplet = health_gradient.map_to_color(0, 100, ip)
60
+ console.print(f"{status}: {ip}/100 Infection", style=color.rgb)
@@ -111,7 +111,7 @@ class TestAskQuestion:
111
111
 
112
112
  assert result == "hello world"
113
113
  mock_console.print.assert_called_with("Enter text: ")
114
- mock_sub.verbose.assert_called_with("str detected")
114
+ mock_console.verbose.assert_called_with("str detected")
115
115
 
116
116
  @patch("bear_utils.cli.prompt_helpers.prompt")
117
117
  @patch("bear_utils.cli.prompt_helpers.get_console")
@@ -126,7 +126,7 @@ class TestAskQuestion:
126
126
 
127
127
  assert result == 42
128
128
  assert isinstance(result, int)
129
- mock_sub.verbose.assert_called_with("int detected")
129
+ mock_console.verbose.assert_called_with("int detected")
130
130
 
131
131
  @patch("bear_utils.cli.prompt_helpers.prompt")
132
132
  @patch("bear_utils.cli.prompt_helpers.get_console")
@@ -140,7 +140,7 @@ class TestAskQuestion:
140
140
  result = ask_question("Continue? ", bool)
141
141
 
142
142
  assert result is True
143
- mock_sub.verbose.assert_called_with("bool detected")
143
+ mock_console.verbose.assert_called_with("bool detected")
144
144
 
145
145
  @patch("bear_utils.cli.prompt_helpers.prompt")
146
146
  @patch("bear_utils.cli.prompt_helpers.get_console")
@@ -168,7 +168,7 @@ class TestAskQuestion:
168
168
 
169
169
  assert result == 42
170
170
  # Should have shown error for invalid input
171
- mock_sub.error.assert_called_with(
171
+ mock_console.error.assert_called_with(
172
172
  "Invalid input: invalid literal for int() with base 10: 'not_a_number'. Please enter a valid int."
173
173
  )
174
174
 
@@ -257,7 +257,7 @@ class TestAskYesNo:
257
257
  result = ask_yes_no("Continue? ")
258
258
 
259
259
  assert result is True
260
- mock_sub.error.assert_called_with("Invalid input. Please enter 'yes', 'no', or 'exit'.")
260
+ mock_console.print.assert_called_with("Invalid input. Please enter 'yes', 'no', or 'exit'.", style="red")
261
261
 
262
262
  @patch("bear_utils.cli.prompt_helpers.prompt")
263
263
  @patch("bear_utils.cli.prompt_helpers.get_console")
@@ -271,7 +271,7 @@ class TestAskYesNo:
271
271
  result = ask_yes_no("Continue? ")
272
272
 
273
273
  assert result is None
274
- mock_sub.warning.assert_called_with("KeyboardInterrupt: Exiting the prompt.")
274
+ mock_console.print.assert_called_with("KeyboardInterrupt: Exiting the prompt.", style="yellow")
275
275
 
276
276
 
277
277
  class TestRestrictedPrompt:
@@ -354,7 +354,7 @@ class TestRestrictedPrompt:
354
354
  result = restricted_prompt("Choose: ", ["option1", "option2"])
355
355
 
356
356
  assert result == "option1"
357
- mock_sub.error.assert_called_with("Please enter a valid option or 'exit'.")
357
+ mock_console.print.assert_called_with("Please enter a valid option or 'exit'.", style="red")
358
358
 
359
359
  @patch("bear_utils.cli.prompt_helpers.prompt")
360
360
  @patch("bear_utils.cli.prompt_helpers.get_console")
@@ -368,7 +368,7 @@ class TestRestrictedPrompt:
368
368
  result = restricted_prompt("Choose: ", ["option1", "option2"])
369
369
 
370
370
  assert result is None
371
- mock_sub.warning.assert_called_with("KeyboardInterrupt: Exiting the prompt.")
371
+ mock_console.print.assert_called_with("KeyboardInterrupt: Exiting the prompt.", style="yellow")
372
372
 
373
373
 
374
374
  class TestPromptHelpersIntegration:
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -13,8 +13,8 @@ import subprocess
13
13
  from subprocess import CompletedProcess
14
14
  from typing import Self, override
15
15
 
16
- from bear_utils.logger_manager.logger_protocol import LoggerProtocol
17
16
  from bear_utils.logger_manager import VERBOSE, BaseLogger, SubConsoleLogger
17
+ from bear_utils.logger_manager.logger_protocol import LoggerProtocol
18
18
 
19
19
  from ._base_command import BaseShellCommand
20
20
  from ._common import DEFAULT_SHELL
File without changes