bear-utils 0.8.12__py3-none-any.whl → 0.8.14__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/config/settings_manager.py +28 -6
- bear_utils/logger_manager/_common.py +21 -22
- bear_utils/logger_manager/_log_level.py +126 -0
- bear_utils/logger_manager/_styles.py +10 -5
- bear_utils/logger_manager/loggers/_level_sin.py +1 -0
- bear_utils/logger_manager/loggers/basic_logger/__init__.py +5 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.py +80 -0
- bear_utils/logger_manager/loggers/basic_logger/logger.pyi +19 -0
- bear_utils/logger_manager/loggers/fastapi_logger.py +66 -37
- bear_utils/logger_manager/loggers/simple_logger.py +34 -17
- {bear_utils-0.8.12.dist-info → bear_utils-0.8.14.dist-info}/METADATA +2 -2
- {bear_utils-0.8.12.dist-info → bear_utils-0.8.14.dist-info}/RECORD +13 -9
- {bear_utils-0.8.12.dist-info → bear_utils-0.8.14.dist-info}/WHEEL +0 -0
@@ -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
|
-
|
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
|
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 =
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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 =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
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
|
-
|
7
|
-
|
8
|
-
|
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):
|
@@ -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
|
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.
|
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 =
|
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
|
-
|
64
|
-
if
|
65
|
-
|
66
|
-
log_entry
|
67
|
-
|
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.
|
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 =
|
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,
|
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
|
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":
|
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(
|
148
|
+
self._fallback_log(lvl, msg, *args, **kwargs)
|
120
149
|
except Exception:
|
121
|
-
self._fallback_log(
|
150
|
+
self._fallback_log(lvl, msg, *args, **kwargs)
|
122
151
|
|
123
|
-
def _fallback_log(self,
|
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}] {
|
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.
|
184
|
+
await self.client.aclose()
|
156
185
|
|
157
186
|
|
158
187
|
if __name__ == "__main__":
|
@@ -3,50 +3,67 @@
|
|
3
3
|
import sys
|
4
4
|
from typing import TextIO
|
5
5
|
|
6
|
-
from bear_utils.logger_manager.
|
6
|
+
from bear_utils.logger_manager._log_level import LogLevel, log_levels
|
7
7
|
from bear_utils.time import EpochTimestamp
|
8
8
|
|
9
9
|
STDOUT: TextIO = sys.stdout
|
10
10
|
STDERR: TextIO = sys.stderr
|
11
11
|
|
12
|
+
VERBOSE: LogLevel = log_levels.get("VERBOSE")
|
13
|
+
DEBUG: LogLevel = log_levels.get("DEBUG")
|
14
|
+
INFO: LogLevel = log_levels.get("INFO")
|
15
|
+
WARNING: LogLevel = log_levels.get("WARNING")
|
16
|
+
ERROR: LogLevel = log_levels.get("ERROR")
|
17
|
+
|
12
18
|
|
13
19
|
class SimpleLogger:
|
14
20
|
"""A simple logger that writes messages to stderr (or STDOUT if preferred) with a timestamp."""
|
15
21
|
|
16
|
-
def __init__(self, min_level: LogLevel = INFO, redirect: TextIO = STDERR) -> None:
|
22
|
+
def __init__(self, min_level: int | str | LogLevel = INFO, redirect: TextIO = STDERR) -> None:
|
17
23
|
"""Initialize the logger with a minimum log level."""
|
18
|
-
self.min_level: LogLevel = min_level
|
24
|
+
self.min_level: LogLevel = log_levels.get(min_level)
|
19
25
|
self.redirect: TextIO = redirect
|
26
|
+
self.buffer: list[str] = []
|
27
|
+
|
28
|
+
def _log(self, level: LogLevel, msg: object, end: str = "\n", *args, **kwargs) -> None:
|
29
|
+
timestamp: str = EpochTimestamp.now().to_string()
|
30
|
+
try:
|
31
|
+
self.buffer.append(f"[{timestamp}] {level.value}: {msg}")
|
32
|
+
if args:
|
33
|
+
self.buffer.append(" ".join(str(arg) for arg in args))
|
34
|
+
if kwargs:
|
35
|
+
for key, value in kwargs.items():
|
36
|
+
self.buffer.append(f"{key}={value}")
|
37
|
+
print(f"{end}".join(self.buffer), file=self.redirect)
|
38
|
+
except Exception as e:
|
39
|
+
print(f"[{timestamp}] {level.value}: {msg} - Error: {e}", file=self.redirect)
|
40
|
+
finally:
|
41
|
+
self.buffer.clear()
|
20
42
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
if args:
|
26
|
-
print(" ".join(str(arg) for arg in args), file=self.redirect)
|
27
|
-
if kwargs:
|
28
|
-
for key, value in kwargs.items():
|
29
|
-
print(f"{key}={value}", file=self.redirect)
|
43
|
+
def log(self, level: LogLevel, msg: object, *args, **kwargs) -> None:
|
44
|
+
"""Log a message at the specified level."""
|
45
|
+
if level.value >= self.min_level.value:
|
46
|
+
self._log(level, msg, *args, **kwargs)
|
30
47
|
|
31
48
|
def verbose(self, msg: object, *args, **kwargs) -> None:
|
32
49
|
"""Alias for debug level logging."""
|
33
|
-
self.
|
50
|
+
self.log(VERBOSE, msg, *args, **kwargs)
|
34
51
|
|
35
52
|
def debug(self, msg: object, *args, **kwargs) -> None:
|
36
53
|
"""Log a debug message."""
|
37
|
-
self.
|
54
|
+
self.log(DEBUG, msg, *args, **kwargs)
|
38
55
|
|
39
56
|
def info(self, msg: object, *args, **kwargs) -> None:
|
40
57
|
"""Log an info message."""
|
41
|
-
self.
|
58
|
+
self.log(INFO, msg, *args, **kwargs)
|
42
59
|
|
43
60
|
def warning(self, msg: object, *args, **kwargs) -> None:
|
44
61
|
"""Log a warning message."""
|
45
|
-
self.
|
62
|
+
self.log(WARNING, msg, *args, **kwargs)
|
46
63
|
|
47
64
|
def error(self, msg: object, *args, **kwargs) -> None:
|
48
65
|
"""Log an error message."""
|
49
|
-
self.
|
66
|
+
self.log(ERROR, msg, *args, **kwargs)
|
50
67
|
|
51
68
|
|
52
69
|
# Example usage:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bear-utils
|
3
|
-
Version: 0.8.
|
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.
|
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
|
|
@@ -20,7 +20,7 @@ bear_utils/cli/shell/_common.py,sha256=_KQyL5lvqOfjonFIwlEOyp3K9G3TSOj19RhgVzfNN
|
|
20
20
|
bear_utils/config/__init__.py,sha256=HC_lWpmLF0kbPr5i1Wa2FLER2b446E_GecgU9EPmc04,353
|
21
21
|
bear_utils/config/config_manager.py,sha256=Xj0xOmY-wo_rwfcWiXyxNZWX9NknX_Jm9W56Gx8yyHQ,8244
|
22
22
|
bear_utils/config/dir_manager.py,sha256=slIy1oRr7VIPdsiwN66-xQiuSvgqm_j6d1IrKhxRsSk,2028
|
23
|
-
bear_utils/config/settings_manager.py,sha256=
|
23
|
+
bear_utils/config/settings_manager.py,sha256=0hvqLzSiA8f9UVN45UY2-SIHMmOsg1eDctbg293qyyU,5978
|
24
24
|
bear_utils/constants/__init__.py,sha256=vmr1_grp658VvYi1Pk_5Q7_Qab0fUIbWxJ5BrsAuEFU,939
|
25
25
|
bear_utils/constants/_exceptions.py,sha256=gnAGTmuD9NYpJakeLrYHAyPrAQPHDNahY_rS42Ct39k,251
|
26
26
|
bear_utils/constants/_lazy_typing.py,sha256=WfuWpRqx9XchvuyPWg3tVjMC5-C4QA-Bhwfskf4YmAE,339
|
@@ -62,26 +62,30 @@ bear_utils/gui/gui_tools/qt_color_picker.py,sha256=5NtLiBHk5r9Goma_oiymriH49D_JG
|
|
62
62
|
bear_utils/gui/gui_tools/qt_file_handler.py,sha256=FgWdS-9WnjVuyGIC8V30ByDCBeJGZKGc8KRTy34SFfI,4404
|
63
63
|
bear_utils/gui/gui_tools/qt_input_dialog.py,sha256=5KaCM9q8kmoy-Fd0j1FbXIVrLlE7W47NEGdhsWtvKwQ,9281
|
64
64
|
bear_utils/logger_manager/__init__.py,sha256=ei8686gFb8vBJ75LTbYizAnJmEIb1zEmowWQkRqfahY,3453
|
65
|
-
bear_utils/logger_manager/_common.py,sha256=
|
65
|
+
bear_utils/logger_manager/_common.py,sha256=kPTE4e0FKI8IBW1B5X7jJPVJtqgJtKYsX8gy8obg9OQ,1866
|
66
66
|
bear_utils/logger_manager/_console_junk.py,sha256=2fwiYjZZps3GrH5An7aU3Bgvb_aAJiqNzTnKaku6m-0,4916
|
67
|
-
bear_utils/logger_manager/
|
67
|
+
bear_utils/logger_manager/_log_level.py,sha256=DBTOQdETazx9jK5rrvsjlEr96ciuB0A1q5GjzKp4UFA,4200
|
68
|
+
bear_utils/logger_manager/_styles.py,sha256=1mYHnENhLUWHB2QSpT9CB3_dzgBjhBxM1sh5UGEUULU,2527
|
68
69
|
bear_utils/logger_manager/logger_protocol.py,sha256=v6rGMcyUGmt08dlAKUrWWwqqogtzAeaE4SXmt76Vcf0,629
|
69
70
|
bear_utils/logger_manager/loggers/__init__.py,sha256=ashcnkvQIUQDLbUtU6QILkMjP_fMaeHAN1w7pHLWqQk,67
|
70
|
-
bear_utils/logger_manager/loggers/_level_sin.py,sha256=
|
71
|
+
bear_utils/logger_manager/loggers/_level_sin.py,sha256=xykZNZKeHw9UH2ng3UAWgwgzPzA5JIkfkXJQGatsXcI,1868
|
71
72
|
bear_utils/logger_manager/loggers/base_logger.py,sha256=VNb_zj450qdvZ6oVsdPxQ7nwoAW9uOf2Jaaiw0msoVQ,9981
|
72
73
|
bear_utils/logger_manager/loggers/base_logger.pyi,sha256=ApSDj_fH12tvwNQcyZSTtWEWwbOP4FmkJUEZ1ih2qWk,2666
|
73
74
|
bear_utils/logger_manager/loggers/buffer_logger.py,sha256=wF8s4jyajbfandkb3ZcTdMGjEjefn8H9Jxn4b74VYDg,1700
|
74
75
|
bear_utils/logger_manager/loggers/console_logger.py,sha256=wysIThH_sn4QpQQmOTFCFhTwiom0pNGOZp_iXaEJcxo,10082
|
75
76
|
bear_utils/logger_manager/loggers/console_logger.pyi,sha256=SCEn_TCNVM7GyxL0ObjW7PCWcgsiUsXbktrRqts2qLM,1873
|
76
|
-
bear_utils/logger_manager/loggers/fastapi_logger.py,sha256=
|
77
|
+
bear_utils/logger_manager/loggers/fastapi_logger.py,sha256=n8puzrVIjIpzbv6If1SQVm41rADzMONML5p90chE3SY,7377
|
77
78
|
bear_utils/logger_manager/loggers/file_logger.py,sha256=gZEAybd2Yjhh1Zvetl4doaKv8EKW7_hc5yImlS8M9eA,4943
|
78
|
-
bear_utils/logger_manager/loggers/simple_logger.py,sha256=
|
79
|
+
bear_utils/logger_manager/loggers/simple_logger.py,sha256=SbV2S7KuMsPbIn0K45BwigPH7S9Db8uo3EPPNhObV0c,2888
|
79
80
|
bear_utils/logger_manager/loggers/sub_logger.py,sha256=5C_8b7juSMbqMIvmWaoC1JklL4pDETfqkMtvpQyO9X4,3501
|
80
81
|
bear_utils/logger_manager/loggers/sub_logger.pyi,sha256=yFTRo99ztzsCaRQ5Oz0bPAPwB_dtvmvR2Al7hIs_doY,1001
|
82
|
+
bear_utils/logger_manager/loggers/basic_logger/__init__.py,sha256=pEUkCpmt79zUVEE8P_OW55LdSu1hXLtpv6Lo2KsbIVo,144
|
83
|
+
bear_utils/logger_manager/loggers/basic_logger/logger.py,sha256=Wytf3QIxZjgCBCJuB3bLqyqQ9ONU7O4WzEHP6DNJT9E,2609
|
84
|
+
bear_utils/logger_manager/loggers/basic_logger/logger.pyi,sha256=VN4DcqegS6RTUn66TwI1QKq2TSuRLDz1gf3ttpjkOPo,948
|
81
85
|
bear_utils/monitoring/__init__.py,sha256=9DKNIWTp_voLnaWgiP-wJ-o_N0hYixo-MzjUmg8RUvI,240
|
82
86
|
bear_utils/monitoring/_common.py,sha256=LYQFxgTP9fk0cH71IQTuGwBYYPWCqHP_mMRNecoD76M,657
|
83
87
|
bear_utils/monitoring/host_monitor.py,sha256=e0TYRJw9iDj5Ga6y3ck1TBFEeH42Cax5mQYaNU8yams,13241
|
84
88
|
bear_utils/time/__init__.py,sha256=VctjJG17SyEHAFXytI1sZrOrq7zm3hVenIDOJFdaMN0,1424
|
85
|
-
bear_utils-0.8.
|
86
|
-
bear_utils-0.8.
|
87
|
-
bear_utils-0.8.
|
89
|
+
bear_utils-0.8.14.dist-info/METADATA,sha256=YpwGNeJKju3r-1LtWE15HMHG6P_NFcY8_sqiiEiZy3o,8699
|
90
|
+
bear_utils-0.8.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
91
|
+
bear_utils-0.8.14.dist-info/RECORD,,
|
File without changes
|