bear-utils 0.8.24__tar.gz → 0.8.26__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.26}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.24 → bear_utils-0.8.26}/PKG-INFO +2 -2
  3. {bear_utils-0.8.24 → bear_utils-0.8.26}/README.md +1 -1
  4. {bear_utils-0.8.24 → bear_utils-0.8.26}/pyproject.toml +1 -1
  5. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/prompt_helpers.py +12 -14
  6. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/shell/_base_command.py +1 -4
  7. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/typer_bridge.py +1 -1
  8. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/__init__.py +2 -2
  9. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/_tools.py +101 -33
  10. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/responses/function_response.py +103 -54
  11. bear_utils-0.8.26/src/bear_utils/extras/wrappers/string_io.py +46 -0
  12. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_function_response.py +1 -12
  13. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_gradient.py +24 -11
  14. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_logger.py +1 -1
  15. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_prompt_helpers.py +8 -8
  16. {bear_utils-0.8.24 → bear_utils-0.8.26}/.gitignore +0 -0
  17. {bear_utils-0.8.24 → bear_utils-0.8.26}/.python-version +0 -0
  18. {bear_utils-0.8.24 → bear_utils-0.8.26}/AGENTS.md +0 -0
  19. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/coverage.ini +0 -0
  20. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/default.toml +0 -0
  21. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/git-changelog.toml +0 -0
  22. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/pytest.ini +0 -0
  23. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/ruff.toml +0 -0
  24. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/vscode/launch.json +0 -0
  25. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/vscode/settings.json +0 -0
  26. {bear_utils-0.8.24 → bear_utils-0.8.26}/config/vscode/tasks.json +0 -0
  27. {bear_utils-0.8.24 → bear_utils-0.8.26}/directory_structure.txt +0 -0
  28. {bear_utils-0.8.24 → bear_utils-0.8.26}/directory_structure.xml +0 -0
  29. {bear_utils-0.8.24 → bear_utils-0.8.26}/maskfile.md +0 -0
  30. {bear_utils-0.8.24 → bear_utils-0.8.26}/noxfile.py +0 -0
  31. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/__init__.py +0 -0
  32. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/__main__.py +0 -0
  33. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/_internal/__init__.py +0 -0
  34. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/_internal/cli.py +0 -0
  35. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/_internal/debug.py +0 -0
  36. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/__init__.py +0 -0
  37. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  38. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  39. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  40. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  41. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  42. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cache/__init__.py +0 -0
  43. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/__init__.py +0 -0
  44. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/commands.py +0 -0
  45. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/shell/__init__.py +0 -0
  46. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/shell/_base_shell.py +1 -1
  47. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/cli/shell/_common.py +0 -0
  48. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/config/__init__.py +0 -0
  49. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/config/config_manager.py +0 -0
  50. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/config/dir_manager.py +0 -0
  51. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/config/settings_manager.py +0 -0
  52. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/__init__.py +0 -0
  53. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/_exceptions.py +0 -0
  54. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/_lazy_typing.py +0 -0
  55. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/date_related.py +0 -0
  56. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/server.py +0 -0
  57. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/constants/time_related.py +0 -0
  58. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/database/__init__.py +0 -0
  59. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/database/_db_manager.py +0 -0
  60. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/events/__init__.py +0 -0
  61. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/events/events_class.py +0 -0
  62. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/events/events_module.py +0 -0
  63. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/_async_helpers.py +0 -0
  64. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/platform_utils.py +0 -0
  65. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/responses/__init__.py +0 -0
  66. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  67. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  68. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/__init__.py +0 -0
  69. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  70. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  71. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  72. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  73. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  74. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  75. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  76. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  77. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/files/ignore_parser.py +0 -0
  78. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/graphics/__init__.py +0 -0
  79. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/graphics/bear_gradient.py +0 -0
  80. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/graphics/image_helpers.py +0 -0
  81. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/__init__.py +0 -0
  82. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  83. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  84. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  85. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/qt_app.py +0 -0
  86. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  87. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  88. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  89. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/__init__.py +0 -0
  90. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/_common.py +0 -0
  91. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  92. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/_log_level.py +0 -0
  93. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/_styles.py +0 -0
  94. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/logger_protocol.py +0 -0
  95. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  96. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/_level_sin.py +0 -0
  97. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/base_logger.py +0 -0
  98. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/base_logger.pyi +0 -0
  99. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/basic_logger/__init__.py +0 -0
  100. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/basic_logger/logger.py +0 -0
  101. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/basic_logger/logger.pyi +0 -0
  102. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/buffer_logger.py +0 -0
  103. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/console_logger.py +0 -0
  104. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/console_logger.pyi +0 -0
  105. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/fastapi_logger.py +0 -0
  106. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/file_logger.py +0 -0
  107. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/simple_logger.py +0 -0
  108. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/sub_logger.py +0 -0
  109. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/logger_manager/loggers/sub_logger.pyi +0 -0
  110. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/monitoring/__init__.py +0 -0
  111. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/monitoring/_common.py +0 -0
  112. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/monitoring/host_monitor.py +0 -0
  113. {bear_utils-0.8.24 → bear_utils-0.8.26}/src/bear_utils/time/__init__.py +0 -0
  114. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/__init__.py +0 -0
  115. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_add_ord_suffix.py +0 -0
  116. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_clipboard.py +0 -0
  117. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_database_manager.py +0 -0
  118. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_default_shell.py +0 -0
  119. {bear_utils-0.8.24 → bear_utils-0.8.26}/tests/test_platform_utils.py +0 -0
  120. {bear_utils-0.8.24 → bear_utils-0.8.26}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.24
2
+ current_version = 0.8.26
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.26
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.26
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.26
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.26"
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]:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from singleton_base import SingletonBase
4
4
 
5
- from ._tools import ClipboardManager, clear_clipboard, copy_to_clipboard, fmt_header, paste_from_clipboard
5
+ from ._tools import ClipboardManager, ascii_header, clear_clipboard, copy_to_clipboard, paste_from_clipboard
6
6
  from .platform_utils import OS, get_platform, is_linux, is_macos, is_windows
7
7
  from .wrappers.add_methods import add_comparison_methods
8
8
 
@@ -11,9 +11,9 @@ __all__ = [
11
11
  "ClipboardManager",
12
12
  "SingletonBase",
13
13
  "add_comparison_methods",
14
+ "ascii_header",
14
15
  "clear_clipboard",
15
16
  "copy_to_clipboard",
16
- "fmt_header",
17
17
  "get_platform",
18
18
  "is_linux",
19
19
  "is_macos",
@@ -5,42 +5,16 @@ from functools import cached_property
5
5
  import shutil
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from rich.console import Console
9
+
8
10
  from bear_utils.cli.shell._base_command import BaseShellCommand as ShellCommand
9
11
  from bear_utils.cli.shell._base_shell import AsyncShellSession
10
12
  from bear_utils.extras.platform_utils import OS, get_platform
11
- from bear_utils.logger_manager.loggers.base_logger import BaseLogger
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from subprocess import CompletedProcess
15
16
 
16
17
 
17
- class TextHelper:
18
- @cached_property
19
- def local_console(self) -> BaseLogger:
20
- from bear_utils.logger_manager import BaseLogger # noqa: PLC0415
21
-
22
- init: bool = not BaseLogger.has_instance()
23
- return BaseLogger.get_instance(init=init)
24
-
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:
34
- """Generate a header string"""
35
- # FIXME: There are probably better ways to do this, but this is OK.
36
- fill: str = sep * length
37
- title = f" {title} ".center(length, sep).replace(title, f"[{s1}]{title}[/{s1}]")
38
- output_text: str = f"\n{fill}\n{title}\n{fill}\n"
39
- if not return_txt:
40
- self.local_console.print(output_text, style=s2)
41
- return output_text
42
-
43
-
44
18
  class ClipboardManager:
45
19
  """A class to manage clipboard operations such as copying, pasting, and clearing.
46
20
 
@@ -201,9 +175,71 @@ async def clear_clipboard_async() -> int:
201
175
  return await clipboard_manager.clear()
202
176
 
203
177
 
204
- def fmt_header(
178
+ class TextHelper:
179
+ @cached_property
180
+ def local_console(self) -> Console:
181
+ return Console()
182
+
183
+ def print_header(
184
+ self,
185
+ title: str,
186
+ top_sep: str = "#",
187
+ left_sep: str = ">",
188
+ right_sep: str = "<",
189
+ bottom_sep: str = "#",
190
+ length: int = 60,
191
+ s1: str = "bold red",
192
+ s2: str = "bold blue",
193
+ return_txt: bool = False,
194
+ ) -> str:
195
+ """Generate a header string with customizable separators for each line.
196
+
197
+ Args:
198
+ title: The title text to display
199
+ top_sep: Character(s) for the top separator line
200
+ left_sep: Character(s) for the left side of title line
201
+ right_sep: Character(s) for the right side of title line
202
+ bottom_sep: Character(s) for the bottom separator line
203
+ length: Total width of each line
204
+ s1: Style for the title text
205
+ s2: Style for the entire header block
206
+ return_txt: If True, return the text instead of printing
207
+ """
208
+ # Top line: all top_sep characters
209
+ top_line: str = top_sep * length
210
+
211
+ # Bottom line: all bottom_sep characters
212
+ bottom_line: str = bottom_sep * length
213
+
214
+ # Title line: left_sep chars + title + right_sep chars
215
+ title_with_spaces = f" {title} "
216
+ styled_title = f"[{s1}]{title}[/{s1}]"
217
+
218
+ # Calculate padding needed on each side
219
+ title_length = len(title_with_spaces)
220
+ remaining_space = length - title_length
221
+ left_padding = remaining_space // 2
222
+ right_padding = remaining_space - left_padding
223
+
224
+ # Build the title line with different left and right separators
225
+ title_line = (
226
+ (left_sep * left_padding) + title_with_spaces.replace(title, styled_title) + (right_sep * right_padding)
227
+ )
228
+
229
+ # Assemble the complete header
230
+ output_text: str = f"\n{top_line}\n{title_line}\n{bottom_line}\n"
231
+
232
+ if not return_txt:
233
+ self.local_console.print(output_text, style=s2)
234
+ return output_text
235
+
236
+
237
+ def ascii_header(
205
238
  title: str,
206
- sep: str = "#",
239
+ top_sep: str = "#",
240
+ left_sep: str = ">",
241
+ right_sep: str = "<",
242
+ bottom_sep: str = "#",
207
243
  length: int = 60,
208
244
  style1: str = "bold red",
209
245
  style2: str = "bold blue",
@@ -213,13 +249,45 @@ def fmt_header(
213
249
 
214
250
  Args:
215
251
  title (str): The title to display in the header.
216
- sep (str): The character to use for the separator. Defaults to '#'.
252
+ top_sep (str): The character to use for the top separator line. Defaults to '#'.
253
+ left_sep (str): The character to use for the left side of title line. Defaults to '>'.
254
+ right_sep (str): The character to use for the right side of title line. Defaults to '<'.
255
+ bottom_sep (str): The character to use for the bottom separator line. Defaults to '#'.
217
256
  length (int): The total length of the header line. Defaults to 60.
218
257
  style1 (str): The style for the title text. Defaults to 'bold red'.
219
258
  style2 (str): The style for the separator text. Defaults to 'bold blue'.
259
+ print_out (bool): Whether to print the header or just return it. Defaults to True.
220
260
  """
221
261
  text_helper = TextHelper()
222
262
  if print_out:
223
- text_helper.print_header(title=title, sep=sep, length=length, s1=style1, s2=style2, return_txt=False)
263
+ text_helper.print_header(
264
+ title=title,
265
+ top_sep=top_sep,
266
+ left_sep=left_sep,
267
+ right_sep=right_sep,
268
+ bottom_sep=bottom_sep,
269
+ length=length,
270
+ s1=style1,
271
+ s2=style2,
272
+ return_txt=False,
273
+ )
224
274
  return ""
225
- return text_helper.print_header(title=title, sep=sep, length=length, s1=style1, s2=style2, return_txt=True)
275
+ return text_helper.print_header(
276
+ title=title,
277
+ top_sep=top_sep,
278
+ left_sep=left_sep,
279
+ right_sep=right_sep,
280
+ bottom_sep=bottom_sep,
281
+ length=length,
282
+ s1=style1,
283
+ s2=style2,
284
+ return_txt=True,
285
+ )
286
+
287
+
288
+ if __name__ == "__main__":
289
+ # Example usage of the TextHelper
290
+ text_helper = TextHelper()
291
+ text_helper.print_header("My Title", top_sep="#", bottom_sep="#")
292
+ text_helper.print_header("My Title", top_sep="=", left_sep=">", right_sep="<", bottom_sep="=")
293
+ text_helper.print_header("My Title", top_sep="-", left_sep="[", right_sep="]", bottom_sep="-")
@@ -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