bear-utils 0.8.23__py3-none-any.whl → 0.8.24__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
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
|
@@ -108,15 +112,13 @@ def ask_yes_no(question: str, default: bool | None = None) -> bool | None:
|
|
108
112
|
try:
|
109
113
|
while True:
|
110
114
|
console.print(question)
|
111
|
-
response = prompt("> ").strip().lower()
|
112
|
-
|
115
|
+
response: str = prompt("> ").strip().lower()
|
113
116
|
if not response:
|
114
117
|
if default is not None:
|
115
118
|
return default
|
116
119
|
sub.error("Please enter 'yes', 'no', or 'exit'.")
|
117
120
|
continue
|
118
|
-
|
119
|
-
if response in ("exit", "quit"):
|
121
|
+
if _parse_exit(response):
|
120
122
|
return None
|
121
123
|
try:
|
122
124
|
return _parse_bool(response)
|
@@ -128,7 +130,10 @@ def ask_yes_no(question: str, default: bool | None = None) -> bool | None:
|
|
128
130
|
|
129
131
|
|
130
132
|
def restricted_prompt(
|
131
|
-
question: str,
|
133
|
+
question: str,
|
134
|
+
valid_options: list[str],
|
135
|
+
exit_command: str = "exit",
|
136
|
+
case_sensitive: bool = False,
|
132
137
|
) -> str | None:
|
133
138
|
"""Continuously prompt the user until they provide a valid response or exit.
|
134
139
|
|
@@ -141,12 +146,12 @@ def restricted_prompt(
|
|
141
146
|
Returns:
|
142
147
|
The user's response or None if they chose to exit
|
143
148
|
"""
|
144
|
-
console,
|
145
|
-
completer_options = [*valid_options, exit_command]
|
149
|
+
console, _ = get_console("prompt_helpers.py")
|
150
|
+
completer_options: list[str] = [*valid_options, exit_command]
|
146
151
|
completer = WordCompleter(completer_options)
|
147
152
|
|
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()
|
153
|
+
comparison_options: list[str] = valid_options if case_sensitive else [opt.lower() for opt in valid_options]
|
154
|
+
comparison_exit: str = exit_command if case_sensitive else exit_command.lower()
|
150
155
|
|
151
156
|
class OptionValidator(Validator):
|
152
157
|
def validate(self, document: Any) -> None:
|
@@ -162,11 +167,14 @@ def restricted_prompt(
|
|
162
167
|
while True:
|
163
168
|
console.print(question)
|
164
169
|
response: str = prompt(
|
165
|
-
"> ",
|
170
|
+
"> ",
|
171
|
+
completer=completer,
|
172
|
+
validator=OptionValidator(),
|
173
|
+
complete_while_typing=True,
|
166
174
|
).strip()
|
167
175
|
comparison_response: str = response if case_sensitive else response.lower()
|
168
176
|
if not response:
|
169
|
-
|
177
|
+
console.error("Please enter a valid option or 'exit'.")
|
170
178
|
continue
|
171
179
|
if comparison_response == comparison_exit:
|
172
180
|
return None
|
@@ -175,7 +183,6 @@ def restricted_prompt(
|
|
175
183
|
idx: int = comparison_options.index(comparison_response)
|
176
184
|
return valid_options[idx]
|
177
185
|
return response
|
178
|
-
|
179
186
|
except KeyboardInterrupt:
|
180
|
-
|
187
|
+
console.warning("KeyboardInterrupt: Exiting the prompt.")
|
181
188
|
return None
|
@@ -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 metadata(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)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bear-utils
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.24
|
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.24
|
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,7 +12,8 @@ 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=0Cwa5bk_7xgLBeLuJje9LwNtrRYlTCAW__mpFpFD1gw,6814
|
16
|
+
bear_utils/cli/typer_bridge.py,sha256=ah2dCJ_LJFRy3hDF5Dd8rYqq70H6ko4KTuCuVcUmRAw,3408
|
16
17
|
bear_utils/cli/shell/__init__.py,sha256=2s3oR6CqLKj1iyERy7YafWT3t3KzTr70Z1yaLKa6IiQ,42
|
17
18
|
bear_utils/cli/shell/_base_command.py,sha256=T1bwY9UX355Mv2u7JjSgI4U7VoWCTibszG8WvC3IEo4,2789
|
18
19
|
bear_utils/cli/shell/_base_shell.py,sha256=cjpgwf8R6iP9T8HZDy_MKIirW5qM0btXLmmCce1Ll58,16702
|
@@ -86,6 +87,6 @@ bear_utils/monitoring/__init__.py,sha256=9DKNIWTp_voLnaWgiP-wJ-o_N0hYixo-MzjUmg8
|
|
86
87
|
bear_utils/monitoring/_common.py,sha256=LYQFxgTP9fk0cH71IQTuGwBYYPWCqHP_mMRNecoD76M,657
|
87
88
|
bear_utils/monitoring/host_monitor.py,sha256=e0TYRJw9iDj5Ga6y3ck1TBFEeH42Cax5mQYaNU8yams,13241
|
88
89
|
bear_utils/time/__init__.py,sha256=VctjJG17SyEHAFXytI1sZrOrq7zm3hVenIDOJFdaMN0,1424
|
89
|
-
bear_utils-0.8.
|
90
|
-
bear_utils-0.8.
|
91
|
-
bear_utils-0.8.
|
90
|
+
bear_utils-0.8.24.dist-info/METADATA,sha256=6nRLOfttz27oKf-N_I-gCBeLmjX7tpSnlOvUc228JyA,8792
|
91
|
+
bear_utils-0.8.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
92
|
+
bear_utils-0.8.24.dist-info/RECORD,,
|
File without changes
|