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.
Files changed (79) hide show
  1. bear_utils/__init__.py +24 -1
  2. bear_utils/ai/__init__.py +5 -5
  3. bear_utils/ai/ai_helpers/__init__.py +24 -18
  4. bear_utils/ai/ai_helpers/_parsers.py +27 -21
  5. bear_utils/ai/ai_helpers/_types.py +2 -7
  6. bear_utils/cache/__init__.py +35 -23
  7. bear_utils/cli/__init__.py +13 -0
  8. bear_utils/cli/commands.py +14 -8
  9. bear_utils/cli/prompt_helpers.py +40 -34
  10. bear_utils/cli/shell/__init__.py +1 -0
  11. bear_utils/cli/shell/_base_command.py +18 -18
  12. bear_utils/cli/shell/_base_shell.py +37 -34
  13. bear_utils/config/__init__.py +4 -2
  14. bear_utils/config/config_manager.py +193 -56
  15. bear_utils/config/dir_manager.py +8 -3
  16. bear_utils/config/settings_manager.py +94 -171
  17. bear_utils/constants/__init__.py +2 -1
  18. bear_utils/constants/_exceptions.py +6 -1
  19. bear_utils/constants/date_related.py +2 -0
  20. bear_utils/constants/logger_protocol.py +28 -0
  21. bear_utils/constants/time_related.py +2 -0
  22. bear_utils/database/__init__.py +2 -0
  23. bear_utils/database/_db_manager.py +10 -11
  24. bear_utils/events/__init__.py +3 -1
  25. bear_utils/events/events_class.py +11 -11
  26. bear_utils/events/events_module.py +17 -8
  27. bear_utils/extras/__init__.py +8 -6
  28. bear_utils/extras/_async_helpers.py +2 -3
  29. bear_utils/extras/_tools.py +62 -52
  30. bear_utils/extras/platform_utils.py +5 -1
  31. bear_utils/extras/responses/__init__.py +1 -0
  32. bear_utils/extras/responses/function_response.py +301 -0
  33. bear_utils/extras/wrappers/__init__.py +1 -0
  34. bear_utils/extras/wrappers/add_methods.py +17 -15
  35. bear_utils/files/__init__.py +3 -1
  36. bear_utils/files/file_handlers/__init__.py +2 -0
  37. bear_utils/files/file_handlers/_base_file_handler.py +23 -3
  38. bear_utils/files/file_handlers/file_handler_factory.py +38 -38
  39. bear_utils/files/file_handlers/json_file_handler.py +49 -22
  40. bear_utils/files/file_handlers/log_file_handler.py +19 -12
  41. bear_utils/files/file_handlers/toml_file_handler.py +13 -5
  42. bear_utils/files/file_handlers/txt_file_handler.py +56 -14
  43. bear_utils/files/file_handlers/yaml_file_handler.py +19 -13
  44. bear_utils/files/ignore_parser.py +52 -57
  45. bear_utils/graphics/__init__.py +3 -1
  46. bear_utils/graphics/bear_gradient.py +17 -12
  47. bear_utils/graphics/image_helpers.py +11 -5
  48. bear_utils/gui/__init__.py +3 -1
  49. bear_utils/gui/gui_tools/__init__.py +3 -1
  50. bear_utils/gui/gui_tools/_settings.py +0 -1
  51. bear_utils/gui/gui_tools/qt_app.py +16 -11
  52. bear_utils/gui/gui_tools/qt_color_picker.py +24 -13
  53. bear_utils/gui/gui_tools/qt_file_handler.py +30 -38
  54. bear_utils/gui/gui_tools/qt_input_dialog.py +11 -14
  55. bear_utils/logging/__init__.py +6 -4
  56. bear_utils/logging/logger_manager/__init__.py +1 -0
  57. bear_utils/logging/logger_manager/_common.py +0 -1
  58. bear_utils/logging/logger_manager/_console_junk.py +14 -10
  59. bear_utils/logging/logger_manager/_styles.py +1 -2
  60. bear_utils/logging/logger_manager/loggers/__init__.py +1 -0
  61. bear_utils/logging/logger_manager/loggers/_base_logger.py +33 -36
  62. bear_utils/logging/logger_manager/loggers/_base_logger.pyi +6 -5
  63. bear_utils/logging/logger_manager/loggers/_buffer_logger.py +2 -3
  64. bear_utils/logging/logger_manager/loggers/_console_logger.py +52 -26
  65. bear_utils/logging/logger_manager/loggers/_console_logger.pyi +7 -21
  66. bear_utils/logging/logger_manager/loggers/_file_logger.py +20 -13
  67. bear_utils/logging/logger_manager/loggers/_level_sin.py +15 -15
  68. bear_utils/logging/logger_manager/loggers/_logger.py +4 -6
  69. bear_utils/logging/logger_manager/loggers/_sub_logger.py +16 -23
  70. bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +4 -19
  71. bear_utils/logging/loggers.py +9 -13
  72. bear_utils/monitoring/__init__.py +7 -4
  73. bear_utils/monitoring/_common.py +28 -0
  74. bear_utils/monitoring/host_monitor.py +44 -48
  75. bear_utils/time/__init__.py +13 -6
  76. {bear_utils-0.7.21.dist-info → bear_utils-0.7.23.dist-info}/METADATA +50 -6
  77. bear_utils-0.7.23.dist-info/RECORD +83 -0
  78. bear_utils-0.7.21.dist-info/RECORD +0 -79
  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("bear-utils")
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 ...logging import BaseLogger
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(default_response=response_type, response_transformers=transformers)
33
- return ModularAIEndpoint(config, logger, parser) # type: ignore[return-value, arg-type]
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
- from rich.markdown import Markdown
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
- from typing import Any, Generic, cast
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 ... import BaseLogger, EpochTimestamp, SettingsManager, get_settings_manager
9
+ from bear_utils import BaseLogger, EpochTimestamp, SettingsManager, get_settings_manager
10
+
10
11
  from . import AIEndpointConfig
11
- from ._types import ResponseParser, T_Response
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(ResponseParser[T_Response], Generic[T_Response]):
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(SingletonBase, Generic[T_Response]):
127
+ class ModularAIEndpoint[T_Response](SingletonBase):
125
128
  """Modular AI endpoint for flexible communication patterns."""
126
129
 
127
- def __init__(self, config: AIEndpointConfig, logger: BaseLogger, response_parser: ResponseParser[T_Response]):
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 == 200:
188
+ if response.status_code == SUCCESSFUL_STATUS_CODE:
182
189
  return await parser.parse(response.json(), self.logger)
183
- else:
184
- self.logger.error(f"Failed to send message to AI: {response.status_code} - {response.text}")
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 ... import BaseLogger
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
@@ -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
- A simple wrapper around diskcache.Cache to provide a consistent interface.
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__(self, directory=None, size_limit=None, default_timeout=None, **kwargs):
17
- """
18
- Initialize the CacheWrapper with a specified directory, size limit, and default timeout.
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(directory=None, size_limit=None, default_timeout=None, **kwargs):
40
- """
41
- Creates and configures a cache decorator factory.
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 is None:
67
- directory = Path(DEFAULT_CACHE_DIR)
68
- if not directory.exists():
69
- directory.mkdir(parents=True, exist_ok=True)
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(directory, size_limit=size_limit, **kwargs)
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
- def wrapper(*args, **kwargs):
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", "cache_factory", "cache"]
131
+ __all__ = ["CacheWrapper", "cache", "cache_factory"]
@@ -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
+ ]
@@ -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
  ]
@@ -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 ..constants._exceptions import UserCancelled
8
- from ..constants._lazy_typing import LitBool, LitFloat, LitInt, LitStr, OptBool, OptFloat, OptInt, OptStr
9
- from ..logging.loggers import get_console
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, **kwargs) -> Any:
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 UserCancelled is raised.
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
- UserCancelled: If the user cancels input with Ctrl+C
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.info(question)
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
- else:
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
- elif lower_response in ("false", "f", "no", "n", "0"):
81
+ if lower_response in ("false", "f", "no", "n", "0"):
80
82
  return False
81
- else:
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 UserCancelled("User cancelled input")
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
- else:
110
- continue
110
+ continue
111
111
 
112
112
  if response.lower() in ["yes", "y"]:
113
113
  return True
114
- elif response.lower() in ["no", "n"]:
114
+ if response.lower() in ["no", "n"]:
115
115
  return False
116
- elif response.lower() in ["exit", "quit"]:
116
+ if response.lower() in ["exit", "quit"]:
117
117
  return None
118
- else:
119
- console.error("Invalid input. Please enter 'yes' or 'no' or exit.")
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 + [exit_command]
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
- text = document.text.lower()
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(f'Please enter one of: {", ".join(valid_options)} (or "{exit_command}" to quit)') # type: ignore
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 or response == "":
163
+ if response == exit_command:
161
164
  return None
162
- elif response in valid_options:
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.")