absfuyu 5.6.1__py3-none-any.whl → 6.1.3__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.

Potentially problematic release.


This version of absfuyu might be problematic. Click here for more details.

Files changed (102) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +2 -2
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +2 -2
  6. absfuyu/cli/config_group.py +2 -2
  7. absfuyu/cli/do_group.py +2 -2
  8. absfuyu/cli/game_group.py +20 -2
  9. absfuyu/cli/tool_group.py +68 -4
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +10 -6
  12. absfuyu/core/baseclass.py +104 -34
  13. absfuyu/core/baseclass2.py +43 -2
  14. absfuyu/core/decorator.py +2 -2
  15. absfuyu/core/docstring.py +4 -2
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +2 -2
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +188 -6
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +72 -4
  23. absfuyu/dxt/listext.py +495 -23
  24. absfuyu/dxt/strext.py +2 -2
  25. absfuyu/extra/__init__.py +2 -2
  26. absfuyu/extra/audio/__init__.py +8 -0
  27. absfuyu/extra/audio/_util.py +57 -0
  28. absfuyu/extra/audio/convert.py +192 -0
  29. absfuyu/extra/audio/lossless.py +281 -0
  30. absfuyu/extra/beautiful.py +2 -2
  31. absfuyu/extra/da/__init__.py +39 -3
  32. absfuyu/extra/da/dadf.py +458 -29
  33. absfuyu/extra/da/dadf_base.py +2 -2
  34. absfuyu/extra/da/df_func.py +89 -5
  35. absfuyu/extra/da/mplt.py +2 -2
  36. absfuyu/extra/ggapi/__init__.py +8 -0
  37. absfuyu/extra/ggapi/gdrive.py +223 -0
  38. absfuyu/extra/ggapi/glicense.py +148 -0
  39. absfuyu/extra/ggapi/glicense_df.py +186 -0
  40. absfuyu/extra/ggapi/gsheet.py +88 -0
  41. absfuyu/extra/img/__init__.py +30 -0
  42. absfuyu/extra/img/converter.py +402 -0
  43. absfuyu/extra/img/dup_check.py +291 -0
  44. absfuyu/extra/pdf.py +4 -6
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +2 -20
  48. absfuyu/fun/rubik.py +2 -2
  49. absfuyu/fun/tarot.py +2 -2
  50. absfuyu/game/__init__.py +2 -2
  51. absfuyu/game/game_stat.py +2 -2
  52. absfuyu/game/schulte.py +78 -0
  53. absfuyu/game/sudoku.py +2 -2
  54. absfuyu/game/tictactoe.py +2 -2
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +2 -2
  57. absfuyu/general/content.py +2 -2
  58. absfuyu/general/human.py +2 -2
  59. absfuyu/general/resrel.py +213 -0
  60. absfuyu/general/shape.py +3 -8
  61. absfuyu/general/tax.py +344 -0
  62. absfuyu/logger.py +806 -59
  63. absfuyu/numbers/__init__.py +13 -0
  64. absfuyu/numbers/number_to_word.py +321 -0
  65. absfuyu/numbers/shorten_number.py +303 -0
  66. absfuyu/numbers/time_duration.py +217 -0
  67. absfuyu/pkg_data/__init__.py +2 -2
  68. absfuyu/pkg_data/deprecated.py +2 -2
  69. absfuyu/pkg_data/logo.py +1462 -0
  70. absfuyu/sort.py +4 -4
  71. absfuyu/tools/__init__.py +2 -2
  72. absfuyu/tools/checksum.py +119 -4
  73. absfuyu/tools/converter.py +2 -2
  74. absfuyu/tools/generator.py +24 -7
  75. absfuyu/tools/inspector.py +2 -2
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +2 -2
  78. absfuyu/tools/passwordlib.py +2 -2
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +213 -10
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +5 -8
  83. absfuyu/util/__init__.py +31 -2
  84. absfuyu/util/api.py +7 -4
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +2 -2
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +313 -4
  91. absfuyu/util/performance.py +2 -2
  92. absfuyu/util/shorten_number.py +206 -13
  93. absfuyu/util/text_table.py +2 -2
  94. absfuyu/util/zipped.py +2 -2
  95. absfuyu/version.py +22 -19
  96. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/METADATA +37 -8
  97. absfuyu-6.1.3.dist-info/RECORD +105 -0
  98. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -21
  100. absfuyu-5.6.1.dist-info/RECORD +0 -79
  101. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/entry_points.txt +0 -0
  102. {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/licenses/LICENSE +0 -0
absfuyu/logger.py CHANGED
@@ -3,14 +3,8 @@ Absfuyu: Logger
3
3
  ---------------
4
4
  Custom Logger Module
5
5
 
6
- Version: 5.6.1
7
- Date updated: 12/09/2025 (dd/mm/yyyy)
8
-
9
- Usage:
10
- ------
11
- >>> from absfuyu.logger import logger, LogLevel
12
- >>> logger.setLevel(LogLevel.DEBUG)
13
- >>> logger.debug("This logs!")
6
+ Version: 6.1.2
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
14
8
  """
15
9
 
16
10
  # Module level
@@ -18,65 +12,86 @@ Usage:
18
12
  __all__ = [
19
13
  # logger
20
14
  "logger",
15
+ "AbsfuyuLogger",
21
16
  "compress_for_log",
22
17
  # log level
23
18
  "LogLevel",
19
+ # Mixin
20
+ "LoggerMixin",
21
+ "AbsfuyuLoggerMixin",
24
22
  ]
25
23
 
26
24
 
27
25
  # Library
28
26
  # ---------------------------------------------------------------------------
27
+ import atexit
28
+ import datetime
29
+ import json
29
30
  import logging
30
31
  import math
31
- from logging.handlers import RotatingFileHandler as _RFH
32
- from logging.handlers import TimedRotatingFileHandler as _TRFH
32
+ import sys
33
+ from collections import Counter
34
+ from collections.abc import Callable
35
+ from logging.handlers import (
36
+ QueueHandler,
37
+ QueueListener,
38
+ RotatingFileHandler,
39
+ TimedRotatingFileHandler,
40
+ )
33
41
  from pathlib import Path
34
- from typing import Any, Optional, Union
42
+ from queue import Queue
43
+ from typing import Any, ClassVar, Self, override
35
44
 
36
45
 
37
46
  # Setup
38
47
  # ---------------------------------------------------------------------------
39
48
  class LogLevel:
40
49
  """
41
- ``logging``'s log level wrapper + custom log level
50
+ Python's ``logging`` module log level wrapper
42
51
  """
43
52
 
44
- TRACE: int = logging.DEBUG - 5
53
+ NOTSET: int = logging.NOTSET
45
54
  DEBUG: int = logging.DEBUG
46
55
  INFO: int = logging.INFO
47
56
  WARNING: int = logging.WARNING
48
57
  ERROR: int = logging.ERROR
49
58
  CRITICAL: int = logging.CRITICAL
50
- EXTREME: int = logging.CRITICAL + 10
51
-
52
59
 
53
- class _LogFormat:
54
- """Some log format styles"""
55
-
56
- FULL = "[%(asctime)s] [%(process)-d] [%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s" # Time|ProcessID|Module|Name|Function|LogType|Message
57
- SHORT = "[%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s" # Module|Name|Function|LogType|Message
58
- CONSOLE = "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s" # Time|LogType|Function|LineNumber|Message
59
- FILE = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s" # Time|LogType|FileName|Function|LineNumber|Message
60
+ _LOG_RECORD_BUILTIN_ATTRS = {
61
+ "args",
62
+ "asctime",
63
+ "created",
64
+ "exc_info",
65
+ "exc_text",
66
+ "filename",
67
+ "funcName",
68
+ "levelname",
69
+ "levelno",
70
+ "lineno",
71
+ "module",
72
+ "msecs",
73
+ "message",
74
+ "msg",
75
+ "name",
76
+ "pathname",
77
+ "process",
78
+ "processName",
79
+ "relativeCreated",
80
+ "stack_info",
81
+ "thread",
82
+ "threadName",
83
+ "taskName",
84
+ }
60
85
 
61
86
 
62
87
  # Create a custom logger
88
+ # Temp logger - delete later
63
89
  logger = logging.getLogger(__name__)
64
- logger.setLevel(logging.WARNING)
65
-
66
- # Create handlers
67
- ## Console log handler
68
- console_handler = logging.StreamHandler()
69
- console_handler.setLevel(LogLevel.TRACE) # Minimum log level
70
- console_handler.setFormatter(
71
- logging.Formatter(_LogFormat.CONSOLE, datefmt="%Y-%m-%d %H:%M:%S")
72
- )
73
- # logger.addHandler(console_handler)
74
- logger.addHandler(logging.NullHandler())
75
90
 
76
91
 
77
92
  # Functions
78
93
  # ---------------------------------------------------------------------------
79
- def _compress_list_for_print(iterable: list, max_visible: Optional[int] = 5) -> str:
94
+ def _compress_list_for_print(iterable: list, max_visible: int | None = 5) -> str:
80
95
  """
81
96
  Compress the list to be more log-readable
82
97
 
@@ -106,7 +121,7 @@ def _compress_list_for_print(iterable: list, max_visible: Optional[int] = 5) ->
106
121
  return f"{out} [Len: {len(iterable)}]"
107
122
 
108
123
 
109
- def _compress_string_for_print(text: str, max_visible: Optional[int] = 120) -> str:
124
+ def _compress_string_for_print(text: str, max_visible: int | None = 120) -> str:
110
125
  """
111
126
  Compress the string to be more log-readable
112
127
 
@@ -128,7 +143,7 @@ def _compress_string_for_print(text: str, max_visible: Optional[int] = 120) -> s
128
143
  return f"{temp} [Len: {len(text)}]"
129
144
 
130
145
 
131
- def compress_for_log(object_: Any, max_visible: Optional[int] = None) -> str:
146
+ def compress_for_log(object_: Any, max_visible: int | None = None) -> str:
132
147
  """
133
148
  Compress the object to be more log-readable
134
149
 
@@ -158,26 +173,29 @@ def compress_for_log(object_: Any, max_visible: Optional[int] = None) -> str:
158
173
  return object_ # type: ignore
159
174
 
160
175
 
161
- # Class
176
+ # Unused Class
162
177
  # ---------------------------------------------------------------------------
163
- class _CustomLogger:
178
+ class __CustomLogger:
164
179
  """
165
- Custom logger [W.I.P]
180
+ DO NOT USE
181
+
182
+ Custom logger [Incompleted, Remove soon]
166
183
 
167
184
  Create a custom logger
185
+
168
186
  *Useable but maybe unstable*
169
187
  """
170
188
 
171
189
  def __init__(
172
190
  self,
173
191
  name: str,
174
- cwd: Union[str, Path] = ".",
175
- log_format: Optional[str] = None,
192
+ cwd: str | Path = ".",
193
+ log_format: str | None = None,
176
194
  *,
177
195
  save_log_file: bool = False,
178
196
  separated_error_file: bool = False,
179
197
  timed_log: bool = False,
180
- date_log_format: Optional[str] = None,
198
+ date_log_format: str | None = None,
181
199
  error_log_size: int = 1_000_000, # 1 MB
182
200
  ) -> None:
183
201
  """
@@ -192,9 +210,7 @@ class _CustomLogger:
192
210
  """
193
211
  self._cwd = Path(cwd)
194
212
  self.log_folder = self._cwd.joinpath("logs")
195
- self.log_folder.mkdir(
196
- exist_ok=True, parents=True
197
- ) # Does not throw exception when folder existed
213
+ self.log_folder.mkdir(exist_ok=True, parents=True) # Does not throw exception when folder existed
198
214
  self.name = name
199
215
  self.log_file = self.log_folder.joinpath(f"{name}.log")
200
216
 
@@ -216,17 +232,13 @@ class _CustomLogger:
216
232
  ## Console log handler
217
233
  if log_format is None:
218
234
  # Time|LogType|Function|LineNumber|Message
219
- _log_format = (
220
- "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"
221
- )
235
+ _log_format = "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"
222
236
  else:
223
237
  _log_format = log_format
224
238
  console_handler = logging.StreamHandler()
225
239
  console_handler.setLevel(logging.DEBUG) # Minimum log level
226
240
  _console_log_format = _log_format # Create formatters and add it to handlers
227
- _console_formatter = logging.Formatter(
228
- _console_log_format, datefmt=_date_format
229
- )
241
+ _console_formatter = logging.Formatter(_console_log_format, datefmt=_date_format)
230
242
  console_handler.setFormatter(_console_formatter)
231
243
  self._console_handler = console_handler
232
244
  self.logger.addHandler(self._console_handler) # Add handlers to the logger
@@ -238,9 +250,7 @@ class _CustomLogger:
238
250
  _log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
239
251
  else:
240
252
  _log_format = log_format
241
- file_handler = logging.FileHandler(
242
- self.log_file, mode="a", encoding="utf-8"
243
- )
253
+ file_handler = logging.FileHandler(self.log_file, mode="a", encoding="utf-8")
244
254
  file_handler.setLevel(logging.DEBUG)
245
255
  _file_log_format = _log_format
246
256
  _file_formatter = logging.Formatter(_file_log_format, datefmt=_date_format)
@@ -250,7 +260,7 @@ class _CustomLogger:
250
260
 
251
261
  if timed_log:
252
262
  ## Time handler (split log every day)
253
- time_handler = _TRFH(
263
+ time_handler = TimedRotatingFileHandler(
254
264
  self.log_folder.joinpath(f"{self.name}_timed.log"),
255
265
  when="midnight",
256
266
  interval=1,
@@ -276,7 +286,7 @@ class _CustomLogger:
276
286
  _log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
277
287
  else:
278
288
  _log_format = log_format
279
- error_handler = _RFH(
289
+ error_handler = RotatingFileHandler(
280
290
  self.log_folder.joinpath(f"{self.name}_error.log"),
281
291
  maxBytes=error_log_size,
282
292
  backupCount=1,
@@ -294,9 +304,7 @@ class _CustomLogger:
294
304
  return self.__str__()
295
305
 
296
306
  @staticmethod
297
- def _add_logging_level(
298
- level_name: str, level_num: int, method_name: Optional[str] = None
299
- ):
307
+ def _add_logging_level(level_name: str, level_num: int, method_name: str | None = None):
300
308
  """
301
309
  Comprehensively adds a new logging level to the `logging` module and the
302
310
  currently configured logging class.
@@ -342,3 +350,742 @@ class _CustomLogger:
342
350
  if level_num < logging.DEBUG:
343
351
  self._console_handler.setLevel(level_num)
344
352
  self.logger.setLevel(level_num)
353
+
354
+
355
+ # Class
356
+ # ---------------------------------------------------------------------------
357
+ class _BasicJsonFormatter(logging.Formatter):
358
+ """
359
+ .json LogRecord
360
+
361
+ *deprecated*
362
+ """
363
+
364
+ @override
365
+ def format(self, record: logging.LogRecord) -> str:
366
+ data = {
367
+ "timestamp": self.formatTime(record, datefmt="%Y-%m-%d %H:%M:%S"),
368
+ "logger": record.name,
369
+ "level": record.levelname,
370
+ "message": record.getMessage(),
371
+ "file": record.filename,
372
+ "line": record.lineno,
373
+ }
374
+ return json.dumps(data)
375
+
376
+
377
+ class LoggingJSONFormatter(logging.Formatter):
378
+ """
379
+ JSONify log record to ``.jsonl`` file
380
+ """
381
+
382
+ def __init__(
383
+ self,
384
+ *,
385
+ fmt_keys: dict[str, str] | None = None,
386
+ ) -> None:
387
+ super().__init__()
388
+ self.fmt_keys = fmt_keys if fmt_keys is not None else {}
389
+
390
+ @override
391
+ def format(self, record: logging.LogRecord) -> str:
392
+ message = self._prepare_log_dict(record)
393
+ return json.dumps(message, default=str)
394
+
395
+ def _prepare_log_dict(self, record: logging.LogRecord):
396
+ always_fields = {
397
+ "message": record.getMessage(),
398
+ "timestamp": datetime.datetime.fromtimestamp(record.created, tz=datetime.timezone.utc).isoformat(),
399
+ }
400
+ if record.exc_info is not None:
401
+ always_fields["exc_info"] = self.formatException(record.exc_info)
402
+
403
+ if record.stack_info is not None:
404
+ always_fields["stack_info"] = self.formatStack(record.stack_info)
405
+
406
+ message = {
407
+ key: (msg_val if (msg_val := always_fields.pop(val, None)) is not None else getattr(record, val))
408
+ for key, val in self.fmt_keys.items()
409
+ }
410
+ message.update(always_fields)
411
+
412
+ # Get data from LogRecord
413
+ # for key, val in record.__dict__.items():
414
+ # if key not in _LOG_RECORD_BUILTIN_ATTRS:
415
+ # message[key] = val
416
+ for x in _LOG_RECORD_BUILTIN_ATTRS:
417
+ v = getattr(record, x, None)
418
+ if x not in message and v is not None:
419
+ message[x] = v
420
+
421
+ return message
422
+
423
+
424
+ class LogLevelUpperFilter(logging.Filter):
425
+ """
426
+ Filter ``LogRecord`` that <= ``filter_level``, by default: ``logging.WARNING``
427
+ """
428
+
429
+ def __init__(self, name: str = "", filter_level: int | str = LogLevel.WARNING) -> None:
430
+ """
431
+ Log level upper filter
432
+
433
+ Parameters
434
+ ----------
435
+ name : str, optional
436
+ Name of the filter, by default ``""``
437
+
438
+ filter_level : int | str, optional
439
+ Log level to to filter, by default ``logging.WARNING``
440
+
441
+ Raises
442
+ ------
443
+ ValueError
444
+ When type of ``filter_level`` is not ``<str>`` or ``<int>``
445
+ """
446
+ super().__init__(name)
447
+
448
+ if isinstance(filter_level, str):
449
+ log_level = {
450
+ "DEBUG": logging.DEBUG,
451
+ "INFO": logging.INFO,
452
+ "WARNING": logging.WARNING,
453
+ "ERROR": logging.ERROR,
454
+ "CRITICAL": logging.CRITICAL,
455
+ }
456
+ new_filter_level = log_level.get(filter_level.strip().upper())
457
+ self._filter_level = new_filter_level if new_filter_level is not None else logging.WARNING
458
+ elif isinstance(filter_level, int):
459
+ self._filter_level = filter_level
460
+ else:
461
+ raise ValueError("filter_level must type <str> or <int>")
462
+
463
+ @override
464
+ def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord:
465
+ return record.levelno <= self._filter_level
466
+
467
+
468
+ class AbsfuyuLogger(logging.Logger):
469
+ """
470
+ Custom ``logging.Logger`` with prebuilt add handler method, and queue logging enabler
471
+
472
+ Available handlers:
473
+ - Stream
474
+ - File
475
+ - Rotating file
476
+ - Timed file
477
+ - Json
478
+
479
+ Note:
480
+ - Add format for formater with: ``add_log_format(...)``
481
+ - Enable queue with: ``enable_queue``
482
+ - Properties: ``available_log_format_style``, ``handlers_name_list``
483
+ """
484
+
485
+ _LOG_FORMAT: ClassVar[dict[str, str]] = {
486
+ # Time|LogType|Function|LineNumber|Message
487
+ "console": "%(asctime)s [%(levelname)8s] %(funcName)s:%(lineno)3d: %(message)s",
488
+ # Time|LogType|FileName|Function|LineNumber|Message
489
+ "file": "%(asctime)s [%(levelname)s] %(filename)s:%(module)s:%(funcName)s:%(lineno)3d: %(message)s",
490
+ }
491
+
492
+ def __init__(
493
+ self,
494
+ name: str,
495
+ level: int = LogLevel.NOTSET,
496
+ *,
497
+ date_format: str | None = None,
498
+ date_format_file: str | None = None,
499
+ ) -> None:
500
+ """
501
+ Initialize the logger with a name and an optional level.
502
+ (Custom version)
503
+
504
+ Parameters
505
+ ----------
506
+ name : str
507
+ Name of the logger
508
+
509
+ level : int, optional
510
+ Log level, by default ``logging.NOTSET``
511
+
512
+ date_format : str | None, optional
513
+ | Date format in log handler, by default ``None``
514
+ | If ``date_format`` is not specified, ``"%Y-%m-%d %H:%M:%S"`` is used
515
+
516
+ date_format_file : str | None, optional
517
+ | Date format in log file handler, by default ``None``
518
+ | If ``date_format`` is not specified, ``"%Y-%m-%dT%H:%M:%S%z"`` is used
519
+ """
520
+ super().__init__(name, level)
521
+
522
+ # Extra
523
+ self._cl_date_fmt = "%Y-%m-%d %H:%M:%S" if date_format is None else date_format
524
+ self._cl_date_fmt_file = "%Y-%m-%dT%H:%M:%S%z" if date_format_file is None else date_format_file
525
+
526
+ # Class method
527
+ # --------------------------------
528
+ @classmethod
529
+ def default_config(cls, name: str, level: int = LogLevel.WARNING, /) -> Self:
530
+ """
531
+ Default configuration for this custom logger
532
+ - 1 debug stream handler
533
+ - 1 info-warning stream handler
534
+ - 1 error-critical stream handler
535
+
536
+ Parameters
537
+ ----------
538
+ name : str
539
+ Name of the logger
540
+
541
+ level : int, optional
542
+ Log level, by default ``logging.NOTSET``
543
+
544
+ Returns
545
+ -------
546
+ Self
547
+ Custom logger
548
+ """
549
+ logger = cls(name=name, level=level)
550
+
551
+ # Debug handler
552
+ logger.add_stream_handler(
553
+ name="handler_stream_debug",
554
+ level=LogLevel.DEBUG,
555
+ stream=sys.stdout,
556
+ upper_bound_level=LogLevel.DEBUG,
557
+ )
558
+
559
+ # Info-Warning handler
560
+ logger.add_stream_handler(
561
+ name="handler_stream_normal",
562
+ level=LogLevel.INFO,
563
+ stream=sys.stdout,
564
+ upper_bound_level=LogLevel.WARNING,
565
+ )
566
+
567
+ # Error-Critical handler
568
+ logger.add_stream_handler(name="handler_stream_error", level=LogLevel.ERROR, stream=sys.stderr)
569
+ return logger
570
+
571
+ # Formater
572
+ # --------------------------------
573
+ @classmethod
574
+ def add_log_format(cls, name: str, format: str) -> None:
575
+ """
576
+ Add a log format to use in ``logging.Formater`` for ``logging.Handler``
577
+
578
+ Parameters
579
+ ----------
580
+ name : str
581
+ Name of the format
582
+
583
+ format : str
584
+ Format string
585
+ """
586
+ cls._LOG_FORMAT[name] = format
587
+
588
+ @property
589
+ def available_log_format_style(self) -> list[str]:
590
+ """
591
+ Available log format style for ``logging.Formatter``
592
+ """
593
+ return list(self._LOG_FORMAT.keys())
594
+
595
+ def _get_log_format(self, format_name: str, /) -> str:
596
+ default = "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"
597
+ return self._LOG_FORMAT.get(format_name, default)
598
+
599
+ # Handlers
600
+ # --------------------------------
601
+ def _handle_log_handler(
602
+ self,
603
+ handler: logging.Handler,
604
+ name: str | None,
605
+ format_name: str,
606
+ level: int,
607
+ upper_bound_level: int | None = None,
608
+ overwrite_date_format: str | None = None,
609
+ ) -> logging.Handler:
610
+ """
611
+ Handle handler configuration
612
+
613
+ Parameters
614
+ ----------
615
+ handler : logging.Handler
616
+ Handler to config
617
+
618
+ name : str | None
619
+ Name of the handler
620
+
621
+ format_name : str
622
+ | LogFormater to use
623
+ | Default options: ``console``, ``file``
624
+
625
+ level : int
626
+ Log level
627
+
628
+ upper_bound_level : int | None, optional
629
+ | Log level to cut off, by default ``None``
630
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
631
+
632
+ overwrite_date_format : str | None, optional
633
+ Overwrite the date format for formater, by default ``None``
634
+
635
+ Returns
636
+ -------
637
+ logging.Handler
638
+ Handler
639
+ """
640
+
641
+ # Set name
642
+ if name is not None:
643
+ handler.set_name(name)
644
+
645
+ # Set level
646
+ handler.setLevel(level)
647
+
648
+ # Filter
649
+ dt_fmt = self._cl_date_fmt
650
+ if isinstance(
651
+ handler,
652
+ (RotatingFileHandler, TimedRotatingFileHandler, logging.FileHandler),
653
+ ):
654
+ dt_fmt = self._cl_date_fmt_file
655
+ if overwrite_date_format is not None:
656
+ dt_fmt = overwrite_date_format
657
+ fmt = logging.Formatter(self._get_log_format(format_name), datefmt=dt_fmt)
658
+ handler.setFormatter(fmt)
659
+
660
+ # Upperbound
661
+ if upper_bound_level is not None:
662
+ log_filter = LogLevelUpperFilter(filter_level=upper_bound_level)
663
+ handler.addFilter(log_filter)
664
+
665
+ return handler
666
+
667
+ @property
668
+ def handlers_name_list(self) -> Counter:
669
+ """
670
+ List of handlers available in the logger
671
+
672
+ Returns
673
+ -------
674
+ Counter
675
+ List of handlers
676
+ """
677
+ # out = {}
678
+ # for x in self.handlers:
679
+ # out.setdefault(x.name, 0)
680
+ # out[x.name] += 1
681
+ # return out
682
+ return Counter(x.name for x in self.handlers)
683
+
684
+ def add_stream_handler(
685
+ self,
686
+ name: str | None = None,
687
+ format_name: str = "console",
688
+ level: int = LogLevel.NOTSET,
689
+ *,
690
+ stream: Any | None = None,
691
+ upper_bound_level: int | None = None,
692
+ overwrite_date_format: str | None = None,
693
+ ) -> None:
694
+ """
695
+ Add stream handler for logger
696
+
697
+ Parameters
698
+ ----------
699
+ name : str | None, optional
700
+ Name of the handler, by default ``None``
701
+
702
+ format_name : str, optional
703
+ | LogFormater to use, by default ``"console"``
704
+ | Default options: ``console``, ``file``
705
+
706
+ level : int, optional
707
+ Log level, by default ``logging.NOTSET``
708
+
709
+ stream : Any | None, optional
710
+ | Stream to use, by default ``None``
711
+ | If ``stream`` is not specified, ``sys.stderr`` is used.
712
+ | Options: ``sys.stdout``, ``sys.stderr``, ...
713
+
714
+ upper_bound_level : int | None, optional
715
+ | Log level to cut off, by default ``None``
716
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
717
+
718
+ overwrite_date_format : str | None, optional
719
+ Overwrite the date format for formater, by default ``None``
720
+ """
721
+ st = sys.stderr if stream is None else stream
722
+ handler = logging.StreamHandler(st)
723
+ handler = self._handle_log_handler(
724
+ handler=handler,
725
+ name=name,
726
+ format_name=format_name,
727
+ level=level,
728
+ upper_bound_level=upper_bound_level,
729
+ overwrite_date_format=overwrite_date_format,
730
+ )
731
+ self.addHandler(handler)
732
+
733
+ def add_file_handler(
734
+ self,
735
+ file_path: str | Path,
736
+ name: str | None = None,
737
+ format_name: str = "file",
738
+ level: int = LogLevel.ERROR,
739
+ *,
740
+ upper_bound_level: int | None = None,
741
+ overwrite_date_format: str | None = None,
742
+ ) -> None:
743
+ """
744
+ Add file handler for logger
745
+
746
+ Parameters
747
+ ----------
748
+ file_path : str | Path
749
+ Path to log file
750
+
751
+ name : str | None, optional
752
+ Name of the handler, by default ``None``
753
+
754
+ format_name : str, optional
755
+ | LogFormater to use, by default ``"file"``
756
+ | Default options: ``console``, ``file``
757
+
758
+ level : int, optional
759
+ Log level, by default ``logging.ERROR``
760
+
761
+ upper_bound_level : int | None, optional
762
+ | Log level to cut off, by default ``None``
763
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
764
+
765
+ overwrite_date_format : str | None, optional
766
+ Overwrite the date format for formater, by default ``None``
767
+ """
768
+ path = Path(file_path)
769
+ handler = logging.FileHandler(str(path.resolve()), mode="a", encoding="utf-8")
770
+ handler = self._handle_log_handler(
771
+ handler=handler,
772
+ name=name,
773
+ format_name=format_name,
774
+ level=level,
775
+ upper_bound_level=upper_bound_level,
776
+ overwrite_date_format=overwrite_date_format,
777
+ )
778
+ self.addHandler(handler)
779
+
780
+ def add_rotating_file_handler(
781
+ self,
782
+ file_path: str | Path,
783
+ name: str | None = None,
784
+ format_name: str = "file",
785
+ level: int = LogLevel.DEBUG,
786
+ *,
787
+ max_bytes: int = 5_000_000,
788
+ backup_count: int = 5,
789
+ upper_bound_level: int | None = None,
790
+ overwrite_date_format: str | None = None,
791
+ ) -> None:
792
+ """
793
+ Add rotating file handler for logger
794
+
795
+ Parameters
796
+ ----------
797
+ file_path : str | Path
798
+ Path to log file
799
+
800
+ name : str | None, optional
801
+ Name of the handler, by default ``None``
802
+
803
+ format_name : str, optional
804
+ | LogFormater to use, by default ``"file"``
805
+ | Default options: ``console``, ``file``
806
+
807
+ level : int, optional
808
+ Log level, by default ``logging.DEBUG``
809
+
810
+ max_bytes : int, optional
811
+ | Max byte to rollover, by default ``0``
812
+ | If ``max_bytes`` is zero, rollover never occurs.
813
+ | Set to ``1_000_000`` for 1 MB
814
+
815
+ backup_count : int, optional
816
+ Number of backup file, by default ``0``
817
+
818
+ upper_bound_level : int | None, optional
819
+ | Log level to cut off, by default ``None``
820
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
821
+
822
+ overwrite_date_format : str | None, optional
823
+ Overwrite the date format for formater, by default ``None``
824
+ """
825
+ path = Path(file_path)
826
+ handler = RotatingFileHandler(
827
+ str(path.resolve()),
828
+ mode="a",
829
+ encoding="utf-8",
830
+ maxBytes=max_bytes,
831
+ backupCount=backup_count,
832
+ )
833
+ handler = self._handle_log_handler(
834
+ handler=handler,
835
+ name=name,
836
+ format_name=format_name,
837
+ level=level,
838
+ upper_bound_level=upper_bound_level,
839
+ overwrite_date_format=overwrite_date_format,
840
+ )
841
+ self.addHandler(handler)
842
+
843
+ def add_timed_rotating_file_handler(
844
+ self,
845
+ file_path: str | Path,
846
+ name: str | None = None,
847
+ format_name: str = "file",
848
+ level: int = LogLevel.DEBUG,
849
+ *,
850
+ when: str = "midnight",
851
+ interval: int = 1,
852
+ backup_count: int = 5,
853
+ upper_bound_level: int | None = None,
854
+ overwrite_date_format: str | None = None,
855
+ ) -> None:
856
+ """
857
+ Add timed rotating file handler for logger
858
+
859
+ Parameters
860
+ ----------
861
+ file_path : str | Path
862
+ Path to log file
863
+
864
+ name : str | None, optional
865
+ Name of the handler, by default ``None``
866
+
867
+ format_name : str, optional
868
+ | LogFormater to use, by default ``"file"``
869
+ | Default options: ``console``, ``file``
870
+
871
+ level : int, optional
872
+ Log level, by default ``logging.DEBUG``
873
+
874
+ when : Literal["S", "M", "H", "D", "midnight", "W"] | str, optional
875
+ When to rollover, by default ``"midnight"``
876
+ - S - Seconds
877
+ - M - Minutes
878
+ - H - Hours
879
+ - D - Days
880
+ - midnight - roll over at midnight
881
+ - W{0-6} - roll over on a certain day; 0 - Monday
882
+
883
+ interval : int, optional
884
+ Interval, by default ``1``
885
+
886
+ backup_count : int, optional
887
+ Number of backup file, by default ``0``
888
+
889
+ upper_bound_level : int | None, optional
890
+ | Log level to cut off, by default ``None``
891
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
892
+
893
+ overwrite_date_format : str | None, optional
894
+ Overwrite the date format for formater, by default ``None``
895
+ """
896
+ path = Path(file_path)
897
+ handler = TimedRotatingFileHandler(
898
+ str(path.resolve()),
899
+ encoding="utf-8",
900
+ when=when,
901
+ interval=interval,
902
+ backupCount=backup_count,
903
+ )
904
+ handler = self._handle_log_handler(
905
+ handler=handler,
906
+ name=name,
907
+ format_name=format_name,
908
+ level=level,
909
+ upper_bound_level=upper_bound_level,
910
+ overwrite_date_format=overwrite_date_format,
911
+ )
912
+ self.addHandler(handler)
913
+
914
+ def add_json_file_handler(
915
+ self,
916
+ file_path: str | Path,
917
+ name: str | None = None,
918
+ level: int = LogLevel.ERROR,
919
+ *,
920
+ max_bytes: int = 0,
921
+ backup_count: int = 0,
922
+ upper_bound_level: int | None = None,
923
+ ) -> None:
924
+ """
925
+ Add ``.jsonl`` file log handler for logger
926
+
927
+ Parameters
928
+ ----------
929
+ file_path : str | Path
930
+ Path to ``.jsonl`` log file
931
+
932
+ name : str | None, optional
933
+ Name of the handler, by default ``None``
934
+
935
+ level : int, optional
936
+ Log level, by default ``logging.ERROR``
937
+
938
+ max_bytes : int, optional
939
+ | Max byte to rollover, by default ``0``
940
+ | If ``max_bytes`` is zero, rollover never occurs.
941
+ | Set to ``1_000_000`` for 1 MB
942
+
943
+ backup_count : int, optional
944
+ Number of backup file, by default ``0``
945
+
946
+ upper_bound_level : int | None, optional
947
+ | Log level to cut off, by default ``None``
948
+ | Eg: ``upper_bound_level=logging.ERROR`` to show logs upto ERROR level
949
+ """
950
+ path = Path(file_path)
951
+ handler = RotatingFileHandler(
952
+ str(path.resolve()),
953
+ mode="a",
954
+ encoding="utf-8",
955
+ maxBytes=max_bytes,
956
+ backupCount=backup_count,
957
+ )
958
+ handler.setLevel(level)
959
+ if name is not None:
960
+ handler.set_name(name)
961
+ handler.setFormatter(LoggingJSONFormatter())
962
+ if upper_bound_level is not None:
963
+ log_filter = LogLevelUpperFilter(filter_level=upper_bound_level)
964
+ handler.addFilter(log_filter)
965
+ self.addHandler(handler)
966
+
967
+ def enable_queue(self, listener_handlers: list[logging.Handler] | None = None) -> None:
968
+ """
969
+ Convert logger to async queue mode.
970
+
971
+ Parameters
972
+ ----------
973
+ listener_handlers : list[logging.Handler] | None, optional
974
+ | List of handlers the listener should write to, by default ``None``
975
+ | If ``listener_handlers`` is not specified, all handlers are used.
976
+ """
977
+
978
+ # Get all available handlers
979
+ if listener_handlers is None:
980
+ listener_handlers = [h for h in self.handlers]
981
+
982
+ self._queue = Queue()
983
+
984
+ # Make listener
985
+ self._listener = QueueListener(self._queue, *listener_handlers, respect_handler_level=True)
986
+
987
+ # Replace existing handlers with Queue handler in self.handlers
988
+ self._queue_handler = QueueHandler(self._queue)
989
+ self._queue_handler.listener = self._listener
990
+ self.remove_all_hanlders()
991
+ self.addHandler(self._queue_handler) # self.handlers = [self._queue_handler]
992
+
993
+ # Log listener
994
+ # self._listener.start()
995
+ # atexit.register(self._listener.stop) # Register to stop listening when exit
996
+ self._queue_handler.listener.start()
997
+ atexit.register(self._queue_handler.listener.stop)
998
+
999
+ def remove_all_hanlders(self) -> None:
1000
+ """
1001
+ Remove all handlers for this logger
1002
+ """
1003
+ for x in self.handlers[:]:
1004
+ x.close()
1005
+ self.removeHandler(x)
1006
+
1007
+ def remove_hander_by_name(self, handler_name: str | None, /) -> None:
1008
+ """
1009
+ Remove an existing handler by its name
1010
+
1011
+ Parameters
1012
+ ----------
1013
+ handler_name : str | None
1014
+ Name of the handler
1015
+ """
1016
+ for x in self.handlers[:]:
1017
+ if x.name == handler_name:
1018
+ x.close()
1019
+ self.removeHandler(x)
1020
+
1021
+ # Test
1022
+ # --------------------------------
1023
+ def test_logger(self) -> None:
1024
+ """
1025
+ Test the logger by logging message in every log level
1026
+ """
1027
+ # test = ["debug", "info", "warning", "error", "exception", "critical"]
1028
+ # for x in test:
1029
+ # log_func = getattr(self, x)
1030
+ # log_func(f"This is {'an'if x.startswith('e')else 'a'} {x} message")
1031
+
1032
+ self.debug("This is a debug message")
1033
+ self.info("This is a info message")
1034
+ self.warning("This is a warning message")
1035
+ self.error("This is an error message")
1036
+ # self.exception("This is an exception message")
1037
+ self.critical("This is a critical message")
1038
+
1039
+
1040
+ # Mixin
1041
+ # ---------------------------------------------------------------------------
1042
+ class LoggerMixin[LoggerLike: logging.Logger]:
1043
+ """
1044
+ Mixin providing a lazily-initialized logger.
1045
+
1046
+ Attributes
1047
+ ----------
1048
+ CUSTOM_LOGGER : Callable[[str], LoggerLike] | None, optional
1049
+ Optional factory for creating a custom logger.
1050
+
1051
+ LOGGER_NAME : str | None, optional
1052
+ Override default logger name.
1053
+
1054
+
1055
+ Example:
1056
+ --------
1057
+ >>> class Test(LoggerMixin[logging.Logger]):
1058
+ ... pass
1059
+
1060
+ >>> class Test2(LoggerMixin[AbsfuyuLogger]):
1061
+ ... CUSTOM_LOGGER = AbsfuyuLogger
1062
+ ... LOGGER_NAME = "App"
1063
+ ... pass
1064
+ """
1065
+
1066
+ CUSTOM_LOGGER: Callable[[str], LoggerLike] | None = None
1067
+ LOGGER_NAME: str | None = None
1068
+
1069
+ # def __init__(self) -> None:
1070
+ # self._logger: LoggerLike
1071
+
1072
+ @property
1073
+ def logger(self) -> LoggerLike:
1074
+ if not hasattr(self, "_logger"):
1075
+ logger_name = self.__class__.__name__ if self.LOGGER_NAME is None else self.LOGGER_NAME
1076
+ if self.CUSTOM_LOGGER is None:
1077
+ self._logger = logging.getLogger(logger_name)
1078
+ else:
1079
+ self._logger = self.CUSTOM_LOGGER(logger_name)
1080
+ return self._logger
1081
+
1082
+
1083
+ class AbsfuyuLoggerMixin(LoggerMixin[AbsfuyuLogger]):
1084
+ CUSTOM_LOGGER = AbsfuyuLogger
1085
+
1086
+
1087
+ class _AbsfuyuLoggerLib(AbsfuyuLoggerMixin):
1088
+ """Logger for this library"""
1089
+
1090
+ CUSTOM_LOGGER = AbsfuyuLogger.default_config
1091
+ LOGGER_NAME = "absfuyu"