bear-utils 0.8.23__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.23 → bear_utils-0.8.25}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.23 → bear_utils-0.8.25}/PKG-INFO +3 -2
  3. {bear_utils-0.8.23 → bear_utils-0.8.25}/README.md +1 -1
  4. {bear_utils-0.8.23 → bear_utils-0.8.25}/pyproject.toml +2 -1
  5. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/prompt_helpers.py +33 -28
  6. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_base_command.py +1 -4
  7. bear_utils-0.8.25/src/bear_utils/cli/typer_bridge.py +90 -0
  8. {bear_utils-0.8.23 → 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.23 → bear_utils-0.8.25}/tests/test_function_response.py +8 -19
  11. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_gradient.py +24 -11
  12. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_prompt_helpers.py +8 -8
  13. {bear_utils-0.8.23 → bear_utils-0.8.25}/uv.lock +34 -1
  14. {bear_utils-0.8.23 → bear_utils-0.8.25}/.gitignore +0 -0
  15. {bear_utils-0.8.23 → bear_utils-0.8.25}/.python-version +0 -0
  16. {bear_utils-0.8.23 → bear_utils-0.8.25}/AGENTS.md +0 -0
  17. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/coverage.ini +0 -0
  18. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/default.toml +0 -0
  19. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/git-changelog.toml +0 -0
  20. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/pytest.ini +0 -0
  21. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/ruff.toml +0 -0
  22. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/vscode/launch.json +0 -0
  23. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/vscode/settings.json +0 -0
  24. {bear_utils-0.8.23 → bear_utils-0.8.25}/config/vscode/tasks.json +0 -0
  25. {bear_utils-0.8.23 → bear_utils-0.8.25}/directory_structure.txt +0 -0
  26. {bear_utils-0.8.23 → bear_utils-0.8.25}/directory_structure.xml +0 -0
  27. {bear_utils-0.8.23 → bear_utils-0.8.25}/maskfile.md +0 -0
  28. {bear_utils-0.8.23 → bear_utils-0.8.25}/noxfile.py +0 -0
  29. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/__init__.py +0 -0
  30. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/__main__.py +0 -0
  31. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/_internal/__init__.py +0 -0
  32. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/_internal/cli.py +0 -0
  33. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/_internal/debug.py +0 -0
  34. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/__init__.py +0 -0
  35. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  36. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  37. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  38. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  39. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  40. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cache/__init__.py +0 -0
  41. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/__init__.py +0 -0
  42. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/commands.py +0 -0
  43. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/shell/__init__.py +0 -0
  44. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_base_shell.py +1 -1
  45. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/cli/shell/_common.py +0 -0
  46. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/config/__init__.py +0 -0
  47. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/config/config_manager.py +0 -0
  48. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/config/dir_manager.py +0 -0
  49. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/config/settings_manager.py +0 -0
  50. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/__init__.py +0 -0
  51. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/_exceptions.py +0 -0
  52. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/_lazy_typing.py +0 -0
  53. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/date_related.py +0 -0
  54. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/server.py +0 -0
  55. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/constants/time_related.py +0 -0
  56. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/database/__init__.py +0 -0
  57. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/database/_db_manager.py +0 -0
  58. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/events/__init__.py +0 -0
  59. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/events/events_class.py +0 -0
  60. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/events/events_module.py +0 -0
  61. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/__init__.py +0 -0
  62. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/_async_helpers.py +0 -0
  63. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/_tools.py +0 -0
  64. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/platform_utils.py +0 -0
  65. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/responses/__init__.py +0 -0
  66. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  67. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  68. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/__init__.py +0 -0
  69. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  70. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  71. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  72. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  73. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  74. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  75. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  76. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  77. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/files/ignore_parser.py +0 -0
  78. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/graphics/__init__.py +0 -0
  79. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/graphics/bear_gradient.py +0 -0
  80. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/graphics/image_helpers.py +0 -0
  81. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/__init__.py +0 -0
  82. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  83. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  84. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  85. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_app.py +0 -0
  86. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  87. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  88. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  89. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/__init__.py +0 -0
  90. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_common.py +0 -0
  91. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  92. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_log_level.py +0 -0
  93. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/_styles.py +0 -0
  94. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/logger_protocol.py +0 -0
  95. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  96. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/_level_sin.py +0 -0
  97. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/base_logger.py +0 -0
  98. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/base_logger.pyi +0 -0
  99. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/__init__.py +0 -0
  100. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/logger.py +0 -0
  101. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/basic_logger/logger.pyi +0 -0
  102. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/buffer_logger.py +0 -0
  103. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/console_logger.py +0 -0
  104. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/console_logger.pyi +0 -0
  105. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/fastapi_logger.py +0 -0
  106. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/file_logger.py +0 -0
  107. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/simple_logger.py +0 -0
  108. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/sub_logger.py +0 -0
  109. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/logger_manager/loggers/sub_logger.pyi +0 -0
  110. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/monitoring/__init__.py +0 -0
  111. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/monitoring/_common.py +0 -0
  112. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/monitoring/host_monitor.py +0 -0
  113. {bear_utils-0.8.23 → bear_utils-0.8.25}/src/bear_utils/time/__init__.py +0 -0
  114. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/__init__.py +0 -0
  115. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_add_ord_suffix.py +0 -0
  116. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_clipboard.py +0 -0
  117. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_database_manager.py +0 -0
  118. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_default_shell.py +0 -0
  119. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_logger.py +0 -0
  120. {bear_utils-0.8.23 → bear_utils-0.8.25}/tests/test_platform_utils.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.23
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.23
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
@@ -19,12 +19,13 @@ Requires-Dist: singleton-base>=1.0.5
19
19
  Requires-Dist: sqlalchemy<3.0.0,>=2.0.40
20
20
  Requires-Dist: tinydb>=4.8.2
21
21
  Requires-Dist: toml>=0.10.2
22
+ Requires-Dist: typer>=0.16.0
22
23
  Requires-Dist: uvicorn>=0.35.0
23
24
  Provides-Extra: gui
24
25
  Requires-Dist: pyqt6>=6.9.0; extra == 'gui'
25
26
  Description-Content-Type: text/markdown
26
27
 
27
- # Bear Utils v# Bear Utils v0.8.23
28
+ # Bear Utils v# Bear Utils v0.8.25
28
29
 
29
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.
30
31
 
@@ -1,4 +1,4 @@
1
- # Bear Utils v# Bear Utils v0.8.23
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.23"
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"
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "fastapi>=0.116.0",
23
23
  "uvicorn>=0.35.0",
24
24
  "bear-epoch-time>=1.1.1",
25
+ "typer>=0.16.0",
25
26
  ]
26
27
 
27
28
  [project.optional-dependencies]
@@ -10,7 +10,11 @@ from bear_utils.constants._exceptions import UserCancelledError
10
10
  from bear_utils.constants._lazy_typing import OptBool, OptFloat, OptInt, OptStr
11
11
  from bear_utils.logger_manager import get_console
12
12
 
13
- # TODO: ehhhhhhhh, it is okay
13
+
14
+ def _parse_exit(value: str) -> bool:
15
+ """Parse a string into a boolean indicating if the user wants to exit."""
16
+ lower_value: str = value.lower().strip()
17
+ return lower_value in ("exit", "quit", "q")
14
18
 
15
19
 
16
20
  def _parse_bool(value: str) -> bool:
@@ -70,7 +74,7 @@ def ask_question(question: str, expected_type: type, default: Any = None) -> Any
70
74
  UserCancelledError: If the user cancels input with Ctrl+C
71
75
  ValueError: If an unsupported type is specified
72
76
  """
73
- console, sub = get_console("prompt_helpers.py")
77
+ console, _ = get_console("prompt_helpers.py")
74
78
 
75
79
  try:
76
80
  while True:
@@ -80,14 +84,14 @@ def ask_question(question: str, expected_type: type, default: Any = None) -> Any
80
84
  if not response:
81
85
  if default is not None:
82
86
  return default
83
- sub.error("Input required. Please enter a value.")
87
+ console.error("Input required. Please enter a value.")
84
88
  continue
85
89
  try:
86
90
  result: str | int | float | bool = _convert_value(response, expected_type)
87
- sub.verbose(f"{expected_type.__name__} detected")
91
+ console.verbose(f"{expected_type.__name__} detected")
88
92
  return result
89
93
  except ValueError as e:
90
- sub.error(f"Invalid input: {e}. Please enter a valid {expected_type.__name__}.")
94
+ console.error(f"Invalid input: {e}. Please enter a valid {expected_type.__name__}.")
91
95
 
92
96
  except KeyboardInterrupt:
93
97
  raise UserCancelledError("User cancelled input") from None
@@ -103,32 +107,32 @@ def ask_yes_no(question: str, default: bool | None = None) -> bool | None:
103
107
  Returns:
104
108
  True for yes, False for no, or None if user exits
105
109
  """
106
- console, sub = get_console("prompt_helpers.py")
107
-
108
- try:
109
- while True:
110
- console.print(question)
111
- response = prompt("> ").strip().lower()
110
+ console, _ = get_console("prompt_helpers.py")
112
111
 
112
+ while True:
113
+ try:
114
+ response: str = prompt(f"{question}\n> ").strip().lower()
113
115
  if not response:
114
116
  if default is not None:
115
117
  return default
116
- sub.error("Please enter 'yes', 'no', or 'exit'.")
118
+ console.print("Please enter 'yes', 'no', or 'exit'.")
117
119
  continue
118
-
119
- if response in ("exit", "quit"):
120
+ if _parse_exit(response):
120
121
  return None
121
122
  try:
122
123
  return _parse_bool(response)
123
124
  except ValueError:
124
- sub.error("Invalid input. Please enter 'yes', 'no', or 'exit'.")
125
- except KeyboardInterrupt:
126
- sub.warning("KeyboardInterrupt: Exiting the prompt.")
127
- 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
128
129
 
129
130
 
130
131
  def restricted_prompt(
131
- question: str, valid_options: list[str], exit_command: str = "exit", case_sensitive: bool = False
132
+ question: str,
133
+ valid_options: list[str],
134
+ exit_command: str = "exit",
135
+ case_sensitive: bool = False,
132
136
  ) -> str | None:
133
137
  """Continuously prompt the user until they provide a valid response or exit.
134
138
 
@@ -141,12 +145,12 @@ def restricted_prompt(
141
145
  Returns:
142
146
  The user's response or None if they chose to exit
143
147
  """
144
- console, sub = get_console("prompt_helpers.py")
145
- completer_options = [*valid_options, exit_command]
148
+ console, _ = get_console("prompt_helpers.py")
149
+ completer_options: list[str] = [*valid_options, exit_command]
146
150
  completer = WordCompleter(completer_options)
147
151
 
148
- comparison_options = valid_options if case_sensitive else [opt.lower() for opt in valid_options]
149
- comparison_exit = exit_command if case_sensitive else exit_command.lower()
152
+ comparison_options: list[str] = valid_options if case_sensitive else [opt.lower() for opt in valid_options]
153
+ comparison_exit: str = exit_command if case_sensitive else exit_command.lower()
150
154
 
151
155
  class OptionValidator(Validator):
152
156
  def validate(self, document: Any) -> None:
@@ -160,13 +164,15 @@ def restricted_prompt(
160
164
 
161
165
  try:
162
166
  while True:
163
- console.print(question)
164
167
  response: str = prompt(
165
- "> ", completer=completer, validator=OptionValidator(), complete_while_typing=True
168
+ f"{question}\n> ",
169
+ completer=completer,
170
+ validator=OptionValidator(),
171
+ complete_while_typing=True,
166
172
  ).strip()
167
173
  comparison_response: str = response if case_sensitive else response.lower()
168
174
  if not response:
169
- sub.error("Please enter a valid option or 'exit'.")
175
+ console.print("Please enter a valid option or 'exit'.", style="red")
170
176
  continue
171
177
  if comparison_response == comparison_exit:
172
178
  return None
@@ -175,7 +181,6 @@ def restricted_prompt(
175
181
  idx: int = comparison_options.index(comparison_response)
176
182
  return valid_options[idx]
177
183
  return response
178
-
179
184
  except KeyboardInterrupt:
180
- sub.warning("KeyboardInterrupt: Exiting the prompt.")
185
+ console.print("KeyboardInterrupt: Exiting the prompt.", style="yellow")
181
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]:
@@ -0,0 +1,90 @@
1
+ """A simple bridge for augmenting Typer with alias support and command execution for interactive use."""
2
+
3
+ from collections.abc import Callable
4
+ import shlex
5
+ from typing import Any, TypedDict
6
+
7
+ from rich.console import Console
8
+ from singleton_base import SingletonBase
9
+ from typer import Exit, Typer
10
+ from typer.models import CommandInfo
11
+
12
+ from bear_utils.logger_manager import AsyncLoggerProtocol, LoggerProtocol
13
+
14
+
15
+ class CommandMeta(TypedDict):
16
+ """Metadata for a Typer command."""
17
+
18
+ name: str
19
+ help: str
20
+ hidden: bool
21
+
22
+
23
+ def get_command_meta(command: CommandInfo) -> CommandMeta:
24
+ """Extract metadata from a Typer command."""
25
+ return {
26
+ "name": command.name or (command.callback.__name__ if command.callback else "unknown"),
27
+ "help": (command.callback.__doc__ if command.callback else None) or "No description available",
28
+ "hidden": command.hidden,
29
+ }
30
+
31
+
32
+ # TODO: Add support for usage statements for a more robust help system
33
+
34
+
35
+ class TyperBridge(SingletonBase):
36
+ """Simple bridge for Typer command execution."""
37
+
38
+ def __init__(self, typer_app: Typer, console: AsyncLoggerProtocol | LoggerProtocol | Console) -> None:
39
+ """Initialize the TyperBridge with a Typer app instance."""
40
+ self.app: Typer = typer_app
41
+ self.console: AsyncLoggerProtocol | LoggerProtocol | Console = console or Console()
42
+ self.command_meta: dict[str, CommandMeta] = {}
43
+
44
+ def alias(self, *alias_names: str) -> Callable[..., Callable[..., Any]]:
45
+ """Register aliases as hidden Typer commands."""
46
+
47
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
48
+ for alias in alias_names:
49
+ self.app.command(name=alias, hidden=True)(func)
50
+ return func
51
+
52
+ return decorator
53
+
54
+ def execute_command(self, command_string: str) -> bool:
55
+ """Execute command via Typer. Return True if successful."""
56
+ try:
57
+ parts: list[str] = shlex.split(command_string.strip())
58
+ if not parts:
59
+ return False
60
+ self.app(parts, standalone_mode=False)
61
+ return True
62
+ except Exit:
63
+ return True
64
+ except Exception as e:
65
+ if isinstance(self.console, Console):
66
+ self.console.print(f"[red]Error executing command: {e}[/red]")
67
+ else:
68
+ self.console.error(f"Error executing command: {e}", exc_info=True)
69
+ return False
70
+
71
+ def bootstrap_command_meta(self) -> None:
72
+ """Bootstrap command metadata from the Typer app."""
73
+ if not self.command_meta:
74
+ for cmd in self.app.registered_commands:
75
+ cmd_meta: CommandMeta = get_command_meta(command=cmd)
76
+ self.command_meta[cmd_meta["name"]] = cmd_meta
77
+
78
+ def get_all_command_info(self, show_hidden: bool = False) -> dict[str, CommandMeta]:
79
+ """Get all command information from the Typer app."""
80
+ if not self.command_meta:
81
+ self.bootstrap_command_meta()
82
+ if not show_hidden:
83
+ return {name: meta for name, meta in self.command_meta.items() if not meta["hidden"]}
84
+ return self.command_meta
85
+
86
+ def get_command_info(self, command_name: str) -> CommandMeta | None:
87
+ """Get metadata for a specific command."""
88
+ if not self.command_meta:
89
+ self.bootstrap_command_meta()
90
+ return self.command_meta.get(command_name)
@@ -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