bear-utils 0.8.26__py3-none-any.whl → 0.9.0__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.
@@ -3,12 +3,14 @@
3
3
  from typing import Any
4
4
 
5
5
  from bear_utils.logger_manager._common import VERBOSE_CONSOLE_FORMAT
6
- from bear_utils.logger_manager._styles import VERBOSE
6
+ from bear_utils.logger_manager._log_level import DEBUG, ERROR, FAILURE, INFO, SUCCESS, VERBOSE, WARNING, LogLevel
7
+ from bear_utils.logger_manager._styles import DEFAULT_THEME
7
8
  from bear_utils.logger_manager.logger_protocol import AsyncLoggerProtocol, LoggerProtocol
9
+ from bear_utils.logger_manager.loggers._console import LogConsole
8
10
  from bear_utils.logger_manager.loggers.base_logger import BaseLogger
9
11
  from bear_utils.logger_manager.loggers.buffer_logger import BufferLogger
10
12
  from bear_utils.logger_manager.loggers.console_logger import ConsoleLogger
11
- from bear_utils.logger_manager.loggers.fastapi_logger import ServerLogger
13
+ from bear_utils.logger_manager.loggers.fastapi_logger import LoggingClient, LoggingServer
12
14
  from bear_utils.logger_manager.loggers.file_logger import FileLogger
13
15
  from bear_utils.logger_manager.loggers.simple_logger import SimpleLogger
14
16
  from bear_utils.logger_manager.loggers.sub_logger import SubConsoleLogger
@@ -80,15 +82,25 @@ def get_sub_logger(
80
82
 
81
83
 
82
84
  __all__ = [
85
+ "DEBUG",
86
+ "DEFAULT_THEME",
87
+ "ERROR",
88
+ "FAILURE",
89
+ "INFO",
90
+ "SUCCESS",
83
91
  "VERBOSE",
84
92
  "VERBOSE_CONSOLE_FORMAT",
93
+ "WARNING",
85
94
  "AsyncLoggerProtocol",
86
95
  "BaseLogger",
87
96
  "BufferLogger",
88
97
  "ConsoleLogger",
89
98
  "FileLogger",
99
+ "LogConsole",
100
+ "LogLevel",
90
101
  "LoggerProtocol",
91
- "ServerLogger",
102
+ "LoggingClient",
103
+ "LoggingServer",
92
104
  "SimpleLogger",
93
105
  "SubConsoleLogger",
94
106
  "get_console",
@@ -1,9 +1,6 @@
1
- from functools import cached_property
2
- from typing import Any, Literal, overload
1
+ from typing import Literal
3
2
 
4
- from pydantic import BaseModel, Field, field_validator
5
-
6
- from bear_utils.extras.wrappers.add_methods import add_comparison_methods
3
+ from bear_utils.constants._meta import RichIntEnum, Value
7
4
 
8
5
  FAILURE: Literal[45] = 45
9
6
  ERROR: Literal[40] = 40
@@ -40,87 +37,14 @@ name_to_level = {
40
37
  }
41
38
 
42
39
 
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
-
40
+ class LogLevel(RichIntEnum):
41
+ """Enumeration for logging levels."""
125
42
 
126
- log_levels = LogLevels()
43
+ NOTSET = Value(NOTSET, "NOTSET")
44
+ VERBOSE = Value(VERBOSE, "VERBOSE")
45
+ DEBUG = Value(DEBUG, "DEBUG")
46
+ INFO = Value(INFO, "INFO")
47
+ WARNING = Value(WARNING, "WARNING")
48
+ ERROR = Value(ERROR, "ERROR")
49
+ FAILURE = Value(FAILURE, "FAILURE")
50
+ SUCCESS = Value(SUCCESS, "SUCCESS")
@@ -1,3 +1,4 @@
1
+ # ruff: noqa: D102
1
2
  """A protocol for logging classes for general use."""
2
3
 
3
4
  from typing import Protocol, runtime_checkable
@@ -5,41 +6,37 @@ from typing import Protocol, runtime_checkable
5
6
 
6
7
  @runtime_checkable
7
8
  class LoggerProtocol(Protocol):
8
- """A protocol for logging classes."""
9
+ """A protocol for logging classes with extra methods."""
9
10
 
10
- def debug(self, msg: object, *args, **kwargs) -> None:
11
- """Log a debug message."""
12
- ...
11
+ def debug(self, msg: object, *args, **kwargs) -> None: ...
13
12
 
14
- def info(self, msg: object, *args, **kwargs) -> None:
15
- """Log an info message."""
16
- ...
13
+ def info(self, msg: object, *args, **kwargs) -> None: ...
17
14
 
18
- def warning(self, msg: object, *args, **kwargs) -> None:
19
- """Log a warning message."""
20
- ...
15
+ def warning(self, msg: object, *args, **kwargs) -> None: ...
21
16
 
22
- def error(self, msg: object, *args, **kwargs) -> None:
23
- """Log an error message."""
24
- ...
17
+ def error(self, msg: object, *args, **kwargs) -> None: ...
18
+
19
+ def verbose(self, msg: object, *args, **kwargs) -> None: ...
20
+
21
+ def success(self, msg: object, *args, **kwargs) -> None: ...
22
+
23
+ def failure(self, msg: object, *args, **kwargs) -> None: ...
25
24
 
26
25
 
27
26
  @runtime_checkable
28
27
  class AsyncLoggerProtocol(Protocol):
29
28
  """A protocol for asynchronous logging classes."""
30
29
 
31
- async def debug(self, msg: object, *args, **kwargs) -> None:
32
- """Log a debug message asynchronously."""
33
- ...
30
+ async def debug(self, msg: object, *args, **kwargs) -> None: ...
31
+
32
+ async def info(self, msg: object, *args, **kwargs) -> None: ...
33
+
34
+ async def warning(self, msg: object, *args, **kwargs) -> None: ...
35
+
36
+ async def error(self, msg: object, *args, **kwargs) -> None: ...
34
37
 
35
- async def info(self, msg: object, *args, **kwargs) -> None:
36
- """Log an info message asynchronously."""
37
- ...
38
+ async def verbose(self, msg: object, *args, **kwargs) -> None: ...
38
39
 
39
- async def warning(self, msg: object, *args, **kwargs) -> None:
40
- """Log a warning message asynchronously."""
41
- ...
40
+ async def success(self, msg: object, *args, **kwargs) -> None: ...
42
41
 
43
- async def error(self, msg: object, *args, **kwargs) -> None:
44
- """Log an error message asynchronously."""
45
- ...
42
+ async def failure(self, msg: object, *args, **kwargs) -> None: ...
@@ -0,0 +1,204 @@
1
+ from collections.abc import Callable, Mapping
2
+ from datetime import datetime
3
+ import sys
4
+ import threading
5
+ from time import monotonic
6
+ from typing import TYPE_CHECKING, Literal, TextIO, cast
7
+
8
+ from rich._log_render import FormatTimeCallable, LogRender
9
+ from rich._null_file import NULL_FILE
10
+ from rich.console import COLOR_SYSTEMS, Console, ConsoleThreadLocals, RenderHook, _is_jupyter, detect_legacy_windows
11
+ from rich.emoji import EmojiVariant
12
+ from rich.highlighter import NullHighlighter, ReprHighlighter
13
+ from rich.style import StyleType
14
+ from rich.text import Text
15
+ from rich.theme import Theme, ThemeStack
16
+ from rich.themes import DEFAULT
17
+
18
+ if TYPE_CHECKING:
19
+ from rich.color import ColorSystem
20
+ from rich.live import Live
21
+ from rich.segment import Segment
22
+
23
+ HighlighterType = Callable[[str, Text], Text] | Text
24
+ JUPYTER_DEFAULT_COLUMNS = 115
25
+ JUPYTER_DEFAULT_LINES = 100
26
+ WINDOWS = sys.platform == "win32"
27
+
28
+ _null_highlighter = NullHighlighter()
29
+
30
+
31
+ class LogConsole[T: TextIO](Console):
32
+ """A Console from Rich that has added methods named after the logger methods."""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ color_system: Literal["auto", "standard", "256", "truecolor", "windows"] | None = "auto",
38
+ force_terminal: bool | None = None,
39
+ force_jupyter: bool | None = None,
40
+ force_interactive: bool | None = None,
41
+ soft_wrap: bool = False,
42
+ theme: Theme | None = None,
43
+ stderr: bool = False,
44
+ file: T | None = None,
45
+ quiet: bool = False,
46
+ width: int | None = None,
47
+ height: int | None = None,
48
+ style: StyleType | None = None,
49
+ no_color: bool | None = None,
50
+ tab_size: int = 8,
51
+ record: bool = False,
52
+ markup: bool = True,
53
+ emoji: bool = True,
54
+ emoji_variant: EmojiVariant | None = None,
55
+ highlight: bool = True,
56
+ log_time: bool = True,
57
+ log_path: bool = True,
58
+ log_time_format: str | FormatTimeCallable = "[%X]",
59
+ highlighter: HighlighterType | None = ReprHighlighter(), # type: ignore[assignment] # noqa: B008
60
+ legacy_windows: bool | None = None,
61
+ safe_box: bool = True,
62
+ get_datetime: Callable[[], datetime] | None = None,
63
+ get_time: Callable[[], float] | None = None,
64
+ _environ: Mapping[str, str] | None = None,
65
+ ):
66
+ # Copy of os.environ allows us to replace it for testing
67
+ if _environ is not None:
68
+ self._environ = _environ
69
+
70
+ self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter
71
+ if self.is_jupyter:
72
+ if width is None:
73
+ jupyter_columns = self._environ.get("JUPYTER_COLUMNS")
74
+ if jupyter_columns is not None and jupyter_columns.isdigit():
75
+ width = int(jupyter_columns)
76
+ else:
77
+ width = JUPYTER_DEFAULT_COLUMNS
78
+ if height is None:
79
+ jupyter_lines = self._environ.get("JUPYTER_LINES")
80
+ if jupyter_lines is not None and jupyter_lines.isdigit():
81
+ height = int(jupyter_lines)
82
+ else:
83
+ height = JUPYTER_DEFAULT_LINES
84
+
85
+ self.tab_size = tab_size
86
+ self.record = record
87
+ self._markup = markup
88
+ self._emoji = emoji
89
+ self._emoji_variant: EmojiVariant | None = emoji_variant
90
+ self._highlight = highlight
91
+ self.legacy_windows: bool = (
92
+ (detect_legacy_windows() and not self.is_jupyter) if legacy_windows is None else legacy_windows
93
+ )
94
+
95
+ if width is None:
96
+ columns = self._environ.get("COLUMNS")
97
+ if columns is not None and columns.isdigit():
98
+ width = int(columns) - self.legacy_windows
99
+ if height is None:
100
+ lines = self._environ.get("LINES")
101
+ if lines is not None and lines.isdigit():
102
+ height = int(lines)
103
+
104
+ self.soft_wrap = soft_wrap
105
+ self._width = width
106
+ self._height = height
107
+
108
+ self._color_system: ColorSystem | None
109
+
110
+ self._force_terminal = None
111
+ if force_terminal is not None:
112
+ self._force_terminal = force_terminal
113
+
114
+ self._file: T | None = file
115
+ self.quiet = quiet
116
+ self.stderr = stderr
117
+
118
+ if color_system is None:
119
+ self._color_system = None
120
+ elif color_system == "auto":
121
+ self._color_system = self._detect_color_system()
122
+ else:
123
+ self._color_system = COLOR_SYSTEMS[color_system]
124
+
125
+ self._lock = threading.RLock()
126
+ self._log_render = LogRender(
127
+ show_time=log_time,
128
+ show_path=log_path,
129
+ time_format=log_time_format,
130
+ )
131
+ self.highlighter: HighlighterType = highlighter or _null_highlighter # type: ignore[assignment]
132
+ self.safe_box = safe_box
133
+ self.get_datetime = get_datetime or datetime.now
134
+ self.get_time = get_time or monotonic
135
+ self.style = style
136
+ self.no_color = no_color if no_color is not None else self._environ.get("NO_COLOR", "") != ""
137
+ self.is_interactive = (
138
+ (self.is_terminal and not self.is_dumb_terminal) if force_interactive is None else force_interactive
139
+ )
140
+
141
+ self._record_buffer_lock = threading.RLock()
142
+ self._thread_locals = ConsoleThreadLocals(theme_stack=ThemeStack(DEFAULT if theme is None else theme))
143
+ self._record_buffer: list[Segment] = []
144
+ self._render_hooks: list[RenderHook] = []
145
+ self._live: Live | None = None
146
+ self._is_alt_screen = False
147
+
148
+ @property
149
+ def file(self) -> T:
150
+ """Get the file object to write to."""
151
+ file = self._file or (sys.stderr if self.stderr else sys.stdout)
152
+ file = getattr(file, "rich_proxied_file", file)
153
+ if file is None:
154
+ file = NULL_FILE
155
+ return cast("T", file)
156
+
157
+ @file.setter
158
+ def file(self, new_file: T) -> None: # type: ignore[override]
159
+ """Set a new file object."""
160
+ self._file = new_file
161
+
162
+ def info(self, msg: object, *args, **kwargs) -> None:
163
+ """Log an informational message to the console."""
164
+ self.log(msg, *args, **kwargs)
165
+
166
+ def warning(self, msg: object, *args, **kwargs) -> None:
167
+ """Log a warning message to the console."""
168
+ self.log(msg, *args, **kwargs)
169
+
170
+ def error(self, msg: object, *args, **kwargs) -> None:
171
+ """Log an error message to the console."""
172
+ self.log(msg, *args, **kwargs)
173
+
174
+ def debug(self, msg: object, *args, **kwargs) -> None:
175
+ """Log a debug message to the console."""
176
+ self.log(msg, *args, **kwargs)
177
+
178
+ def verbose(self, msg: object, *args, **kwargs) -> None:
179
+ """Log a verbose message to the console."""
180
+ self.log(msg, *args, **kwargs)
181
+
182
+ def success(self, msg: object, *args, **kwargs) -> None:
183
+ """Log a success message to the console."""
184
+ self.log(msg, *args, **kwargs)
185
+
186
+ def failure(self, msg: object, *args, **kwargs) -> None:
187
+ """Log a failure message to the console."""
188
+ self.log(msg, *args, **kwargs)
189
+
190
+ def exception(self, msg: object, *args, **kwargs) -> None:
191
+ """Log an exception message to the console."""
192
+ self.log(msg, *args, **kwargs)
193
+
194
+
195
+ if __name__ == "__main__":
196
+ from io import StringIO
197
+
198
+ console = LogConsole(file=StringIO())
199
+
200
+ console.info("This is an info message")
201
+
202
+ value = console.file
203
+
204
+ print(value.getvalue()) # Print the captured log messages from StringIO
@@ -0,0 +1,19 @@
1
+ from logging import Logger
2
+
3
+ from bear_utils.logger_manager._log_level import FAILURE, SUCCESS, VERBOSE
4
+
5
+
6
+ class LoggerExtra(Logger):
7
+ """A custom logger that just includes a few extra methods."""
8
+
9
+ def verbose(self, msg: object, *args, **kwargs) -> None:
10
+ """Log a verbose message."""
11
+ self.log(VERBOSE, msg, *args, **kwargs)
12
+
13
+ def success(self, msg: object, *args, **kwargs) -> None:
14
+ """Log a success message."""
15
+ self.log(SUCCESS, msg, *args, **kwargs)
16
+
17
+ def failure(self, msg: object, *args, **kwargs) -> None:
18
+ """Log a failure message."""
19
+ self.log(FAILURE, msg, *args, **kwargs)
@@ -7,7 +7,6 @@ from typing import Any, Self
7
7
 
8
8
  from prompt_toolkit.formatted_text import ANSI, FormattedText, to_formatted_text
9
9
  from prompt_toolkit.shortcuts import print_formatted_text
10
- from rich.console import Console
11
10
  from rich.text import Text
12
11
  from rich.theme import Theme
13
12
  from rich.traceback import Traceback
@@ -15,6 +14,7 @@ from singleton_base import SingletonBase
15
14
 
16
15
  from bear_utils.logger_manager._common import ExecValues, StackLevelTracker
17
16
  from bear_utils.logger_manager._styles import DEFAULT_THEME, LOGGER_METHODS, LoggerExtraInfo
17
+ from bear_utils.logger_manager.loggers._console import LogConsole
18
18
 
19
19
  from ._level_sin import INFO, add_level_name, check_level, lvl_exists
20
20
  from .sub_logger import SubConsoleLogger
@@ -43,23 +43,23 @@ class BaseLogger(SingletonBase):
43
43
  self.logger_mode: bool = logger_mode
44
44
  self.theme: Theme = DEFAULT_THEME if theme is None else theme
45
45
  self.style_disabled: bool = style_disabled
46
- self.console: Console = self.get_console(self.theme, style_disabled)
47
- self.console_buffer: StringIO = self.console.file # type: ignore[assignment]
48
- self.backup_console = Console(theme=self.theme, highlight=True, force_terminal=True)
46
+ self.console: LogConsole[StringIO] = self.get_console(self.theme, style_disabled)
47
+ self.console_buffer: StringIO = self.console.file
48
+ self.backup_console = LogConsole(theme=self.theme, highlight=True, force_terminal=True)
49
49
  self._generate_style_methods()
50
50
 
51
51
  @staticmethod
52
- def get_console(theme: Theme, style_disabled: bool) -> Console:
52
+ def get_console(theme: Theme, style_disabled: bool) -> LogConsole:
53
53
  """Create and return a Console instance with the specified theme and styling options."""
54
54
  if style_disabled:
55
- console = Console(
55
+ console = LogConsole(
56
56
  file=StringIO(),
57
57
  highlight=False,
58
58
  force_terminal=True,
59
59
  style=None, # Disable styling
60
60
  )
61
61
  else:
62
- console: Console = Console(
62
+ console: LogConsole = LogConsole(
63
63
  file=StringIO(),
64
64
  highlight=False,
65
65
  force_terminal=True,