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.
Files changed (107) hide show
  1. bear_utils/__init__.py +51 -0
  2. bear_utils/__main__.py +14 -0
  3. bear_utils/_internal/__init__.py +0 -0
  4. bear_utils/_internal/_version.py +1 -0
  5. bear_utils/_internal/cli.py +119 -0
  6. bear_utils/_internal/debug.py +174 -0
  7. bear_utils/ai/__init__.py +30 -0
  8. bear_utils/ai/ai_helpers/__init__.py +136 -0
  9. bear_utils/ai/ai_helpers/_common.py +19 -0
  10. bear_utils/ai/ai_helpers/_config.py +24 -0
  11. bear_utils/ai/ai_helpers/_parsers.py +194 -0
  12. bear_utils/ai/ai_helpers/_types.py +15 -0
  13. bear_utils/cache/__init__.py +131 -0
  14. bear_utils/cli/__init__.py +22 -0
  15. bear_utils/cli/_args.py +12 -0
  16. bear_utils/cli/_get_version.py +207 -0
  17. bear_utils/cli/commands.py +105 -0
  18. bear_utils/cli/prompt_helpers.py +186 -0
  19. bear_utils/cli/shell/__init__.py +1 -0
  20. bear_utils/cli/shell/_base_command.py +81 -0
  21. bear_utils/cli/shell/_base_shell.py +430 -0
  22. bear_utils/cli/shell/_common.py +19 -0
  23. bear_utils/cli/typer_bridge.py +90 -0
  24. bear_utils/config/__init__.py +13 -0
  25. bear_utils/config/config_manager.py +229 -0
  26. bear_utils/config/dir_manager.py +69 -0
  27. bear_utils/config/settings_manager.py +179 -0
  28. bear_utils/constants/__init__.py +90 -0
  29. bear_utils/constants/_exceptions.py +8 -0
  30. bear_utils/constants/_exit_code.py +60 -0
  31. bear_utils/constants/_http_status_code.py +37 -0
  32. bear_utils/constants/_lazy_typing.py +15 -0
  33. bear_utils/constants/_meta.py +196 -0
  34. bear_utils/constants/date_related.py +25 -0
  35. bear_utils/constants/time_related.py +24 -0
  36. bear_utils/database/__init__.py +8 -0
  37. bear_utils/database/_db_manager.py +98 -0
  38. bear_utils/events/__init__.py +18 -0
  39. bear_utils/events/events_class.py +52 -0
  40. bear_utils/events/events_module.py +74 -0
  41. bear_utils/extras/__init__.py +28 -0
  42. bear_utils/extras/_async_helpers.py +67 -0
  43. bear_utils/extras/_tools.py +185 -0
  44. bear_utils/extras/_zapper.py +399 -0
  45. bear_utils/extras/platform_utils.py +57 -0
  46. bear_utils/extras/responses/__init__.py +5 -0
  47. bear_utils/extras/responses/function_response.py +451 -0
  48. bear_utils/extras/wrappers/__init__.py +1 -0
  49. bear_utils/extras/wrappers/add_methods.py +100 -0
  50. bear_utils/extras/wrappers/string_io.py +46 -0
  51. bear_utils/files/__init__.py +6 -0
  52. bear_utils/files/file_handlers/__init__.py +5 -0
  53. bear_utils/files/file_handlers/_base_file_handler.py +107 -0
  54. bear_utils/files/file_handlers/file_handler_factory.py +280 -0
  55. bear_utils/files/file_handlers/json_file_handler.py +71 -0
  56. bear_utils/files/file_handlers/log_file_handler.py +40 -0
  57. bear_utils/files/file_handlers/toml_file_handler.py +76 -0
  58. bear_utils/files/file_handlers/txt_file_handler.py +76 -0
  59. bear_utils/files/file_handlers/yaml_file_handler.py +64 -0
  60. bear_utils/files/ignore_parser.py +293 -0
  61. bear_utils/graphics/__init__.py +6 -0
  62. bear_utils/graphics/bear_gradient.py +145 -0
  63. bear_utils/graphics/font/__init__.py +13 -0
  64. bear_utils/graphics/font/_raw_block_letters.py +463 -0
  65. bear_utils/graphics/font/_theme.py +31 -0
  66. bear_utils/graphics/font/_utils.py +220 -0
  67. bear_utils/graphics/font/block_font.py +192 -0
  68. bear_utils/graphics/font/glitch_font.py +63 -0
  69. bear_utils/graphics/image_helpers.py +45 -0
  70. bear_utils/gui/__init__.py +8 -0
  71. bear_utils/gui/gui_tools/__init__.py +10 -0
  72. bear_utils/gui/gui_tools/_settings.py +36 -0
  73. bear_utils/gui/gui_tools/_types.py +12 -0
  74. bear_utils/gui/gui_tools/qt_app.py +150 -0
  75. bear_utils/gui/gui_tools/qt_color_picker.py +130 -0
  76. bear_utils/gui/gui_tools/qt_file_handler.py +130 -0
  77. bear_utils/gui/gui_tools/qt_input_dialog.py +303 -0
  78. bear_utils/logger_manager/__init__.py +109 -0
  79. bear_utils/logger_manager/_common.py +63 -0
  80. bear_utils/logger_manager/_console_junk.py +135 -0
  81. bear_utils/logger_manager/_log_level.py +50 -0
  82. bear_utils/logger_manager/_styles.py +95 -0
  83. bear_utils/logger_manager/logger_protocol.py +42 -0
  84. bear_utils/logger_manager/loggers/__init__.py +1 -0
  85. bear_utils/logger_manager/loggers/_console.py +223 -0
  86. bear_utils/logger_manager/loggers/_level_sin.py +61 -0
  87. bear_utils/logger_manager/loggers/_logger.py +19 -0
  88. bear_utils/logger_manager/loggers/base_logger.py +244 -0
  89. bear_utils/logger_manager/loggers/base_logger.pyi +51 -0
  90. bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
  91. bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
  92. bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
  93. bear_utils/logger_manager/loggers/buffer_logger.py +57 -0
  94. bear_utils/logger_manager/loggers/console_logger.py +278 -0
  95. bear_utils/logger_manager/loggers/console_logger.pyi +50 -0
  96. bear_utils/logger_manager/loggers/fastapi_logger.py +333 -0
  97. bear_utils/logger_manager/loggers/file_logger.py +151 -0
  98. bear_utils/logger_manager/loggers/simple_logger.py +98 -0
  99. bear_utils/logger_manager/loggers/sub_logger.py +105 -0
  100. bear_utils/logger_manager/loggers/sub_logger.pyi +23 -0
  101. bear_utils/monitoring/__init__.py +13 -0
  102. bear_utils/monitoring/_common.py +28 -0
  103. bear_utils/monitoring/host_monitor.py +346 -0
  104. bear_utils/time/__init__.py +59 -0
  105. bear_utils-0.0.1.dist-info/METADATA +305 -0
  106. bear_utils-0.0.1.dist-info/RECORD +107 -0
  107. 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
+ ]
@@ -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
+ ]