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,19 @@
1
+ """Shared shell utilities.
2
+
3
+ ``DEFAULT_SHELL`` attempts to locate ``zsh`` on the host using :func:`shutil.which`.
4
+ If that fails, ``/bin/sh`` is used instead.
5
+ """
6
+
7
+ from shutil import which
8
+
9
+ DEFAULT_SHELL: str = which("zsh") or which("bash") or "/bin/sh"
10
+ """Dynamically detected shell path, falling back to ``/bin/sh``."""
11
+
12
+ BASH: str | None = which("bash")
13
+ """Path to the Bash shell, falling back to ``/bin/bash`` if not found."""
14
+
15
+ ZSH: str | None = which("zsh") or which("/bin/zsh")
16
+ """Path to the Zsh shell, falling back to ``/bin/zsh`` if not found."""
17
+
18
+ SH: str | None = which("sh") or "bin/sh"
19
+ """Path to the Bourne shell, falling back to ``/bin/sh`` if not found."""
@@ -0,0 +1,90 @@
1
+ """A simple bridge for augmenting Typer with alias support and command execution for interactive use."""
2
+
3
+ from collections.abc import Callable
4
+ import shlex
5
+ from typing import Any, TypedDict
6
+
7
+ from rich.console import Console
8
+ from singleton_base import SingletonBase
9
+ from typer import Exit, Typer
10
+ from typer.models import CommandInfo
11
+
12
+ from bear_utils.logger_manager import AsyncLoggerProtocol, LoggerProtocol
13
+
14
+
15
+ class CommandMeta(TypedDict):
16
+ """Metadata for a Typer command."""
17
+
18
+ name: str
19
+ help: str
20
+ hidden: bool
21
+
22
+
23
+ def get_command_meta(command: CommandInfo) -> CommandMeta:
24
+ """Extract metadata from a Typer command."""
25
+ return {
26
+ "name": command.name or (command.callback.__name__ if command.callback else "unknown"),
27
+ "help": (command.callback.__doc__ if command.callback else None) or "No description available",
28
+ "hidden": command.hidden,
29
+ }
30
+
31
+
32
+ # TODO: Add support for usage statements for a more robust help system
33
+
34
+
35
+ class TyperBridge(SingletonBase):
36
+ """Simple bridge for Typer command execution."""
37
+
38
+ def __init__(self, typer_app: Typer, console: AsyncLoggerProtocol | LoggerProtocol | Console) -> None:
39
+ """Initialize the TyperBridge with a Typer app instance."""
40
+ self.app: Typer = typer_app
41
+ self.console: AsyncLoggerProtocol | LoggerProtocol | Console = console or Console()
42
+ self.command_meta: dict[str, CommandMeta] = {}
43
+
44
+ def alias(self, *alias_names: str) -> Callable[..., Callable[..., Any]]:
45
+ """Register aliases as hidden Typer commands."""
46
+
47
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
48
+ for alias in alias_names:
49
+ self.app.command(name=alias, hidden=True)(func)
50
+ return func
51
+
52
+ return decorator
53
+
54
+ def execute_command(self, command_string: str) -> bool:
55
+ """Execute command via Typer. Return True if successful."""
56
+ try:
57
+ parts: list[str] = shlex.split(command_string.strip())
58
+ if not parts:
59
+ return False
60
+ self.app(parts, standalone_mode=False)
61
+ return True
62
+ except Exit:
63
+ return True
64
+ except Exception as e:
65
+ if isinstance(self.console, Console):
66
+ self.console.print(f"[red]Error executing command: {e}[/red]")
67
+ else:
68
+ self.console.error(f"Error executing command: {e}", exc_info=True)
69
+ return False
70
+
71
+ def bootstrap_command_meta(self) -> None:
72
+ """Bootstrap command metadata from the Typer app."""
73
+ if not self.command_meta:
74
+ for cmd in self.app.registered_commands:
75
+ cmd_meta: CommandMeta = get_command_meta(command=cmd)
76
+ self.command_meta[cmd_meta["name"]] = cmd_meta
77
+
78
+ def get_all_command_info(self, show_hidden: bool = False) -> dict[str, CommandMeta]:
79
+ """Get all command information from the Typer app."""
80
+ if not self.command_meta:
81
+ self.bootstrap_command_meta()
82
+ if not show_hidden:
83
+ return {name: meta for name, meta in self.command_meta.items() if not meta["hidden"]}
84
+ return self.command_meta
85
+
86
+ def get_command_info(self, command_name: str) -> CommandMeta | None:
87
+ """Get metadata for a specific command."""
88
+ if not self.command_meta:
89
+ self.bootstrap_command_meta()
90
+ return self.command_meta.get(command_name)
@@ -0,0 +1,13 @@
1
+ """Config and settings management utilities for Bear Utils."""
2
+
3
+ from .config_manager import ConfigManager
4
+ from .dir_manager import DirectoryManager
5
+ from .settings_manager import SettingsManager, get_settings_manager, settings
6
+
7
+ __all__ = [
8
+ "ConfigManager",
9
+ "DirectoryManager",
10
+ "SettingsManager",
11
+ "get_settings_manager",
12
+ "settings",
13
+ ]
@@ -0,0 +1,229 @@
1
+ """Config Manager Module for Bear Utils."""
2
+
3
+ from collections.abc import Callable
4
+ from functools import cached_property
5
+ import os
6
+ from pathlib import Path
7
+ import tomllib
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, ValidationError, field_validator
11
+
12
+
13
+ def nullable_string_validator(field_name: str) -> Callable[..., str | None]:
14
+ """Create a validator that converts 'null' strings to None."""
15
+
16
+ @field_validator(field_name)
17
+ @classmethod
18
+ def _validate(cls: object, v: str | None) -> str | None: # noqa: ARG001
19
+ if isinstance(v, str) and v.lower() in ("null", "none", ""):
20
+ return None
21
+ return v
22
+
23
+ return _validate
24
+
25
+
26
+ class ConfigManager[ConfigType: BaseModel]:
27
+ """A generic configuration manager with environment-based overrides."""
28
+
29
+ def __init__(self, config_model: type[ConfigType], config_path: Path | None = None, env: str = "dev") -> None:
30
+ """Initialize the ConfigManager with a Pydantic model and configuration path."""
31
+ self._model: type[ConfigType] = config_model
32
+ self._config_path: Path = config_path or Path("config")
33
+ self._env: str = env
34
+ self._config: ConfigType | None = None
35
+ self._config_path.mkdir(parents=True, exist_ok=True)
36
+
37
+ def _get_env_overrides(self) -> dict[str, Any]:
38
+ """Convert environment variables to nested dictionary structure."""
39
+ env_config: dict[str, Any] = {}
40
+
41
+ for key, value in os.environ.items():
42
+ if not key.startswith("APP_"):
43
+ continue
44
+
45
+ # Convert APP_DATABASE_HOST to ['database', 'host']
46
+ parts: list[str] = key.lower().replace("app_", "").split("_")
47
+
48
+ current: dict[str, Any] = env_config
49
+ for part in parts[:-1]:
50
+ current = current.setdefault(part, {})
51
+
52
+ final_value: Any = self._convert_env_value(value)
53
+ current[parts[-1]] = final_value
54
+ return env_config
55
+
56
+ def _convert_env_value(self, value: str) -> Any:
57
+ """Convert string environment variables to appropriate types."""
58
+ if value.lower() in ("true", "false"):
59
+ return value.lower() == "true"
60
+
61
+ if value.isdigit():
62
+ return int(value)
63
+
64
+ try:
65
+ if "." in value:
66
+ return float(value)
67
+ except ValueError:
68
+ pass
69
+
70
+ if "," in value:
71
+ return [item.strip() for item in value.split(",")]
72
+
73
+ return value
74
+
75
+ def _load_toml_file(self, file_path: Path) -> dict[str, Any]:
76
+ """Load a TOML file and return its contents."""
77
+ try:
78
+ with open(file_path, "rb") as f:
79
+ return tomllib.load(f)
80
+ except (FileNotFoundError, tomllib.TOMLDecodeError):
81
+ return {}
82
+
83
+ @cached_property
84
+ def load(self) -> ConfigType:
85
+ """Load configuration from files and environment variables."""
86
+ # Load order (later overrides earlier):
87
+ # 1. default.toml
88
+ # 2. {env}.toml
89
+ # 3. local.toml (gitignored)
90
+ # 4. environment variables
91
+ config_data: dict[str, Any] = {}
92
+
93
+ # TODO: Update this so it looks for it in more than one place
94
+ config_files: list[Path] = [
95
+ self._config_path / "default.toml",
96
+ self._config_path / f"{self._env}.toml",
97
+ self._config_path / "local.toml",
98
+ ]
99
+
100
+ for config_file in config_files:
101
+ if config_file.exists():
102
+ file_data = self._load_toml_file(config_file)
103
+ config_data = self._deep_merge(config_data, file_data)
104
+
105
+ env_overrides: dict[str, Any] = self._get_env_overrides()
106
+ config_data = self._deep_merge(config_data, env_overrides)
107
+
108
+ try:
109
+ return self._model.model_validate(config_data)
110
+ except ValidationError as e:
111
+ raise ValueError(f"Configuration validation failed: {e}") from e
112
+
113
+ def _deep_merge(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
114
+ """Deep merge two dictionaries."""
115
+ result: dict[str, Any] = base.copy()
116
+ for key, value in override.items():
117
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
118
+ result[key] = self._deep_merge(result[key], value)
119
+ else:
120
+ result[key] = value
121
+ return result
122
+
123
+ @property
124
+ def config(self) -> ConfigType:
125
+ """Get the loaded configuration."""
126
+ if self._config is None:
127
+ self._config = self.load
128
+ return self._config
129
+
130
+ def reload(self) -> ConfigType:
131
+ """Force reload the configuration."""
132
+ if "config" in self.__dict__:
133
+ del self.__dict__["config"]
134
+ return self.config
135
+
136
+ def create_default_config(self) -> None:
137
+ """Create a default.toml file with example values."""
138
+ default_path = self._config_path / "default.toml"
139
+ if default_path.exists():
140
+ return
141
+
142
+ try:
143
+ default_instance: ConfigType = self._model()
144
+ toml_content: str = self._model_to_toml(default_instance)
145
+ default_path.write_text(toml_content)
146
+ except Exception as e:
147
+ print(f"Could not create default config: {e}")
148
+
149
+ def _model_to_toml(self, instance: ConfigType) -> str:
150
+ """Convert a Pydantic model to TOML format."""
151
+ lines: list[str] = ["# Default configuration"]
152
+
153
+ def _dict_to_toml(data: dict[str, Any], prefix: str = "") -> None:
154
+ for key, value in data.items():
155
+ full_key: str = f"{prefix}.{key}" if prefix else key
156
+
157
+ if isinstance(value, dict):
158
+ lines.append(f"\n[{full_key}]")
159
+ for sub_key, sub_value in value.items():
160
+ lines.append(f"{sub_key} = {self._format_toml_value(sub_value)}")
161
+ elif not prefix:
162
+ lines.append(f"{key} = {self._format_toml_value(value)}")
163
+
164
+ _dict_to_toml(instance.model_dump())
165
+ return "\n".join(lines)
166
+
167
+ def _format_toml_value(self, value: Any) -> str:
168
+ """Format a value for TOML output."""
169
+ if isinstance(value, str):
170
+ return f'"{value}"'
171
+ if isinstance(value, bool):
172
+ return str(value).lower()
173
+ if isinstance(value, list):
174
+ formatted_items = [self._format_toml_value(item) for item in value]
175
+ return f"[{', '.join(formatted_items)}]"
176
+ if value is None:
177
+ return '"null"'
178
+ return str(value)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ # Example usage and models
183
+ class DatabaseConfig(BaseModel):
184
+ """Configuration for an example database connection."""
185
+
186
+ host: str = "localhost"
187
+ port: int = 5432
188
+ username: str = "app"
189
+ password: str = "secret" # noqa: S105 This is just an example
190
+ database: str = "myapp"
191
+
192
+ class LoggingConfig(BaseModel):
193
+ """Configuration for an example logging setup."""
194
+
195
+ level: str = "INFO"
196
+ format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
197
+ file: str | None = None
198
+
199
+ _validate_file = nullable_string_validator("file")
200
+
201
+ class AppConfig(BaseModel):
202
+ """Example application configuration model."""
203
+
204
+ database: DatabaseConfig = DatabaseConfig()
205
+ logging: LoggingConfig = LoggingConfig()
206
+ environment: str = "development"
207
+ debug: bool = False
208
+ api_key: str = "your-api-key-here"
209
+ allowed_hosts: list[str] = ["localhost", "127.0.0.1"]
210
+
211
+ def get_config_manager(env: str = "dev") -> ConfigManager[AppConfig]:
212
+ """Get a configured ConfigManager instance."""
213
+ return ConfigManager[AppConfig](
214
+ config_model=AppConfig,
215
+ config_path=Path("config"),
216
+ env=env,
217
+ )
218
+
219
+ config_manager: ConfigManager[AppConfig] = get_config_manager("development")
220
+ config_manager.create_default_config()
221
+ config: AppConfig = config_manager.config
222
+
223
+ print(f"Database host: {config.database.host}")
224
+ print(f"Debug mode: {config.debug}")
225
+ print(f"Environment: {config.environment}")
226
+
227
+ # Test environment variable override
228
+ # Set: APP_DATABASE_HOST=production-db.example.com
229
+ # Set: APP_DEBUG=true
@@ -0,0 +1,69 @@
1
+ """Directory Manager Module for Bear Utils."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import ClassVar
6
+
7
+
8
+ @dataclass
9
+ class DirectoryManager:
10
+ """A class to manage directories for bear_utils."""
11
+
12
+ _base_path: ClassVar[Path] = Path.home() / ".config" / "bear_utils"
13
+ _settings_path: ClassVar[Path] = _base_path / "settings"
14
+ _temp_path: ClassVar[Path] = _base_path / "temp"
15
+
16
+ def setup(self) -> None:
17
+ """Ensure the base, settings, and temp directories exist."""
18
+ self._base_path.mkdir(parents=True, exist_ok=True)
19
+ self._settings_path.mkdir(parents=True, exist_ok=True)
20
+ self._temp_path.mkdir(parents=True, exist_ok=True)
21
+
22
+ def clear_temp(self) -> None:
23
+ """Clear the temporary directory."""
24
+ if self.temp_path.exists():
25
+ for item in self.temp_path.iterdir():
26
+ if item.is_file() or item.is_symlink():
27
+ item.unlink()
28
+ elif item.is_dir():
29
+ item.rmdir()
30
+
31
+ @property
32
+ def base_path(self) -> Path:
33
+ """Get the base path for bear_utils."""
34
+ return self._base_path
35
+
36
+ @property
37
+ def settings_path(self) -> Path:
38
+ """Get the path to the settings directory."""
39
+ return self._settings_path
40
+
41
+ @property
42
+ def temp_path(self) -> Path:
43
+ """Get the path to the temporary directory."""
44
+ return self._temp_path
45
+
46
+
47
+ def get_base_path() -> Path:
48
+ """Get the base path for bear_utils."""
49
+ return DirectoryManager().base_path
50
+
51
+
52
+ def get_settings_path() -> Path:
53
+ """Get the path to the settings directory."""
54
+ return DirectoryManager().settings_path
55
+
56
+
57
+ def get_temp_path() -> Path:
58
+ """Get the path to the temporary directory."""
59
+ return DirectoryManager().temp_path
60
+
61
+
62
+ def setup_directories() -> None:
63
+ """Set up the necessary directories for bear_utils."""
64
+ DirectoryManager().setup()
65
+
66
+
67
+ def clear_temp_directory() -> None:
68
+ """Clear the temporary directory."""
69
+ DirectoryManager().clear_temp()
@@ -0,0 +1,179 @@
1
+ """Settings Manager Module for Bear Utils."""
2
+
3
+ import atexit
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ import hashlib
7
+ from pathlib import Path
8
+ from typing import Any, Self
9
+
10
+ from tinydb import Query, TinyDB
11
+
12
+ DEFAULT_PATH: Path = Path.home() / ".config" / "bear_utils"
13
+
14
+
15
+ def get_config_folder(path: str | Path | None = None) -> Path:
16
+ """Get the path to the bear configuration directory."""
17
+ config_path: Path = Path(path) if isinstance(path, str) else path or DEFAULT_PATH
18
+ config_path.mkdir(parents=True, exist_ok=True)
19
+ return config_path
20
+
21
+
22
+ def get_file_hash(file_path: Path) -> str:
23
+ """Return the blake2 hash of the file at the given path."""
24
+ hasher = hashlib.blake2b()
25
+ with file_path.open("rb") as file:
26
+ while chunk := file.read(8192):
27
+ hasher.update(chunk)
28
+ return hasher.hexdigest()
29
+
30
+
31
+ class SettingsManager:
32
+ """A class to manage settings using TinyDB and an in-memory cache."""
33
+
34
+ __slots__ = ("cache", "db", "file_hash", "file_path", "settings_name")
35
+
36
+ def __init__(self, settings_name: str, folder_path: str | Path | None = None) -> None:
37
+ """Initialize the SettingsManager with a specific settings name."""
38
+ self.settings_name: str = settings_name
39
+ self.cache: dict[str, Any] = {}
40
+ file_name: str = f"{settings_name}.json"
41
+ self.file_path: Path = get_config_folder(folder_path) / file_name
42
+ self.db: TinyDB = TinyDB(self.file_path, indent=4, ensure_ascii=False)
43
+ self.file_hash: str = get_file_hash(self.file_path) if self.file_path.exists() else ""
44
+ atexit.register(self.close)
45
+ self._load_cache()
46
+
47
+ def __getattr__(self, key: str) -> Any:
48
+ """Handle dot notation access for settings."""
49
+ if key in self.__slots__:
50
+ raise AttributeError(f"'{key}' not initialized")
51
+ if key.startswith("_"):
52
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
53
+ return self.get(key)
54
+
55
+ def __setattr__(self, key: str, value: Any) -> None:
56
+ """Handle dot notation assignment for settings."""
57
+ if key in self.__slots__:
58
+ object.__setattr__(self, key, value)
59
+ return
60
+ self.set(key=key, value=value)
61
+
62
+ def invalidate_cache(self) -> None:
63
+ """Invalidate the in-memory cache."""
64
+ self.cache.clear()
65
+ self._load_cache()
66
+
67
+ def get(self, key: str, default: Any = None) -> Any:
68
+ """Get a setting value."""
69
+ file_hash = get_file_hash(self.file_path)
70
+
71
+ if file_hash != self.file_hash:
72
+ self.invalidate_cache()
73
+ self.file_hash = file_hash
74
+
75
+ if key in self.cache:
76
+ return self.cache[key]
77
+ if result := self.db.search(Query().key == key):
78
+ value = result[0]["value"]
79
+ self.cache[key] = value
80
+ return value
81
+ return default
82
+
83
+ def set(self, key: str, value: Any) -> None:
84
+ """Set a setting value."""
85
+ self.db.upsert({"key": key, "value": value}, Query().key == key)
86
+ self.cache[key] = value
87
+
88
+ def has(self, key: str) -> bool:
89
+ """Check if a setting exists."""
90
+ return key in self.cache or self.db.contains(Query().key == key)
91
+
92
+ def _load_cache(self) -> None:
93
+ """Load all settings into cache."""
94
+ for record in self.db.all():
95
+ self.cache[record["key"]] = record["value"]
96
+
97
+ def open(self) -> None:
98
+ """Reopen the settings file after it's been closed/destroyed."""
99
+ self.db = TinyDB(self.file_path, indent=4, ensure_ascii=False)
100
+ self.cache = {}
101
+ self._load_cache()
102
+
103
+ def close(self) -> None:
104
+ """Close the database."""
105
+ if hasattr(self, "db"):
106
+ self.db.close()
107
+ if hasattr(self, "cache"):
108
+ self.cache.clear()
109
+
110
+ def destroy_settings(self) -> bool:
111
+ """Delete the settings file."""
112
+ if self.file_path.exists():
113
+ self.close()
114
+ self.file_path.unlink()
115
+ self.cache.clear()
116
+ return True
117
+ return False
118
+
119
+ def __contains__(self, key: str) -> bool:
120
+ return self.has(key)
121
+
122
+ def keys(self) -> list[str]:
123
+ """Get all setting keys."""
124
+ return list(self.cache.keys())
125
+
126
+ def items(self) -> list[tuple[str, Any]]:
127
+ """Get all setting key-value pairs."""
128
+ return list(self.cache.items())
129
+
130
+ def values(self) -> list[Any]:
131
+ """Get all setting values."""
132
+ return list(self.cache.values())
133
+
134
+ def __len__(self):
135
+ return len(self.cache)
136
+
137
+ def __enter__(self) -> Self:
138
+ return self
139
+
140
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
141
+ self.close()
142
+
143
+ def __repr__(self) -> str:
144
+ return f"<SettingsManager settings_name='{self.settings_name}'>"
145
+
146
+ def __str__(self) -> str:
147
+ return f"SettingsManager for '{self.settings_name}' with {len(self.keys())} settings."
148
+
149
+
150
+ _settings_managers: dict[str, SettingsManager] = {}
151
+
152
+
153
+ def get_settings_manager(settings_name: str) -> SettingsManager:
154
+ """Get or create a SettingsManager instance."""
155
+ if settings_name not in _settings_managers:
156
+ _settings_managers[settings_name] = SettingsManager(settings_name=settings_name)
157
+ return _settings_managers[settings_name]
158
+
159
+
160
+ @contextmanager
161
+ def settings(settings_name: str) -> Generator[SettingsManager]:
162
+ """Context manager for SettingsManager."""
163
+ sm: SettingsManager = get_settings_manager(settings_name)
164
+ try:
165
+ yield sm
166
+ finally:
167
+ sm.close()
168
+
169
+
170
+ if __name__ == "__main__":
171
+ # Example usage of the SettingsManager
172
+ with settings(settings_name="example_settings") as sm:
173
+ sm.sample_setting = "This is a sample setting"
174
+ print(sm.sample_setting)
175
+ print(sm.keys())
176
+ for key, value in sm.items():
177
+ print("This is items()")
178
+ print(f"Key: {key}, Value: {value}")
179
+ print(sm.file_path)
@@ -0,0 +1,90 @@
1
+ """Constants Module for Bear Utils."""
2
+
3
+ from pathlib import Path
4
+ import sys
5
+ from typing import TextIO
6
+
7
+ from bear_utils.constants._exit_code import (
8
+ COMMAND_CANNOT_EXECUTE,
9
+ COMMAND_NOT_FOUND,
10
+ EXIT_STATUS_OUT_OF_RANGE,
11
+ FAIL,
12
+ FAILURE,
13
+ INVALID_ARGUMENT_TO_EXIT,
14
+ MISUSE_OF_SHELL_COMMAND,
15
+ PROCESS_KILLED_BY_SIGKILL,
16
+ PROCESS_TERMINATED_BY_SIGTERM,
17
+ SCRIPT_TERMINATED_BY_CONTROL_C,
18
+ SEGMENTATION_FAULT,
19
+ SUCCESS,
20
+ ExitCode,
21
+ )
22
+ from bear_utils.constants._http_status_code import (
23
+ BAD_REQUEST,
24
+ CONFLICT,
25
+ FORBIDDEN,
26
+ PAGE_NOT_FOUND,
27
+ SERVER_ERROR,
28
+ SERVER_OK,
29
+ UNAUTHORIZED,
30
+ HTTPStatusCode,
31
+ )
32
+ from bear_utils.constants._meta import IntValue, NullFile, RichIntEnum, RichStrEnum, StrValue
33
+
34
+ VIDEO_EXTS = [".mp4", ".mov", ".avi", ".mkv"]
35
+ """Extensions for video files."""
36
+ IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".gif"]
37
+ """Extensions for image files."""
38
+ FILE_EXTS = IMAGE_EXTS + VIDEO_EXTS
39
+ """Extensions for both image and video files."""
40
+
41
+ PATH_TO_DOWNLOADS = Path.home() / "Downloads"
42
+ """Path to the Downloads folder."""
43
+ PATH_TO_PICTURES = Path.home() / "Pictures"
44
+ """Path to the Pictures folder."""
45
+ GLOBAL_VENV = Path.home() / ".global_venv"
46
+ """Path to the global virtual environment."""
47
+
48
+ STDOUT: TextIO = sys.stdout
49
+ """Standard output stream."""
50
+ STDERR: TextIO = sys.stderr
51
+ """Standard error stream."""
52
+ DEVNULL: TextIO = NullFile()
53
+ """A null file that discards all writes."""
54
+
55
+ __all__ = [
56
+ "BAD_REQUEST",
57
+ "COMMAND_CANNOT_EXECUTE",
58
+ "COMMAND_NOT_FOUND",
59
+ "CONFLICT",
60
+ "EXIT_STATUS_OUT_OF_RANGE",
61
+ "FAIL",
62
+ "FAILURE",
63
+ "FILE_EXTS",
64
+ "FORBIDDEN",
65
+ "GLOBAL_VENV",
66
+ "IMAGE_EXTS",
67
+ "INVALID_ARGUMENT_TO_EXIT",
68
+ "MISUSE_OF_SHELL_COMMAND",
69
+ "PAGE_NOT_FOUND",
70
+ "PATH_TO_DOWNLOADS",
71
+ "PATH_TO_PICTURES",
72
+ "PROCESS_KILLED_BY_SIGKILL",
73
+ "PROCESS_TERMINATED_BY_SIGTERM",
74
+ "SCRIPT_TERMINATED_BY_CONTROL_C",
75
+ "SEGMENTATION_FAULT",
76
+ "SERVER_ERROR",
77
+ "SERVER_OK",
78
+ "STDERR",
79
+ "STDOUT",
80
+ "SUCCESS",
81
+ "UNAUTHORIZED",
82
+ "VIDEO_EXTS",
83
+ "ExitCode",
84
+ "HTTPStatusCode",
85
+ "IntValue",
86
+ "NullFile",
87
+ "RichIntEnum",
88
+ "RichStrEnum",
89
+ "StrValue",
90
+ ]