bear-utils 0.8.6__tar.gz → 0.8.7__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 (110) hide show
  1. {bear_utils-0.8.6 → bear_utils-0.8.7}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.6 → bear_utils-0.8.7}/PKG-INFO +2 -2
  3. {bear_utils-0.8.6 → bear_utils-0.8.7}/README.md +1 -1
  4. {bear_utils-0.8.6 → bear_utils-0.8.7}/pyproject.toml +1 -1
  5. bear_utils-0.8.7/src/bear_utils/cli/prompt_helpers.py +185 -0
  6. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/responses/function_response.py +116 -83
  7. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_function_response.py +42 -7
  8. bear_utils-0.8.6/src/bear_utils/cli/prompt_helpers.py +0 -172
  9. {bear_utils-0.8.6 → bear_utils-0.8.7}/.gitignore +0 -0
  10. {bear_utils-0.8.6 → bear_utils-0.8.7}/.python-version +0 -0
  11. {bear_utils-0.8.6 → bear_utils-0.8.7}/AGENTS.md +0 -0
  12. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/coverage.ini +0 -0
  13. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/default.toml +0 -0
  14. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/git-changelog.toml +0 -0
  15. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/pytest.ini +0 -0
  16. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/ruff.toml +0 -0
  17. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/vscode/launch.json +0 -0
  18. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/vscode/settings.json +0 -0
  19. {bear_utils-0.8.6 → bear_utils-0.8.7}/config/vscode/tasks.json +0 -0
  20. {bear_utils-0.8.6 → bear_utils-0.8.7}/directory_structure.txt +0 -0
  21. {bear_utils-0.8.6 → bear_utils-0.8.7}/maskfile.md +0 -0
  22. {bear_utils-0.8.6 → bear_utils-0.8.7}/noxfile.py +0 -0
  23. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/__init__.py +0 -0
  24. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/__main__.py +0 -0
  25. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/_internal/__init__.py +0 -0
  26. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/_internal/cli.py +0 -0
  27. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/_internal/debug.py +0 -0
  28. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/__init__.py +0 -0
  29. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  30. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  31. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  32. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  33. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  34. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cache/__init__.py +0 -0
  35. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/__init__.py +0 -0
  36. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/commands.py +0 -0
  37. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/shell/__init__.py +0 -0
  38. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/shell/_base_command.py +0 -0
  39. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/shell/_base_shell.py +0 -0
  40. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/cli/shell/_common.py +0 -0
  41. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/config/__init__.py +0 -0
  42. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/config/config_manager.py +0 -0
  43. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/config/dir_manager.py +0 -0
  44. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/config/settings_manager.py +0 -0
  45. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/__init__.py +0 -0
  46. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/_exceptions.py +0 -0
  47. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/_lazy_typing.py +0 -0
  48. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/date_related.py +0 -0
  49. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/logger_protocol.py +0 -0
  50. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/constants/time_related.py +0 -0
  51. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/database/__init__.py +0 -0
  52. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/database/_db_manager.py +0 -0
  53. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/events/__init__.py +0 -0
  54. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/events/events_class.py +0 -0
  55. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/events/events_module.py +0 -0
  56. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/__init__.py +0 -0
  57. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/_async_helpers.py +0 -0
  58. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/_tools.py +0 -0
  59. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/platform_utils.py +0 -0
  60. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/responses/__init__.py +0 -0
  61. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  62. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  63. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/__init__.py +0 -0
  64. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  65. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  66. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  67. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  68. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  69. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  70. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  71. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  72. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/files/ignore_parser.py +0 -0
  73. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/graphics/__init__.py +0 -0
  74. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/graphics/bear_gradient.py +0 -0
  75. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/graphics/image_helpers.py +0 -0
  76. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/__init__.py +0 -0
  77. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  78. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  79. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  80. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/qt_app.py +0 -0
  81. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  82. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  83. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  84. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/__init__.py +0 -0
  85. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/_common.py +0 -0
  86. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  87. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/_styles.py +0 -0
  88. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  89. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_base_logger.py +0 -0
  90. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_base_logger.pyi +0 -0
  91. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_buffer_logger.py +0 -0
  92. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_console_logger.py +0 -0
  93. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_console_logger.pyi +0 -0
  94. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_file_logger.py +0 -0
  95. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_level_sin.py +0 -0
  96. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_logger.py +0 -0
  97. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_sub_logger.py +0 -0
  98. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/logger_manager/loggers/_sub_logger.pyi +0 -0
  99. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/monitoring/__init__.py +0 -0
  100. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/monitoring/_common.py +0 -0
  101. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/monitoring/host_monitor.py +0 -0
  102. {bear_utils-0.8.6 → bear_utils-0.8.7}/src/bear_utils/time/__init__.py +0 -0
  103. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/__init__.py +0 -0
  104. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_add_ord_suffix.py +0 -0
  105. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_clipboard.py +0 -0
  106. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_default_shell.py +0 -0
  107. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_gradient.py +0 -0
  108. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_logger.py +0 -0
  109. {bear_utils-0.8.6 → bear_utils-0.8.7}/tests/test_platform_utils.py +0 -0
  110. {bear_utils-0.8.6 → bear_utils-0.8.7}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.6
2
+ current_version = 0.8.7
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.6
3
+ Version: 0.8.7
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
@@ -20,7 +20,7 @@ Requires-Dist: tinydb>=4.8.2
20
20
  Requires-Dist: toml>=0.10.2
21
21
  Description-Content-Type: text/markdown
22
22
 
23
- # Bear Utils v# Bear Utils v0.8.6
23
+ # Bear Utils v# Bear Utils v0.8.7
24
24
 
25
25
  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.
26
26
 
@@ -1,4 +1,4 @@
1
- # Bear Utils v# Bear Utils v0.8.6
1
+ # Bear Utils v# Bear Utils v0.8.7
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.6"
3
+ version = "0.8.7"
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"
@@ -0,0 +1,185 @@
1
+ """Prompt Helpers Module for user input handling."""
2
+
3
+ from typing import Any, overload
4
+
5
+ from prompt_toolkit import prompt
6
+ from prompt_toolkit.completion import WordCompleter
7
+ from prompt_toolkit.validation import ValidationError, Validator
8
+
9
+ from bear_utils.constants._exceptions import UserCancelledError
10
+ from bear_utils.constants._lazy_typing import OptBool, OptFloat, OptInt, OptStr
11
+ from bear_utils.logger_manager import get_console
12
+
13
+ # TODO: ehhhhhhhh, it is okay
14
+
15
+
16
+ def _parse_bool(value: str) -> bool:
17
+ """Parse a string into a boolean value."""
18
+ lower_value: str = value.lower().strip()
19
+ if lower_value in ("true", "t", "yes", "y", "1"):
20
+ return True
21
+ if lower_value in ("false", "f", "no", "n", "0"):
22
+ return False
23
+ raise ValueError(f"Cannot convert '{value}' to boolean")
24
+
25
+
26
+ def _convert_value(value: str, target_type: type) -> str | int | float | bool:
27
+ """Convert a string value to the target type."""
28
+ if target_type is str:
29
+ return value
30
+ if target_type is int:
31
+ return int(value)
32
+ if target_type is float:
33
+ return float(value)
34
+ if target_type is bool:
35
+ return _parse_bool(value)
36
+ raise ValueError(f"Unsupported type: {target_type}")
37
+
38
+
39
+ @overload
40
+ def ask_question(question: str, expected_type: type[bool], default: OptBool = None) -> bool: ...
41
+
42
+
43
+ @overload
44
+ def ask_question(question: str, expected_type: type[int], default: OptInt = None) -> int: ...
45
+
46
+
47
+ @overload
48
+ def ask_question(question: str, expected_type: type[float], default: OptFloat = None) -> float: ...
49
+
50
+
51
+ @overload
52
+ def ask_question(question: str, expected_type: type[str], default: OptStr = None) -> str: ...
53
+
54
+
55
+ def ask_question(question: str, expected_type: type, default: Any = None) -> Any:
56
+ """Ask a question and return the answer, ensuring the entered type is correct.
57
+
58
+ This function will keep asking until it gets a valid response or the user cancels with Ctrl+C.
59
+ If the user cancels, a UserCancelledError is raised.
60
+
61
+ Args:
62
+ question: The prompt question to display
63
+ expected_type: The expected type class (int, float, str, bool)
64
+ default: Default value if no input is provided
65
+
66
+ Returns:
67
+ The user's response in the expected type
68
+
69
+ Raises:
70
+ UserCancelledError: If the user cancels input with Ctrl+C
71
+ ValueError: If an unsupported type is specified
72
+ """
73
+ console, sub = get_console("prompt_helpers.py")
74
+
75
+ try:
76
+ while True:
77
+ console.print(question)
78
+ response = prompt("> ").strip()
79
+
80
+ if not response:
81
+ if default is not None:
82
+ return default
83
+ sub.error("Input required. Please enter a value.")
84
+ continue
85
+ try:
86
+ result: str | int | float | bool = _convert_value(response, expected_type)
87
+ sub.verbose(f"{expected_type.__name__} detected")
88
+ return result
89
+ except ValueError as e:
90
+ sub.error(f"Invalid input: {e}. Please enter a valid {expected_type.__name__}.")
91
+
92
+ except KeyboardInterrupt:
93
+ raise UserCancelledError("User cancelled input") from None
94
+
95
+
96
+ def ask_yes_no(question: str, default: bool | None = None) -> bool | None:
97
+ """Ask a yes or no question and return the answer.
98
+
99
+ Args:
100
+ question: The prompt question to display
101
+ default: Default value if no input is provided
102
+
103
+ Returns:
104
+ True for yes, False for no, or None if user exits
105
+ """
106
+ console, sub = get_console("prompt_helpers.py")
107
+
108
+ try:
109
+ while True:
110
+ console.print(question)
111
+ response = prompt("> ").strip().lower()
112
+
113
+ if not response:
114
+ if default is not None:
115
+ return default
116
+ sub.error("Please enter 'yes', 'no', or 'exit'.")
117
+ continue
118
+
119
+ if response in ("exit", "quit"):
120
+ return None
121
+ try:
122
+ return _parse_bool(response)
123
+ 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
128
+
129
+
130
+ def restricted_prompt(
131
+ question: str, valid_options: list[str], exit_command: str = "exit", case_sensitive: bool = False
132
+ ) -> str | None:
133
+ """Continuously prompt the user until they provide a valid response or exit.
134
+
135
+ Args:
136
+ question: The prompt question to display
137
+ valid_options: List of valid responses
138
+ exit_command: Command to exit the prompt (default: "exit")
139
+ case_sensitive: Whether options are case-sensitive (default: False)
140
+
141
+ Returns:
142
+ The user's response or None if they chose to exit
143
+ """
144
+ console, sub = get_console("prompt_helpers.py")
145
+ completer_options = [*valid_options, exit_command]
146
+ completer = WordCompleter(completer_options)
147
+
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()
150
+
151
+ class OptionValidator(Validator):
152
+ def validate(self, document: Any) -> None:
153
+ """Validate the user's input against the valid options."""
154
+ text: Any = document.text if case_sensitive else document.text.lower()
155
+ if text and text != comparison_exit and text not in comparison_options:
156
+ raise ValidationError(
157
+ message=f"Invalid option. Choose from: {', '.join(valid_options)} or '{exit_command}'",
158
+ cursor_position=len(document.text),
159
+ )
160
+
161
+ try:
162
+ while True:
163
+ console.print(question)
164
+ response: str = prompt(
165
+ "> ", completer=completer, validator=OptionValidator(), complete_while_typing=True
166
+ ).strip()
167
+ comparison_response: str = response if case_sensitive else response.lower()
168
+ if not response:
169
+ sub.error("Please enter a valid option or 'exit'.")
170
+ continue
171
+ if comparison_response == comparison_exit:
172
+ return None
173
+ if comparison_response in comparison_options:
174
+ if not case_sensitive:
175
+ idx: int = comparison_options.index(comparison_response)
176
+ return valid_options[idx]
177
+ return response
178
+
179
+ except KeyboardInterrupt:
180
+ sub.warning("KeyboardInterrupt: Exiting the prompt.")
181
+ return None
182
+
183
+
184
+ if __name__ == "__main__":
185
+ ask_question("What is your age?", int)
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from io import StringIO
5
+ from collections.abc import Callable
6
6
  import json
7
7
  from subprocess import CompletedProcess
8
+ from types import SimpleNamespace as Namespace
8
9
  from typing import Any, Literal, Self, overload
9
10
 
10
11
  from pydantic import BaseModel, Field, field_validator
@@ -23,35 +24,46 @@ class FunctionResponse(BaseModel):
23
24
  extra: dict = Field(default_factory=dict, description="Additional metadata or information related to the response.")
24
25
  content: list[str] = Field(default=[], description="Content returned by the function call")
25
26
  error: list[str] = Field(default=[], description="Error message if the function call failed")
27
+ sub_tasks: list[FunctionResponse] = Field(default_factory=list, description="List of sub-tasks.")
26
28
  number_of_tasks: int = Field(default=0, description="Number of tasks processed in this response.")
27
29
  logger: LoggerProtocol | None = Field(default=None, description="Logger instance for logging messages.")
30
+ attrs: Namespace = Field(default_factory=Namespace, description="Storing additional attributes dynamically.")
28
31
 
29
32
  model_config = {
30
33
  "arbitrary_types_allowed": True,
31
34
  }
32
35
 
36
+ def __getattr__(self, key: str, default: Any = None) -> Any:
37
+ if key in FunctionResponse.model_fields:
38
+ raise AttributeError(f"This should never be called, {key} is a model field.")
39
+ if hasattr(self.attrs, key):
40
+ return getattr(self.attrs, key)
41
+ return default
42
+
43
+ def __setattr__(self, key: str, value: Any) -> None:
44
+ if key in FunctionResponse.model_fields:
45
+ object.__setattr__(self, key, value)
46
+ return
47
+ setattr(self.attrs, key, value)
48
+
33
49
  def __repr__(self) -> str:
34
50
  """Return a string representation of Response."""
35
- result = StringIO()
36
- result.write("Response(")
37
- if self.name:
38
- result.write(f"name={self.name!r}, ")
39
- if self.returncode:
40
- result.write(f"success={self.success!r}, ")
41
- if self.content:
42
- content: str = ", ".join(self.content)
43
- result.write(f"content={content!r}, ")
44
- if self.error:
45
- error: str = ", ".join(self.error)
46
- result.write(f"error={error!r}, ")
47
- if self.extra:
48
- result.write(f"extra={json.dumps(self.extra)!r}, ")
49
- if self.number_of_tasks > 0:
50
- result.write(f"number_of_tasks={self.number_of_tasks!r}, ")
51
- result.write(")")
52
- returned_result: str = result.getvalue().replace(", )", ")")
53
- result.close()
54
- return returned_result
51
+ parts: list[str] = []
52
+
53
+ def add(k: str, v: Any, _bool: bool = True, formatter: Callable | None = None) -> None:
54
+ if _bool:
55
+ formatted_value: str = formatter(v) if formatter else repr(v)
56
+ parts.append(f"{k}={formatted_value}")
57
+
58
+ add("name", self.name, bool(self.name))
59
+ add("returncode", self.returncode, self.returncode != 0)
60
+ add("success", self.success, bool(self.returncode))
61
+ add("content", ", ".join(self.content), bool(self.content))
62
+ add("error", ", ".join(self.error), bool(self.error))
63
+ add("extra", self.extra, bool(self.extra), json.dumps)
64
+ add("number_of_tasks", self.number_of_tasks, self.number_of_tasks > 0)
65
+
66
+ return f"Response({', '.join(parts)})"
55
67
 
56
68
  def __str__(self) -> str:
57
69
  """Return a string representation of Response."""
@@ -124,6 +136,19 @@ class FunctionResponse(BaseModel):
124
136
 
125
137
  return cls().add(returncode=returncode, content=content, error=error, **kwargs)
126
138
 
139
+ def from_response(self, response: FunctionResponse | Any, **kwargs) -> Self:
140
+ """Create a FunctionResponse from another FunctionResponse object."""
141
+ if not isinstance(response, FunctionResponse):
142
+ raise TypeError("Expected a FunctionResponse instance.")
143
+ self.sub_tasks.append(response)
144
+ return self.add(
145
+ content=response.content,
146
+ error=response.error,
147
+ returncode=response.returncode,
148
+ log_output=kwargs.pop("log_output", False),
149
+ **kwargs,
150
+ )
151
+
127
152
  @property
128
153
  def success(self) -> bool:
129
154
  """Check if the response indicates success."""
@@ -137,7 +162,7 @@ class FunctionResponse(BaseModel):
137
162
  extra: dict[str, Any] | None = None,
138
163
  returncode: int | None = None,
139
164
  log_output: bool = False,
140
- ) -> None:
165
+ ) -> Self:
141
166
  """Add a sub-task response to the FunctionResponse."""
142
167
  func_response: FunctionResponse = FunctionResponse(name=name, logger=self.logger).add(
143
168
  content=content,
@@ -147,6 +172,8 @@ class FunctionResponse(BaseModel):
147
172
  extra=extra,
148
173
  )
149
174
  self.add(content=func_response)
175
+ self.sub_tasks.append(func_response)
176
+ return self
150
177
 
151
178
  def successful(
152
179
  self,
@@ -170,49 +197,41 @@ class FunctionResponse(BaseModel):
170
197
  self.add(content=content, error=error, returncode=returncode or 1, **kwargs)
171
198
  return self
172
199
 
173
- def _add_error(self, error: str) -> None:
174
- """Append an error message to the existing error."""
175
- if error != "":
176
- self.error.append(error)
200
+ def _add_item(self, item: str, target_list: list[str]) -> None:
201
+ """Append an item to the target list if not empty."""
202
+ if item != "":
203
+ target_list.append(item)
177
204
 
178
- def _add_to_error(self, error: str | list[str], name: str | None = None) -> None:
179
- """Append additional error messages to the existing error."""
205
+ def _add_to_list(self, items: str | list[str], target_list: list[str], name: str | None = None) -> None:
206
+ """Append items to the target list with optional name prefix."""
180
207
  try:
181
- if isinstance(error, list):
182
- for err in error:
183
- self._add_error(error=f"{name}: {err}" if name else err)
184
- elif isinstance(error, str):
185
- self._add_error(error=f"{name}: {error}" if name else error)
208
+ if isinstance(items, list):
209
+ for item in items:
210
+ self._add_item(f"{name}: {item}" if name else item, target_list)
211
+ elif isinstance(items, str):
212
+ self._add_item(f"{name}: {items}" if name else items, target_list)
186
213
  except Exception as e:
187
- raise ValueError(f"Failed to add error: {e!s}") from e
214
+ raise ValueError(f"Failed to add items: {e!s}") from e
188
215
 
189
- def _add_content(self, content: str) -> None:
216
+ def _add_content(self, content: str | list[str], name: str | None = None) -> None:
190
217
  """Append content to the existing content."""
191
- if content != "":
192
- self.content.append(content)
218
+ self._add_to_list(content, self.content, name)
193
219
 
194
- def _add_to_content(self, content: str | list[str], name: str | None = None) -> None:
195
- """Append additional content to the existing content."""
196
- try:
197
- if isinstance(content, list):
198
- for item in content:
199
- self._add_content(content=f"{name}: {item}" if name else item)
200
- elif isinstance(content, str):
201
- self._add_content(content=f"{name}: {content}" if name else content)
202
- except Exception as e:
203
- raise ValueError(f"Failed to add content: {e!s}") from e
220
+ def _add_error(self, error: str | list[str], name: str | None = None) -> None:
221
+ """Append error to the existing error."""
222
+ self._add_to_list(error, self.error, name)
204
223
 
205
224
  def _handle_function_response(self, func_response: FunctionResponse) -> None:
206
225
  """Handle a FunctionResponse object and update the current response."""
207
226
  if func_response.extra:
208
227
  self.extra.update(func_response.extra)
209
- self._add_to_error(error=func_response.error, name=func_response.name)
210
- self._add_to_content(content=func_response.content, name=func_response.name)
228
+ self._add_error(error=func_response.error, name=func_response.name)
229
+ self._add_content(content=func_response.content, name=func_response.name)
211
230
 
212
231
  def _handle_completed_process(self, result: CompletedProcess[str]) -> None:
213
232
  """Handle a CompletedProcess object and update the FunctionResponse."""
214
- self._add_to_content(content=result.stdout.strip() if result.stdout else "")
215
- self._add_to_error(error=result.stderr.strip() if result.stderr else "")
233
+ self._add_content(content=result.stdout.strip() if result.stdout else "")
234
+ self._add_error(error=result.stderr.strip() if result.stderr else "")
216
235
  self.returncode = result.returncode
217
236
 
218
237
  def add(
@@ -244,13 +263,13 @@ class FunctionResponse(BaseModel):
244
263
  self._handle_completed_process(result=content)
245
264
  self.number_of_tasks += 1
246
265
  case str() | list() if content:
247
- self._add_to_content(content=content)
266
+ self._add_content(content=content)
248
267
  self.number_of_tasks += 1
249
268
  case None:
250
269
  content = None
251
270
  case _:
252
271
  content = None
253
- self._add_to_error(error=error) if isinstance(error, (str | list)) else None
272
+ self._add_error(error=error) if isinstance(error, (str | list)) else None
254
273
  self.returncode = returncode if returncode is not None else self.returncode
255
274
  self.extra.update(extra) if isinstance(extra, dict) else None
256
275
  if log_output and self.logger is not None and (content is not None or error is not None):
@@ -266,18 +285,19 @@ class FunctionResponse(BaseModel):
266
285
  logger: LoggerProtocol,
267
286
  ) -> None:
268
287
  """Log the content and error messages if they exist."""
269
- if content is not None and error is None:
270
- if isinstance(content, list):
271
- for item in content:
272
- logger.info(f"{self.name}: {item}" if self.name else item)
273
- elif isinstance(content, str):
274
- logger.info(f"{self.name}: {content}" if self.name else content)
275
- elif error is not None and content is None:
276
- if isinstance(error, list):
277
- for err in error:
278
- logger.error(f"{self.name}: {err}" if self.name else err)
279
- elif isinstance(error, str):
280
- logger.error(f"{self.name}: {error}" if self.name else error)
288
+
289
+ def _log_messages(messages: str | list[str], log_func: Callable) -> None:
290
+ if isinstance(messages, str):
291
+ messages = [messages]
292
+ if isinstance(messages, list):
293
+ for msg in messages:
294
+ log_func(f"{self.name}: {msg}" if self.name else msg)
295
+
296
+ if content and isinstance(content, (str | list)):
297
+ _log_messages(content, logger.info)
298
+
299
+ if error and isinstance(error, (str | list)):
300
+ _log_messages(error, logger.error)
281
301
 
282
302
  @overload
283
303
  def done(self, to_dict: Literal[True], suppress: list[str] | None = None) -> dict[str, Any]: ...
@@ -295,26 +315,26 @@ class FunctionResponse(BaseModel):
295
315
  Returns:
296
316
  dict[str, Any] | Self: The dictionary representation or the FunctionResponse instance.
297
317
  """
318
+ if not to_dict:
319
+ return self
320
+
298
321
  if suppress is None:
299
322
  suppress = []
300
- if to_dict:
301
- result: dict[str, Any] = {}
302
- if self.name and "name" not in suppress:
303
- result["name"] = self.name
304
- if "success" not in suppress:
305
- result.update({"success": self.success})
306
- if self.returncode > 0 and "returncode" not in suppress:
307
- result["returncode"] = self.returncode
308
- if self.number_of_tasks > 0 and "number_of_tasks" not in suppress:
309
- result["number_of_tasks"] = self.number_of_tasks
310
- if self.content and "content" not in suppress:
311
- result["content"] = self.content
312
- if self.error and "error" not in suppress:
313
- result["error"] = self.error
314
- if self.extra:
315
- result.update(self.extra)
316
- return result
317
- return self
323
+
324
+ result: dict[str, Any] = {}
325
+
326
+ def add(k: str, v: Any, _bool: bool = True) -> None:
327
+ if k not in suppress and _bool:
328
+ result[k] = v
329
+
330
+ add("name", self.name, bool(self.name))
331
+ add("success", self.success)
332
+ add("returncode", self.returncode, self.returncode > 0)
333
+ add("number_of_tasks", self.number_of_tasks, self.number_of_tasks > 0)
334
+ add("content", self.content, bool(self.content))
335
+ add("error", self.error, bool(self.error))
336
+ result.update(self.extra)
337
+ return result
318
338
 
319
339
 
320
340
  def success(
@@ -334,3 +354,16 @@ def fail(
334
354
  ) -> FunctionResponse:
335
355
  """Create a failed FunctionResponse."""
336
356
  return FunctionResponse().fail(content=content, error=error, returncode=returncode, **kwargs)
357
+
358
+
359
+ if __name__ == "__main__":
360
+ # Example usage
361
+ from rich import inspect
362
+
363
+ response = FunctionResponse(name="example_function", returncode=0, content=["Task completed successfully."])
364
+ response.task_id = 124
365
+
366
+ inspect(response)
367
+ print(response)
368
+ print(response.done(to_dict=True))
369
+ print(response.done(to_dict=False))
@@ -3,7 +3,9 @@
3
3
  from subprocess import CompletedProcess
4
4
 
5
5
  import pytest
6
+ from rich.text import Text
6
7
 
8
+ from bear_utils import BufferLogger
7
9
  from bear_utils.extras.responses.function_response import (
8
10
  FAILURE,
9
11
  SUCCESS,
@@ -223,10 +225,7 @@ class TestSubTaskMethod:
223
225
  main_response.sub_task(name="subtask", content="test")
224
226
  # The sub_task should inherit the parent's returncode
225
227
  assert main_response.number_of_tasks == 1
226
- # Can't directly test the sub-response returncode, but the behavior should be consistent
227
-
228
- # The sub_task should inherit the parent's returncode
229
- assert main_response.number_of_tasks == 1
228
+ assert main_response.returncode == 5
230
229
  # Can't directly test the sub-response returncode, but the behavior should be consistent
231
230
 
232
231
  def test_sub_task_override_returncode(self):
@@ -235,6 +234,7 @@ class TestSubTaskMethod:
235
234
  main_response.sub_task(name="subtask", content="test", returncode=2)
236
235
 
237
236
  assert main_response.number_of_tasks == 1
237
+ assert main_response.returncode == 0
238
238
  # The sub-task uses the overridden returncode
239
239
 
240
240
  def test_sub_task_with_extra_and_kwargs(self):
@@ -351,10 +351,45 @@ class TestAddMethod:
351
351
  assert response.returncode == 2
352
352
  assert response.extra == {"metadata": "value"}
353
353
 
354
- def test_add_with_logging(self):
354
+ def get_buffer_logger(self) -> BufferLogger:
355
+ """Get a BufferLogger instance for testing."""
356
+ from bear_utils import BufferLogger # noqa: PLC0415
357
+
358
+ if BufferLogger.has_instance():
359
+ BufferLogger.reset_instance()
360
+
361
+ import logging # noqa: PLC0415
362
+
363
+ logging.getLogger().handlers.clear()
364
+ return BufferLogger.get_instance(init=True, name="TestBufferLogger")
365
+
366
+ def test_add_with_logging_content(self) -> None:
355
367
  """Test add method with logging enabled."""
356
- # Skip this test since LoggerProtocol validation is strict
357
- pytest.skip("LoggerProtocol validation prevents mock objects")
368
+ buffer_logger: BufferLogger = self.get_buffer_logger()
369
+ response = FunctionResponse(logger=buffer_logger)
370
+ response.add(content="log entry", log_output=True)
371
+
372
+ output: str | Text = buffer_logger.trigger_buffer_flush()
373
+ assert "log entry" in str(output)
374
+
375
+ def test_add_with_logging_error(self) -> None:
376
+ """Test add method with logging error."""
377
+ buffer_logger: BufferLogger = self.get_buffer_logger()
378
+ response = FunctionResponse(logger=buffer_logger)
379
+ response.add(error="log error", log_output=True)
380
+
381
+ output: str | Text = buffer_logger.trigger_buffer_flush()
382
+ assert "log error" in str(output)
383
+
384
+ def test_add_with_logging_content_and_error(self) -> None:
385
+ """Test add method with logging content and error."""
386
+ buffer_logger: BufferLogger = self.get_buffer_logger()
387
+ response = FunctionResponse(logger=buffer_logger)
388
+ response.add(content="log content", error="log error", log_output=True)
389
+
390
+ output: str | Text = buffer_logger.trigger_buffer_flush()
391
+ assert "log content" in str(output)
392
+ assert "log error" in str(output)
358
393
 
359
394
 
360
395
  class TestDoneMethod:
@@ -1,172 +0,0 @@
1
- """Prompt Helpers Module for user input handling."""
2
-
3
- from typing import Any, overload
4
-
5
- from prompt_toolkit import prompt
6
- from prompt_toolkit.completion import WordCompleter
7
- from prompt_toolkit.validation import ValidationError, Validator
8
-
9
- from bear_utils.constants._exceptions import UserCancelledError
10
- from bear_utils.constants._lazy_typing import LitBool, LitFloat, LitInt, LitStr, OptBool, OptFloat, OptInt, OptStr
11
- from bear_utils.logger_manager import get_console
12
-
13
- # TODO: Overhaul this trash, it is written like absolute garbage.
14
-
15
-
16
- @overload
17
- def ask_question(question: str, expected_type: LitInt, default: OptInt = None, **kwargs) -> int: ...
18
-
19
-
20
- @overload
21
- def ask_question(question: str, expected_type: LitFloat, default: OptFloat = None, **kwargs) -> float: ...
22
-
23
-
24
- @overload
25
- def ask_question(question: str, expected_type: LitStr, default: OptStr = None, **kwargs) -> str: ...
26
-
27
-
28
- @overload
29
- def ask_question(question: str, expected_type: LitBool, default: OptBool = None, **kwargs) -> bool: ...
30
-
31
-
32
- def ask_question(question: str, expected_type: Any, default: Any = None, **_) -> Any:
33
- """Ask a question and return the answer, ensuring the entered type is correct and a value is entered.
34
-
35
- This function will keep asking until it gets a valid response or the user cancels with Ctrl+C.
36
- If the user cancels, a UserCancelledError is raised.
37
-
38
- Args:
39
- question: The prompt question to display
40
- expected_type: The expected type of the answer (int, float, str, bool)
41
- default: Default value if no input is provided
42
-
43
- Returns:
44
- The user's response in the expected type
45
-
46
- Raises:
47
- UserCancelledError: If the user cancels input with Ctrl+C
48
- ValueError: If an unsupported type is specified
49
- """
50
- console, sub = get_console("prompt_helpers.py")
51
- try:
52
- while True:
53
- console.print(question)
54
- response: str = prompt("> ")
55
- if response == "":
56
- if default is not None:
57
- return default
58
- continue
59
- match expected_type:
60
- case "int":
61
- try:
62
- result = int(response)
63
- sub.verbose("int detected")
64
- return result
65
- except ValueError:
66
- sub.error("Invalid input. Please enter a valid integer.")
67
- case "float":
68
- try:
69
- result = float(response)
70
- sub.verbose("float detected")
71
- return result
72
- except ValueError:
73
- sub.error("Invalid input. Please enter a valid float.")
74
- case "str":
75
- sub.verbose("str detected")
76
- return response
77
- case "bool":
78
- lower_response = response.lower()
79
- if lower_response in ("true", "t", "yes", "y", "1"):
80
- return True
81
- if lower_response in ("false", "f", "no", "n", "0"):
82
- return False
83
- sub.error("Invalid input. Please enter a valid boolean (true/false, yes/no, etc).")
84
- case _:
85
- raise ValueError(f"Unsupported type: {expected_type}")
86
- except KeyboardInterrupt:
87
- raise UserCancelledError("User cancelled input") from None
88
-
89
-
90
- def ask_yes_no(question: str, default: None | Any = None, **kwargs) -> None | bool:
91
- """Ask a yes or no question and return the answer.
92
-
93
- Args:
94
- question: The prompt question to display
95
- default: Default value if no input is provided
96
-
97
- Returns:
98
- True for yes, False for no, or None if no valid response is given
99
- """
100
- kwargs = kwargs or {}
101
- sub, console = get_console("prompt_helpers.py")
102
- try:
103
- while True:
104
- console.info(question)
105
- response = prompt("> ")
106
-
107
- if response == "":
108
- if default is not None:
109
- return default
110
- continue
111
-
112
- if response.lower() in ["yes", "y"]:
113
- return True
114
- if response.lower() in ["no", "n"]:
115
- return False
116
- if response.lower() in ["exit", "quit"]:
117
- return None
118
- console.error("Invalid input. Please enter 'yes' or 'no' or exit.")
119
- continue
120
- except KeyboardInterrupt:
121
- console.warning("KeyboardInterrupt: Exiting the prompt.")
122
- return None
123
-
124
-
125
- def restricted_prompt(question: str, valid_options: list[str], exit_command: str = "exit", **kwargs) -> None | str:
126
- """Continuously prompt the user until they provide a valid response or exit.
127
-
128
- Args:
129
- question: The prompt question to display
130
- valid_options: List of valid responses
131
- exit_command: Command to exit the prompt (default: "exit")
132
-
133
- Returns:
134
- The user's response or None if they chose to exit
135
- """
136
- kwargs = kwargs or {}
137
- sub, console = get_console("prompt_helpers.py")
138
- completer_options: list[str] = [*valid_options, exit_command]
139
- completer = WordCompleter(completer_options)
140
-
141
- class OptionValidator(Validator):
142
- def validate(self, document: Any) -> None:
143
- """Validate the user's input against the valid options."""
144
- text: str = document.text.lower()
145
- if text != exit_command and text not in valid_options:
146
- raise ValidationError(
147
- message=f"Invalid option: {text}. Please choose from {', '.join(valid_options)} or type '{exit_command}' to exit.",
148
- cursor_position=len(document.text),
149
- )
150
-
151
- try:
152
- while True:
153
- if console is not None:
154
- console.info(question)
155
- response = prompt("> ", completer=completer, validator=OptionValidator(), complete_while_typing=True)
156
- response = response.lower()
157
- else:
158
- response = prompt(
159
- question, completer=completer, validator=OptionValidator(), complete_while_typing=True
160
- )
161
- response = response.lower()
162
-
163
- if response == exit_command:
164
- return None
165
- if response == "":
166
- sub.error("No input provided. Please enter a valid option or exit.")
167
- continue
168
- if response in valid_options:
169
- return response
170
- except KeyboardInterrupt:
171
- sub.warning("KeyboardInterrupt: Exiting the prompt.")
172
- return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes