bear-utils 0.8.12__tar.gz → 0.8.14__tar.gz

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 (119) hide show
  1. {bear_utils-0.8.12 → bear_utils-0.8.14}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.12 → bear_utils-0.8.14}/PKG-INFO +2 -2
  3. {bear_utils-0.8.12 → bear_utils-0.8.14}/README.md +1 -1
  4. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/pytest.ini +7 -0
  5. {bear_utils-0.8.12 → bear_utils-0.8.14}/pyproject.toml +7 -1
  6. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/config/settings_manager.py +28 -6
  7. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/_common.py +21 -22
  8. bear_utils-0.8.14/src/bear_utils/logger_manager/_log_level.py +126 -0
  9. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/_styles.py +10 -5
  10. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/_level_sin.py +1 -0
  11. bear_utils-0.8.14/src/bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
  12. bear_utils-0.8.14/src/bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
  13. bear_utils-0.8.14/src/bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
  14. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/fastapi_logger.py +66 -37
  15. bear_utils-0.8.14/src/bear_utils/logger_manager/loggers/simple_logger.py +77 -0
  16. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_database_manager.py +1 -16
  17. bear_utils-0.8.12/src/bear_utils/logger_manager/loggers/simple_logger.py +0 -60
  18. {bear_utils-0.8.12 → bear_utils-0.8.14}/.gitignore +0 -0
  19. {bear_utils-0.8.12 → bear_utils-0.8.14}/.python-version +0 -0
  20. {bear_utils-0.8.12 → bear_utils-0.8.14}/AGENTS.md +0 -0
  21. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/coverage.ini +0 -0
  22. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/default.toml +0 -0
  23. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/git-changelog.toml +0 -0
  24. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/ruff.toml +0 -0
  25. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/vscode/launch.json +0 -0
  26. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/vscode/settings.json +0 -0
  27. {bear_utils-0.8.12 → bear_utils-0.8.14}/config/vscode/tasks.json +0 -0
  28. {bear_utils-0.8.12 → bear_utils-0.8.14}/directory_structure.txt +0 -0
  29. {bear_utils-0.8.12 → bear_utils-0.8.14}/directory_structure.xml +0 -0
  30. {bear_utils-0.8.12 → bear_utils-0.8.14}/maskfile.md +0 -0
  31. {bear_utils-0.8.12 → bear_utils-0.8.14}/noxfile.py +0 -0
  32. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/__init__.py +0 -0
  33. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/__main__.py +0 -0
  34. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/_internal/__init__.py +0 -0
  35. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/_internal/cli.py +0 -0
  36. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/_internal/debug.py +0 -0
  37. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/__init__.py +0 -0
  38. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  39. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  40. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  41. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  42. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  43. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cache/__init__.py +0 -0
  44. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/__init__.py +0 -0
  45. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/commands.py +0 -0
  46. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/prompt_helpers.py +0 -0
  47. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/shell/__init__.py +0 -0
  48. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/shell/_base_command.py +0 -0
  49. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/shell/_base_shell.py +0 -0
  50. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/cli/shell/_common.py +0 -0
  51. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/config/__init__.py +0 -0
  52. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/config/config_manager.py +0 -0
  53. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/config/dir_manager.py +0 -0
  54. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/__init__.py +0 -0
  55. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/_exceptions.py +0 -0
  56. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/_lazy_typing.py +0 -0
  57. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/date_related.py +0 -0
  58. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/server.py +0 -0
  59. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/constants/time_related.py +0 -0
  60. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/database/__init__.py +0 -0
  61. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/database/_db_manager.py +0 -0
  62. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/events/__init__.py +0 -0
  63. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/events/events_class.py +0 -0
  64. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/events/events_module.py +0 -0
  65. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/__init__.py +0 -0
  66. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/_async_helpers.py +0 -0
  67. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/_tools.py +0 -0
  68. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/platform_utils.py +0 -0
  69. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/responses/__init__.py +0 -0
  70. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/responses/function_response.py +0 -0
  71. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  72. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  73. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/__init__.py +0 -0
  74. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  75. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  76. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  77. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  78. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  79. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  80. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  81. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  82. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/files/ignore_parser.py +0 -0
  83. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/graphics/__init__.py +0 -0
  84. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/graphics/bear_gradient.py +0 -0
  85. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/graphics/image_helpers.py +0 -0
  86. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/__init__.py +0 -0
  87. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  88. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  89. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  90. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/qt_app.py +0 -0
  91. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  92. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  93. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  94. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/__init__.py +0 -0
  95. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  96. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/logger_protocol.py +0 -0
  97. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  98. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/base_logger.py +0 -0
  99. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/base_logger.pyi +0 -0
  100. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/buffer_logger.py +0 -0
  101. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/console_logger.py +0 -0
  102. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/console_logger.pyi +0 -0
  103. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/file_logger.py +0 -0
  104. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/sub_logger.py +0 -0
  105. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/logger_manager/loggers/sub_logger.pyi +0 -0
  106. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/monitoring/__init__.py +0 -0
  107. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/monitoring/_common.py +0 -0
  108. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/monitoring/host_monitor.py +0 -0
  109. {bear_utils-0.8.12 → bear_utils-0.8.14}/src/bear_utils/time/__init__.py +0 -0
  110. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/__init__.py +0 -0
  111. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_add_ord_suffix.py +0 -0
  112. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_clipboard.py +0 -0
  113. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_default_shell.py +0 -0
  114. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_function_response.py +0 -0
  115. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_gradient.py +0 -0
  116. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_logger.py +0 -0
  117. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_platform_utils.py +0 -0
  118. {bear_utils-0.8.12 → bear_utils-0.8.14}/tests/test_prompt_helpers.py +0 -0
  119. {bear_utils-0.8.12 → bear_utils-0.8.14}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.12
2
+ current_version = 0.8.14
3
3
 
4
4
  [bumpversion:file:pyproject.toml]
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bear-utils
3
- Version: 0.8.12
3
+ Version: 0.8.14
4
4
  Summary: Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things.
5
5
  Author-email: chaz <bright.lid5647@fastmail.com>
6
6
  Requires-Python: >=3.12
@@ -22,7 +22,7 @@ Requires-Dist: toml>=0.10.2
22
22
  Requires-Dist: uvicorn>=0.35.0
23
23
  Description-Content-Type: text/markdown
24
24
 
25
- # Bear Utils v# Bear Utils v0.8.12
25
+ # Bear Utils v# Bear Utils v0.8.14
26
26
 
27
27
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
28
28
 
@@ -1,4 +1,4 @@
1
- # Bear Utils v# Bear Utils v0.8.12
1
+ # Bear Utils v# Bear Utils v0.8.14
2
2
 
3
3
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
4
4
 
@@ -6,6 +6,13 @@ addopts =
6
6
  --cov-config config/coverage.ini
7
7
  testpaths =
8
8
  tests
9
+ filterwarnings =
10
+ ignore::DeprecationWarning
11
+ ignore::PendingDeprecationWarning
12
+ ignore::FutureWarning
13
+ ignore::ImportWarning
14
+ ignore::ResourceWarning
15
+ ignore::UserWarning
9
16
 
10
17
  # action:message_regex:warning_class:module_regex:line
11
18
  filterwarnings =
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bear-utils"
3
- version = "0.8.12"
3
+ version = "0.8.14"
4
4
  description = "Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things."
5
5
  authors = [{ name = "chaz", email = "bright.lid5647@fastmail.com" }]
6
6
  readme = "README.md"
@@ -61,6 +61,12 @@ addopts = "-v"
61
61
  markers = [
62
62
  "visual: marks tests as visual verification tests (deselect with '-m \"not visual\"')"
63
63
  ]
64
+ filterwarnings = [
65
+ "ignore::PendingDeprecationWarning",
66
+ "ignore::ImportWarning",
67
+ "ignore::ResourceWarning",
68
+ "ignore::UserWarning",
69
+ ]
64
70
 
65
71
  [tool.pyright]
66
72
  include = ["src"]
@@ -3,33 +3,44 @@
3
3
  import atexit
4
4
  from collections.abc import Generator
5
5
  from contextlib import contextmanager
6
+ import hashlib
6
7
  from pathlib import Path
7
8
  from typing import Any, Self
8
9
 
9
10
  from tinydb import Query, TinyDB
10
11
 
12
+ DEFAULT_PATH: Path = Path.home() / ".config" / "bear_utils"
11
13
 
12
- def get_config_folder() -> Path:
14
+
15
+ def get_config_folder(path: str | Path | None = None) -> Path:
13
16
  """Get the path to the bear configuration directory."""
14
- config_path: Path = Path.home() / ".bear_utils"
17
+ config_path: Path = Path(path) if isinstance(path, str) else path or DEFAULT_PATH
15
18
  config_path.mkdir(parents=True, exist_ok=True)
16
19
  return config_path
17
20
 
18
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
+
19
31
  class SettingsManager:
20
32
  """A class to manage settings using TinyDB and an in-memory cache."""
21
33
 
22
- __slots__ = ("cache", "db", "file_path", "settings_name")
34
+ __slots__ = ("cache", "db", "file_hash", "file_path", "settings_name")
23
35
 
24
36
  def __init__(self, settings_name: str, folder_path: str | Path | None = None) -> None:
25
37
  """Initialize the SettingsManager with a specific settings name."""
26
38
  self.settings_name: str = settings_name
27
39
  self.cache: dict[str, Any] = {}
28
40
  file_name: str = f"{settings_name}.json"
29
- self.file_path: Path = Path(folder_path) / file_name if folder_path else get_config_folder() / file_name
30
-
41
+ self.file_path: Path = get_config_folder(folder_path) / file_name
31
42
  self.db: TinyDB = TinyDB(self.file_path, indent=4, ensure_ascii=False)
32
-
43
+ self.file_hash: str = get_file_hash(self.file_path) if self.file_path.exists() else ""
33
44
  atexit.register(self.close)
34
45
  self._load_cache()
35
46
 
@@ -48,8 +59,19 @@ class SettingsManager:
48
59
  return
49
60
  self.set(key=key, value=value)
50
61
 
62
+ def invalidate_cache(self) -> None:
63
+ """Invalidate the in-memory cache."""
64
+ self.cache.clear()
65
+ self._load_cache()
66
+
51
67
  def get(self, key: str, default: Any = None) -> Any:
52
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
+
53
75
  if key in self.cache:
54
76
  return self.cache[key]
55
77
  if result := self.db.search(Query().key == key):
@@ -1,7 +1,6 @@
1
- from enum import Enum
2
1
  import inspect
3
2
  from types import TracebackType
4
- from typing import Required, TypedDict
3
+ from typing import Literal, Required, TypedDict
5
4
 
6
5
 
7
6
  class ExecValues(TypedDict, total=True):
@@ -10,12 +9,6 @@ class ExecValues(TypedDict, total=True):
10
9
  exc_traceback: Required[TracebackType]
11
10
 
12
11
 
13
- VERBOSE_FORMAT = "%(asctime)s |%(levelname)s| {%(module)s|%(funcName)s|%(lineno)d} %(message)s"
14
- VERBOSE_CONSOLE_FORMAT = "%(asctime)s <[{}]{}[/{}]> %(message)s"
15
- SIMPLE_FORMAT = "%(message)s"
16
- FIVE_MEGABYTES = 1024 * 1024 * 5
17
-
18
-
19
12
  class StackLevelTracker:
20
13
  STACK_LEVEL_OFFSET = 1
21
14
 
@@ -47,18 +40,24 @@ class StackLevelTracker:
47
40
  return self.end_depth - (self.start_depth + self.STACK_LEVEL_OFFSET)
48
41
 
49
42
 
50
- class LogLevel(Enum):
51
- """Enumeration for log levels."""
52
-
53
- VERBOSE = "VERBOSE"
54
- DEBUG = "DEBUG"
55
- INFO = "INFO"
56
- WARNING = "WARNING"
57
- ERROR = "ERROR"
58
-
43
+ VERBOSE_FORMAT = "%(asctime)s |%(levelname)s| {%(module)s|%(funcName)s|%(lineno)d} %(message)s"
44
+ VERBOSE_CONSOLE_FORMAT = "%(asctime)s <[{}]{}[/{}]> %(message)s"
45
+ SIMPLE_FORMAT = "%(message)s"
46
+ FIVE_MEGABYTES = 1024 * 1024 * 5
59
47
 
60
- VERBOSE = LogLevel.VERBOSE
61
- DEBUG = LogLevel.DEBUG
62
- INFO = LogLevel.INFO
63
- WARNING = LogLevel.WARNING
64
- ERROR = LogLevel.ERROR
48
+ VERBOSE: Literal[5] = 5
49
+ SUCCESS: Literal[15] = 15
50
+ FAILURE: Literal[45] = 45
51
+
52
+
53
+ __all__ = [
54
+ "FAILURE",
55
+ "FIVE_MEGABYTES",
56
+ "SIMPLE_FORMAT",
57
+ "SUCCESS",
58
+ "VERBOSE",
59
+ "VERBOSE_CONSOLE_FORMAT",
60
+ "VERBOSE_FORMAT",
61
+ "ExecValues",
62
+ "StackLevelTracker",
63
+ ]
@@ -0,0 +1,126 @@
1
+ from functools import cached_property
2
+ from typing import Any, Literal, overload
3
+
4
+ from pydantic import BaseModel, Field, field_validator
5
+
6
+ from bear_utils.extras.wrappers.add_methods import add_comparison_methods
7
+
8
+ FAILURE: Literal[45] = 45
9
+ ERROR: Literal[40] = 40
10
+ WARNING: Literal[30] = 30
11
+ WARN: Literal[30] = WARNING
12
+ INFO: Literal[20] = 20
13
+ SUCCESS: Literal[15] = 15
14
+ DEBUG: Literal[10] = 10
15
+ VERBOSE: Literal[5] = 5
16
+ NOTSET: Literal[0] = 0
17
+
18
+
19
+ level_to_name = {
20
+ FAILURE: "FAILURE",
21
+ ERROR: "ERROR",
22
+ WARNING: "WARNING",
23
+ INFO: "INFO",
24
+ SUCCESS: "SUCCESS",
25
+ DEBUG: "DEBUG",
26
+ VERBOSE: "VERBOSE",
27
+ NOTSET: "NOTSET",
28
+ }
29
+
30
+ name_to_level = {
31
+ "FAILURE": FAILURE,
32
+ "ERROR": ERROR,
33
+ "WARN": WARNING,
34
+ "WARNING": WARNING,
35
+ "INFO": INFO,
36
+ "SUCCESS": SUCCESS,
37
+ "DEBUG": DEBUG,
38
+ "VERBOSE": VERBOSE,
39
+ "NOTSET": NOTSET,
40
+ }
41
+
42
+
43
+ @add_comparison_methods("value")
44
+ class LogLevel(BaseModel):
45
+ """Model to represent a logging level."""
46
+
47
+ name: str = Field(default="NOTSET", description="Name of the logging level")
48
+ value: int = Field(default=NOTSET, description="Numeric value of the logging level")
49
+
50
+ @field_validator("value")
51
+ @classmethod
52
+ def validate_value(cls, value: int) -> int:
53
+ if value not in level_to_name:
54
+ raise ValueError(f"Invalid logging level value: {value!r}. Valid values are: {list(level_to_name.keys())}")
55
+ return value
56
+
57
+ @field_validator("name")
58
+ @classmethod
59
+ def validate_name(cls, name: str) -> str:
60
+ if name not in name_to_level:
61
+ raise ValueError(f"Invalid logging level name: {name!r}. Valid names are: {list(name_to_level.keys())}")
62
+ return name
63
+
64
+
65
+ class LogLevels(BaseModel):
66
+ """Model to represent a collection of logging levels."""
67
+
68
+ notset: LogLevel = Field(default=LogLevel(name="NOTSET", value=NOTSET))
69
+ verbose: LogLevel = Field(default=LogLevel(name="VERBOSE", value=VERBOSE))
70
+ debug: LogLevel = Field(default=LogLevel(name="DEBUG", value=DEBUG))
71
+ info: LogLevel = Field(default=LogLevel(name="INFO", value=INFO))
72
+ success: LogLevel = Field(default=LogLevel(name="SUCCESS", value=SUCCESS))
73
+ warning: LogLevel = Field(default=LogLevel(name="WARNING", value=WARNING))
74
+ error: LogLevel = Field(default=LogLevel(name="ERROR", value=ERROR))
75
+ failure: LogLevel = Field(default=LogLevel(name="FAILURE", value=FAILURE))
76
+
77
+ model_config = {"arbitrary_types_allowed": True, "extra": "forbid"}
78
+
79
+ @cached_property
80
+ def keys(self) -> list[str]:
81
+ """Get the names of all logging levels."""
82
+ return [key.upper() for key in LogLevels.model_fields]
83
+
84
+ @cached_property
85
+ def levels(self): # noqa: ANN202
86
+ item_dict: dict[str, Any] = {
87
+ level_name.lower(): getattr(self, level_name.lower()).value for level_name in self.keys
88
+ }
89
+ return item_dict.items()
90
+
91
+ def get_int(self, name: str) -> int:
92
+ """Get the integer value of a logging level by name."""
93
+ if not hasattr(self, name):
94
+ raise ValueError(f"Invalid logging level name: {name!r}. Valid names are: {self.keys}")
95
+ return getattr(self, name).value
96
+
97
+ @overload
98
+ def get(self, v: LogLevel) -> LogLevel: ...
99
+
100
+ @overload
101
+ def get(self, v: str) -> LogLevel: ...
102
+
103
+ @overload
104
+ def get(self, v: int) -> LogLevel: ...
105
+
106
+ def get(self, v: int | str | LogLevel) -> LogLevel:
107
+ """Get a logging level by name or value."""
108
+ if isinstance(v, LogLevel):
109
+ return v
110
+ if isinstance(v, str) and v.lower() in self.keys:
111
+ return getattr(self, v.lower())
112
+ if isinstance(v, int):
113
+ for level_name, level_value in self.levels:
114
+ if level_value == v:
115
+ return getattr(self, level_name.lower())
116
+ return self.notset # Default to NOTSET if no match found
117
+
118
+ def get_name(self, value: int) -> str:
119
+ """Get the name of a logging level by its integer value."""
120
+ for level_name, level_value in self.levels:
121
+ if level_value == value:
122
+ return level_name
123
+ raise ValueError(f"Invalid logging level value: {value!r}. Valid values are: {self.keys}")
124
+
125
+
126
+ log_levels = LogLevels()
@@ -1,11 +1,16 @@
1
- from logging import DEBUG, ERROR, INFO, WARNING
2
- from typing import Literal, NotRequired, Required, TypedDict
1
+ from typing import NotRequired, Required, TypedDict
3
2
 
4
3
  from rich.theme import Theme
5
4
 
6
- VERBOSE: Literal[5] = 5
7
- SUCCESS: Literal[15] = 15
8
- FAILURE: Literal[45] = 45
5
+ from bear_utils.logger_manager._log_level import (
6
+ DEBUG,
7
+ ERROR,
8
+ FAILURE,
9
+ INFO,
10
+ SUCCESS,
11
+ VERBOSE,
12
+ WARNING,
13
+ )
9
14
 
10
15
 
11
16
  class LoggerExtraInfo(TypedDict):
@@ -18,6 +18,7 @@ level_to_name = {
18
18
  DEBUG: "DEBUG",
19
19
  NOTSET: "NOTSET",
20
20
  }
21
+
21
22
  name_to_level = {
22
23
  "ERROR": ERROR,
23
24
  "WARN": WARNING,
@@ -0,0 +1,5 @@
1
+ """A basic logger to have around just in case the user doesn't already have one."""
2
+
3
+ from .logger import BasicLogger
4
+
5
+ __all__ = ["BasicLogger"]
@@ -0,0 +1,80 @@
1
+ """Basic logger using the Rich library."""
2
+
3
+ from collections.abc import Callable
4
+
5
+ from rich import inspect
6
+ from rich.console import Console
7
+ from rich.theme import Theme
8
+
9
+ THEME: dict[str, str] = {
10
+ "info": "dim green",
11
+ "debug": "bold blue",
12
+ "warning": "bold yellow",
13
+ "error": "bold red",
14
+ "exception": "bold red",
15
+ "success": "bold green",
16
+ "failure": "bold red underline",
17
+ "verbose": "bold blue",
18
+ }
19
+
20
+
21
+ class BasicLogger:
22
+ """A basic logger that uses the Rich library to print messages to the console."""
23
+
24
+ def __init__(self) -> None:
25
+ """Initialize the BasicLogger with a Rich Console instance."""
26
+ self.console = Console(theme=Theme(THEME))
27
+ for level in THEME:
28
+ method = self.replacement_method(level)
29
+ setattr(self, level, method)
30
+
31
+ def replacement_method(self, level: str) -> Callable:
32
+ """Create a method that logs messages at the specified level."""
33
+
34
+ def method(msg: object, **kwargs) -> None:
35
+ """Log a message at the specified level with the given style.
36
+
37
+ Args:
38
+ msg (object): The message to log.
39
+ **kwargs: Additional keyword arguments for formatting.
40
+ """
41
+ self.log(level, msg, **kwargs)
42
+
43
+ return method
44
+
45
+ def log(self, level: str, msg: object, **kwargs) -> None:
46
+ """Log a message at the specified level.
47
+
48
+ Args:
49
+ level (str): The logging level (e.g., 'info', 'debug', 'warning', etc.).
50
+ msg (object): The message to log.
51
+ **kwargs: Additional keyword arguments for formatting.
52
+ """
53
+ self.console.print(msg, style=level, **kwargs)
54
+
55
+ def print(self, msg: object, **kwargs) -> None:
56
+ """Print a message to the console with the specified style.
57
+
58
+ Args:
59
+ msg (object): The message to print.
60
+ **kwargs: Additional keyword arguments for formatting.
61
+ """
62
+ self.console.print(msg, **kwargs)
63
+
64
+ def inspect(self, obj: object, **kwargs) -> None:
65
+ """Inspect an object and print its details to the console.
66
+
67
+ Args:
68
+ obj (object): The object to inspect.
69
+ **kwargs: Additional keyword arguments for formatting.
70
+ """
71
+ inspect(obj, console=self.console, **kwargs)
72
+
73
+ def print_json(self, data: object, **kwargs) -> None:
74
+ """Print a JSON object to the console.
75
+
76
+ Args:
77
+ data (object): The JSON data to print.
78
+ **kwargs: Additional keyword arguments for formatting.
79
+ """
80
+ self.console.print_json(data=data, **kwargs)
@@ -0,0 +1,19 @@
1
+ from collections.abc import Callable
2
+
3
+ from rich.console import Console
4
+
5
+ class BasicLogger:
6
+ def __init__(self, console: Console | None = None) -> None: ...
7
+ def info(self, msg: object, **kwargs) -> None: ...
8
+ def debug(self, msg: object, **kwargs) -> None: ...
9
+ def warning(self, msg: object, **kwargs) -> None: ...
10
+ def error(self, msg: object, **kwargs) -> None: ...
11
+ def exception(self, msg: object, **kwargs) -> None: ...
12
+ def success(self, msg: object, **kwargs) -> None: ...
13
+ def failure(self, msg: object, **kwargs) -> None: ...
14
+ def verbose(self, msg: object, **kwargs) -> None: ...
15
+ def print(self, msg: object, style: str | None = None, **kwargs) -> None: ...
16
+ def log(self, level: str, msg: object, **kwargs) -> None: ...
17
+ def inspect(self, obj: object, **kwargs) -> None: ...
18
+ def print_json(self, data: object, **kwargs) -> None: ...
19
+ def replacement_method(self, level: str) -> Callable: ...
@@ -6,23 +6,42 @@ import threading
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from fastapi import FastAPI
9
- from httpx import Client
9
+ from httpx import AsyncClient
10
10
  from pydantic import BaseModel
11
11
  import uvicorn
12
12
 
13
13
  from bear_utils.constants import SERVER_OK
14
- from bear_utils.logger_manager._common import DEBUG, ERROR, INFO, VERBOSE, WARNING, LogLevel
14
+ from bear_utils.logger_manager._log_level import LogLevel, log_levels
15
15
  from bear_utils.time import EpochTimestamp
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from httpx import Response
19
19
 
20
20
 
21
+ VERBOSE: LogLevel = log_levels.get("VERBOSE")
22
+ DEBUG: LogLevel = log_levels.get("DEBUG")
23
+ INFO: LogLevel = log_levels.get("INFO")
24
+ WARNING: LogLevel = log_levels.get("WARNING")
25
+ ERROR: LogLevel = log_levels.get("ERROR")
26
+
27
+
28
+ def get_level(level: str) -> int:
29
+ """Get the numeric value for a given level string."""
30
+ return log_levels.get_int(level)
31
+
32
+
33
+ def get_name(level: int) -> str:
34
+ """Get the name of a logging level by its integer value."""
35
+ return log_levels.get_name(level)
36
+
37
+
21
38
  class LogRequest(BaseModel):
22
39
  """Request model for logging messages."""
23
40
 
24
41
  level: str
25
42
  message: str
43
+ args: list[str] = []
44
+ kwargs: dict[str, str] = {}
26
45
 
27
46
 
28
47
  class LocalLoggingServer:
@@ -33,44 +52,51 @@ class LocalLoggingServer:
33
52
  host: str = "localhost",
34
53
  port: int = 8080,
35
54
  log_file: str = "server.log",
36
- min_level: LogLevel = VERBOSE,
55
+ min_level: LogLevel | int | str = DEBUG,
37
56
  ) -> None:
38
57
  """Initialize the logging server."""
39
58
  self.host: str = host
40
59
  self.port: int = port
41
60
  self.log_file: str = log_file
42
- self.min_level: LogLevel = min_level
61
+ self.min_level: LogLevel = log_levels.get(min_level)
43
62
  self.app = FastAPI()
44
63
  self.server_thread = None
45
64
  self._running = False
46
65
  self._setup_routes()
66
+ self.buffer: list[str] = []
47
67
 
48
68
  def _setup_routes(self) -> None:
49
69
  """Set up the FastAPI routes for logging and health check."""
50
70
 
51
71
  @self.app.post("/log")
52
72
  async def log_message(request: LogRequest) -> dict[str, str]:
53
- self.write_log(request.level, request.message)
73
+ self.write_log(request.level, request.message, *request.args, **request.kwargs)
54
74
  return {"status": "success"}
55
75
 
56
76
  @self.app.get("/health")
57
77
  async def health_check() -> dict[str, str]:
58
78
  return {"status": "healthy"}
59
79
 
60
- def write_log(self, level: str, message: str) -> None:
80
+ def write_log(self, level: str, message: str, *args: str, end: str = "\n", **kwargs: str) -> None:
61
81
  """Write a log entry to the file - same logic as original logger."""
82
+ timestamp: str = EpochTimestamp.now().to_string()
62
83
  try:
63
- log_level = LogLevel(level)
64
- if log_level.value >= self.min_level.value:
65
- timestamp = EpochTimestamp.now().to_string()
66
- log_entry = f"[{timestamp}] {level}: {message}\n"
67
- print(log_entry, file=sys.stderr)
84
+ level_t: LogLevel = log_levels.get(level)
85
+ if level_t.value >= self.min_level.value:
86
+ log_entry: str = f"[{timestamp}] {level}: {message}"
87
+ self.buffer.append(log_entry)
88
+ if args:
89
+ self.buffer.append(f"{end}".join(str(arg) for arg in args))
90
+ if kwargs:
91
+ for key, value in kwargs.items():
92
+ self.buffer.append(f"{key}={value}{end}")
68
93
  with open(self.log_file, "a", encoding="utf-8") as f:
69
- f.write(log_entry)
94
+ f.writelines(self.buffer)
95
+ print(f"{end}".join(self.buffer), file=sys.stderr)
70
96
  except Exception:
71
- # Fallback to stderr like original
72
- timestamp = EpochTimestamp.now().to_string()
73
97
  print(f"[{timestamp}] {level}: {message}", file=sys.stderr)
98
+ finally:
99
+ self.buffer.clear()
74
100
 
75
101
  def start(self) -> None:
76
102
  """Start the logging server in a separate thread."""
@@ -98,61 +124,64 @@ class LocalLoggingServer:
98
124
  class ServerLogger:
99
125
  """Logger that calls HTTP endpoints but behaves like SimpleLogger."""
100
126
 
101
- def __init__(self, server_url: str = "http://localhost:8080", min_level: LogLevel = INFO) -> None:
127
+ def __init__(self, server_url: str = "http://localhost:8080", min_level: LogLevel | int | str = INFO) -> None:
102
128
  """Initialize the ServerLogger."""
103
129
  self.server_url: str = server_url.rstrip("/")
104
- self.min_level: LogLevel = min_level
105
- self.client = Client(timeout=5.0)
130
+ self.min_level: LogLevel = log_levels.get(min_level)
131
+ self.client: AsyncClient = AsyncClient(timeout=5.0)
132
+ self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
106
133
 
107
- def _log(self, level: LogLevel, msg: object, *args, **kwargs) -> None:
134
+ async def _log(self, lvl: LogLevel, msg: object, *args, **kwargs) -> None:
108
135
  """Same interface as SimpleLogger._log but calls HTTP endpoint."""
109
- if isinstance(level, LogLevel) and level.value >= self.min_level.value:
136
+ if lvl.value >= self.min_level.value:
110
137
  try:
111
- response: Response = self.client.post(
138
+ response: Response = await self.client.post(
112
139
  url=f"{self.server_url}/log",
113
140
  json={
114
- "level": level.value,
141
+ "level": lvl.value,
115
142
  "message": msg,
143
+ "args": args,
144
+ "kwargs": kwargs,
116
145
  },
117
146
  )
118
147
  if response.status_code != SERVER_OK:
119
- self._fallback_log(level, msg, *args, **kwargs)
148
+ self._fallback_log(lvl, msg, *args, **kwargs)
120
149
  except Exception:
121
- self._fallback_log(level, msg, *args, **kwargs)
150
+ self._fallback_log(lvl, msg, *args, **kwargs)
122
151
 
123
- def _fallback_log(self, level: LogLevel, msg: object, *args, **kwargs) -> None:
152
+ def _fallback_log(self, lvl: LogLevel, msg: object, *args, **kwargs) -> None:
124
153
  """Fallback - same as original SimpleLogger._log."""
125
154
  timestamp: str = EpochTimestamp.now().to_string()
126
- print(f"[{timestamp}] {level.value}: {msg}", file=sys.stderr)
155
+ print(f"[{timestamp}] {lvl.value}: {msg}", file=sys.stderr)
127
156
  if args:
128
157
  print(" ".join(str(arg) for arg in args), file=sys.stderr)
129
158
  if kwargs:
130
159
  for key, value in kwargs.items():
131
160
  print(f"{key}={value}", file=sys.stderr)
132
161
 
133
- def verbose(self, msg: object, *args, **kwargs) -> None:
162
+ async def verbose(self, msg: object, *args, **kwargs) -> None:
134
163
  """Log a verbose message."""
135
- self._log(VERBOSE, msg, *args, **kwargs)
164
+ await self._log(VERBOSE, msg, *args, **kwargs)
136
165
 
137
- def debug(self, msg: object, *args, **kwargs) -> None:
166
+ async def debug(self, msg: object, *args, **kwargs) -> None:
138
167
  """Log a debug message."""
139
- self._log(DEBUG, msg, *args, **kwargs)
168
+ await self._log(DEBUG, msg, *args, **kwargs)
140
169
 
141
- def info(self, msg: object, *args, **kwargs) -> None:
170
+ async def info(self, msg: object, *args, **kwargs) -> None:
142
171
  """Log an info message."""
143
- self._log(INFO, msg, *args, **kwargs)
172
+ await self._log(INFO, msg, *args, **kwargs)
144
173
 
145
- def warning(self, msg: object, *args, **kwargs) -> None:
174
+ async def warning(self, msg: object, *args, **kwargs) -> None:
146
175
  """Log a warning message."""
147
- self._log(WARNING, msg, *args, **kwargs)
176
+ await self._log(WARNING, msg, *args, **kwargs)
148
177
 
149
- def error(self, msg: object, *args, **kwargs) -> None:
178
+ async def error(self, msg: object, *args, **kwargs) -> None:
150
179
  """Log an error message."""
151
- self._log(ERROR, msg, *args, **kwargs)
180
+ await self._log(ERROR, msg, *args, **kwargs)
152
181
 
153
- def close(self) -> None:
182
+ async def close(self) -> None:
154
183
  """Close the HTTP client."""
155
- self.client.close()
184
+ await self.client.aclose()
156
185
 
157
186
 
158
187
  if __name__ == "__main__":