dycw-utilities 0.129.14__py3-none-any.whl → 0.130.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.
utilities/logging.py CHANGED
@@ -2,14 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
4
  import re
5
- from contextlib import contextmanager
6
5
  from dataclasses import dataclass, field
7
6
  from functools import cached_property
8
- from itertools import product
9
7
  from logging import (
10
- DEBUG,
11
- ERROR,
12
- NOTSET,
13
8
  FileHandler,
14
9
  Formatter,
15
10
  Handler,
@@ -24,14 +19,14 @@ from logging import (
24
19
  from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
25
20
  from pathlib import Path
26
21
  from re import Pattern
27
- from sys import stdout
28
22
  from time import time
29
23
  from typing import (
30
24
  TYPE_CHECKING,
31
25
  Any,
32
- ClassVar,
33
26
  Literal,
27
+ NotRequired,
34
28
  Self,
29
+ TypedDict,
35
30
  assert_never,
36
31
  cast,
37
32
  override,
@@ -40,29 +35,18 @@ from typing import (
40
35
  from utilities.dataclasses import replace_non_sentinel
41
36
  from utilities.datetime import (
42
37
  SECOND,
43
- maybe_sub_pct_y,
44
38
  parse_datetime_compact,
45
39
  round_datetime,
46
40
  serialize_compact,
47
41
  )
48
42
  from utilities.errors import ImpossibleCaseError
49
43
  from utilities.iterables import OneEmptyError, always_iterable, one
50
- from utilities.pathlib import ensure_suffix, get_path, get_root
51
- from utilities.reprlib import (
52
- RICH_EXPAND_ALL,
53
- RICH_INDENT_SIZE,
54
- RICH_MAX_DEPTH,
55
- RICH_MAX_LENGTH,
56
- RICH_MAX_STRING,
57
- RICH_MAX_WIDTH,
58
- )
44
+ from utilities.pathlib import ensure_suffix, get_path
59
45
  from utilities.sentinel import Sentinel, sentinel
60
- from utilities.traceback import RichTracebackFormatter
61
46
 
62
47
  if TYPE_CHECKING:
63
- from collections.abc import Callable, Iterable, Iterator
48
+ from collections.abc import Callable, Iterable, Mapping
64
49
  from logging import _FilterType
65
- from zoneinfo import ZoneInfo
66
50
 
67
51
  from utilities.types import (
68
52
  LoggerOrName,
@@ -71,12 +55,256 @@ if TYPE_CHECKING:
71
55
  MaybeIterable,
72
56
  PathLike,
73
57
  )
74
- from utilities.version import MaybeCallableVersionLike
75
58
 
76
- try:
77
- from whenever import ZonedDateTime
78
- except ModuleNotFoundError: # pragma: no cover
79
- ZonedDateTime = None
59
+
60
+ _DEFAULT_FORMAT = "{asctime} | {name}:{funcName}:{lineno} | {levelname:8} | {message}"
61
+ _DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S"
62
+ _DEFAULT_BACKUP_COUNT: int = 100
63
+ _DEFAULT_MAX_BYTES: int = 10 * 1024**2
64
+ _DEFAULT_WHEN: _When = "D"
65
+
66
+
67
+ ##
68
+
69
+
70
+ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
71
+ """Add a set of filters to a handler."""
72
+ for filter_i in filters:
73
+ handler.addFilter(filter_i)
74
+
75
+
76
+ ##
77
+
78
+
79
+ def basic_config(
80
+ *,
81
+ obj: LoggerOrName | Handler | None = None,
82
+ whenever: bool = False,
83
+ format_: str = _DEFAULT_FORMAT,
84
+ datefmt: str = _DEFAULT_DATEFMT,
85
+ level: LogLevel = "INFO",
86
+ filters: MaybeIterable[_FilterType] | None = None,
87
+ plain: bool = False,
88
+ color_field_styles: Mapping[str, _FieldStyleKeys] | None = None,
89
+ ) -> None:
90
+ """Do the basic config."""
91
+ match obj:
92
+ case None:
93
+ basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
94
+ case Logger() as logger:
95
+ logger.setLevel(level)
96
+ logger.addHandler(handler := StreamHandler())
97
+ basic_config(
98
+ obj=handler,
99
+ whenever=whenever,
100
+ format_=format_,
101
+ datefmt=datefmt,
102
+ level=level,
103
+ filters=filters,
104
+ plain=plain,
105
+ color_field_styles=color_field_styles,
106
+ )
107
+ case str() as name:
108
+ basic_config(
109
+ obj=get_logger(logger=name),
110
+ whenever=whenever,
111
+ format_=format_,
112
+ datefmt=datefmt,
113
+ level=level,
114
+ filters=filters,
115
+ plain=plain,
116
+ color_field_styles=color_field_styles,
117
+ )
118
+ case Handler() as handler:
119
+ handler.setLevel(level)
120
+ if filters is not None:
121
+ add_filters(handler, *always_iterable(filters))
122
+ formatter = get_formatter(
123
+ whenever=whenever,
124
+ format_=format_,
125
+ datefmt=datefmt,
126
+ plain=plain,
127
+ color_field_styles=color_field_styles,
128
+ )
129
+ handler.setFormatter(formatter)
130
+ case _ as never:
131
+ assert_never(never)
132
+
133
+
134
+ ##
135
+
136
+
137
+ def filter_for_key(
138
+ key: str, /, *, default: bool = False
139
+ ) -> Callable[[LogRecord], bool]:
140
+ """Make a filter for a given attribute."""
141
+ if (key in _FILTER_FOR_KEY_BLACKLIST) or key.startswith("__"):
142
+ raise FilterForKeyError(key=key)
143
+
144
+ def filter_(record: LogRecord, /) -> bool:
145
+ try:
146
+ value = getattr(record, key)
147
+ except AttributeError:
148
+ return default
149
+ return bool(value)
150
+
151
+ return filter_
152
+
153
+
154
+ # fmt: off
155
+ _FILTER_FOR_KEY_BLACKLIST = {
156
+ "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName"
157
+ }
158
+ # fmt: on
159
+
160
+
161
+ @dataclass(kw_only=True, slots=True)
162
+ class FilterForKeyError(Exception):
163
+ key: str
164
+
165
+ @override
166
+ def __str__(self) -> str:
167
+ return f"Invalid key: {self.key!r}"
168
+
169
+
170
+ ##
171
+
172
+
173
+ type _FieldStyleKeys = Literal[
174
+ "asctime", "hostname", "levelname", "name", "programname", "username"
175
+ ]
176
+
177
+
178
+ class _FieldStyleDict(TypedDict):
179
+ color: str
180
+ bold: NotRequired[bool]
181
+
182
+
183
+ def get_formatter(
184
+ *,
185
+ whenever: bool = False,
186
+ format_: str = _DEFAULT_FORMAT,
187
+ datefmt: str = _DEFAULT_DATEFMT,
188
+ plain: bool = False,
189
+ color_field_styles: Mapping[str, _FieldStyleKeys] | None = None,
190
+ ) -> Formatter:
191
+ """Get the formatter; colored if available."""
192
+ if whenever:
193
+ from utilities.whenever import WheneverLogRecord
194
+
195
+ setLogRecordFactory(WheneverLogRecord)
196
+ format_ = format_.replace("{asctime}", "{zoned_datetime}")
197
+ if plain:
198
+ return _get_plain_formatter(format_=format_, datefmt=datefmt)
199
+ try:
200
+ from coloredlogs import DEFAULT_FIELD_STYLES, ColoredFormatter
201
+ except ModuleNotFoundError: # pragma: no cover
202
+ return _get_plain_formatter(format_=format_, datefmt=datefmt)
203
+ default = cast("dict[_FieldStyleKeys, _FieldStyleDict]", DEFAULT_FIELD_STYLES)
204
+ field_styles = {cast("str", k): v for k, v in default.items()}
205
+ if whenever:
206
+ field_styles["zoned_datetime"] = default["asctime"]
207
+ if color_field_styles is not None:
208
+ field_styles.update({k: default[v] for k, v in color_field_styles.items()})
209
+ return ColoredFormatter(
210
+ fmt=format_, datefmt=datefmt, style="{", field_styles=field_styles
211
+ )
212
+
213
+
214
+ def _get_plain_formatter(
215
+ *, format_: str = _DEFAULT_FORMAT, datefmt: str = _DEFAULT_DATEFMT
216
+ ) -> Formatter:
217
+ """Get the plain formatter."""
218
+ return Formatter(fmt=format_, datefmt=datefmt, style="{")
219
+
220
+
221
+ ##
222
+
223
+
224
+ def get_logger(*, logger: LoggerOrName | None = None) -> Logger:
225
+ """Get a logger."""
226
+ match logger:
227
+ case Logger():
228
+ return logger
229
+ case str() | None:
230
+ return getLogger(logger)
231
+ case _ as never:
232
+ assert_never(never)
233
+
234
+
235
+ ##
236
+
237
+
238
+ def get_logging_level_number(level: LogLevel, /) -> int:
239
+ """Get the logging level number."""
240
+ mapping = getLevelNamesMapping()
241
+ try:
242
+ return mapping[level]
243
+ except KeyError:
244
+ raise GetLoggingLevelNumberError(level=level) from None
245
+
246
+
247
+ @dataclass(kw_only=True, slots=True)
248
+ class GetLoggingLevelNumberError(Exception):
249
+ level: LogLevel
250
+
251
+ @override
252
+ def __str__(self) -> str:
253
+ return f"Invalid logging level: {self.level!r}"
254
+
255
+
256
+ ##
257
+
258
+
259
+ def setup_logging(
260
+ *,
261
+ logger: LoggerOrName | None = None,
262
+ whenever: bool = False,
263
+ format_: str = _DEFAULT_FORMAT,
264
+ datefmt: str = _DEFAULT_DATEFMT,
265
+ console_level: LogLevel = "INFO",
266
+ console_prefix: str = "❯", # noqa: RUF001
267
+ console_filters: MaybeIterable[_FilterType] | None = None,
268
+ files_dir: MaybeCallablePathLike | None = None,
269
+ files_max_bytes: int = _DEFAULT_MAX_BYTES,
270
+ files_when: _When = _DEFAULT_WHEN,
271
+ files_interval: int = 1,
272
+ files_backup_count: int = _DEFAULT_BACKUP_COUNT,
273
+ files_filters: Iterable[_FilterType] | None = None,
274
+ ) -> None:
275
+ """Set up logger."""
276
+ basic_config(
277
+ obj=logger,
278
+ whenever=whenever,
279
+ format_=f"{console_prefix} {format_}",
280
+ datefmt=datefmt,
281
+ level=console_level,
282
+ filters=console_filters,
283
+ )
284
+ logger_use = get_logger(logger=logger)
285
+ name = logger_use.name
286
+ dir_ = get_path(path=files_dir)
287
+ levels: list[LogLevel] = ["DEBUG", "INFO", "ERROR"]
288
+ for level in levels:
289
+ lower = level.lower()
290
+ for stem in [lower, f"{name}-{lower}"]:
291
+ handler = SizeAndTimeRotatingFileHandler(
292
+ dir_.joinpath(stem).with_suffix(".txt"),
293
+ maxBytes=files_max_bytes,
294
+ when=files_when,
295
+ interval=files_interval,
296
+ backupCount=files_backup_count,
297
+ )
298
+ logger_use.addHandler(handler)
299
+ basic_config(
300
+ obj=handler,
301
+ whenever=whenever,
302
+ format_=format_,
303
+ datefmt=datefmt,
304
+ level=level,
305
+ filters=files_filters,
306
+ plain=True,
307
+ )
80
308
 
81
309
 
82
310
  ##
@@ -85,9 +313,6 @@ except ModuleNotFoundError: # pragma: no cover
85
313
  type _When = Literal[
86
314
  "S", "M", "H", "D", "midnight", "W0", "W1", "W2", "W3", "W4", "W5", "W6"
87
315
  ]
88
- _BACKUP_COUNT: int = 100
89
- _MAX_BYTES: int = 10 * 1024**2
90
- _WHEN: _When = "D"
91
316
 
92
317
 
93
318
  class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
@@ -103,10 +328,10 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
103
328
  encoding: str | None = None,
104
329
  delay: bool = False,
105
330
  errors: Literal["strict", "ignore", "replace"] | None = None,
106
- maxBytes: int = _MAX_BYTES,
107
- when: _When = _WHEN,
331
+ maxBytes: int = _DEFAULT_MAX_BYTES,
332
+ when: _When = _DEFAULT_WHEN,
108
333
  interval: int = 1,
109
- backupCount: int = _BACKUP_COUNT,
334
+ backupCount: int = _DEFAULT_BACKUP_COUNT,
110
335
  utc: bool = False,
111
336
  atTime: dt.time | None = None,
112
337
  ) -> None:
@@ -134,9 +359,11 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
134
359
 
135
360
  @override
136
361
  def emit(self, record: LogRecord) -> None:
137
- try: # skipif-ci-and-windows
362
+ try:
138
363
  if (self._backup_count is not None) and self._should_rollover(record):
139
- self._do_rollover(backup_count=self._backup_count)
364
+ self._do_rollover( # skipif-ci-and-windows
365
+ backup_count=self._backup_count
366
+ )
140
367
  FileHandler.emit(self, record)
141
368
  except Exception: # noqa: BLE001 # pragma: no cover
142
369
  self.handleError(record)
@@ -199,20 +426,20 @@ def _compute_rollover_actions(
199
426
  patterns: _RolloverPatterns | None = None,
200
427
  backup_count: int = 1,
201
428
  ) -> _RolloverActions:
202
- from utilities.tzlocal import get_now_local # skipif-ci-and-windows
429
+ from utilities.tzlocal import get_now_local
203
430
 
204
- patterns = ( # skipif-ci-and-windows
431
+ patterns = (
205
432
  _compute_rollover_patterns(stem, suffix) if patterns is None else patterns
206
433
  )
207
- files = { # skipif-ci-and-windows
434
+ files = {
208
435
  file
209
436
  for path in directory.iterdir()
210
437
  if (file := _RotatingLogFile.from_path(path, stem, suffix, patterns=patterns))
211
438
  is not None
212
439
  }
213
- deletions: set[_Deletion] = set() # skipif-ci-and-windows
214
- rotations: set[_Rotation] = set() # skipif-ci-and-windows
215
- for file in files: # skipif-ci-and-windows
440
+ deletions: set[_Deletion] = set()
441
+ rotations: set[_Rotation] = set()
442
+ for file in files:
216
443
  match file.index, file.start, file.end:
217
444
  case int() as index, _, _ if index >= backup_count:
218
445
  deletions.add(_Deletion(file=file))
@@ -234,9 +461,7 @@ def _compute_rollover_actions(
234
461
  rotations.add(_Rotation(file=file, index=index + 1))
235
462
  case _: # pragma: no cover
236
463
  raise NotImplementedError
237
- return _RolloverActions( # skipif-ci-and-windows
238
- deletions=deletions, rotations=rotations
239
- )
464
+ return _RolloverActions(deletions=deletions, rotations=rotations)
240
465
 
241
466
 
242
467
  @dataclass(order=True, unsafe_hash=True, kw_only=True)
@@ -245,13 +470,11 @@ class _RolloverActions:
245
470
  rotations: set[_Rotation] = field(default_factory=set)
246
471
 
247
472
  def do(self) -> None:
248
- from utilities.atomicwrites import move_many # skipif-ci-and-windows
473
+ from utilities.atomicwrites import move_many
249
474
 
250
- for deletion in self.deletions: # skipif-ci-and-windows
475
+ for deletion in self.deletions:
251
476
  deletion.delete()
252
- move_many( # skipif-ci-and-windows
253
- *((r.file.path, r.destination) for r in self.rotations)
254
- )
477
+ move_many(*((r.file.path, r.destination) for r in self.rotations))
255
478
 
256
479
 
257
480
  @dataclass(order=True, unsafe_hash=True, kw_only=True)
@@ -281,7 +504,7 @@ class _RotatingLogFile:
281
504
  ) -> Self | None:
282
505
  if (not path.stem.startswith(stem)) or path.suffix != suffix:
283
506
  return None
284
- if patterns is None: # skipif-ci-and-windows
507
+ if patterns is None:
285
508
  patterns = _compute_rollover_patterns(stem, suffix)
286
509
  try:
287
510
  (index,) = patterns.pattern1.findall(path.name)
@@ -343,9 +566,7 @@ class _RotatingLogFile:
343
566
  start: dt.datetime | None | Sentinel = sentinel,
344
567
  end: dt.datetime | None | Sentinel = sentinel,
345
568
  ) -> Self:
346
- return replace_non_sentinel( # skipif-ci-and-windows
347
- self, index=index, start=start, end=end
348
- )
569
+ return replace_non_sentinel(self, index=index, start=start, end=end)
349
570
 
350
571
 
351
572
  @dataclass(order=True, unsafe_hash=True, kw_only=True)
@@ -353,7 +574,7 @@ class _Deletion:
353
574
  file: _RotatingLogFile
354
575
 
355
576
  def delete(self) -> None:
356
- self.file.path.unlink(missing_ok=True) # skipif-ci-and-windows
577
+ self.file.path.unlink(missing_ok=True)
357
578
 
358
579
 
359
580
  @dataclass(order=True, unsafe_hash=True, kw_only=True)
@@ -364,467 +585,24 @@ class _Rotation:
364
585
  end: dt.datetime | Sentinel = sentinel
365
586
 
366
587
  def __post_init__(self) -> None:
367
- if isinstance(self.start, dt.datetime): # skipif-ci-and-windows
588
+ if isinstance(self.start, dt.datetime):
368
589
  self.start = round_datetime(self.start, SECOND)
369
- if isinstance(self.end, dt.datetime): # skipif-ci-and-windows
590
+ if isinstance(self.end, dt.datetime):
370
591
  self.end = round_datetime(self.end, SECOND)
371
592
 
372
593
  @cached_property
373
594
  def destination(self) -> Path:
374
- return self.file.replace( # skipif-ci-and-windows
375
- index=self.index, start=self.start, end=self.end
376
- ).path
377
-
378
-
379
- ##
380
-
381
-
382
- class StandaloneFileHandler(Handler):
383
- """Handler for emitting tracebacks to individual files."""
384
-
385
- @override
386
- def __init__(
387
- self, *, level: int = NOTSET, path: MaybeCallablePathLike | None = None
388
- ) -> None:
389
- super().__init__(level=level)
390
- self._path = get_path(path=path)
391
-
392
- @override
393
- def emit(self, record: LogRecord) -> None:
394
- from utilities.atomicwrites import writer
395
- from utilities.tzlocal import get_now_local
396
-
397
- try:
398
- path = self._path.joinpath(serialize_compact(get_now_local())).with_suffix(
399
- ".txt"
400
- )
401
- formatted = self.format(record)
402
- with writer(path, overwrite=True) as temp, temp.open(mode="w") as fh:
403
- _ = fh.write(formatted)
404
- except Exception: # noqa: BLE001 # pragma: no cover
405
- self.handleError(record)
406
-
407
-
408
- ##
409
-
410
-
411
- def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
412
- """Add a set of filters to a handler."""
413
- for filter_i in filters:
414
- handler.addFilter(filter_i)
415
-
416
-
417
- ##
418
-
419
-
420
- def basic_config(
421
- *,
422
- obj: LoggerOrName | Handler | None = None,
423
- format_: str = "{asctime} | {name} | {levelname:8} | {message}",
424
- whenever: bool = False,
425
- level: LogLevel = "INFO",
426
- plain: bool = False,
427
- ) -> None:
428
- """Do the basic config."""
429
- if whenever:
430
- format_ = format_.replace("{asctime}", "{zoned_datetime}")
431
- datefmt = maybe_sub_pct_y("%Y-%m-%d %H:%M:%S")
432
- match obj:
433
- case None:
434
- basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
435
- case Logger() as logger:
436
- logger.setLevel(level)
437
- logger.addHandler(handler := StreamHandler())
438
- basic_config(
439
- obj=handler,
440
- format_=format_,
441
- whenever=whenever,
442
- level=level,
443
- plain=plain,
444
- )
445
- case str() as name:
446
- basic_config(
447
- obj=get_logger(logger=name),
448
- format_=format_,
449
- whenever=whenever,
450
- level=level,
451
- plain=plain,
452
- )
453
- case Handler() as handler:
454
- handler.setLevel(level)
455
- if plain:
456
- formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
457
- else:
458
- try:
459
- from coloredlogs import ColoredFormatter
460
- except ModuleNotFoundError: # pragma: no cover
461
- formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
462
- else:
463
- formatter = ColoredFormatter(
464
- fmt=format_, datefmt=datefmt, style="{"
465
- )
466
- handler.setFormatter(formatter)
467
- case _ as never:
468
- assert_never(never)
469
-
470
-
471
- ##
472
-
473
-
474
- def filter_for_key(
475
- key: str, /, *, default: bool = False
476
- ) -> Callable[[LogRecord], bool]:
477
- """Make a filter for a given attribute."""
478
- if (key in _FILTER_FOR_KEY_BLACKLIST) or key.startswith("__"):
479
- raise FilterForKeyError(key=key)
480
-
481
- def filter_(record: LogRecord, /) -> bool:
482
- try:
483
- value = getattr(record, key)
484
- except AttributeError:
485
- return default
486
- return bool(value)
487
-
488
- return filter_
489
-
490
-
491
- # fmt: off
492
- _FILTER_FOR_KEY_BLACKLIST = {
493
- "args", "created", "exc_info", "exc_text", "filename", "funcName", "getMessage", "levelname", "levelno", "lineno", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "taskName", "thread", "threadName"
494
- }
495
- # fmt: on
496
-
497
-
498
- @dataclass(kw_only=True, slots=True)
499
- class FilterForKeyError(Exception):
500
- key: str
501
-
502
- @override
503
- def __str__(self) -> str:
504
- return f"Invalid key: {self.key!r}"
505
-
506
-
507
- ##
508
-
509
-
510
- def get_default_logging_path() -> Path:
511
- """Get the logging default path."""
512
- return get_root().joinpath(".logs")
513
-
514
-
515
- ##
516
-
517
-
518
- def get_logger(*, logger: LoggerOrName | None = None) -> Logger:
519
- """Get a logger."""
520
- match logger:
521
- case Logger():
522
- return logger
523
- case str() | None:
524
- return getLogger(logger)
525
- case _ as never:
526
- assert_never(never)
527
-
528
-
529
- ##
530
-
531
-
532
- def get_logging_level_number(level: LogLevel, /) -> int:
533
- """Get the logging level number."""
534
- mapping = getLevelNamesMapping()
535
- try:
536
- return mapping[level]
537
- except KeyError:
538
- raise GetLoggingLevelNumberError(level=level) from None
539
-
540
-
541
- @dataclass(kw_only=True, slots=True)
542
- class GetLoggingLevelNumberError(Exception):
543
- level: LogLevel
544
-
545
- @override
546
- def __str__(self) -> str:
547
- return f"Invalid logging level: {self.level!r}"
548
-
549
-
550
- ##
551
-
552
-
553
- def setup_logging(
554
- *,
555
- logger: LoggerOrName | None = None,
556
- console_level: LogLevel | None = "INFO",
557
- console_filters: Iterable[_FilterType] | None = None,
558
- console_fmt: str = "❯ {_zoned_datetime_str} | {name}:{funcName}:{lineno} | {message}", # noqa: RUF001
559
- files_dir: MaybeCallablePathLike | None = get_default_logging_path,
560
- files_when: _When = _WHEN,
561
- files_interval: int = 1,
562
- files_backup_count: int = _BACKUP_COUNT,
563
- files_max_bytes: int = _MAX_BYTES,
564
- files_filters: Iterable[_FilterType] | None = None,
565
- files_fmt: str = "{_zoned_datetime_str} | {name}:{funcName}:{lineno} | {levelname:8} | {message}",
566
- filters: MaybeIterable[_FilterType] | None = None,
567
- formatter_version: MaybeCallableVersionLike | None = None,
568
- formatter_max_width: int = RICH_MAX_WIDTH,
569
- formatter_indent_size: int = RICH_INDENT_SIZE,
570
- formatter_max_length: int | None = RICH_MAX_LENGTH,
571
- formatter_max_string: int | None = RICH_MAX_STRING,
572
- formatter_max_depth: int | None = RICH_MAX_DEPTH,
573
- formatter_expand_all: bool = RICH_EXPAND_ALL,
574
- extra: Callable[[LoggerOrName | None], None] | None = None,
575
- ) -> None:
576
- """Set up logger."""
577
- # log record factory
578
- from utilities.tzlocal import get_local_time_zone # skipif-ci-and-windows
579
-
580
- class LogRecordNanoLocal( # skipif-ci-and-windows
581
- _AdvancedLogRecord, time_zone=get_local_time_zone()
582
- ): ...
583
-
584
- setLogRecordFactory(LogRecordNanoLocal) # skipif-ci-and-windows
585
-
586
- console_fmt, files_fmt = [ # skipif-ci-and-windows
587
- f.replace("{_zoned_datetime_str}", LogRecordNanoLocal.get_zoned_datetime_fmt())
588
- for f in [console_fmt, files_fmt]
589
- ]
590
-
591
- # logger
592
- logger_use = get_logger(logger=logger) # skipif-ci-and-windows
593
- logger_use.setLevel(DEBUG) # skipif-ci-and-windows
594
-
595
- # filters
596
- console_filters = ( # skipif-ci-and-windows
597
- [] if console_filters is None else list(console_filters)
598
- )
599
- files_filters = ( # skipif-ci-and-windows
600
- [] if files_filters is None else list(files_filters)
601
- )
602
- filters = ( # skipif-ci-and-windows
603
- [] if filters is None else list(always_iterable(filters))
604
- )
605
-
606
- # formatters
607
- try: # skipif-ci-and-windows
608
- from coloredlogs import DEFAULT_FIELD_STYLES, ColoredFormatter
609
- except ModuleNotFoundError: # pragma: no cover
610
- console_formatter = Formatter(fmt=console_fmt, style="{")
611
- files_formatter = Formatter(fmt=files_fmt, style="{")
612
- else: # skipif-ci-and-windows
613
- field_styles = DEFAULT_FIELD_STYLES | {
614
- "_zoned_datetime_str": DEFAULT_FIELD_STYLES["asctime"]
615
- }
616
- console_formatter = ColoredFormatter(
617
- fmt=console_fmt, style="{", field_styles=field_styles
618
- )
619
- files_formatter = ColoredFormatter(
620
- fmt=files_fmt, style="{", field_styles=field_styles
621
- )
622
- plain_formatter = Formatter(fmt=files_fmt, style="{") # skipif-ci-and-windows
623
-
624
- # console
625
- if console_level is not None: # skipif-ci-and-windows
626
- console_low_or_no_exc_handler = StreamHandler(stream=stdout)
627
- add_filters(console_low_or_no_exc_handler, _console_low_or_no_exc_filter)
628
- add_filters(console_low_or_no_exc_handler, *console_filters)
629
- add_filters(console_low_or_no_exc_handler, *filters)
630
- console_low_or_no_exc_handler.setFormatter(console_formatter)
631
- console_low_or_no_exc_handler.setLevel(console_level)
632
- logger_use.addHandler(console_low_or_no_exc_handler)
633
-
634
- console_high_and_exc_handler = StreamHandler(stream=stdout)
635
- add_filters(console_high_and_exc_handler, *console_filters)
636
- add_filters(console_high_and_exc_handler, *filters)
637
- _ = RichTracebackFormatter.create_and_set(
638
- console_high_and_exc_handler,
639
- version=formatter_version,
640
- max_width=formatter_max_width,
641
- indent_size=formatter_indent_size,
642
- max_length=formatter_max_length,
643
- max_string=formatter_max_string,
644
- max_depth=formatter_max_depth,
645
- expand_all=formatter_expand_all,
646
- detail=True,
647
- post=_ansi_wrap_red,
648
- )
649
- console_high_and_exc_handler.setLevel(
650
- max(get_logging_level_number(console_level), ERROR)
651
- )
652
- logger_use.addHandler(console_high_and_exc_handler)
653
-
654
- # debug & info
655
- directory = get_path(path=files_dir) # skipif-ci-and-windows
656
- levels: list[LogLevel] = ["DEBUG", "INFO"] # skipif-ci-and-windows
657
- for level, (subpath, files_or_plain_formatter) in product( # skipif-ci-and-windows
658
- levels, [(Path(), files_formatter), (Path("plain"), plain_formatter)]
659
- ):
660
- path = ensure_suffix(directory.joinpath(subpath, level.lower()), ".txt")
661
- path.parent.mkdir(parents=True, exist_ok=True)
662
- file_handler = SizeAndTimeRotatingFileHandler(
663
- filename=path,
664
- when=files_when,
665
- interval=files_interval,
666
- backupCount=files_backup_count,
667
- maxBytes=files_max_bytes,
668
- )
669
- add_filters(file_handler, *files_filters)
670
- add_filters(file_handler, *filters)
671
- file_handler.setFormatter(files_or_plain_formatter)
672
- file_handler.setLevel(level)
673
- logger_use.addHandler(file_handler)
674
-
675
- # errors
676
- standalone_file_handler = StandaloneFileHandler( # skipif-ci-and-windows
677
- level=ERROR, path=directory.joinpath("errors")
678
- )
679
- add_filters(standalone_file_handler, _standalone_file_filter)
680
- standalone_file_handler.setFormatter(
681
- RichTracebackFormatter(
682
- version=formatter_version,
683
- max_width=formatter_max_width,
684
- indent_size=formatter_indent_size,
685
- max_length=formatter_max_length,
686
- max_string=formatter_max_string,
687
- max_depth=formatter_max_depth,
688
- expand_all=formatter_expand_all,
689
- detail=True,
690
- )
691
- )
692
- logger_use.addHandler(standalone_file_handler) # skipif-ci-and-windows
693
-
694
- # extra
695
- if extra is not None: # skipif-ci-and-windows
696
- extra(logger_use)
697
-
698
-
699
- def _console_low_or_no_exc_filter(record: LogRecord, /) -> bool:
700
- return (record.levelno < ERROR) or (
701
- (record.levelno >= ERROR) and (record.exc_info is None)
702
- )
703
-
704
-
705
- def _standalone_file_filter(record: LogRecord, /) -> bool:
706
- return record.exc_info is not None
707
-
708
-
709
- ##
710
-
711
-
712
- @contextmanager
713
- def temp_handler(
714
- handler: Handler, /, *, logger: LoggerOrName | None = None
715
- ) -> Iterator[None]:
716
- """Context manager with temporary handler set."""
717
- logger_use = get_logger(logger=logger)
718
- logger_use.addHandler(handler)
719
- try:
720
- yield
721
- finally:
722
- _ = logger_use.removeHandler(handler)
723
-
724
-
725
- ##
726
-
727
-
728
- @contextmanager
729
- def temp_logger(
730
- logger: LoggerOrName,
731
- /,
732
- *,
733
- disabled: bool | None = None,
734
- level: LogLevel | None = None,
735
- propagate: bool | None = None,
736
- ) -> Iterator[Logger]:
737
- """Context manager with temporary logger settings."""
738
- logger_use = get_logger(logger=logger)
739
- init_disabled = logger_use.disabled
740
- init_level = logger_use.level
741
- init_propagate = logger_use.propagate
742
- if disabled is not None:
743
- logger_use.disabled = disabled
744
- if level is not None:
745
- logger_use.setLevel(level)
746
- if propagate is not None:
747
- logger_use.propagate = propagate
748
- try:
749
- yield logger_use
750
- finally:
751
- if disabled is not None:
752
- logger_use.disabled = init_disabled
753
- if level is not None:
754
- logger_use.setLevel(init_level)
755
- if propagate is not None:
756
- logger_use.propagate = init_propagate
757
-
758
-
759
- ##
760
-
761
-
762
- class _AdvancedLogRecord(LogRecord):
763
- """Advanced log record."""
764
-
765
- time_zone: ClassVar[str] = NotImplemented
766
-
767
- @override
768
- def __init__(
769
- self,
770
- name: str,
771
- level: int,
772
- pathname: str,
773
- lineno: int,
774
- msg: object,
775
- args: Any,
776
- exc_info: Any,
777
- func: str | None = None,
778
- sinfo: str | None = None,
779
- ) -> None:
780
- self._zoned_datetime = self.get_now() # skipif-ci-and-windows
781
- self._zoned_datetime_str = ( # skipif-ci-and-windows
782
- self._zoned_datetime.format_common_iso()
783
- )
784
- super().__init__( # skipif-ci-and-windows
785
- name, level, pathname, lineno, msg, args, exc_info, func, sinfo
786
- )
787
-
788
- @override
789
- def __init_subclass__(cls, *, time_zone: ZoneInfo, **kwargs: Any) -> None:
790
- cls.time_zone = time_zone.key # skipif-ci-and-windows
791
- super().__init_subclass__(**kwargs) # skipif-ci-and-windows
792
-
793
- @classmethod
794
- def get_now(cls) -> Any:
795
- """Get the current zoned datetime."""
796
- return cast("Any", ZonedDateTime).now(cls.time_zone) # skipif-ci-and-windows
797
-
798
- @classmethod
799
- def get_zoned_datetime_fmt(cls) -> str:
800
- """Get the zoned datetime format string."""
801
- length = len(cls.get_now().format_common_iso()) # skipif-ci-and-windows
802
- return f"{{_zoned_datetime_str:{length}}}" # skipif-ci-and-windows
803
-
804
-
805
- ##
806
-
807
-
808
- def _ansi_wrap_red(text: str, /) -> str:
809
- try:
810
- from humanfriendly.terminal import ansi_wrap
811
- except ModuleNotFoundError: # pragma: no cover
812
- return text
813
- return ansi_wrap(text, color="red")
595
+ return self.file.replace(index=self.index, start=self.start, end=self.end).path
814
596
 
815
597
 
816
598
  __all__ = [
817
599
  "FilterForKeyError",
818
600
  "GetLoggingLevelNumberError",
819
601
  "SizeAndTimeRotatingFileHandler",
820
- "StandaloneFileHandler",
821
602
  "add_filters",
822
603
  "basic_config",
823
604
  "filter_for_key",
824
- "get_default_logging_path",
825
605
  "get_logger",
826
606
  "get_logging_level_number",
827
607
  "setup_logging",
828
- "temp_handler",
829
- "temp_logger",
830
608
  ]