beans-logging 6.0.2__py3-none-any.whl → 7.0.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.
beans_logging/__init__.py CHANGED
@@ -1,14 +1,14 @@
1
- from ._base import Logger, logger, LoggerLoader
2
- from .schemas import LoggerConfigPM
3
- from ._consts import WarnEnum
1
+ from __future__ import annotations
2
+
4
3
  from .__version__ import __version__
4
+ from .config import LoggerConfigPM
5
+ from ._core import Logger, logger, LoggerLoader
5
6
 
6
7
 
7
8
  __all__ = [
9
+ "__version__",
10
+ "LoggerConfigPM",
8
11
  "Logger",
9
12
  "logger",
10
13
  "LoggerLoader",
11
- "LoggerConfigPM",
12
- "WarnEnum",
13
- "__version__",
14
14
  ]
@@ -1 +1 @@
1
- __version__ = "6.0.2"
1
+ __version__ = "7.0.0"
@@ -0,0 +1,154 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ from pydantic import validate_call
5
+
6
+ from ._constants import LogHandlerTypeEnum, LogLevelEnum
7
+ from .schemas import LogHandlerPM
8
+ from .config import LoggerConfigPM
9
+ from .sinks import std_sink
10
+ from .formats import json_formatter
11
+ from .filters import (
12
+ use_all_filter,
13
+ use_std_filter,
14
+ use_file_filter,
15
+ use_file_err_filter,
16
+ use_file_json_filter,
17
+ use_file_json_err_filter,
18
+ )
19
+ from .rotators import Rotator
20
+
21
+
22
+ @validate_call
23
+ def build_handler(handler: LogHandlerPM, config: LoggerConfigPM) -> dict[str, Any]:
24
+ """Build handler config as dictionary for Loguru logger to add new handler.
25
+
26
+ Args:
27
+ handler (LogHandlerPM , required): Target log handler model.
28
+ config (LoggerConfigPM, required): Default main config model to fill missing values.
29
+
30
+ Raises:
31
+ ValueError: 'sink' attribute is empty, required for any log handler except std and file handlers!
32
+
33
+ Returns:
34
+ dict[str, Any]: Loguru handler config as dictionary.
35
+ """
36
+
37
+ _handler_dict = handler.model_dump(by_alias=True, exclude_none=True)
38
+
39
+ if _handler_dict.get("sink") is None:
40
+ if _handler_dict.get("type") == LogHandlerTypeEnum.STD:
41
+ _handler_dict["sink"] = std_sink
42
+ elif _handler_dict.get("type") == LogHandlerTypeEnum.FILE:
43
+ _logs_path: str = ""
44
+ if _handler_dict.get("serialize") or _handler_dict.get("custom_serialize"):
45
+ if _handler_dict.get("error"):
46
+ _logs_path = os.path.join(
47
+ config.default.file.logs_dir,
48
+ config.default.file.json_.err_path,
49
+ )
50
+ else:
51
+ _logs_path = os.path.join(
52
+ config.default.file.logs_dir,
53
+ config.default.file.json_.log_path,
54
+ )
55
+ else:
56
+ if _handler_dict.get("error"):
57
+ _logs_path = os.path.join(
58
+ config.default.file.logs_dir,
59
+ config.default.file.plain.err_path,
60
+ )
61
+ else:
62
+ _logs_path = os.path.join(
63
+ config.default.file.logs_dir,
64
+ config.default.file.plain.log_path,
65
+ )
66
+
67
+ if "{app_name}" in _logs_path:
68
+ _logs_path = _logs_path.format(app_name=config.app_name)
69
+
70
+ _handler_dict["sink"] = _logs_path
71
+ else:
72
+ raise ValueError(
73
+ "'sink' attribute is empty, required for any log handler except std and file handlers!"
74
+ )
75
+
76
+ if _handler_dict.get("level") is None:
77
+ if _handler_dict.get("error"):
78
+ _handler_dict["level"] = config.default.level.err
79
+ else:
80
+ _handler_dict["level"] = config.default.level.base
81
+
82
+ if (_handler_dict.get("custom_serialize") is None) and _handler_dict.get(
83
+ "serialize"
84
+ ):
85
+ _handler_dict["custom_serialize"] = config.default.custom_serialize
86
+
87
+ if _handler_dict.get("custom_serialize"):
88
+ _handler_dict["serialize"] = False
89
+ _handler_dict["format"] = json_formatter
90
+
91
+ if (_handler_dict.get("format") is None) and (not _handler_dict.get("serialize")):
92
+ if _handler_dict.get("type") == LogHandlerTypeEnum.STD:
93
+ _handler_dict["format"] = config.default.std.format_str
94
+ else:
95
+ _handler_dict["format"] = config.default.format_str
96
+
97
+ if _handler_dict.get("filter") is None:
98
+ if _handler_dict.get("type") == LogHandlerTypeEnum.STD:
99
+ _handler_dict["filter"] = use_std_filter
100
+ elif _handler_dict.get("type") == LogHandlerTypeEnum.FILE:
101
+ if _handler_dict.get("serialize") or _handler_dict.get("custom_serialize"):
102
+ if _handler_dict.get("error"):
103
+ _handler_dict["filter"] = use_file_json_err_filter
104
+ else:
105
+ _handler_dict["filter"] = use_file_json_filter
106
+ else:
107
+ if _handler_dict.get("error"):
108
+ _handler_dict["filter"] = use_file_err_filter
109
+ else:
110
+ _handler_dict["filter"] = use_file_filter
111
+ else:
112
+ _handler_dict["filter"] = use_all_filter
113
+
114
+ if _handler_dict.get("backtrace") is None:
115
+ _handler_dict["backtrace"] = True
116
+
117
+ if (_handler_dict.get("diagnose") is None) and (
118
+ (_handler_dict.get("level") == LogLevelEnum.TRACE)
119
+ or (_handler_dict.get("level") == 5)
120
+ ):
121
+ _handler_dict["diagnose"] = True
122
+
123
+ if (_handler_dict.get("colorize") is None) and (
124
+ _handler_dict.get("type") == LogHandlerTypeEnum.STD
125
+ ):
126
+ _handler_dict["colorize"] = config.default.std.colorize
127
+
128
+ if _handler_dict.get("type") == LogHandlerTypeEnum.FILE:
129
+ if _handler_dict.get("enqueue") is None:
130
+ _handler_dict["enqueue"] = True
131
+
132
+ if _handler_dict.get("rotation") is None:
133
+ _handler_dict["rotation"] = Rotator(
134
+ rotate_size=config.default.file.rotate_size,
135
+ rotate_time=config.default.file.rotate_time,
136
+ ).should_rotate
137
+
138
+ if _handler_dict.get("retention") is None:
139
+ _handler_dict["retention"] = config.default.file.retention
140
+
141
+ if _handler_dict.get("encoding") is None:
142
+ _handler_dict["encoding"] = config.default.file.encoding
143
+
144
+ _handler_dict.pop("type", None)
145
+ _handler_dict.pop("error", None)
146
+ _handler_dict.pop("custom_serialize", None)
147
+ _handler_dict.pop("enabled", None)
148
+
149
+ return _handler_dict
150
+
151
+
152
+ __all__ = [
153
+ "build_handler",
154
+ ]
@@ -0,0 +1,30 @@
1
+ from enum import Enum
2
+
3
+
4
+ class LogHandlerTypeEnum(str, Enum):
5
+ STD = "STD"
6
+ FILE = "FILE"
7
+ SOCKET = "SOCKET"
8
+ HTTP = "HTTP"
9
+ SYSLOG = "SYSLOG"
10
+ QUEUE = "QUEUE"
11
+ MEMORY = "MEMORY"
12
+ NULL = "NULL"
13
+ CUSTOM = "CUSTOM"
14
+ UNKNOWN = "UNKNOWN"
15
+
16
+
17
+ class LogLevelEnum(str, Enum):
18
+ TRACE = "TRACE"
19
+ DEBUG = "DEBUG"
20
+ INFO = "INFO"
21
+ SUCCESS = "SUCCESS"
22
+ WARNING = "WARNING"
23
+ ERROR = "ERROR"
24
+ CRITICAL = "CRITICAL"
25
+
26
+
27
+ __all__ = [
28
+ "LogHandlerTypeEnum",
29
+ "LogLevelEnum",
30
+ ]
beans_logging/_core.py ADDED
@@ -0,0 +1,295 @@
1
+ # Standard libraries
2
+ import os
3
+ import copy
4
+ import uuid
5
+ from pathlib import Path
6
+ from typing import Any, TYPE_CHECKING
7
+
8
+ # Third-party libraries
9
+ import potato_util as utils
10
+
11
+ if TYPE_CHECKING:
12
+ from loguru import Logger
13
+ else:
14
+ from loguru._logger import Logger
15
+ from loguru import logger
16
+ from pydantic import validate_call
17
+ from potato_util import io as io_utils
18
+
19
+ # Internal modules
20
+ from .schemas import LogHandlerPM, LoguruHandlerPM
21
+ from .config import LoggerConfigPM
22
+ from ._builder import build_handler
23
+ from ._intercept import init_intercepter
24
+
25
+
26
+ class LoggerLoader:
27
+ """LoggerLoader class for setting up loguru logger.
28
+
29
+ Attributes:
30
+ _CONFIG_PATH (str): Default config file path. Default is '${PWD}/configs/logger.yml'.
31
+
32
+ handlers_map (dict[str, int]): Map of handler names to their IDs. Default is {'default.loguru_handler': 0}.
33
+ config (LoggerConfigPM): Main logger configuration model. Default is LoggerConfigPM().
34
+ config_path (str ): Path to logger configuration file. Default is _CONFIG_PATH.
35
+
36
+ Methods:
37
+ load() : Load logger handlers based on logger config.
38
+ _load_config_file(): Load logger config from file.
39
+ update_config() : Update current logger config with new config values.
40
+ remove_handler() : Remove handler from logger.
41
+ add_handler() : Add handler to logger.
42
+ """
43
+
44
+ _CONFIG_PATH = os.path.join(os.getcwd(), "configs", "logger.yml")
45
+
46
+ @validate_call
47
+ def __init__(
48
+ self,
49
+ config: LoggerConfigPM | dict[str, Any] | None = None,
50
+ config_path: str = _CONFIG_PATH,
51
+ auto_load: bool = False,
52
+ **kwargs,
53
+ ) -> None:
54
+
55
+ self.handlers_map = {"default.loguru_handler": 0}
56
+ if not config:
57
+ config = LoggerConfigPM()
58
+
59
+ self.config = config
60
+ if kwargs:
61
+ self.update_config(config=kwargs)
62
+
63
+ self.config_path = config_path
64
+
65
+ if auto_load:
66
+ self.load()
67
+
68
+ @validate_call
69
+ def load(self, load_config_file: bool = True) -> "Logger":
70
+ """Load logger handlers based on logger config.
71
+
72
+ Args:
73
+ load_config_file (bool, optional): Whether to load config from file before loading handlers.
74
+ Default is True.
75
+
76
+ Returns:
77
+ Logger: Main loguru logger instance.
78
+ """
79
+
80
+ self.remove_handler()
81
+ if load_config_file:
82
+ self._load_config_file()
83
+
84
+ for _key, _handler in self.config.handlers.items():
85
+ self.add_handler(name=_key, handler=_handler)
86
+
87
+ init_intercepter(config=self.config)
88
+ return logger
89
+
90
+ def _load_config_file(self) -> None:
91
+ """Load logger config from file."""
92
+
93
+ if self.config_path and os.path.isfile(self.config_path):
94
+ _config_data = io_utils.read_config_file(config_path=self.config_path)
95
+ if _config_data and ("logger" in _config_data):
96
+ _config_data = _config_data.get("logger", {})
97
+ if _config_data:
98
+ self.update_config(config=_config_data)
99
+
100
+ return
101
+
102
+ @validate_call
103
+ def update_config(self, config: dict[str, Any]) -> None:
104
+ """Update current logger config with new config values.
105
+
106
+ Args:
107
+ config (dict[str, Any], required): New config values to update current logger config.
108
+ """
109
+
110
+ _config_dict = self.config.model_dump()
111
+ _merged_dict = utils.deep_merge(_config_dict, config)
112
+ try:
113
+ self.config = LoggerConfigPM(**_merged_dict)
114
+ except Exception:
115
+ logger.critical(
116
+ "Failed to load `config` argument into <class 'LoggerConfigPM'>."
117
+ )
118
+ raise
119
+
120
+ return
121
+
122
+ @validate_call
123
+ def remove_handler(self, handler: str | int | None = None) -> None:
124
+ """Remove handler from logger.
125
+
126
+ Args:
127
+ handler (str | int | None, optional): Handler name or ID to remove from logger.
128
+ Default is None, which removes all handlers.
129
+
130
+ Raises:
131
+ ValueError: If handler name or ID is not found in handlers map.
132
+ """
133
+
134
+ if handler:
135
+ if isinstance(handler, str):
136
+ if handler in self.handlers_map:
137
+ _handler_id = self.handlers_map.get(handler)
138
+ logger.remove(_handler_id)
139
+ self.handlers_map.pop(handler)
140
+ else:
141
+ raise ValueError(
142
+ f"Not found handler name '{handler}' in handlers map!"
143
+ )
144
+
145
+ elif isinstance(handler, int):
146
+ if handler in self.handlers_map.values():
147
+ logger.remove(handler)
148
+ for _handler_name, _handler_id in list(self.handlers_map.items()):
149
+ if handler == _handler_id:
150
+ self.handlers_map.pop(_handler_name)
151
+ break
152
+ else:
153
+ raise ValueError(
154
+ f"Not found handler ID '{handler}' in handlers map!"
155
+ )
156
+ else:
157
+ logger.remove()
158
+ self.handlers_map.clear()
159
+
160
+ return
161
+
162
+ @validate_call
163
+ def add_handler(
164
+ self,
165
+ handler: LogHandlerPM | LoguruHandlerPM | dict[str, Any],
166
+ name: str | None = None,
167
+ ) -> int | None:
168
+ """Add handler to logger.
169
+
170
+ Args:
171
+ handler (LogHandlerPM | LoguruHandlerPM | dict[str, Any], required): Handler model or dictionary to add to
172
+ logger.
173
+ name (str | None , optional): Handler name. Default is None.
174
+
175
+ Returns:
176
+ int | None: Handler ID if added successfully, otherwise None.
177
+ """
178
+
179
+ _handler_id: int | None = None
180
+ try:
181
+ if isinstance(handler, dict):
182
+ handler = LogHandlerPM(**handler)
183
+ elif isinstance(handler, LoguruHandlerPM):
184
+ handler = LogHandlerPM(
185
+ **handler.model_dump(exclude_unset=True, exclude_none=True)
186
+ )
187
+
188
+ if handler.enabled:
189
+ _handler_dict = build_handler(handler=handler, config=self.config)
190
+ _sink = _handler_dict.get("sink")
191
+ if isinstance(_sink, (str, Path)):
192
+ _logs_dir = os.path.dirname(_sink)
193
+ if _logs_dir:
194
+ io_utils.create_dir(create_dir=_logs_dir)
195
+
196
+ _handler_id = logger.add(**_handler_dict)
197
+ if not name:
198
+ name = f"log_handler.{uuid.uuid4().hex}"
199
+
200
+ self.handlers_map[name] = _handler_id
201
+
202
+ except Exception:
203
+ logger.critical("Failed to add custom log handler to logger!")
204
+ raise
205
+
206
+ return _handler_id
207
+
208
+ # ATTRIBUTES
209
+ # handlers_map
210
+ @property
211
+ def handlers_map(self) -> dict[str, int]:
212
+ try:
213
+ return self.__handlers_map
214
+ except AttributeError:
215
+ raise AttributeError("`handlers_map` attribute is not set!")
216
+
217
+ @handlers_map.setter
218
+ def handlers_map(self, handlers_map: dict[str, int]) -> None:
219
+ if not isinstance(handlers_map, dict):
220
+ raise TypeError(
221
+ f"`handlers_map` attribute type {type(handlers_map)} is invalid, must be <dict>!."
222
+ )
223
+
224
+ self.__handlers_map = copy.deepcopy(handlers_map)
225
+ return
226
+
227
+ # handlers_map
228
+
229
+ # config
230
+ @property
231
+ def config(self) -> LoggerConfigPM:
232
+ try:
233
+ return self.__config
234
+ except AttributeError:
235
+ self.__config = LoggerConfigPM()
236
+
237
+ return self.__config
238
+
239
+ @config.setter
240
+ def config(self, config: LoggerConfigPM | dict[str, Any]) -> None:
241
+ if (not isinstance(config, LoggerConfigPM)) and (not isinstance(config, dict)):
242
+ raise TypeError(
243
+ f"`config` attribute type {type(config)} is invalid, must be a <class 'LoggerConfigPM'> or <dict>!"
244
+ )
245
+
246
+ if isinstance(config, dict):
247
+ config = LoggerConfigPM(**config)
248
+ elif isinstance(config, LoggerConfigPM):
249
+ config = config.model_copy(deep=True)
250
+
251
+ self.__config = config
252
+ return
253
+
254
+ # config
255
+
256
+ # config_path
257
+ @property
258
+ def config_path(self) -> str:
259
+ try:
260
+ return self.__config_path
261
+ except AttributeError:
262
+ self.__config_path = LoggerLoader._CONFIG_PATH
263
+
264
+ return self.__config_path
265
+
266
+ @config_path.setter
267
+ def config_path(self, config_path: str) -> None:
268
+ if not isinstance(config_path, str):
269
+ raise TypeError(
270
+ f"`config_path` attribute type {type(config_path)} is invalid, must be a <str>!"
271
+ )
272
+
273
+ config_path = config_path.strip()
274
+ if config_path == "":
275
+ raise ValueError("`config_path` attribute value is empty!")
276
+
277
+ if (
278
+ (not config_path.lower().endswith((".yml", ".yaml")))
279
+ and (not config_path.lower().endswith(".json"))
280
+ and (not config_path.lower().endswith(".toml"))
281
+ ):
282
+ raise ValueError(
283
+ f"`config_path` attribute value '{config_path}' is invalid, "
284
+ f"file must be '.yml', '.yaml', '.json' or '.toml' format!"
285
+ )
286
+
287
+ self.__config_path = config_path
288
+
289
+ # config_path
290
+ # ATTRIBUTES
291
+
292
+
293
+ __all__ = [
294
+ "LoggerLoader",
295
+ ]
@@ -0,0 +1,106 @@
1
+ import inspect
2
+ import logging
3
+ from logging import LogRecord, Handler
4
+
5
+ from loguru import logger
6
+ from pydantic import validate_call
7
+
8
+ from .config import LoggerConfigPM
9
+
10
+
11
+ class InterceptHandler(Handler):
12
+ """A handler class that intercepts logs from standard logging and redirects them to loguru logger.
13
+
14
+ Inherits:
15
+ Handler: Handler class from standard logging.
16
+
17
+ Overrides:
18
+ emit(): Handle intercepted log record.
19
+ """
20
+
21
+ def emit(self, record: LogRecord) -> None:
22
+ """Handle intercepted log record.
23
+
24
+ Args:
25
+ record (LogRecord, required): Log needs to be handled.
26
+ """
27
+
28
+ # Get corresponding Loguru level if it exists.
29
+ try:
30
+ _level: str | int = logger.level(record.levelname).name
31
+ except ValueError:
32
+ _level = record.levelno
33
+
34
+ # Find caller from where originated the logged message.
35
+ _frame, _depth = inspect.currentframe(), 0
36
+ while _frame and (_depth == 0 or _frame.f_code.co_filename == logging.__file__):
37
+ _frame = _frame.f_back
38
+ _depth += 1
39
+
40
+ logger.opt(depth=_depth, exception=record.exc_info).log(
41
+ _level, record.getMessage()
42
+ )
43
+
44
+ return
45
+
46
+
47
+ @validate_call
48
+ def init_intercepter(config: LoggerConfigPM) -> None:
49
+ """Initialize log interceptor based on provided config.
50
+
51
+ Args:
52
+ config (LoggerConfigPM, required): Main logger config model to use intercepter settings.
53
+ """
54
+
55
+ _intercept_handler = InterceptHandler()
56
+
57
+ # Intercepting all logs from standard (root logger) logging:
58
+ logging.basicConfig(handlers=[_intercept_handler], level=0, force=True)
59
+
60
+ _intercepted_modules = set()
61
+ _muted_modules = set()
62
+
63
+ if config.intercept.enabled:
64
+ for _module_name in list(logging.root.manager.loggerDict.keys()):
65
+ if config.intercept.only_base:
66
+ _module_name = _module_name.split(".")[0]
67
+
68
+ if (_module_name not in _intercepted_modules) and (
69
+ _module_name not in config.intercept.ignore_modules
70
+ ):
71
+ _logger = logging.getLogger(_module_name)
72
+ _logger.handlers = [_intercept_handler]
73
+ _logger.propagate = False
74
+ _intercepted_modules.add(_module_name)
75
+
76
+ for _include_module_name in config.intercept.include_modules:
77
+ _logger = logging.getLogger(_include_module_name)
78
+ _logger.handlers = [_intercept_handler]
79
+ # _logger.propagate = False
80
+
81
+ if _include_module_name not in _intercepted_modules:
82
+ _intercepted_modules.add(_include_module_name)
83
+
84
+ for _mute_module_name in config.intercept.mute_modules:
85
+ _logger = logging.getLogger(_mute_module_name)
86
+ _logger.handlers = []
87
+ _logger.propagate = False
88
+ _logger.disabled = True
89
+
90
+ if _mute_module_name in _intercepted_modules:
91
+ _intercepted_modules.remove(_mute_module_name)
92
+
93
+ if _mute_module_name not in _muted_modules:
94
+ _muted_modules.add(_mute_module_name)
95
+
96
+ logger.trace(
97
+ f"Intercepted modules: {list(_intercepted_modules)}; Muted modules: {list(_muted_modules)};"
98
+ )
99
+
100
+ return
101
+
102
+
103
+ __all__ = [
104
+ "InterceptHandler",
105
+ "init_intercepter",
106
+ ]
beans_logging/auto.py CHANGED
@@ -1,24 +1,15 @@
1
1
  # flake8: noqa
2
2
 
3
- import os
4
-
5
3
  from . import *
6
4
 
7
- logger_loader: LoggerLoader | None = None
8
- _DISABLE_DEFAULT_LOGGER = (
9
- str(os.getenv("BEANS_LOGGING_DISABLE_DEFAULT")).strip().lower()
10
- )
11
- if (_DISABLE_DEFAULT_LOGGER != "true") and (_DISABLE_DEFAULT_LOGGER != "1"):
12
- logger_loader: LoggerLoader = LoggerLoader()
13
- logger: Logger = logger_loader.load()
5
+ logger_loader: LoggerLoader = LoggerLoader(auto_load=True)
14
6
 
15
7
 
16
8
  __all__ = [
9
+ "__version__",
10
+ "LoggerConfigPM",
17
11
  "Logger",
18
12
  "logger",
19
13
  "LoggerLoader",
20
14
  "logger_loader",
21
- "LoggerConfigPM",
22
- "WarnEnum",
23
- "__version__",
24
15
  ]