bear-utils 0.0.1__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 +51 -0
- bear_utils/__main__.py +14 -0
- bear_utils/_internal/__init__.py +0 -0
- bear_utils/_internal/_version.py +1 -0
- bear_utils/_internal/cli.py +119 -0
- bear_utils/_internal/debug.py +174 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +136 -0
- bear_utils/ai/ai_helpers/_common.py +19 -0
- bear_utils/ai/ai_helpers/_config.py +24 -0
- bear_utils/ai/ai_helpers/_parsers.py +194 -0
- bear_utils/ai/ai_helpers/_types.py +15 -0
- bear_utils/cache/__init__.py +131 -0
- bear_utils/cli/__init__.py +22 -0
- bear_utils/cli/_args.py +12 -0
- bear_utils/cli/_get_version.py +207 -0
- bear_utils/cli/commands.py +105 -0
- bear_utils/cli/prompt_helpers.py +186 -0
- bear_utils/cli/shell/__init__.py +1 -0
- bear_utils/cli/shell/_base_command.py +81 -0
- bear_utils/cli/shell/_base_shell.py +430 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/cli/typer_bridge.py +90 -0
- bear_utils/config/__init__.py +13 -0
- bear_utils/config/config_manager.py +229 -0
- bear_utils/config/dir_manager.py +69 -0
- bear_utils/config/settings_manager.py +179 -0
- bear_utils/constants/__init__.py +90 -0
- bear_utils/constants/_exceptions.py +8 -0
- bear_utils/constants/_exit_code.py +60 -0
- bear_utils/constants/_http_status_code.py +37 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/_meta.py +196 -0
- bear_utils/constants/date_related.py +25 -0
- bear_utils/constants/time_related.py +24 -0
- bear_utils/database/__init__.py +8 -0
- bear_utils/database/_db_manager.py +98 -0
- bear_utils/events/__init__.py +18 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +74 -0
- bear_utils/extras/__init__.py +28 -0
- bear_utils/extras/_async_helpers.py +67 -0
- bear_utils/extras/_tools.py +185 -0
- bear_utils/extras/_zapper.py +399 -0
- bear_utils/extras/platform_utils.py +57 -0
- bear_utils/extras/responses/__init__.py +5 -0
- bear_utils/extras/responses/function_response.py +451 -0
- bear_utils/extras/wrappers/__init__.py +1 -0
- bear_utils/extras/wrappers/add_methods.py +100 -0
- bear_utils/extras/wrappers/string_io.py +46 -0
- bear_utils/files/__init__.py +6 -0
- bear_utils/files/file_handlers/__init__.py +5 -0
- bear_utils/files/file_handlers/_base_file_handler.py +107 -0
- bear_utils/files/file_handlers/file_handler_factory.py +280 -0
- bear_utils/files/file_handlers/json_file_handler.py +71 -0
- bear_utils/files/file_handlers/log_file_handler.py +40 -0
- bear_utils/files/file_handlers/toml_file_handler.py +76 -0
- bear_utils/files/file_handlers/txt_file_handler.py +76 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
- bear_utils/files/ignore_parser.py +293 -0
- bear_utils/graphics/__init__.py +6 -0
- bear_utils/graphics/bear_gradient.py +145 -0
- bear_utils/graphics/font/__init__.py +13 -0
- bear_utils/graphics/font/_raw_block_letters.py +463 -0
- bear_utils/graphics/font/_theme.py +31 -0
- bear_utils/graphics/font/_utils.py +220 -0
- bear_utils/graphics/font/block_font.py +192 -0
- bear_utils/graphics/font/glitch_font.py +63 -0
- bear_utils/graphics/image_helpers.py +45 -0
- bear_utils/gui/__init__.py +8 -0
- bear_utils/gui/gui_tools/__init__.py +10 -0
- bear_utils/gui/gui_tools/_settings.py +36 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +150 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
- bear_utils/logger_manager/__init__.py +109 -0
- bear_utils/logger_manager/_common.py +63 -0
- bear_utils/logger_manager/_console_junk.py +135 -0
- bear_utils/logger_manager/_log_level.py +50 -0
- bear_utils/logger_manager/_styles.py +95 -0
- bear_utils/logger_manager/logger_protocol.py +42 -0
- bear_utils/logger_manager/loggers/__init__.py +1 -0
- bear_utils/logger_manager/loggers/_console.py +223 -0
- bear_utils/logger_manager/loggers/_level_sin.py +61 -0
- bear_utils/logger_manager/loggers/_logger.py +19 -0
- bear_utils/logger_manager/loggers/base_logger.py +244 -0
- bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
- bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
- bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
- bear_utils/logger_manager/loggers/console_logger.py +278 -0
- bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
- bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
- bear_utils/logger_manager/loggers/file_logger.py +151 -0
- bear_utils/logger_manager/loggers/simple_logger.py +98 -0
- bear_utils/logger_manager/loggers/sub_logger.py +105 -0
- bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
- bear_utils/monitoring/__init__.py +13 -0
- bear_utils/monitoring/_common.py +28 -0
- bear_utils/monitoring/host_monitor.py +346 -0
- bear_utils/time/__init__.py +59 -0
- bear_utils-0.0.1.dist-info/METADATA +305 -0
- bear_utils-0.0.1.dist-info/RECORD +107 -0
- bear_utils-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import json
|
3
|
+
from typing import Any, cast
|
4
|
+
|
5
|
+
from httpx import AsyncClient, Headers, Response
|
6
|
+
from rich.markdown import Markdown
|
7
|
+
from singleton_base.singleton_base_new import SingletonBase
|
8
|
+
|
9
|
+
from bear_utils import BaseLogger, EpochTimestamp, SettingsManager, get_settings_manager
|
10
|
+
|
11
|
+
from . import AIEndpointConfig
|
12
|
+
from ._types import ResponseParser
|
13
|
+
|
14
|
+
SUCCESSFUL_STATUS_CODE = 200
|
15
|
+
|
16
|
+
|
17
|
+
class JSONResponseParser(ResponseParser[dict[str, Any]]):
|
18
|
+
"""Parser for JSON responses with flexible output structure."""
|
19
|
+
|
20
|
+
def __init__(self, required_fields: list | None = None, response_transformers: dict[str, Callable] | None = None):
|
21
|
+
self.required_fields = required_fields or []
|
22
|
+
self.response_transformers = response_transformers or {}
|
23
|
+
|
24
|
+
async def parse(self, raw_response: dict, logger: BaseLogger) -> dict[str, Any]:
|
25
|
+
"""Parse JSON response with configurable validation and transformation."""
|
26
|
+
default: dict[str, Any] = self.get_default_response()
|
27
|
+
|
28
|
+
output = raw_response.get("output", "")
|
29
|
+
if not output:
|
30
|
+
logger.error("No output received from AI.")
|
31
|
+
return default
|
32
|
+
|
33
|
+
try:
|
34
|
+
response_dict = json.loads(output)
|
35
|
+
if not isinstance(response_dict, dict):
|
36
|
+
logger.error("Response is not a valid JSON object.")
|
37
|
+
return default
|
38
|
+
|
39
|
+
logger.verbose(json.dumps(response_dict, indent=4))
|
40
|
+
|
41
|
+
if self.required_fields:
|
42
|
+
missing_fields = [field for field in self.required_fields if field not in response_dict]
|
43
|
+
if missing_fields:
|
44
|
+
logger.error(f"Response JSON missing required fields: {missing_fields}")
|
45
|
+
return default
|
46
|
+
|
47
|
+
for field_name, transformer in self.response_transformers.items():
|
48
|
+
if field_name in response_dict:
|
49
|
+
response_dict[field_name] = transformer(response_dict[field_name])
|
50
|
+
|
51
|
+
return response_dict
|
52
|
+
|
53
|
+
except json.JSONDecodeError as e:
|
54
|
+
logger.error(f"Failed to parse response JSON: {e}")
|
55
|
+
return default
|
56
|
+
|
57
|
+
def get_default_response(self) -> dict[str, Any]:
|
58
|
+
"""Return a basic default response."""
|
59
|
+
return {"error": "Failed to parse response"}
|
60
|
+
|
61
|
+
|
62
|
+
class TypedResponseParser[T_Response](ResponseParser[T_Response]):
|
63
|
+
def __init__(self, default_response: T_Response, response_transformers: dict[str, Callable] | None = None):
|
64
|
+
self.default_response: T_Response = default_response
|
65
|
+
self.response_transformers = response_transformers or {}
|
66
|
+
self.required_fields = list(cast("dict", self.default_response).keys())
|
67
|
+
|
68
|
+
def get_default_response(self) -> T_Response:
|
69
|
+
return cast("T_Response", self.default_response)
|
70
|
+
|
71
|
+
async def parse(self, raw_response: dict, logger: BaseLogger) -> T_Response:
|
72
|
+
"""Parse JSON response with strict typing."""
|
73
|
+
default = self.get_default_response()
|
74
|
+
|
75
|
+
output = raw_response.get("output", "")
|
76
|
+
if not output:
|
77
|
+
logger.error("No output received from AI.")
|
78
|
+
return default
|
79
|
+
|
80
|
+
try:
|
81
|
+
response_dict = json.loads(output)
|
82
|
+
if not isinstance(response_dict, dict):
|
83
|
+
logger.error("Response is not a valid JSON object.")
|
84
|
+
return default
|
85
|
+
|
86
|
+
logger.verbose(json.dumps(response_dict, indent=4))
|
87
|
+
|
88
|
+
missing_fields = [field for field in self.required_fields if field not in response_dict]
|
89
|
+
if missing_fields:
|
90
|
+
logger.error(f"Response JSON missing required fields: {missing_fields}")
|
91
|
+
return default
|
92
|
+
|
93
|
+
for field_name, transformer in self.response_transformers.items():
|
94
|
+
if field_name in response_dict:
|
95
|
+
response_dict[field_name] = transformer(response_dict[field_name])
|
96
|
+
return cast("T_Response", response_dict)
|
97
|
+
|
98
|
+
except json.JSONDecodeError as e:
|
99
|
+
logger.error(f"Failed to parse response JSON: {e}")
|
100
|
+
return default
|
101
|
+
|
102
|
+
|
103
|
+
class CommandResponseParser[T_Response](TypedResponseParser[T_Response]):
|
104
|
+
"""Specialized parser for command-based responses."""
|
105
|
+
|
106
|
+
def __init__(self, response_type: type[T_Response]):
|
107
|
+
super().__init__(
|
108
|
+
default_response=response_type(),
|
109
|
+
response_transformers={
|
110
|
+
"output": lambda x: Markdown(x) if isinstance(x, str) else x,
|
111
|
+
},
|
112
|
+
)
|
113
|
+
|
114
|
+
|
115
|
+
class PassthroughResponseParser(ResponseParser[dict[str, Any]]):
|
116
|
+
"""Parser that returns the raw output without JSON parsing."""
|
117
|
+
|
118
|
+
async def parse(self, raw_response: dict, logger: BaseLogger) -> dict[str, Any]:
|
119
|
+
"""Return the raw output from the response without parsing."""
|
120
|
+
output = raw_response.get("output", "")
|
121
|
+
return {"output": output, "logger": logger}
|
122
|
+
|
123
|
+
def get_default_response(self) -> dict[str, Any]:
|
124
|
+
return {"output": ""}
|
125
|
+
|
126
|
+
|
127
|
+
class ModularAIEndpoint[T_Response](SingletonBase):
|
128
|
+
"""Modular AI endpoint for flexible communication patterns."""
|
129
|
+
|
130
|
+
def __init__(
|
131
|
+
self,
|
132
|
+
config: AIEndpointConfig,
|
133
|
+
logger: BaseLogger,
|
134
|
+
response_parser: ResponseParser[T_Response],
|
135
|
+
) -> None:
|
136
|
+
self.config: AIEndpointConfig = config
|
137
|
+
self.logger: BaseLogger = logger
|
138
|
+
self.response_parser: ResponseParser[T_Response] = response_parser
|
139
|
+
self.session_id: str | None = None
|
140
|
+
|
141
|
+
self.logger.verbose(f"Using URL: {self.config.url}")
|
142
|
+
self.logger.verbose(f"Using prompt: {self.config.prompt[:50]}...")
|
143
|
+
|
144
|
+
self.settings_manager: SettingsManager = get_settings_manager(self.config.project_name)
|
145
|
+
self.set_session_id(new=True)
|
146
|
+
|
147
|
+
def set_session_id(self, new: bool = False) -> None:
|
148
|
+
"""Set the session ID for the current interaction.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
new (bool): If True, start a new session; otherwise, continue the existing session.
|
152
|
+
"""
|
153
|
+
if new or not self.settings_manager.has("session_id"):
|
154
|
+
self.logger.verbose("Starting a new session.")
|
155
|
+
self.session_id = str(EpochTimestamp.now())
|
156
|
+
self.settings_manager.set("session_id", self.session_id)
|
157
|
+
else:
|
158
|
+
self.logger.verbose("Continuing existing session with AI.")
|
159
|
+
self.session_id = self.settings_manager.get("session_id")
|
160
|
+
self.logger.debug(f"Using session ID: {self.session_id}")
|
161
|
+
|
162
|
+
def _prepare_message(self, message: str) -> str:
|
163
|
+
"""Prepare the message with optional JSON suffix."""
|
164
|
+
if self.config.append_json_suffix:
|
165
|
+
return f"{message}{self.config.json_suffix}"
|
166
|
+
return message
|
167
|
+
|
168
|
+
async def send_message(self, message: str, override_parser: ResponseParser[T_Response] | None = None) -> T_Response:
|
169
|
+
"""Send a message to the AI endpoint with flexible response parsing."""
|
170
|
+
parser: ResponseParser[T_Response] = override_parser or self.response_parser
|
171
|
+
async with AsyncClient(timeout=self.config.connection_timeout) as client:
|
172
|
+
try:
|
173
|
+
response: Response = await client.post(
|
174
|
+
url=self.config.url,
|
175
|
+
json={
|
176
|
+
"chatModel": self.config.chat_model,
|
177
|
+
"chatInput": self._prepare_message(message),
|
178
|
+
"sessionId": self.session_id,
|
179
|
+
"systemPrompt": self.config.prompt,
|
180
|
+
},
|
181
|
+
headers=Headers(
|
182
|
+
{
|
183
|
+
"Content-Type": "application/json",
|
184
|
+
"Authorization": f"Bearer {self.config.bearer_token}",
|
185
|
+
}
|
186
|
+
),
|
187
|
+
)
|
188
|
+
if response.status_code == SUCCESSFUL_STATUS_CODE:
|
189
|
+
return await parser.parse(response.json(), self.logger)
|
190
|
+
self.logger.error(f"Failed to send message to AI: {response.status_code} - {response.text}")
|
191
|
+
return parser.get_default_response()
|
192
|
+
except Exception as e:
|
193
|
+
self.logger.error(f"Exception during AI communication: {e}")
|
194
|
+
return parser.get_default_response()
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from bear_utils.logger_manager import BaseLogger
|
4
|
+
|
5
|
+
|
6
|
+
class ResponseParser[T_Response](ABC):
|
7
|
+
"""Abstract base class for response parsers."""
|
8
|
+
|
9
|
+
@abstractmethod
|
10
|
+
async def parse(self, raw_response: dict, logger: BaseLogger) -> T_Response:
|
11
|
+
"""Parse the raw response into the desired format."""
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def get_default_response(self) -> T_Response:
|
15
|
+
"""Return a default response structure."""
|
@@ -0,0 +1,131 @@
|
|
1
|
+
"""A set of helper caching utilities for bear_utils."""
|
2
|
+
|
3
|
+
import functools
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from diskcache import Cache
|
8
|
+
|
9
|
+
DEFAULT_CACHE_DIR = Path("~/.cache/app_cache").expanduser()
|
10
|
+
|
11
|
+
|
12
|
+
class CacheWrapper:
|
13
|
+
"""A simple wrapper around diskcache.Cache to provide a consistent interface.
|
14
|
+
|
15
|
+
This class allows for easy caching of function results with a specified directory,
|
16
|
+
size limit, and default timeout.
|
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.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
directory (str, optional): Directory path for the cache. Defaults to ~/.cache/app_cache.
|
30
|
+
size_limit (int, optional): Maximum size of the cache in bytes. Defaults to 1_000_000_000.
|
31
|
+
default_timeout (int, optional): Default timeout for cache entries in seconds. Defaults to None.
|
32
|
+
"""
|
33
|
+
self.cache = Cache(directory or DEFAULT_CACHE_DIR, size_limit=size_limit or 1_000_000_000, **kwargs)
|
34
|
+
self.default_timeout = default_timeout
|
35
|
+
|
36
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
37
|
+
"""Get a value from the cache."""
|
38
|
+
return self.cache.get(key, default=default)
|
39
|
+
|
40
|
+
def set(self, key: Any, value: Any, expire: int | None = None) -> None:
|
41
|
+
"""Set a value in the cache."""
|
42
|
+
if expire is None:
|
43
|
+
expire = self.default_timeout
|
44
|
+
self.cache.set(key, value, expire=expire)
|
45
|
+
|
46
|
+
|
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.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
directory (str, optional): Cache directory path. Defaults to ~/.cache/app_cache.
|
57
|
+
size_limit (int, optional): Maximum size in bytes. Defaults to None.
|
58
|
+
default_timeout (int, optional): Default timeout in seconds. Defaults to None.
|
59
|
+
**kwargs: Additional arguments to pass to the Cache constructor.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
function: A decorator function that can be used to cache function results.
|
63
|
+
|
64
|
+
Examples:
|
65
|
+
# Create a custom cache
|
66
|
+
my_cache = cache_factory(directory='/tmp/mycache', default_timeout=3600)
|
67
|
+
|
68
|
+
# Use as a simple decorator
|
69
|
+
@my_cache
|
70
|
+
def expensive_function(x, y):
|
71
|
+
return x + y
|
72
|
+
|
73
|
+
# Use with custom parameters
|
74
|
+
@my_cache(expire=60)
|
75
|
+
def another_function(x, y):
|
76
|
+
return x * y
|
77
|
+
"""
|
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)
|
82
|
+
|
83
|
+
if size_limit is None:
|
84
|
+
size_limit = 1_000_000_000
|
85
|
+
|
86
|
+
cache_instance = Cache(local_directory, size_limit=size_limit, **kwargs)
|
87
|
+
|
88
|
+
def decorator(func: object | None = None, *, expire: int | None = default_timeout, key: Any = None) -> object:
|
89
|
+
"""Decorator that caches function results.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
func: The function to cache (when used as @cache)
|
93
|
+
expire (int, optional): Expiration time in seconds.
|
94
|
+
key (callable, optional): Custom key function.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
callable: Decorated function or decorator
|
98
|
+
"""
|
99
|
+
|
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, ...]:
|
104
|
+
if key is not None:
|
105
|
+
cache_key = key(fn, *args, **kwargs)
|
106
|
+
else:
|
107
|
+
cache_key = (fn.__module__, fn.__qualname__, args, frozenset(kwargs.items()))
|
108
|
+
|
109
|
+
result = cache_instance.get(cache_key, default=None)
|
110
|
+
if result is not None:
|
111
|
+
return result
|
112
|
+
|
113
|
+
# If not in cache, compute and store
|
114
|
+
result: Any = fn(*args, **kwargs)
|
115
|
+
cache_instance.set(cache_key, result, expire=expire)
|
116
|
+
return result
|
117
|
+
|
118
|
+
# Preserve function metadata
|
119
|
+
return functools.update_wrapper(wrapper, fn)
|
120
|
+
|
121
|
+
# Handle both @cache and @cache(expire=300) styles
|
122
|
+
if func is None:
|
123
|
+
return actual_decorator
|
124
|
+
return actual_decorator(func)
|
125
|
+
|
126
|
+
return decorator
|
127
|
+
|
128
|
+
|
129
|
+
cache = cache_factory()
|
130
|
+
|
131
|
+
__all__ = ["CacheWrapper", "cache", "cache_factory"]
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""A set of command-line interface (CLI) utilities for bear_utils."""
|
2
|
+
|
3
|
+
from ._args import FAILURE, SUCCESS, ExitCode, args_process
|
4
|
+
from .commands import GitCommand, MaskShellCommand, OPShellCommand, UVShellCommand
|
5
|
+
from .shell._base_command import BaseShellCommand
|
6
|
+
from .shell._base_shell import SimpleShellSession, shell_session
|
7
|
+
from .shell._common import DEFAULT_SHELL
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"DEFAULT_SHELL",
|
11
|
+
"FAILURE",
|
12
|
+
"SUCCESS",
|
13
|
+
"BaseShellCommand",
|
14
|
+
"ExitCode",
|
15
|
+
"GitCommand",
|
16
|
+
"MaskShellCommand",
|
17
|
+
"OPShellCommand",
|
18
|
+
"SimpleShellSession",
|
19
|
+
"UVShellCommand",
|
20
|
+
"args_process",
|
21
|
+
"shell_session",
|
22
|
+
]
|
bear_utils/cli/_args.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
from bear_utils.constants._exit_code import FAILURE, SUCCESS, ExitCode
|
4
|
+
|
5
|
+
|
6
|
+
def args_process(args: list[str] | None = None) -> tuple[list[str], ExitCode]:
|
7
|
+
args = sys.argv[1:] if args is None else args
|
8
|
+
|
9
|
+
if not args:
|
10
|
+
return [], FAILURE
|
11
|
+
|
12
|
+
return args, SUCCESS
|
@@ -0,0 +1,207 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from argparse import ArgumentParser, Namespace
|
4
|
+
from contextlib import redirect_stdout
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
6
|
+
from io import StringIO
|
7
|
+
import sys
|
8
|
+
from typing import Literal, Self
|
9
|
+
|
10
|
+
from pydantic import BaseModel
|
11
|
+
|
12
|
+
from bear_utils.constants import ExitCode
|
13
|
+
from bear_utils.constants._meta import IntValue as Value, RichIntEnum
|
14
|
+
from bear_utils.extras import zap_as
|
15
|
+
|
16
|
+
|
17
|
+
class VerParts(RichIntEnum):
|
18
|
+
"""Enumeration for version parts."""
|
19
|
+
|
20
|
+
MAJOR = Value(0, "major")
|
21
|
+
MINOR = Value(1, "minor")
|
22
|
+
PATCH = Value(2, "patch")
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def choices(cls) -> list[str]:
|
26
|
+
"""Return a list of valid version parts."""
|
27
|
+
return [version_part.text for version_part in cls]
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def parts(cls) -> int:
|
31
|
+
"""Return the total number of version parts."""
|
32
|
+
return len(cls.choices())
|
33
|
+
|
34
|
+
|
35
|
+
VALID_BUMP_TYPES: list[str] = VerParts.choices()
|
36
|
+
ALL_PARTS: int = VerParts.parts()
|
37
|
+
|
38
|
+
|
39
|
+
class Version(BaseModel):
|
40
|
+
"""Model to represent a version string."""
|
41
|
+
|
42
|
+
major: int = 0
|
43
|
+
"""Major version number."""
|
44
|
+
minor: int = 0
|
45
|
+
"""Minor version number."""
|
46
|
+
patch: int = 0
|
47
|
+
"""Patch version number."""
|
48
|
+
|
49
|
+
@classmethod
|
50
|
+
def from_string(cls, version_str: str) -> Self:
|
51
|
+
"""Create a Version instance from a version string.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
version_str: A version string in the format "major.minor.patch".
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
A Version instance.
|
58
|
+
|
59
|
+
Raises:
|
60
|
+
ValueError: If the version string is not in the correct format.
|
61
|
+
"""
|
62
|
+
try:
|
63
|
+
major, minor, patch = zap_as("-+", version_str, 3, replace=".", func=int)
|
64
|
+
return cls(major=int(major), minor=int(minor), patch=int(patch))
|
65
|
+
except ValueError as e:
|
66
|
+
raise ValueError(
|
67
|
+
f"Invalid version string format: {version_str}. Expected integers for major, minor, and patch."
|
68
|
+
) from e
|
69
|
+
|
70
|
+
def increment(self, attr_name: str) -> None:
|
71
|
+
"""Increment the specified part of the version."""
|
72
|
+
setattr(self, attr_name, getattr(self, attr_name) + 1)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def version_string(self) -> str:
|
76
|
+
"""Return the version as a string in the format "major.minor.patch".
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
A string representation of the version.
|
80
|
+
"""
|
81
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
82
|
+
|
83
|
+
def default(self, part: str) -> None:
|
84
|
+
"""Clear the specified part of the version.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
part: The part of the version to clear.
|
88
|
+
"""
|
89
|
+
if hasattr(self, part):
|
90
|
+
setattr(self, part, 0)
|
91
|
+
|
92
|
+
def new_version(self, bump_type: str) -> Version:
|
93
|
+
"""Return a new version string based on the bump type."""
|
94
|
+
bump_part: VerParts = VerParts.get(bump_type, default=VerParts.PATCH)
|
95
|
+
self.increment(bump_part.text)
|
96
|
+
for part in VerParts:
|
97
|
+
if part.value > bump_part.value:
|
98
|
+
self.default(part.text)
|
99
|
+
return self
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def from_func(cls, package_name: str) -> Self:
|
103
|
+
"""Create a Version instance from the current package version.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
A Version instance with the current package version.
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
PackageNotFoundError: If the package is not found.
|
110
|
+
"""
|
111
|
+
try:
|
112
|
+
current_version = version(package_name)
|
113
|
+
return cls.from_string(current_version)
|
114
|
+
except PackageNotFoundError as e:
|
115
|
+
raise PackageNotFoundError(f"Package '{package_name}' not found: {e}") from e
|
116
|
+
|
117
|
+
|
118
|
+
def _bump_version(version: str, bump_type: Literal["major", "minor", "patch"]) -> Version:
|
119
|
+
"""Bump the version based on the specified type.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
version: The current version string (e.g., "1.2.3").
|
123
|
+
bump_type: The type of bump ("major", "minor", or "patch").
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
The new version string.
|
127
|
+
|
128
|
+
Raises:
|
129
|
+
ValueError: If the version format is invalid or bump_type is unsupported.
|
130
|
+
"""
|
131
|
+
ver: Version = Version.from_string(version)
|
132
|
+
return ver.new_version(bump_type)
|
133
|
+
|
134
|
+
|
135
|
+
def _get_version(package_name: str) -> str:
|
136
|
+
"""Get the version of the specified package.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
package_name: The name of the package to get the version for.
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
A Version instance representing the current version of the package.
|
143
|
+
|
144
|
+
Raises:
|
145
|
+
PackageNotFoundError: If the package is not found.
|
146
|
+
"""
|
147
|
+
record = StringIO()
|
148
|
+
with redirect_stdout(record):
|
149
|
+
cli_get_version([package_name])
|
150
|
+
return record.getvalue().strip()
|
151
|
+
|
152
|
+
|
153
|
+
def cli_get_version(args: list[str] | None = None) -> ExitCode:
|
154
|
+
"""Get the version of the current package.
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
The version of the package.
|
158
|
+
"""
|
159
|
+
if args is None:
|
160
|
+
args = sys.argv[1:]
|
161
|
+
parser = ArgumentParser(description="Get the version of the package.")
|
162
|
+
parser.add_argument("package_name", nargs="?", type=str, help="Name of the package to get the version for.")
|
163
|
+
arguments: Namespace = parser.parse_args(args)
|
164
|
+
if not arguments.package_name:
|
165
|
+
print("No package name provided. Please specify a package name.")
|
166
|
+
return ExitCode.FAILURE
|
167
|
+
package_name: str = arguments.package_name
|
168
|
+
try:
|
169
|
+
current_version = version(package_name)
|
170
|
+
print(current_version)
|
171
|
+
except PackageNotFoundError:
|
172
|
+
print(f"Package '{package_name}' not found.")
|
173
|
+
return ExitCode.FAILURE
|
174
|
+
return ExitCode.SUCCESS
|
175
|
+
|
176
|
+
|
177
|
+
def cli_bump(args: list[str] | None = None) -> ExitCode:
|
178
|
+
if args is None:
|
179
|
+
args = sys.argv[1:]
|
180
|
+
parser = ArgumentParser(description="Bump the version of the package.")
|
181
|
+
parser.add_argument("bump_type", type=str, choices=VALID_BUMP_TYPES, default="patch")
|
182
|
+
parser.add_argument("package_name", nargs="?", type=str, help="Name of the package to bump the version for.")
|
183
|
+
parser.add_argument("current_version", type=str, help="Current version of the package.")
|
184
|
+
arguments: Namespace = parser.parse_args(args)
|
185
|
+
bump_type: Literal["major", "minor", "patch"] = arguments.bump_type
|
186
|
+
if not arguments.package_name:
|
187
|
+
print("No package name provided.")
|
188
|
+
return ExitCode.FAILURE
|
189
|
+
package_name: str = arguments.package_name
|
190
|
+
if bump_type not in VALID_BUMP_TYPES:
|
191
|
+
print(f"Invalid argument '{bump_type}'. Use one of: {', '.join(VALID_BUMP_TYPES)}.")
|
192
|
+
return ExitCode.FAILURE
|
193
|
+
current_version: str = arguments.current_version or _get_version(package_name)
|
194
|
+
try:
|
195
|
+
new_version: Version = _bump_version(version=current_version, bump_type=bump_type)
|
196
|
+
print(new_version.version_string)
|
197
|
+
return ExitCode.SUCCESS
|
198
|
+
except ValueError as e:
|
199
|
+
print(f"Error: {e}")
|
200
|
+
return ExitCode.FAILURE
|
201
|
+
except Exception as e:
|
202
|
+
print(f"Unexpected error: {e}")
|
203
|
+
return ExitCode.FAILURE
|
204
|
+
|
205
|
+
|
206
|
+
if __name__ == "__main__":
|
207
|
+
cli_bump(["patch", "bear-utils", "0.9.2-fart.build-alpha"])
|
@@ -0,0 +1,105 @@
|
|
1
|
+
"""Shell Commands Module for Bear Utils."""
|
2
|
+
|
3
|
+
from typing import Self
|
4
|
+
|
5
|
+
from .shell._base_command import BaseShellCommand
|
6
|
+
|
7
|
+
|
8
|
+
class OPShellCommand(BaseShellCommand):
|
9
|
+
"""OP command for running 1Password CLI commands"""
|
10
|
+
|
11
|
+
command_name = "op"
|
12
|
+
|
13
|
+
def __init__(self, *args, **kwargs) -> None:
|
14
|
+
"""Initialize the OPShellCommand with the op command."""
|
15
|
+
super().__init__(*args, **kwargs)
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def read(cls, *args, **kwargs) -> Self:
|
19
|
+
"""Create a read command for 1Password"""
|
20
|
+
return cls.sub("read", *args, **kwargs)
|
21
|
+
|
22
|
+
|
23
|
+
class UVShellCommand(BaseShellCommand):
|
24
|
+
"""UV command for running Python scripts with uv"""
|
25
|
+
|
26
|
+
command_name = "uv"
|
27
|
+
|
28
|
+
def __init__(self, *args, **kwargs) -> None:
|
29
|
+
"""Initialize the UVShellCommand with the uv command."""
|
30
|
+
super().__init__(*args, **kwargs)
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def pip(cls, s: str = "", *args, **kwargs) -> Self:
|
34
|
+
"""Create a piped command for uv"""
|
35
|
+
if s:
|
36
|
+
return cls.sub(f"pip {s}", *args, **kwargs)
|
37
|
+
return cls.sub("pip", *args, **kwargs)
|
38
|
+
|
39
|
+
|
40
|
+
class MaskShellCommand(BaseShellCommand):
|
41
|
+
"""Mask command for running masked commands"""
|
42
|
+
|
43
|
+
command_name = "mask"
|
44
|
+
|
45
|
+
def __init__(self, *args, **kwargs) -> None:
|
46
|
+
"""Initialize the MaskShellCommand with the mask command."""
|
47
|
+
super().__init__(*args, **kwargs)
|
48
|
+
|
49
|
+
@classmethod
|
50
|
+
def maskfile(cls, maskfile: str, *args, **kwargs) -> Self:
|
51
|
+
"""Create a maskfile command with the specified maskfile"""
|
52
|
+
return cls.sub("--maskfile", *args, **kwargs).value(maskfile)
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def init(cls, *args, **kwargs) -> Self:
|
56
|
+
"""Create an init command for mask"""
|
57
|
+
return cls.sub("init", *args, **kwargs)
|
58
|
+
|
59
|
+
|
60
|
+
class GitCommand(BaseShellCommand):
|
61
|
+
"""Base class for Git commands"""
|
62
|
+
|
63
|
+
command_name = "git"
|
64
|
+
|
65
|
+
def __init__(self, *args, **kwargs) -> None:
|
66
|
+
"""Initialize the GitCommand with the git command."""
|
67
|
+
super().__init__(*args, **kwargs)
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def init(cls, *args, **kwargs) -> "GitCommand":
|
71
|
+
"""Initialize a new Git repository"""
|
72
|
+
return cls.sub("init", *args, **kwargs)
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def status(cls, *args, **kwargs) -> "GitCommand":
|
76
|
+
"""Get the status of the Git repository"""
|
77
|
+
return cls.sub("status", *args, **kwargs)
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
def log(cls, *args, **kwargs) -> "GitCommand":
|
81
|
+
"""Show the commit logs"""
|
82
|
+
return cls.sub("log", *args, **kwargs)
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def add(cls, files: str, *args, **kwargs) -> "GitCommand":
|
86
|
+
"""Add files to the staging area"""
|
87
|
+
return cls.sub("add", *args, **kwargs).value(files)
|
88
|
+
|
89
|
+
@classmethod
|
90
|
+
def diff(cls, *args, **kwargs) -> "GitCommand":
|
91
|
+
"""Show changes between commits, commit and working tree, etc."""
|
92
|
+
return cls.sub("diff", *args, **kwargs)
|
93
|
+
|
94
|
+
@classmethod
|
95
|
+
def commit(cls, message: str, *args, **kwargs) -> "GitCommand":
|
96
|
+
"""Commit changes with a message"""
|
97
|
+
return cls.sub("commit -m", *args, **kwargs).value(f"'{message}'")
|
98
|
+
|
99
|
+
|
100
|
+
__all__ = [
|
101
|
+
"GitCommand",
|
102
|
+
"MaskShellCommand",
|
103
|
+
"OPShellCommand",
|
104
|
+
"UVShellCommand",
|
105
|
+
]
|