bear-utils 0.7.11__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 +13 -0
- bear_utils/ai/__init__.py +30 -0
- bear_utils/ai/ai_helpers/__init__.py +130 -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 +188 -0
- bear_utils/ai/ai_helpers/_types.py +20 -0
- bear_utils/cache/__init__.py +119 -0
- bear_utils/cli/__init__.py +4 -0
- bear_utils/cli/commands.py +59 -0
- bear_utils/cli/prompt_helpers.py +166 -0
- bear_utils/cli/shell/__init__.py +0 -0
- bear_utils/cli/shell/_base_command.py +74 -0
- bear_utils/cli/shell/_base_shell.py +390 -0
- bear_utils/cli/shell/_common.py +19 -0
- bear_utils/config/__init__.py +11 -0
- bear_utils/config/config_manager.py +92 -0
- bear_utils/config/dir_manager.py +64 -0
- bear_utils/config/settings_manager.py +232 -0
- bear_utils/constants/__init__.py +16 -0
- bear_utils/constants/_exceptions.py +3 -0
- bear_utils/constants/_lazy_typing.py +15 -0
- bear_utils/constants/date_related.py +36 -0
- bear_utils/constants/time_related.py +22 -0
- bear_utils/database/__init__.py +6 -0
- bear_utils/database/_db_manager.py +104 -0
- bear_utils/events/__init__.py +16 -0
- bear_utils/events/events_class.py +52 -0
- bear_utils/events/events_module.py +65 -0
- bear_utils/extras/__init__.py +17 -0
- bear_utils/extras/_async_helpers.py +15 -0
- bear_utils/extras/_tools.py +178 -0
- bear_utils/extras/platform_utils.py +53 -0
- bear_utils/extras/wrappers/__init__.py +0 -0
- bear_utils/extras/wrappers/add_methods.py +98 -0
- bear_utils/files/__init__.py +4 -0
- bear_utils/files/file_handlers/__init__.py +3 -0
- bear_utils/files/file_handlers/_base_file_handler.py +93 -0
- bear_utils/files/file_handlers/file_handler_factory.py +278 -0
- bear_utils/files/file_handlers/json_file_handler.py +44 -0
- bear_utils/files/file_handlers/log_file_handler.py +33 -0
- bear_utils/files/file_handlers/txt_file_handler.py +34 -0
- bear_utils/files/file_handlers/yaml_file_handler.py +57 -0
- bear_utils/files/ignore_parser.py +298 -0
- bear_utils/graphics/__init__.py +4 -0
- bear_utils/graphics/bear_gradient.py +140 -0
- bear_utils/graphics/image_helpers.py +39 -0
- bear_utils/gui/__init__.py +3 -0
- bear_utils/gui/gui_tools/__init__.py +5 -0
- bear_utils/gui/gui_tools/_settings.py +37 -0
- bear_utils/gui/gui_tools/_types.py +12 -0
- bear_utils/gui/gui_tools/qt_app.py +145 -0
- bear_utils/gui/gui_tools/qt_color_picker.py +119 -0
- bear_utils/gui/gui_tools/qt_file_handler.py +138 -0
- bear_utils/gui/gui_tools/qt_input_dialog.py +306 -0
- bear_utils/logging/__init__.py +25 -0
- bear_utils/logging/logger_manager/__init__.py +0 -0
- bear_utils/logging/logger_manager/_common.py +47 -0
- bear_utils/logging/logger_manager/_console_junk.py +131 -0
- bear_utils/logging/logger_manager/_styles.py +91 -0
- bear_utils/logging/logger_manager/loggers/__init__.py +0 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.py +238 -0
- bear_utils/logging/logger_manager/loggers/_base_logger.pyi +50 -0
- bear_utils/logging/logger_manager/loggers/_buffer_logger.py +55 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.py +249 -0
- bear_utils/logging/logger_manager/loggers/_console_logger.pyi +64 -0
- bear_utils/logging/logger_manager/loggers/_file_logger.py +141 -0
- bear_utils/logging/logger_manager/loggers/_level_sin.py +58 -0
- bear_utils/logging/logger_manager/loggers/_logger.py +18 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.py +110 -0
- bear_utils/logging/logger_manager/loggers/_sub_logger.pyi +38 -0
- bear_utils/logging/loggers.py +76 -0
- bear_utils/monitoring/__init__.py +10 -0
- bear_utils/monitoring/host_monitor.py +350 -0
- bear_utils/time/__init__.py +16 -0
- bear_utils/time/_helpers.py +91 -0
- bear_utils/time/_time_class.py +316 -0
- bear_utils/time/_timer.py +80 -0
- bear_utils/time/_tools.py +17 -0
- bear_utils/time/time_manager.py +218 -0
- bear_utils-0.7.11.dist-info/METADATA +260 -0
- bear_utils-0.7.11.dist-info/RECORD +83 -0
- bear_utils-0.7.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
import os
|
2
|
+
from functools import lru_cache
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any, Generic, TypeVar
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
ConfigType = TypeVar("ConfigType", bound=BaseModel)
|
9
|
+
|
10
|
+
# TODO: Get around to potentially using this, right now it is just masturbation
|
11
|
+
|
12
|
+
|
13
|
+
class ConfigManager(Generic[ConfigType]):
|
14
|
+
def __init__(self, config_model: type[ConfigType], config_path: Path | None = None, env: str = "dev") -> None:
|
15
|
+
self._model = config_model
|
16
|
+
self._config_path = config_path or Path("config")
|
17
|
+
self._env = env
|
18
|
+
self._config: ConfigType | None = None
|
19
|
+
|
20
|
+
def _get_env_overrides(self) -> dict[str, Any]:
|
21
|
+
"""Convert environment variables to nested dictionary structure."""
|
22
|
+
env_config: dict[str, Any] = {}
|
23
|
+
|
24
|
+
for key, value in os.environ.items():
|
25
|
+
if not key.startswith("APP_"):
|
26
|
+
continue
|
27
|
+
|
28
|
+
# Convert APP_DATABASE_HOST to ['database', 'host']
|
29
|
+
parts = key.lower().replace("app_", "").split("_")
|
30
|
+
|
31
|
+
# Build nested dictionary
|
32
|
+
current = env_config
|
33
|
+
for part in parts[:-1]:
|
34
|
+
current = current.setdefault(part, {})
|
35
|
+
current[parts[-1]] = value
|
36
|
+
|
37
|
+
return env_config
|
38
|
+
|
39
|
+
@lru_cache
|
40
|
+
def load(self) -> ConfigType:
|
41
|
+
# Load order (later overrides earlier):
|
42
|
+
# 1. default.yaml
|
43
|
+
# 2. {env}.yaml
|
44
|
+
# 3. local.yaml (gitignored)
|
45
|
+
# 4. environment variables
|
46
|
+
config_data: dict[str, Any] = {}
|
47
|
+
|
48
|
+
# for config_file in [
|
49
|
+
# self._config_path / "default.yaml",
|
50
|
+
# self._config_path / f"{self._env}.yaml",
|
51
|
+
# self._config_path / "local.yaml",
|
52
|
+
# ]:
|
53
|
+
# if config_file.exists():
|
54
|
+
# with open(config_file) as f:
|
55
|
+
# config_data.update(yaml.safe_load(f))
|
56
|
+
|
57
|
+
config_data.update(self._get_env_overrides())
|
58
|
+
|
59
|
+
return self._model.model_validate(config_data)
|
60
|
+
|
61
|
+
@property
|
62
|
+
def config(self) -> ConfigType:
|
63
|
+
if self._config is None:
|
64
|
+
self._config = self.load()
|
65
|
+
return self._config
|
66
|
+
|
67
|
+
|
68
|
+
# Example usage:
|
69
|
+
# class DatabaseConfig(BaseModel):
|
70
|
+
# host: str
|
71
|
+
# port: int
|
72
|
+
# username: str
|
73
|
+
# password: str
|
74
|
+
# database: str
|
75
|
+
|
76
|
+
|
77
|
+
# class AppConfig(BaseModel):
|
78
|
+
# database: DatabaseConfig
|
79
|
+
# environment: str
|
80
|
+
# debug: bool = False
|
81
|
+
# logging_mode: str = "console"
|
82
|
+
# log_level: str = "INFO"
|
83
|
+
|
84
|
+
|
85
|
+
# config_manager = ConfigManager[AppConfig](
|
86
|
+
# config_model=AppConfig,
|
87
|
+
# config_path=Path("config"),
|
88
|
+
# env="development",
|
89
|
+
# )
|
90
|
+
|
91
|
+
# Access config
|
92
|
+
# db_config = config_manager.config.database
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class DirectoryManager:
|
7
|
+
_base_path: Path = Path.home() / ".config" / "bear_utils"
|
8
|
+
_settings_path: Path = _base_path / "settings"
|
9
|
+
_temp_path: Path = _base_path / "temp"
|
10
|
+
|
11
|
+
def setup(self) -> None:
|
12
|
+
"""Ensure the base, settings, and temp directories exist."""
|
13
|
+
self._base_path.mkdir(parents=True, exist_ok=True)
|
14
|
+
self._settings_path.mkdir(parents=True, exist_ok=True)
|
15
|
+
self._temp_path.mkdir(parents=True, exist_ok=True)
|
16
|
+
|
17
|
+
def clear_temp(self) -> None:
|
18
|
+
"""Clear the temporary directory."""
|
19
|
+
if self.temp_path.exists():
|
20
|
+
for item in self.temp_path.iterdir():
|
21
|
+
if item.is_file() or item.is_symlink():
|
22
|
+
item.unlink()
|
23
|
+
elif item.is_dir():
|
24
|
+
item.rmdir()
|
25
|
+
|
26
|
+
@property
|
27
|
+
def base_path(self) -> Path:
|
28
|
+
"""Get the base path for bear_utils."""
|
29
|
+
return self._base_path
|
30
|
+
|
31
|
+
@property
|
32
|
+
def settings_path(self) -> Path:
|
33
|
+
"""Get the path to the settings directory."""
|
34
|
+
return self._settings_path
|
35
|
+
|
36
|
+
@property
|
37
|
+
def temp_path(self) -> Path:
|
38
|
+
"""Get the path to the temporary directory."""
|
39
|
+
return self._temp_path
|
40
|
+
|
41
|
+
|
42
|
+
def get_base_path() -> Path:
|
43
|
+
"""Get the base path for bear_utils."""
|
44
|
+
return DirectoryManager().base_path
|
45
|
+
|
46
|
+
|
47
|
+
def get_settings_path() -> Path:
|
48
|
+
"""Get the path to the settings directory."""
|
49
|
+
return DirectoryManager().settings_path
|
50
|
+
|
51
|
+
|
52
|
+
def get_temp_path() -> Path:
|
53
|
+
"""Get the path to the temporary directory."""
|
54
|
+
return DirectoryManager().temp_path
|
55
|
+
|
56
|
+
|
57
|
+
def setup_directories() -> None:
|
58
|
+
"""Set up the necessary directories for bear_utils."""
|
59
|
+
DirectoryManager().setup()
|
60
|
+
|
61
|
+
|
62
|
+
def clear_temp_directory() -> None:
|
63
|
+
"""Clear the temporary directory."""
|
64
|
+
DirectoryManager().clear_temp()
|
@@ -0,0 +1,232 @@
|
|
1
|
+
import atexit
|
2
|
+
from argparse import Namespace
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from pathlib import Path
|
5
|
+
from shelve import Shelf
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from singleton_base.singleton_base_new import SingletonBase
|
9
|
+
|
10
|
+
|
11
|
+
def get_bear_config_path() -> Path:
|
12
|
+
"""Get the path to the bear configuration path"""
|
13
|
+
path = Path.home() / ".config" / "bear_utils"
|
14
|
+
if not path.exists():
|
15
|
+
path.mkdir(parents=True, exist_ok=True)
|
16
|
+
return path
|
17
|
+
|
18
|
+
|
19
|
+
def get_config_file_path(filename: str) -> Path:
|
20
|
+
"""Get the path to a specific configuration file in the bear configuration path"""
|
21
|
+
config_path = get_bear_config_path()
|
22
|
+
return config_path / filename
|
23
|
+
|
24
|
+
|
25
|
+
class SettingsCache(Namespace):
|
26
|
+
"""A Namespace to hold settings in memory."""
|
27
|
+
|
28
|
+
def __init__(self):
|
29
|
+
super().__init__()
|
30
|
+
|
31
|
+
def clear(self):
|
32
|
+
"""Clear the settings cache."""
|
33
|
+
self.__dict__.clear()
|
34
|
+
|
35
|
+
def set(self, key: str, value):
|
36
|
+
"""Set a value in the settings cache."""
|
37
|
+
setattr(self, key, value)
|
38
|
+
|
39
|
+
def get(self, key: str, default=None):
|
40
|
+
"""Get a value from the settings cache."""
|
41
|
+
return getattr(self, key, default)
|
42
|
+
|
43
|
+
def has(self, key: str) -> bool:
|
44
|
+
"""Check if a key exists in the settings cache."""
|
45
|
+
return hasattr(self, key)
|
46
|
+
|
47
|
+
|
48
|
+
class SettingsManager:
|
49
|
+
settings_name: str
|
50
|
+
shelf: Shelf
|
51
|
+
settings_cache: SettingsCache
|
52
|
+
|
53
|
+
def __init__(self, settings_name: str):
|
54
|
+
object.__setattr__(self, "settings_name", settings_name)
|
55
|
+
object.__setattr__(self, "settings_cache", SettingsCache())
|
56
|
+
object.__setattr__(self, "shelf", self._open())
|
57
|
+
object.__setattr__(self, "_initialized", True)
|
58
|
+
|
59
|
+
atexit.register(self.close)
|
60
|
+
self._load_existing_settings()
|
61
|
+
|
62
|
+
def __getattr__(self, key: str):
|
63
|
+
"""Handle dot notation access for settings."""
|
64
|
+
if key.startswith("_"):
|
65
|
+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
|
66
|
+
return self.get(key)
|
67
|
+
|
68
|
+
def __setattr__(self, key: str, value):
|
69
|
+
"""Handle dot notation assignment for settings."""
|
70
|
+
if not hasattr(self, "_initialized"):
|
71
|
+
object.__setattr__(self, key, value)
|
72
|
+
return
|
73
|
+
|
74
|
+
if key in ["settings_name", "settings_cache", "shelf"] or key.startswith("_"):
|
75
|
+
raise AttributeError(f"Cannot modify '{key}' after initialization")
|
76
|
+
|
77
|
+
self.set(key, value)
|
78
|
+
|
79
|
+
def get(self, key: str, default=None):
|
80
|
+
"""Get a setting value by key with optional default."""
|
81
|
+
try:
|
82
|
+
if self.settings_cache.has(key):
|
83
|
+
return self.settings_cache.get(key, default)
|
84
|
+
elif self._shelf_has(key):
|
85
|
+
return self.shelf[key]
|
86
|
+
else:
|
87
|
+
return default
|
88
|
+
except Exception as e:
|
89
|
+
return default
|
90
|
+
|
91
|
+
def set(self, key: str, value):
|
92
|
+
"""Set a setting value by key."""
|
93
|
+
self.shelf[key] = value
|
94
|
+
self.settings_cache.set(key, value)
|
95
|
+
|
96
|
+
def has(self, key: str) -> bool:
|
97
|
+
"""Check if a setting exists by key."""
|
98
|
+
return self.settings_cache.has(key) or self._shelf_has(key)
|
99
|
+
|
100
|
+
def _shelf_has(self, key: str) -> bool:
|
101
|
+
"""Check if a setting exists by key."""
|
102
|
+
return key in self.shelf
|
103
|
+
|
104
|
+
def open(self):
|
105
|
+
object.__setattr__(self, "shelf", self._open())
|
106
|
+
self._load_existing_settings()
|
107
|
+
|
108
|
+
def _open(self) -> Shelf:
|
109
|
+
"""Open the settings file."""
|
110
|
+
import shelve
|
111
|
+
|
112
|
+
try:
|
113
|
+
shelf: Shelf[Any] = shelve.open(get_config_file_path(self.settings_name))
|
114
|
+
return shelf
|
115
|
+
except Exception as e:
|
116
|
+
raise RuntimeError(f"Warning: Could not open settings file '{self.settings_name}': {e}")
|
117
|
+
|
118
|
+
def _load_existing_settings(self):
|
119
|
+
"""Load existing settings from shelf into namespace."""
|
120
|
+
for key in self.shelf:
|
121
|
+
if not self.settings_cache.has(key):
|
122
|
+
self.settings_cache.set(key, self.shelf[key])
|
123
|
+
|
124
|
+
def close(self):
|
125
|
+
"""Close the settings file."""
|
126
|
+
if self.shelf is not None:
|
127
|
+
self.shelf.close()
|
128
|
+
if self.settings_cache is not None:
|
129
|
+
self.settings_cache.clear()
|
130
|
+
|
131
|
+
def destroy_settings(self) -> bool:
|
132
|
+
"""Delete the settings file, a bit nuclear and will require calling open() again."""
|
133
|
+
file_path = get_config_file_path(self.settings_name)
|
134
|
+
if file_path.exists():
|
135
|
+
self.close()
|
136
|
+
file_path.unlink()
|
137
|
+
self.settings_cache.clear()
|
138
|
+
object.__setattr__(self, "shelf", None)
|
139
|
+
return True
|
140
|
+
return False
|
141
|
+
|
142
|
+
def __del__(self):
|
143
|
+
"""Destructor to ensure the shelf is closed."""
|
144
|
+
self.close()
|
145
|
+
|
146
|
+
def __enter__(self):
|
147
|
+
"""Context manager entry."""
|
148
|
+
return self
|
149
|
+
|
150
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
151
|
+
"""Context manager exit."""
|
152
|
+
self.close()
|
153
|
+
|
154
|
+
def __contains__(self, key: str) -> bool:
|
155
|
+
"""Check if a setting exists."""
|
156
|
+
return self.has(key)
|
157
|
+
|
158
|
+
def keys(self) -> list[str]:
|
159
|
+
"""Get all setting keys."""
|
160
|
+
return list(vars(self.settings_cache).keys())
|
161
|
+
|
162
|
+
def items(self):
|
163
|
+
"""Get all setting key-value pairs."""
|
164
|
+
for key in self.keys():
|
165
|
+
yield key, self.settings_cache.get(key)
|
166
|
+
|
167
|
+
def values(self):
|
168
|
+
"""Get all setting values."""
|
169
|
+
for key in self.keys():
|
170
|
+
yield self.settings_cache.get(key)
|
171
|
+
|
172
|
+
def __repr__(self):
|
173
|
+
"""String representation of the SettingsManager."""
|
174
|
+
return f"<SettingsManager settings_name='{self.settings_name}'>"
|
175
|
+
|
176
|
+
def __str__(self):
|
177
|
+
"""String representation of the SettingsManager."""
|
178
|
+
return f"SettingsManager for '{self.settings_name}' with {len(self.keys())} settings."
|
179
|
+
|
180
|
+
def __len__(self):
|
181
|
+
"""Get the number of settings."""
|
182
|
+
return len(self.keys())
|
183
|
+
|
184
|
+
|
185
|
+
class SettingsSupervisor(SingletonBase):
|
186
|
+
def __init__(self):
|
187
|
+
self._settings_managers = {}
|
188
|
+
|
189
|
+
def _get_instance(self, settings_name: str) -> SettingsManager:
|
190
|
+
"""Get or create a SettingsManager instance."""
|
191
|
+
if settings_name in self._settings_managers:
|
192
|
+
return self._settings_managers[settings_name]
|
193
|
+
return SettingsManager(settings_name)
|
194
|
+
|
195
|
+
|
196
|
+
def get_settings_manager(settings_name: str) -> SettingsManager:
|
197
|
+
"""Get the SettingsManager instance for a given settings name."""
|
198
|
+
supervisor: SettingsSupervisor = SettingsSupervisor.get_instance(init=True)
|
199
|
+
return supervisor._get_instance(settings_name)
|
200
|
+
|
201
|
+
|
202
|
+
@contextmanager
|
203
|
+
def settings(settings_name: str):
|
204
|
+
"""Context manager for SettingsManager."""
|
205
|
+
sm: SettingsManager = get_settings_manager(settings_name)
|
206
|
+
try:
|
207
|
+
yield sm
|
208
|
+
finally:
|
209
|
+
sm.close()
|
210
|
+
|
211
|
+
|
212
|
+
__all__: list[str] = ["SettingsManager", "settings", "get_settings_manager"]
|
213
|
+
|
214
|
+
# if __name__ == "__main__":
|
215
|
+
# with settings("example_settings") as sm:
|
216
|
+
# sm.sample_setting = "This is a sample setting"
|
217
|
+
# print(sm.sample_setting)
|
218
|
+
# print(sm.keys())
|
219
|
+
# for key, value in sm.items():
|
220
|
+
# print("This is items()")
|
221
|
+
# print(f"Key: {key}, Value: {value}")
|
222
|
+
# for value in sm.values():
|
223
|
+
# print("This is values()")
|
224
|
+
# print(value)
|
225
|
+
# print(len(sm))
|
226
|
+
# print(sm)
|
227
|
+
# if sm.destroy_settings():
|
228
|
+
# print("Settings destroyed successfully.")
|
229
|
+
# sm.open()
|
230
|
+
# print(sm.keys())
|
231
|
+
# sm.test = "Test setting"
|
232
|
+
# print(len(sm))
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from os import getenv
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
VIDEO_EXTS = [".mp4", ".mov", ".avi", ".mkv"]
|
5
|
+
"""Extensions for video files."""
|
6
|
+
IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".gif"]
|
7
|
+
"""Extensions for image files."""
|
8
|
+
FILE_EXTS = IMAGE_EXTS + VIDEO_EXTS
|
9
|
+
"""Extensions for both image and video files."""
|
10
|
+
|
11
|
+
PATH_TO_DOWNLOADS = Path.home() / "Downloads"
|
12
|
+
"""Path to the Downloads folder."""
|
13
|
+
PATH_TO_PICTURES = Path.home() / "Pictures"
|
14
|
+
"""Path to the Pictures folder."""
|
15
|
+
GLOBAL_VENV = Path.home() / ".global_venv"
|
16
|
+
"""Path to the global virtual environment."""
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from typing import Literal, TypeVar
|
2
|
+
|
3
|
+
from sqlalchemy.ext.declarative import DeclarativeMeta
|
4
|
+
|
5
|
+
LitInt = Literal["int"]
|
6
|
+
LitFloat = Literal["float"]
|
7
|
+
LitStr = Literal["str"]
|
8
|
+
LitBool = Literal["bool"]
|
9
|
+
|
10
|
+
OptInt = int | None
|
11
|
+
OptFloat = float | None
|
12
|
+
OptStr = str | None
|
13
|
+
OptBool = bool | None
|
14
|
+
|
15
|
+
TableType = TypeVar("TableType", bound=DeclarativeMeta)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from typing import LiteralString
|
2
|
+
|
3
|
+
from pytz import UTC, timezone
|
4
|
+
from pytz.tzinfo import BaseTzInfo, DstTzInfo, StaticTzInfo
|
5
|
+
|
6
|
+
TimeZoneType = BaseTzInfo | DstTzInfo | StaticTzInfo
|
7
|
+
|
8
|
+
DATE_FORMAT = "%m-%d-%Y"
|
9
|
+
"""Date format"""
|
10
|
+
|
11
|
+
TIME_FORMAT = "%I:%M %p"
|
12
|
+
"""Time format with 12 hour format"""
|
13
|
+
|
14
|
+
TIME_FORMAT_WITH_SECONDS: LiteralString = "%I:%M:%S %p"
|
15
|
+
"""Time format with 12 hour format and seconds"""
|
16
|
+
|
17
|
+
DATE_TIME_FORMAT: LiteralString = f"{DATE_FORMAT} {TIME_FORMAT}"
|
18
|
+
"""Datetime format with 12 hour format"""
|
19
|
+
|
20
|
+
DT_FORMAT_WITH_SECONDS: LiteralString = f"{DATE_FORMAT} {TIME_FORMAT_WITH_SECONDS}"
|
21
|
+
"""Datetime format with 12 hour format and seconds"""
|
22
|
+
|
23
|
+
DT_FORMAT_WITH_TZ: LiteralString = f"{DATE_TIME_FORMAT} %Z"
|
24
|
+
"""Datetime format with 12 hour format and timezone"""
|
25
|
+
|
26
|
+
DT_FORMAT_WITH_TZ_AND_SECONDS: LiteralString = f"{DT_FORMAT_WITH_SECONDS} %Z"
|
27
|
+
"""Datetime format with 12 hour format, seconds, and timezone"""
|
28
|
+
|
29
|
+
PT_TIME_ZONE: TimeZoneType = timezone("America/Los_Angeles")
|
30
|
+
"""Default timezone, a Pacific Time Zone using a pytz timezone object"""
|
31
|
+
|
32
|
+
UTC_TIME_ZONE: TimeZoneType = UTC
|
33
|
+
"""UTC timezone, a UTC timezone using a pytz timezone object"""
|
34
|
+
|
35
|
+
CEST_TIME_ZONE: TimeZoneType = timezone("Europe/Berlin") # Central European Summer Time, I GUESS!
|
36
|
+
"""Central European Summer Time timezone, a CEST timezone using a pytz timezone object"""
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
MINUTES_IN_HOUR: Literal[60] = 60
|
4
|
+
"""60 minutes in an hour"""
|
5
|
+
|
6
|
+
HOURS_IN_DAY: Literal[24] = 24
|
7
|
+
"""24 hours in a day"""
|
8
|
+
|
9
|
+
DAYS_IN_MONTH: Literal[30] = 30
|
10
|
+
"""30 days in a month, approximation for a month"""
|
11
|
+
|
12
|
+
SECONDS_IN_MINUTE: Literal[60] = 60
|
13
|
+
"""60 seconds in a minute"""
|
14
|
+
|
15
|
+
SECONDS_IN_HOUR: Literal[3600] = SECONDS_IN_MINUTE * MINUTES_IN_HOUR
|
16
|
+
"""60 * 60 = 3600 seconds in an hour"""
|
17
|
+
|
18
|
+
SECONDS_IN_DAY: Literal[86400] = SECONDS_IN_HOUR * HOURS_IN_DAY
|
19
|
+
"""24 * 60 * 60 = 86400 seconds in a day"""
|
20
|
+
|
21
|
+
SECONDS_IN_MONTH: Literal[2592000] = SECONDS_IN_DAY * DAYS_IN_MONTH
|
22
|
+
"""30 * 24 * 60 * 60 = 2592000 seconds in a month"""
|
@@ -0,0 +1,104 @@
|
|
1
|
+
import atexit
|
2
|
+
from collections.abc import Generator
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, ClassVar
|
6
|
+
|
7
|
+
from singleton_base import SingletonBase
|
8
|
+
from sqlalchemy import Engine, MetaData, create_engine
|
9
|
+
from sqlalchemy.ext.declarative import DeclarativeMeta
|
10
|
+
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
|
11
|
+
from sqlalchemy.orm.session import Session
|
12
|
+
|
13
|
+
from ..constants._lazy_typing import TableType
|
14
|
+
|
15
|
+
|
16
|
+
class DatabaseManager:
|
17
|
+
_base: ClassVar[DeclarativeMeta | None] = None
|
18
|
+
|
19
|
+
@classmethod
|
20
|
+
def set_base(cls, base: DeclarativeMeta) -> None:
|
21
|
+
"""Set the base class for the database manager."""
|
22
|
+
cls._base = base
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def get_base(cls) -> DeclarativeMeta:
|
26
|
+
"""Get the base class for the database manager."""
|
27
|
+
if cls._base is None:
|
28
|
+
cls.set_base(declarative_base())
|
29
|
+
if cls._base is None:
|
30
|
+
raise ValueError("Base class is not set, failed to set base.")
|
31
|
+
return cls._base
|
32
|
+
|
33
|
+
def __init__(self, db_url: str | Path | None = None):
|
34
|
+
if db_url is None or db_url == "":
|
35
|
+
raise ValueError("Database URL cannot be None or empty.")
|
36
|
+
self.db_url: str = str(db_url)
|
37
|
+
self.engine: Engine = create_engine(self.db_url, echo=False)
|
38
|
+
base: DeclarativeMeta = DatabaseManager.get_base()
|
39
|
+
self.metadata: MetaData = base.metadata
|
40
|
+
self.SessionFactory = sessionmaker(bind=self.engine)
|
41
|
+
self.session = scoped_session(self.SessionFactory)
|
42
|
+
atexit.register(self.close_all)
|
43
|
+
self.create_tables()
|
44
|
+
|
45
|
+
def get_all_records(self, table_obj: type[TableType]) -> list[TableType]:
|
46
|
+
"""Get all records from a table."""
|
47
|
+
with self.open_session() as session:
|
48
|
+
records = session.query(table_obj).all()
|
49
|
+
return records
|
50
|
+
|
51
|
+
def count_records(self, table_obj: type[TableType]) -> int:
|
52
|
+
"""Count the number of records in a table."""
|
53
|
+
with self.open_session() as session:
|
54
|
+
count: int = session.query(table_obj).count()
|
55
|
+
return count
|
56
|
+
|
57
|
+
def get_records_by_var(self, table_obj: type[TableType], variable: str, value) -> list[TableType]:
|
58
|
+
"""Get records from a table by a specific variable."""
|
59
|
+
with self.open_session() as session:
|
60
|
+
records: list[TableType] = session.query(table_obj).filter(getattr(table_obj, variable) == value).all()
|
61
|
+
return records
|
62
|
+
|
63
|
+
def count_records_by_var(self, table_obj: type[TableType], variable: str, value) -> int:
|
64
|
+
"""Count the number of records in a table by a specific variable."""
|
65
|
+
with self.open_session() as session:
|
66
|
+
count: int = session.query(table_obj).filter(getattr(table_obj, variable) == value).count()
|
67
|
+
return count
|
68
|
+
|
69
|
+
@contextmanager
|
70
|
+
def open_session(self) -> Generator[Session, Any, None]:
|
71
|
+
"""Provide a transactional scope around a series of operations."""
|
72
|
+
session: Session = self.session()
|
73
|
+
try:
|
74
|
+
yield session
|
75
|
+
session.commit()
|
76
|
+
except Exception:
|
77
|
+
session.rollback()
|
78
|
+
raise
|
79
|
+
|
80
|
+
def get_session(self) -> Session:
|
81
|
+
"""Get a new session."""
|
82
|
+
return self.session()
|
83
|
+
|
84
|
+
def close_session(self):
|
85
|
+
"""Close the session."""
|
86
|
+
self.session.remove()
|
87
|
+
|
88
|
+
def create_tables(self):
|
89
|
+
"""Create all tables defined by Base"""
|
90
|
+
self.metadata.create_all(self.engine)
|
91
|
+
|
92
|
+
def close_all(self):
|
93
|
+
"""Close all sessions and connections."""
|
94
|
+
self.session.close()
|
95
|
+
self.engine.dispose()
|
96
|
+
|
97
|
+
|
98
|
+
class SingletonDB(DatabaseManager, SingletonBase):
|
99
|
+
"""Singleton class for DatabaseManager, uses SingletonBase to inject singleton pattern."""
|
100
|
+
|
101
|
+
...
|
102
|
+
|
103
|
+
|
104
|
+
__all__ = ["DatabaseManager", "SingletonDB"]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from .events_class import Events
|
2
|
+
from .events_module import clear_all, clear_handlers_for_event, dispatch_event, event_handler, set_handler
|
3
|
+
|
4
|
+
subscribe = event_handler
|
5
|
+
publish = dispatch_event
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"Events",
|
9
|
+
"clear_all",
|
10
|
+
"clear_handlers_for_event",
|
11
|
+
"dispatch_event",
|
12
|
+
"event_handler",
|
13
|
+
"set_handler",
|
14
|
+
"subscribe",
|
15
|
+
"publish",
|
16
|
+
]
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from .events_module import clear_all as _clear_all
|
5
|
+
from .events_module import clear_handlers_for_event as _clear_handlers_for_event
|
6
|
+
from .events_module import dispatch_event as _dispatch_event
|
7
|
+
from .events_module import event_handler as _event_handler
|
8
|
+
from .events_module import set_handler as _set_handler
|
9
|
+
|
10
|
+
Callback = Callable[..., Any]
|
11
|
+
|
12
|
+
|
13
|
+
class Events:
|
14
|
+
"""Simple wrapper exposing :mod:`events_module` functionality as methods."""
|
15
|
+
|
16
|
+
# Method names mirror functions from ``events_module`` for familiarity
|
17
|
+
|
18
|
+
def event_handler(self, event_name: str, func: Callback | None = None):
|
19
|
+
"""Register ``func`` as a handler for ``event_name``.
|
20
|
+
|
21
|
+
Can be used as a decorator when ``func`` is omitted."""
|
22
|
+
|
23
|
+
if func is None:
|
24
|
+
return _event_handler(event_name)
|
25
|
+
_set_handler(event_name, func)
|
26
|
+
return func
|
27
|
+
|
28
|
+
def dispatch_event(self, event_name: str, *args, **kwargs) -> Any | None:
|
29
|
+
"""Dispatch ``event_name`` to all subscribed handlers."""
|
30
|
+
|
31
|
+
return _dispatch_event(event_name, *args, **kwargs)
|
32
|
+
|
33
|
+
def set_handler(self, event_name: str, func: Callback) -> None:
|
34
|
+
"""Register ``func`` as a handler for ``event_name``."""
|
35
|
+
|
36
|
+
_set_handler(event_name, func)
|
37
|
+
|
38
|
+
def clear_handlers_for_event(self, event_name: str) -> None:
|
39
|
+
"""Remove all handlers associated with ``event_name``."""
|
40
|
+
|
41
|
+
_clear_handlers_for_event(event_name)
|
42
|
+
|
43
|
+
def clear_all(self) -> None:
|
44
|
+
"""Remove all registered event handlers."""
|
45
|
+
|
46
|
+
_clear_all()
|
47
|
+
|
48
|
+
subscribe = event_handler
|
49
|
+
publish = dispatch_event
|
50
|
+
|
51
|
+
|
52
|
+
__all__ = ["Events"]
|