bear-utils 0.8.23__py3-none-any.whl → 0.8.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bear_utils/cli/prompt_helpers.py +33 -28
- bear_utils/cli/shell/_base_command.py +1 -4
- bear_utils/cli/shell/_base_shell.py +1 -1
- bear_utils/cli/typer_bridge.py +90 -0
- bear_utils/extras/responses/function_response.py +103 -54
- bear_utils/extras/wrappers/string_io.py +46 -0
- {bear_utils-0.8.23.dist-info → bear_utils-0.8.25.dist-info}/METADATA +3 -2
- {bear_utils-0.8.23.dist-info → bear_utils-0.8.25.dist-info}/RECORD +9 -7
- {bear_utils-0.8.23.dist-info → bear_utils-0.8.25.dist-info}/WHEEL +0 -0
bear_utils/cli/prompt_helpers.py
CHANGED
@@ -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
|
-
|
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,
|
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
|
-
|
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
|
-
|
91
|
+
console.verbose(f"{expected_type.__name__} detected")
|
88
92
|
return result
|
89
93
|
except ValueError as e:
|
90
|
-
|
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,
|
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
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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,
|
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,
|
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
|
-
"> ",
|
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
|
-
|
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
|
-
|
185
|
+
console.print("KeyboardInterrupt: Exiting the prompt.", style="yellow")
|
181
186
|
return None
|
@@ -13,8 +13,8 @@ import subprocess
|
|
13
13
|
from subprocess import CompletedProcess
|
14
14
|
from typing import Self, override
|
15
15
|
|
16
|
-
from bear_utils.logger_manager.logger_protocol import LoggerProtocol
|
17
16
|
from bear_utils.logger_manager import VERBOSE, BaseLogger, SubConsoleLogger
|
17
|
+
from bear_utils.logger_manager.logger_protocol import LoggerProtocol
|
18
18
|
|
19
19
|
from ._base_command import BaseShellCommand
|
20
20
|
from ._common import DEFAULT_SHELL
|
@@ -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
|
-
|
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,
|
73
|
+
def add(k: str, v: Any, _bool: bool = True, fmt_func: Callable | None = None) -> None:
|
62
74
|
if _bool:
|
63
|
-
formatted_value: str =
|
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
|
-
|
148
|
-
|
149
|
-
if
|
150
|
-
|
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
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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.
|
237
|
-
self.
|
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(
|
242
|
-
self._add_error(
|
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.
|
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.
|
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(
|
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(
|
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(
|
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("
|
386
|
-
|
387
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bear-utils
|
3
|
-
Version: 0.8.
|
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.
|
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
|
|
@@ -12,10 +12,11 @@ bear_utils/ai/ai_helpers/_types.py,sha256=rmnl8mTlUj0LyL9USzTb-EN_31TtXY6qzhkOEu
|
|
12
12
|
bear_utils/cache/__init__.py,sha256=c9z1mLhWpZJHZdXRlviYQXl8tc9KTJCM8vin3moDO3I,4578
|
13
13
|
bear_utils/cli/__init__.py,sha256=H2QpLyHpQS_Yn3sF2px7n4KqT97LEe7Oyzafg2iHcpc,503
|
14
14
|
bear_utils/cli/commands.py,sha256=5ppEjvVV_g28WLaIFtKgz-ctzwoo-g-KpHTXNx9xBzo,3161
|
15
|
-
bear_utils/cli/prompt_helpers.py,sha256=
|
15
|
+
bear_utils/cli/prompt_helpers.py,sha256=VGNwbPDYXO-FoYkACI0Zq7_IUWThexGlycvt9RBzjEU,6844
|
16
|
+
bear_utils/cli/typer_bridge.py,sha256=RE30vf7LQzdGeWvP08W_N_FZ3r2hoD372uHOqTmzcK0,3405
|
16
17
|
bear_utils/cli/shell/__init__.py,sha256=2s3oR6CqLKj1iyERy7YafWT3t3KzTr70Z1yaLKa6IiQ,42
|
17
|
-
bear_utils/cli/shell/_base_command.py,sha256
|
18
|
-
bear_utils/cli/shell/_base_shell.py,sha256=
|
18
|
+
bear_utils/cli/shell/_base_command.py,sha256=-uKvfSYo4f3vMCST19yN19ZtY3N80zQcfu39ogc9vPY,2711
|
19
|
+
bear_utils/cli/shell/_base_shell.py,sha256=6CWAdQTiSQp9t01Cb5rVSgUOD3zRKRR2CLT-XomM-ro,16702
|
19
20
|
bear_utils/cli/shell/_common.py,sha256=_KQyL5lvqOfjonFIwlEOyp3K9G3TSOj19RhgVzfNNpg,669
|
20
21
|
bear_utils/config/__init__.py,sha256=HC_lWpmLF0kbPr5i1Wa2FLER2b446E_GecgU9EPmc04,353
|
21
22
|
bear_utils/config/config_manager.py,sha256=Xj0xOmY-wo_rwfcWiXyxNZWX9NknX_Jm9W56Gx8yyHQ,8244
|
@@ -37,9 +38,10 @@ bear_utils/extras/_async_helpers.py,sha256=4buULZZkR0n2z13Q8_FK59dMchj0Mup03YBaq
|
|
37
38
|
bear_utils/extras/_tools.py,sha256=-rOH_-0pRd7_-o_l2QqByYBB2yhbXcTC8fuQf4Sx-eg,7671
|
38
39
|
bear_utils/extras/platform_utils.py,sha256=Ai7ow7S-_cKb5zFwFh8dkC8xmbMJFy-0_-w3NCERdEw,1362
|
39
40
|
bear_utils/extras/responses/__init__.py,sha256=XbE4VKemrKRwx9E5jqy__OiM_AAjA58ebnqQ2hytnT0,225
|
40
|
-
bear_utils/extras/responses/function_response.py,sha256=
|
41
|
+
bear_utils/extras/responses/function_response.py,sha256=rt_fvAx2WC3o0_T_cN2IcW4JC6FN1UXUb3UauE9EJ0A,18065
|
41
42
|
bear_utils/extras/wrappers/__init__.py,sha256=crh4sKOLvuhNMVX5bJYjCFWtXtH7G47UgNPOHq3HXTk,43
|
42
43
|
bear_utils/extras/wrappers/add_methods.py,sha256=z2XZG2ZoYOB1MaGiLli4NRyyTeRgBy7tuYsiy8mTa9s,4422
|
44
|
+
bear_utils/extras/wrappers/string_io.py,sha256=8VoIoX9G7bgasvQRF7m3zaySahCA_kKU8OveQPItN2k,1563
|
43
45
|
bear_utils/files/__init__.py,sha256=mIdnFSXoDE64ElM43bN2m6KuafURnN82ki0pdqN8q2o,201
|
44
46
|
bear_utils/files/ignore_parser.py,sha256=2KlbfbEn012KGMqW4TdcV6jFbHXGmuCmA5DjjmIZ6_Y,10958
|
45
47
|
bear_utils/files/file_handlers/__init__.py,sha256=VF2IlWNr3UqeSvsbh3YCbLw9cLmlyf64mfeOKuhBdvk,136
|
@@ -86,6 +88,6 @@ bear_utils/monitoring/__init__.py,sha256=9DKNIWTp_voLnaWgiP-wJ-o_N0hYixo-MzjUmg8
|
|
86
88
|
bear_utils/monitoring/_common.py,sha256=LYQFxgTP9fk0cH71IQTuGwBYYPWCqHP_mMRNecoD76M,657
|
87
89
|
bear_utils/monitoring/host_monitor.py,sha256=e0TYRJw9iDj5Ga6y3ck1TBFEeH42Cax5mQYaNU8yams,13241
|
88
90
|
bear_utils/time/__init__.py,sha256=VctjJG17SyEHAFXytI1sZrOrq7zm3hVenIDOJFdaMN0,1424
|
89
|
-
bear_utils-0.8.
|
90
|
-
bear_utils-0.8.
|
91
|
-
bear_utils-0.8.
|
91
|
+
bear_utils-0.8.25.dist-info/METADATA,sha256=iTz_dSSAaHbbtjbE5wcQzqCu9z1f7sGbmJjL9_2e8sY,8792
|
92
|
+
bear_utils-0.8.25.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
93
|
+
bear_utils-0.8.25.dist-info/RECORD,,
|
File without changes
|