bear-utils 0.7.21__py3-none-any.whl → 0.7.23__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/__init__.py +24 -1
- bear_utils/ai/__init__.py +5 -5
- bear_utils/ai/ai_helpers/__init__.py +24 -18
- bear_utils/ai/ai_helpers/_parsers.py +27 -21
- bear_utils/ai/ai_helpers/_types.py +2 -7
- bear_utils/cache/__init__.py +35 -23
- bear_utils/cli/__init__.py +13 -0
- bear_utils/cli/commands.py +14 -8
- bear_utils/cli/prompt_helpers.py +40 -34
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +18 -18
- bear_utils/cli/shell/_base_shell.py +37 -34
- bear_utils/config/__init__.py +4 -2
- bear_utils/config/config_manager.py +193 -56
- bear_utils/config/dir_manager.py +8 -3
- bear_utils/config/settings_manager.py +94 -171
- bear_utils/constants/__init__.py +2 -1
- bear_utils/constants/_exceptions.py +6 -1
- bear_utils/constants/date_related.py +2 -0
- bear_utils/constants/logger_protocol.py +28 -0
- bear_utils/constants/time_related.py +2 -0
- bear_utils/database/__init__.py +2 -0
- bear_utils/database/_db_manager.py +10 -11
- bear_utils/events/__init__.py +3 -1
- bear_utils/events/events_class.py +11 -11
- bear_utils/events/events_module.py +17 -8
- bear_utils/extras/__init__.py +8 -6
- bear_utils/extras/_async_helpers.py +2 -3
- bear_utils/extras/_tools.py +62 -52
- bear_utils/extras/platform_utils.py +5 -1
- bear_utils/extras/responses/__init__.py +1 -0
- bear_utils/extras/responses/function_response.py +301 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +17 -15
- bear_utils/files/__init__.py +3 -1
- bear_utils/files/file_handlers/__init__.py +2 -0
- bear_utils/files/file_handlers/_base_file_handler.py +23 -3
- bear_utils/files/file_handlers/file_handler_factory.py +38 -38
- bear_utils/files/file_handlers/json_file_handler.py +49 -22
- bear_utils/files/file_handlers/log_file_handler.py +19 -12
- bear_utils/files/file_handlers/toml_file_handler.py +13 -5
- bear_utils/files/file_handlers/txt_file_handler.py +56 -14
- bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
- bear_utils/files/ignore_parser.py +52 -57
- bear_utils/graphics/__init__.py +3 -1
- bear_utils/graphics/bear_gradient.py +17 -12
- bear_utils/graphics/image_helpers.py +11 -5
- bear_utils/gui/__init__.py +3 -1
- bear_utils/gui/gui_tools/__init__.py +3 -1
- bear_utils/gui/gui_tools/_settings.py +0 -1
- bear_utils/gui/gui_tools/qt_app.py +16 -11
- bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
- bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
- bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
- bear_utils/logging/__init__.py +6 -4
- bear_utils/logging/logger_manager/__init__.py +1 -0
- bear_utils/logging/logger_manager/_common.py +0 -1
- bear_utils/logging/logger_manager/_console_junk.py +14 -10
- bear_utils/logging/logger_manager/_styles.py +1 -2
- bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -36
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
- bear_utils/logging/logger_manager/loggers/_console_logger.py +52 -26
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
- bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
- bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
- bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
- bear_utils/logging/loggers.py +9 -13
- bear_utils/monitoring/__init__.py +7 -4
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +44 -48
- bear_utils/time/__init__.py +13 -6
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/METADATA +50 -6
- bear_utils-0.7.23.dist-info/RECORD +83 -0
- bear_utils-0.7.21.dist-info/RECORD +0 -79
- {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/WHEEL +0 -0
bear_utils/__init__.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
"""A module for Bear Utils, providing various utilities and tools."""
|
2
|
+
|
1
3
|
from importlib.metadata import version
|
2
4
|
|
3
5
|
from bear_epoch_time import EpochTimestamp, TimeTools
|
@@ -12,4 +14,25 @@ from .logging.logger_manager._common import VERBOSE_CONSOLE_FORMAT
|
|
12
14
|
from .logging.logger_manager._styles import VERBOSE
|
13
15
|
from .logging.loggers import BaseLogger, BufferLogger, ConsoleLogger, FileLogger
|
14
16
|
|
15
|
-
__version__: str = version("
|
17
|
+
__version__: str = version(distribution_name="bear_utils")
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
"DATE_FORMAT",
|
21
|
+
"DATE_TIME_FORMAT",
|
22
|
+
"VERBOSE",
|
23
|
+
"VERBOSE_CONSOLE_FORMAT",
|
24
|
+
"BaseLogger",
|
25
|
+
"BufferLogger",
|
26
|
+
"CacheWrapper",
|
27
|
+
"ConsoleLogger",
|
28
|
+
"DatabaseManager",
|
29
|
+
"EpochTimestamp",
|
30
|
+
"Events",
|
31
|
+
"FileHandlerFactory",
|
32
|
+
"FileLogger",
|
33
|
+
"SettingsManager",
|
34
|
+
"TimeTools",
|
35
|
+
"cache",
|
36
|
+
"cache_factory",
|
37
|
+
"get_settings_manager",
|
38
|
+
]
|
bear_utils/ai/__init__.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
"""AI Helpers Module for Bear Utils."""
|
2
|
+
|
1
3
|
from .ai_helpers._common import (
|
2
4
|
GPT_4_1,
|
3
5
|
GPT_4_1_MINI,
|
@@ -10,7 +12,6 @@ from .ai_helpers._common import (
|
|
10
12
|
from .ai_helpers._config import AIEndpointConfig
|
11
13
|
from .ai_helpers._parsers import CommandResponseParser, PassthroughResponseParser, ResponseParser, TypedResponseParser
|
12
14
|
from .ai_helpers._types import ResponseParser as BaseResponseParser
|
13
|
-
from .ai_helpers._types import T_Response
|
14
15
|
|
15
16
|
__all__ = [
|
16
17
|
"GPT_4_1",
|
@@ -18,13 +19,12 @@ __all__ = [
|
|
18
19
|
"GPT_4_1_NANO",
|
19
20
|
"PRODUCTION_MODE",
|
20
21
|
"TESTING_MODE",
|
21
|
-
"AIModel",
|
22
|
-
"EnvironmentMode",
|
23
22
|
"AIEndpointConfig",
|
23
|
+
"AIModel",
|
24
|
+
"BaseResponseParser",
|
24
25
|
"CommandResponseParser",
|
26
|
+
"EnvironmentMode",
|
25
27
|
"PassthroughResponseParser",
|
26
28
|
"ResponseParser",
|
27
29
|
"TypedResponseParser",
|
28
|
-
"T_Response",
|
29
|
-
"BaseResponseParser",
|
30
30
|
]
|
@@ -1,8 +1,13 @@
|
|
1
|
+
"""This module provides helper functions to create AI endpoints with various response parsing strategies."""
|
2
|
+
|
1
3
|
from collections.abc import Callable
|
2
|
-
from typing import Any
|
4
|
+
from typing import Any, cast
|
5
|
+
|
6
|
+
from rich.markdown import Markdown
|
7
|
+
|
8
|
+
from bear_utils.logging import BaseLogger
|
3
9
|
|
4
|
-
from
|
5
|
-
from ._common import GPT_4_1, GPT_4_1_MINI, GPT_4_1_NANO, PRODUCTION_MODE, TESTING_MODE, AIModel, EnvironmentMode
|
10
|
+
from ._common import GPT_4_1_NANO, PRODUCTION_MODE, TESTING_MODE, AIModel, EnvironmentMode
|
6
11
|
from ._config import AIEndpointConfig
|
7
12
|
from ._parsers import JSONResponseParser, ModularAIEndpoint, PassthroughResponseParser, TypedResponseParser
|
8
13
|
|
@@ -29,8 +34,11 @@ def create_typed_endpoint[T_Response](
|
|
29
34
|
chat_model=chat_model,
|
30
35
|
**kwargs,
|
31
36
|
)
|
32
|
-
parser = TypedResponseParser(
|
33
|
-
|
37
|
+
parser: TypedResponseParser[type[T_Response]] = TypedResponseParser(
|
38
|
+
default_response=response_type,
|
39
|
+
response_transformers=transformers,
|
40
|
+
)
|
41
|
+
return cast("ModularAIEndpoint[T_Response]", ModularAIEndpoint(config, logger, parser))
|
34
42
|
|
35
43
|
|
36
44
|
def create_command_endpoint[T_Response](
|
@@ -44,8 +52,7 @@ def create_command_endpoint[T_Response](
|
|
44
52
|
chat_model: AIModel = GPT_4_1_NANO,
|
45
53
|
**kwargs,
|
46
54
|
) -> ModularAIEndpoint[T_Response]:
|
47
|
-
|
48
|
-
|
55
|
+
"""Create an endpoint that returns a command response with Markdown formatting."""
|
49
56
|
config = AIEndpointConfig(
|
50
57
|
project_name=project_name,
|
51
58
|
prompt=prompt,
|
@@ -56,12 +63,11 @@ def create_command_endpoint[T_Response](
|
|
56
63
|
**kwargs,
|
57
64
|
)
|
58
65
|
|
59
|
-
parser = TypedResponseParser(
|
66
|
+
parser: TypedResponseParser[T_Response] = TypedResponseParser(
|
60
67
|
default_response=default_response,
|
61
68
|
response_transformers={"response": lambda x: Markdown(str(x))},
|
62
69
|
)
|
63
|
-
|
64
|
-
return ModularAIEndpoint(config, logger, parser)
|
70
|
+
return ModularAIEndpoint(config=config, logger=logger, response_parser=parser)
|
65
71
|
|
66
72
|
|
67
73
|
def create_flexible_endpoint(
|
@@ -88,8 +94,8 @@ def create_flexible_endpoint(
|
|
88
94
|
chat_model=chat_model,
|
89
95
|
**kwargs,
|
90
96
|
)
|
91
|
-
parser = JSONResponseParser(required_fields, transformers)
|
92
|
-
return ModularAIEndpoint(config, logger, parser)
|
97
|
+
parser = JSONResponseParser(required_fields=required_fields, response_transformers=transformers)
|
98
|
+
return ModularAIEndpoint(config=config, logger=logger, response_parser=parser)
|
93
99
|
|
94
100
|
|
95
101
|
def create_simple_endpoint(
|
@@ -118,13 +124,13 @@ def create_simple_endpoint(
|
|
118
124
|
|
119
125
|
|
120
126
|
__all__: list[str] = [
|
121
|
-
"create_typed_endpoint",
|
122
|
-
"create_command_endpoint",
|
123
|
-
"create_flexible_endpoint",
|
124
|
-
"create_simple_endpoint",
|
125
|
-
"AIEndpointConfig",
|
126
|
-
"EnvironmentMode",
|
127
127
|
"PRODUCTION_MODE",
|
128
128
|
"TESTING_MODE",
|
129
|
+
"AIEndpointConfig",
|
130
|
+
"EnvironmentMode",
|
129
131
|
"ModularAIEndpoint",
|
132
|
+
"create_command_endpoint",
|
133
|
+
"create_flexible_endpoint",
|
134
|
+
"create_simple_endpoint",
|
135
|
+
"create_typed_endpoint",
|
130
136
|
]
|
@@ -1,14 +1,17 @@
|
|
1
|
-
import json
|
2
1
|
from collections.abc import Callable
|
3
|
-
|
2
|
+
import json
|
3
|
+
from typing import Any, cast
|
4
4
|
|
5
5
|
from httpx import AsyncClient, Headers, Response
|
6
6
|
from rich.markdown import Markdown
|
7
7
|
from singleton_base.singleton_base_new import SingletonBase
|
8
8
|
|
9
|
-
from
|
9
|
+
from bear_utils import BaseLogger, EpochTimestamp, SettingsManager, get_settings_manager
|
10
|
+
|
10
11
|
from . import AIEndpointConfig
|
11
|
-
from ._types import ResponseParser
|
12
|
+
from ._types import ResponseParser
|
13
|
+
|
14
|
+
SUCCESSFUL_STATUS_CODE = 200
|
12
15
|
|
13
16
|
|
14
17
|
class JSONResponseParser(ResponseParser[dict[str, Any]]):
|
@@ -20,7 +23,7 @@ class JSONResponseParser(ResponseParser[dict[str, Any]]):
|
|
20
23
|
|
21
24
|
async def parse(self, raw_response: dict, logger: BaseLogger) -> dict[str, Any]:
|
22
25
|
"""Parse JSON response with configurable validation and transformation."""
|
23
|
-
default = self.get_default_response()
|
26
|
+
default: dict[str, Any] = self.get_default_response()
|
24
27
|
|
25
28
|
output = raw_response.get("output", "")
|
26
29
|
if not output:
|
@@ -56,14 +59,14 @@ class JSONResponseParser(ResponseParser[dict[str, Any]]):
|
|
56
59
|
return {"error": "Failed to parse response"}
|
57
60
|
|
58
61
|
|
59
|
-
class TypedResponseParser
|
62
|
+
class TypedResponseParser[T_Response](ResponseParser[T_Response]):
|
60
63
|
def __init__(self, default_response: T_Response, response_transformers: dict[str, Callable] | None = None):
|
61
64
|
self.default_response: T_Response = default_response
|
62
65
|
self.response_transformers = response_transformers or {}
|
63
|
-
self.required_fields = list(cast(dict, self.default_response).keys())
|
66
|
+
self.required_fields = list(cast("dict", self.default_response).keys())
|
64
67
|
|
65
68
|
def get_default_response(self) -> T_Response:
|
66
|
-
return cast(T_Response, self.default_response)
|
69
|
+
return cast("T_Response", self.default_response)
|
67
70
|
|
68
71
|
async def parse(self, raw_response: dict, logger: BaseLogger) -> T_Response:
|
69
72
|
"""Parse JSON response with strict typing."""
|
@@ -90,18 +93,17 @@ class TypedResponseParser(ResponseParser[T_Response], Generic[T_Response]):
|
|
90
93
|
for field_name, transformer in self.response_transformers.items():
|
91
94
|
if field_name in response_dict:
|
92
95
|
response_dict[field_name] = transformer(response_dict[field_name])
|
93
|
-
return cast(T_Response, response_dict)
|
96
|
+
return cast("T_Response", response_dict)
|
94
97
|
|
95
98
|
except json.JSONDecodeError as e:
|
96
99
|
logger.error(f"Failed to parse response JSON: {e}")
|
97
100
|
return default
|
98
101
|
|
99
102
|
|
100
|
-
class CommandResponseParser(TypedResponseParser):
|
103
|
+
class CommandResponseParser[T_Response](TypedResponseParser[T_Response]):
|
101
104
|
"""Specialized parser for command-based responses."""
|
102
105
|
|
103
106
|
def __init__(self, response_type: type[T_Response]):
|
104
|
-
|
105
107
|
super().__init__(
|
106
108
|
default_response=response_type(),
|
107
109
|
response_transformers={
|
@@ -114,17 +116,23 @@ class PassthroughResponseParser(ResponseParser[dict[str, Any]]):
|
|
114
116
|
"""Parser that returns the raw output without JSON parsing."""
|
115
117
|
|
116
118
|
async def parse(self, raw_response: dict, logger: BaseLogger) -> dict[str, Any]:
|
119
|
+
"""Return the raw output from the response without parsing."""
|
117
120
|
output = raw_response.get("output", "")
|
118
|
-
return {"output": output}
|
121
|
+
return {"output": output, "logger": logger}
|
119
122
|
|
120
123
|
def get_default_response(self) -> dict[str, Any]:
|
121
124
|
return {"output": ""}
|
122
125
|
|
123
126
|
|
124
|
-
class ModularAIEndpoint
|
127
|
+
class ModularAIEndpoint[T_Response](SingletonBase):
|
125
128
|
"""Modular AI endpoint for flexible communication patterns."""
|
126
129
|
|
127
|
-
def __init__(
|
130
|
+
def __init__(
|
131
|
+
self,
|
132
|
+
config: AIEndpointConfig,
|
133
|
+
logger: BaseLogger,
|
134
|
+
response_parser: ResponseParser[T_Response],
|
135
|
+
) -> None:
|
128
136
|
self.config: AIEndpointConfig = config
|
129
137
|
self.logger: BaseLogger = logger
|
130
138
|
self.response_parser: ResponseParser[T_Response] = response_parser
|
@@ -136,9 +144,8 @@ class ModularAIEndpoint(SingletonBase, Generic[T_Response]):
|
|
136
144
|
self.settings_manager: SettingsManager = get_settings_manager(self.config.project_name)
|
137
145
|
self.set_session_id(new=True)
|
138
146
|
|
139
|
-
def set_session_id(self, new: bool = False):
|
140
|
-
"""
|
141
|
-
Set the session ID for the current interaction.
|
147
|
+
def set_session_id(self, new: bool = False) -> None:
|
148
|
+
"""Set the session ID for the current interaction.
|
142
149
|
|
143
150
|
Args:
|
144
151
|
new (bool): If True, start a new session; otherwise, continue the existing session.
|
@@ -178,11 +185,10 @@ class ModularAIEndpoint(SingletonBase, Generic[T_Response]):
|
|
178
185
|
}
|
179
186
|
),
|
180
187
|
)
|
181
|
-
if response.status_code ==
|
188
|
+
if response.status_code == SUCCESSFUL_STATUS_CODE:
|
182
189
|
return await parser.parse(response.json(), self.logger)
|
183
|
-
|
184
|
-
|
185
|
-
return parser.get_default_response()
|
190
|
+
self.logger.error(f"Failed to send message to AI: {response.status_code} - {response.text}")
|
191
|
+
return parser.get_default_response()
|
186
192
|
except Exception as e:
|
187
193
|
self.logger.error(f"Exception during AI communication: {e}")
|
188
194
|
return parser.get_default_response()
|
@@ -1,20 +1,15 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
|
-
from typing import Generic, TypeVar
|
3
2
|
|
4
|
-
from
|
3
|
+
from bear_utils.logging import BaseLogger
|
5
4
|
|
6
|
-
T_Response = TypeVar("T_Response")
|
7
5
|
|
8
|
-
|
9
|
-
class ResponseParser(ABC, Generic[T_Response]):
|
6
|
+
class ResponseParser[T_Response](ABC):
|
10
7
|
"""Abstract base class for response parsers."""
|
11
8
|
|
12
9
|
@abstractmethod
|
13
10
|
async def parse(self, raw_response: dict, logger: BaseLogger) -> T_Response:
|
14
11
|
"""Parse the raw response into the desired format."""
|
15
|
-
pass
|
16
12
|
|
17
13
|
@abstractmethod
|
18
14
|
def get_default_response(self) -> T_Response:
|
19
15
|
"""Return a default response structure."""
|
20
|
-
pass
|
bear_utils/cache/__init__.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
"""A set of helper caching utilities for bear_utils."""
|
2
|
+
|
1
3
|
import functools
|
2
4
|
from pathlib import Path
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from diskcache import Cache
|
5
8
|
|
@@ -7,15 +10,20 @@ DEFAULT_CACHE_DIR = Path("~/.cache/app_cache").expanduser()
|
|
7
10
|
|
8
11
|
|
9
12
|
class CacheWrapper:
|
10
|
-
"""
|
11
|
-
|
13
|
+
"""A simple wrapper around diskcache.Cache to provide a consistent interface.
|
14
|
+
|
12
15
|
This class allows for easy caching of function results with a specified directory,
|
13
16
|
size limit, and default timeout.
|
14
17
|
"""
|
15
18
|
|
16
|
-
def __init__(
|
17
|
-
|
18
|
-
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
directory: str | None = None,
|
22
|
+
size_limit: int | None = None,
|
23
|
+
default_timeout: int | None = None,
|
24
|
+
**kwargs,
|
25
|
+
) -> None:
|
26
|
+
"""Initialize the CacheWrapper with a specified directory, size limit, and default timeout.
|
19
27
|
|
20
28
|
Args:
|
21
29
|
directory (str, optional): Directory path for the cache. Defaults to ~/.cache/app_cache.
|
@@ -25,20 +33,24 @@ class CacheWrapper:
|
|
25
33
|
self.cache = Cache(directory or DEFAULT_CACHE_DIR, size_limit=size_limit or 1_000_000_000, **kwargs)
|
26
34
|
self.default_timeout = default_timeout
|
27
35
|
|
28
|
-
def get(self, key, default=None):
|
36
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
29
37
|
"""Get a value from the cache."""
|
30
38
|
return self.cache.get(key, default=default)
|
31
39
|
|
32
|
-
def set(self, key, value, expire=None) -> None:
|
40
|
+
def set(self, key: Any, value: Any, expire: int | None = None) -> None:
|
33
41
|
"""Set a value in the cache."""
|
34
42
|
if expire is None:
|
35
43
|
expire = self.default_timeout
|
36
44
|
self.cache.set(key, value, expire=expire)
|
37
45
|
|
38
46
|
|
39
|
-
def cache_factory(
|
40
|
-
|
41
|
-
|
47
|
+
def cache_factory(
|
48
|
+
directory: str | None = None,
|
49
|
+
size_limit: int | None = None,
|
50
|
+
default_timeout: int | None = None,
|
51
|
+
**kwargs: Any,
|
52
|
+
) -> Any:
|
53
|
+
"""Creates and configures a cache decorator factory.
|
42
54
|
|
43
55
|
Args:
|
44
56
|
directory (str, optional): Cache directory path. Defaults to ~/.cache/app_cache.
|
@@ -63,19 +75,18 @@ def cache_factory(directory=None, size_limit=None, default_timeout=None, **kwarg
|
|
63
75
|
def another_function(x, y):
|
64
76
|
return x * y
|
65
77
|
"""
|
66
|
-
if directory
|
67
|
-
|
68
|
-
|
69
|
-
|
78
|
+
local_directory: Path | None = Path(directory).expanduser() if directory else None
|
79
|
+
if local_directory is None:
|
80
|
+
local_directory = Path(DEFAULT_CACHE_DIR)
|
81
|
+
local_directory.mkdir(parents=True, exist_ok=True)
|
70
82
|
|
71
83
|
if size_limit is None:
|
72
84
|
size_limit = 1_000_000_000
|
73
85
|
|
74
|
-
cache_instance = Cache(
|
86
|
+
cache_instance = Cache(local_directory, size_limit=size_limit, **kwargs)
|
75
87
|
|
76
|
-
def decorator(func=None, *, expire=default_timeout, key=None):
|
77
|
-
"""
|
78
|
-
Decorator that caches function results.
|
88
|
+
def decorator(func: object | None = None, *, expire: int | None = default_timeout, key: Any = None) -> object:
|
89
|
+
"""Decorator that caches function results.
|
79
90
|
|
80
91
|
Args:
|
81
92
|
func: The function to cache (when used as @cache)
|
@@ -86,20 +97,21 @@ def cache_factory(directory=None, size_limit=None, default_timeout=None, **kwarg
|
|
86
97
|
callable: Decorated function or decorator
|
87
98
|
"""
|
88
99
|
|
89
|
-
def actual_decorator(fn):
|
90
|
-
|
100
|
+
def actual_decorator(fn: Any) -> object:
|
101
|
+
"""Actual decorator that wraps the function with caching logic."""
|
102
|
+
|
103
|
+
def wrapper(*args, **kwargs) -> tuple[Any, ...]:
|
91
104
|
if key is not None:
|
92
105
|
cache_key = key(fn, *args, **kwargs)
|
93
106
|
else:
|
94
107
|
cache_key = (fn.__module__, fn.__qualname__, args, frozenset(kwargs.items()))
|
95
108
|
|
96
|
-
# Try to get from cache
|
97
109
|
result = cache_instance.get(cache_key, default=None)
|
98
110
|
if result is not None:
|
99
111
|
return result
|
100
112
|
|
101
113
|
# If not in cache, compute and store
|
102
|
-
result = fn(*args, **kwargs)
|
114
|
+
result: Any = fn(*args, **kwargs)
|
103
115
|
cache_instance.set(cache_key, result, expire=expire)
|
104
116
|
return result
|
105
117
|
|
@@ -116,4 +128,4 @@ def cache_factory(directory=None, size_limit=None, default_timeout=None, **kwarg
|
|
116
128
|
|
117
129
|
cache = cache_factory()
|
118
130
|
|
119
|
-
__all__ = ["CacheWrapper", "
|
131
|
+
__all__ = ["CacheWrapper", "cache", "cache_factory"]
|
bear_utils/cli/__init__.py
CHANGED
@@ -1,4 +1,17 @@
|
|
1
|
+
"""A set of command-line interface (CLI) utilities for bear_utils."""
|
2
|
+
|
1
3
|
from .commands import GitCommand, MaskShellCommand, OPShellCommand, UVShellCommand
|
2
4
|
from .shell._base_command import BaseShellCommand
|
3
5
|
from .shell._base_shell import SimpleShellSession, shell_session
|
4
6
|
from .shell._common import DEFAULT_SHELL
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"DEFAULT_SHELL",
|
10
|
+
"BaseShellCommand",
|
11
|
+
"GitCommand",
|
12
|
+
"MaskShellCommand",
|
13
|
+
"OPShellCommand",
|
14
|
+
"SimpleShellSession",
|
15
|
+
"UVShellCommand",
|
16
|
+
"shell_session",
|
17
|
+
]
|
bear_utils/cli/commands.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
"""Shell Commands Module for Bear Utils."""
|
2
|
+
|
1
3
|
from typing import Self
|
2
4
|
|
3
5
|
from .shell._base_command import BaseShellCommand
|
@@ -8,7 +10,8 @@ class OPShellCommand(BaseShellCommand):
|
|
8
10
|
|
9
11
|
command_name = "op"
|
10
12
|
|
11
|
-
def __init__(self, *args, **kwargs):
|
13
|
+
def __init__(self, *args, **kwargs) -> None:
|
14
|
+
"""Initialize the OPShellCommand with the op command."""
|
12
15
|
super().__init__(*args, **kwargs)
|
13
16
|
|
14
17
|
@classmethod
|
@@ -22,11 +25,12 @@ class UVShellCommand(BaseShellCommand):
|
|
22
25
|
|
23
26
|
command_name = "uv"
|
24
27
|
|
25
|
-
def __init__(self, *args, **kwargs):
|
28
|
+
def __init__(self, *args, **kwargs) -> None:
|
29
|
+
"""Initialize the UVShellCommand with the uv command."""
|
26
30
|
super().__init__(*args, **kwargs)
|
27
31
|
|
28
32
|
@classmethod
|
29
|
-
def pip(cls, s="", *args, **kwargs) -> Self:
|
33
|
+
def pip(cls, s: str = "", *args, **kwargs) -> Self:
|
30
34
|
"""Create a piped command for uv"""
|
31
35
|
if s:
|
32
36
|
return cls.sub(f"pip {s}", *args, **kwargs)
|
@@ -38,11 +42,12 @@ class MaskShellCommand(BaseShellCommand):
|
|
38
42
|
|
39
43
|
command_name = "mask"
|
40
44
|
|
41
|
-
def __init__(self, *args, **kwargs):
|
45
|
+
def __init__(self, *args, **kwargs) -> None:
|
46
|
+
"""Initialize the MaskShellCommand with the mask command."""
|
42
47
|
super().__init__(*args, **kwargs)
|
43
48
|
|
44
49
|
@classmethod
|
45
|
-
def maskfile(cls, maskfile, *args, **kwargs) -> Self:
|
50
|
+
def maskfile(cls, maskfile: str, *args, **kwargs) -> Self:
|
46
51
|
"""Create a maskfile command with the specified maskfile"""
|
47
52
|
return cls.sub("--maskfile", *args, **kwargs).value(maskfile)
|
48
53
|
|
@@ -57,7 +62,8 @@ class GitCommand(BaseShellCommand):
|
|
57
62
|
|
58
63
|
command_name = "git"
|
59
64
|
|
60
|
-
def __init__(self, *args, **kwargs):
|
65
|
+
def __init__(self, *args, **kwargs) -> None:
|
66
|
+
"""Initialize the GitCommand with the git command."""
|
61
67
|
super().__init__(*args, **kwargs)
|
62
68
|
|
63
69
|
@classmethod
|
@@ -76,7 +82,7 @@ class GitCommand(BaseShellCommand):
|
|
76
82
|
return cls.sub("log", *args, **kwargs)
|
77
83
|
|
78
84
|
@classmethod
|
79
|
-
def add(cls, files, *args, **kwargs) -> "GitCommand":
|
85
|
+
def add(cls, files: str, *args, **kwargs) -> "GitCommand":
|
80
86
|
"""Add files to the staging area"""
|
81
87
|
return cls.sub("add", *args, **kwargs).value(files)
|
82
88
|
|
@@ -92,8 +98,8 @@ class GitCommand(BaseShellCommand):
|
|
92
98
|
|
93
99
|
|
94
100
|
__all__ = [
|
101
|
+
"GitCommand",
|
95
102
|
"MaskShellCommand",
|
96
103
|
"OPShellCommand",
|
97
104
|
"UVShellCommand",
|
98
|
-
"GitCommand",
|
99
105
|
]
|
bear_utils/cli/prompt_helpers.py
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
+
"""Prompt Helpers Module for user input handling."""
|
2
|
+
|
1
3
|
from typing import Any, overload
|
2
4
|
|
3
5
|
from prompt_toolkit import prompt
|
4
6
|
from prompt_toolkit.completion import WordCompleter
|
5
7
|
from prompt_toolkit.validation import ValidationError, Validator
|
6
8
|
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
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.logging.loggers import get_console
|
12
|
+
|
13
|
+
# TODO: Overhaul this trash, it is written like absolute garbage.
|
10
14
|
|
11
15
|
|
12
16
|
@overload
|
@@ -25,12 +29,11 @@ def ask_question(question: str, expected_type: LitStr, default: OptStr = None, *
|
|
25
29
|
def ask_question(question: str, expected_type: LitBool, default: OptBool = None, **kwargs) -> bool: ...
|
26
30
|
|
27
31
|
|
28
|
-
def ask_question(question: str, expected_type: Any, default: Any = None, **
|
29
|
-
"""
|
30
|
-
Ask a question and return the answer, ensuring the entered type is correct and a value is entered.
|
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.
|
31
34
|
|
32
35
|
This function will keep asking until it gets a valid response or the user cancels with Ctrl+C.
|
33
|
-
If the user cancels, a
|
36
|
+
If the user cancels, a UserCancelledError is raised.
|
34
37
|
|
35
38
|
Args:
|
36
39
|
question: The prompt question to display
|
@@ -41,19 +44,18 @@ def ask_question(question: str, expected_type: Any, default: Any = None, **kwarg
|
|
41
44
|
The user's response in the expected type
|
42
45
|
|
43
46
|
Raises:
|
44
|
-
|
47
|
+
UserCancelledError: If the user cancels input with Ctrl+C
|
45
48
|
ValueError: If an unsupported type is specified
|
46
49
|
"""
|
47
50
|
console, sub = get_console("prompt_helpers.py")
|
48
51
|
try:
|
49
52
|
while True:
|
50
|
-
console.
|
53
|
+
console.print(question)
|
51
54
|
response: str = prompt("> ")
|
52
55
|
if response == "":
|
53
56
|
if default is not None:
|
54
57
|
return default
|
55
|
-
|
56
|
-
continue
|
58
|
+
continue
|
57
59
|
match expected_type:
|
58
60
|
case "int":
|
59
61
|
try:
|
@@ -76,19 +78,17 @@ def ask_question(question: str, expected_type: Any, default: Any = None, **kwarg
|
|
76
78
|
lower_response = response.lower()
|
77
79
|
if lower_response in ("true", "t", "yes", "y", "1"):
|
78
80
|
return True
|
79
|
-
|
81
|
+
if lower_response in ("false", "f", "no", "n", "0"):
|
80
82
|
return False
|
81
|
-
|
82
|
-
sub.error("Invalid input. Please enter a valid boolean (true/false, yes/no, etc).")
|
83
|
+
sub.error("Invalid input. Please enter a valid boolean (true/false, yes/no, etc).")
|
83
84
|
case _:
|
84
85
|
raise ValueError(f"Unsupported type: {expected_type}")
|
85
86
|
except KeyboardInterrupt:
|
86
|
-
raise
|
87
|
+
raise UserCancelledError("User cancelled input") from None
|
87
88
|
|
88
89
|
|
89
|
-
def ask_yes_no(question, default=None, **kwargs) -> None | bool:
|
90
|
-
"""
|
91
|
-
Ask a yes or no question and return the answer.
|
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
92
|
|
93
93
|
Args:
|
94
94
|
question: The prompt question to display
|
@@ -97,6 +97,7 @@ def ask_yes_no(question, default=None, **kwargs) -> None | bool:
|
|
97
97
|
Returns:
|
98
98
|
True for yes, False for no, or None if no valid response is given
|
99
99
|
"""
|
100
|
+
kwargs = kwargs or {}
|
100
101
|
sub, console = get_console("prompt_helpers.py")
|
101
102
|
try:
|
102
103
|
while True:
|
@@ -106,26 +107,23 @@ def ask_yes_no(question, default=None, **kwargs) -> None | bool:
|
|
106
107
|
if response == "":
|
107
108
|
if default is not None:
|
108
109
|
return default
|
109
|
-
|
110
|
-
continue
|
110
|
+
continue
|
111
111
|
|
112
112
|
if response.lower() in ["yes", "y"]:
|
113
113
|
return True
|
114
|
-
|
114
|
+
if response.lower() in ["no", "n"]:
|
115
115
|
return False
|
116
|
-
|
116
|
+
if response.lower() in ["exit", "quit"]:
|
117
117
|
return None
|
118
|
-
|
119
|
-
|
120
|
-
continue
|
118
|
+
console.error("Invalid input. Please enter 'yes' or 'no' or exit.")
|
119
|
+
continue
|
121
120
|
except KeyboardInterrupt:
|
122
121
|
console.warning("KeyboardInterrupt: Exiting the prompt.")
|
123
122
|
return None
|
124
123
|
|
125
124
|
|
126
|
-
def restricted_prompt(question, valid_options, exit_command="exit", **kwargs):
|
127
|
-
"""
|
128
|
-
Continuously prompt the user until they provide a valid response or exit.
|
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.
|
129
127
|
|
130
128
|
Args:
|
131
129
|
question: The prompt question to display
|
@@ -135,15 +133,20 @@ def restricted_prompt(question, valid_options, exit_command="exit", **kwargs):
|
|
135
133
|
Returns:
|
136
134
|
The user's response or None if they chose to exit
|
137
135
|
"""
|
136
|
+
kwargs = kwargs or {}
|
138
137
|
sub, console = get_console("prompt_helpers.py")
|
139
|
-
completer_options = valid_options
|
138
|
+
completer_options: list[str] = [*valid_options, exit_command]
|
140
139
|
completer = WordCompleter(completer_options)
|
141
140
|
|
142
141
|
class OptionValidator(Validator):
|
143
|
-
def validate(self, document):
|
144
|
-
|
142
|
+
def validate(self, document: Any) -> None:
|
143
|
+
"""Validate the user's input against the valid options."""
|
144
|
+
text: str = document.text.lower()
|
145
145
|
if text != exit_command and text not in valid_options:
|
146
|
-
raise ValidationError(
|
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
|
+
)
|
147
150
|
|
148
151
|
try:
|
149
152
|
while True:
|
@@ -157,9 +160,12 @@ def restricted_prompt(question, valid_options, exit_command="exit", **kwargs):
|
|
157
160
|
)
|
158
161
|
response = response.lower()
|
159
162
|
|
160
|
-
if response == exit_command
|
163
|
+
if response == exit_command:
|
161
164
|
return None
|
162
|
-
|
165
|
+
if response == "":
|
166
|
+
sub.error("No input provided. Please enter a valid option or exit.")
|
167
|
+
continue
|
168
|
+
if response in valid_options:
|
163
169
|
return response
|
164
170
|
except KeyboardInterrupt:
|
165
171
|
sub.warning("KeyboardInterrupt: Exiting the prompt.")
|