dycw-utilities 0.129.6__py3-none-any.whl → 0.129.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.129.6
3
+ Version: 0.129.8
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -93,12 +93,11 @@ Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-iterables'
93
93
  Requires-Dist: whenever<0.9,>=0.8.4; extra == 'zzz-test-iterables'
94
94
  Provides-Extra: zzz-test-jupyter
95
95
  Requires-Dist: jupyterlab<4.3,>=4.2.0; extra == 'zzz-test-jupyter'
96
- Requires-Dist: pandas<2.3,>=2.2.2; extra == 'zzz-test-jupyter'
96
+ Requires-Dist: pandas<2.4,>=2.3.0; extra == 'zzz-test-jupyter'
97
97
  Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-jupyter'
98
98
  Provides-Extra: zzz-test-logging
99
99
  Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-logging'
100
100
  Requires-Dist: coloredlogs<15.1,>=15.0.1; extra == 'zzz-test-logging'
101
- Requires-Dist: concurrent-log-handler<0.10,>=0.9.26; extra == 'zzz-test-logging'
102
101
  Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-logging'
103
102
  Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-logging'
104
103
  Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-logging'
@@ -173,7 +172,7 @@ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
173
172
  Provides-Extra: zzz-test-sentinel
174
173
  Provides-Extra: zzz-test-shelve
175
174
  Provides-Extra: zzz-test-slack-sdk
176
- Requires-Dist: aiohttp<3.12.8,>=3.12.7; extra == 'zzz-test-slack-sdk'
175
+ Requires-Dist: aiohttp<3.12.10,>=3.12.9; extra == 'zzz-test-slack-sdk'
177
176
  Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
178
177
  Provides-Extra: zzz-test-socket
179
178
  Provides-Extra: zzz-test-sqlalchemy
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=Odk_y5HDH2OquSZJAdWdoTST_nZpx27fTwquoNg32dI,60
1
+ utilities/__init__.py,sha256=nud8TbHJzm02w0ycVV5vCV-F8CpfmCs3mB9e6p4DrdQ,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=3n5EIcSq2xtEF1i4oR0oY2JmBq3NyugeHKFK39Mt22s,37987
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -29,7 +29,7 @@ utilities/iterables.py,sha256=mDqw2_0MUVp-P8FklgcaVTi2TXduH0MxbhTDzzhSBho,44915
29
29
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
30
30
  utilities/libcst.py,sha256=Jto5ppzRzsxn4AD32IS8n0lbgLYXwsVJB6EY8giNZyY,4974
31
31
  utilities/lightweight_charts.py,sha256=0xNfcsrgFI0R9xL25LtSm-W5yhfBI93qQNT6HyaXAhg,2769
32
- utilities/logging.py,sha256=9vo6vz4sDMoF2nkc8eT-eWpymBb5QboIu0kMhEwPiqk,25206
32
+ utilities/logging.py,sha256=dA54i2gmULBLuEJ2roGYWt9pW2NvNBmx0YxlMns347M,26126
33
33
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
34
34
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
35
35
  utilities/math.py,sha256=-mQgbah-dPJwOEWf3SonrFoVZ2AVxMgpeQ3dfVa-oJA,26764
@@ -78,7 +78,7 @@ utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
78
78
  utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
79
79
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
80
80
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
81
- utilities/traceback.py,sha256=Jg7HS3AwQ-W-msdwHp22_PSHZcR54PbmsSf115B6TSM,27435
81
+ utilities/traceback.py,sha256=Rf_4XIz6AQaBcTRr7Tiw7RCIv_O_bN7Hd-Cnr8SPXN4,28920
82
82
  utilities/types.py,sha256=gP04CcCOyFrG7BgblVCsrrChiuO2x842NDVW-GF7odo,18370
83
83
  utilities/typing.py,sha256=H6ysJkI830aRwLsMKz0SZIw4cpcsm7d6KhQOwr-SDh0,13817
84
84
  utilities/tzdata.py,sha256=yCf70NICwAeazN3_JcXhWvRqCy06XJNQ42j7r6gw3HY,1217
@@ -86,10 +86,10 @@ utilities/tzlocal.py,sha256=3upDNFBvGh1l9njmLR2z2S6K6VxQSb7QizYGUbAH3JU,960
86
86
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
87
87
  utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
88
88
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
89
- utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
89
+ utilities/whenever.py,sha256=QbXgFAPuUL7PCp2hajmIP-FFIfIR1J6Y0TxJbeoj60I,18434
90
90
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
91
91
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
92
- dycw_utilities-0.129.6.dist-info/METADATA,sha256=tC_nJcnk-38KjYuNvAhfUg4mKnytwE71-7ndZfBs0bQ,12803
93
- dycw_utilities-0.129.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- dycw_utilities-0.129.6.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
- dycw_utilities-0.129.6.dist-info/RECORD,,
92
+ dycw_utilities-0.129.8.dist-info/METADATA,sha256=JaBYjbXepIOBDjYciDSC1_bSCE8VlzxhpOPAs22-nMI,12723
93
+ dycw_utilities-0.129.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ dycw_utilities-0.129.8.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.129.8.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.129.6"
3
+ __version__ = "0.129.8"
utilities/logging.py CHANGED
@@ -107,8 +107,9 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
107
107
  utc: bool = False,
108
108
  atTime: dt.time | None = None,
109
109
  ) -> None:
110
- filename = str(Path(filename))
111
- super().__init__(filename, mode, encoding=encoding, delay=delay, errors=errors)
110
+ path = Path(filename)
111
+ path.parent.mkdir(parents=True, exist_ok=True)
112
+ super().__init__(path, mode, encoding=encoding, delay=delay, errors=errors)
112
113
  self._max_bytes = maxBytes if maxBytes >= 1 else None
113
114
  self._backup_count = backupCount if backupCount >= 1 else None
114
115
  self._filename = Path(self.baseFilename)
@@ -117,7 +118,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
117
118
  self._suffix = self._filename.suffix
118
119
  self._patterns = _compute_rollover_patterns(self._stem, self._suffix)
119
120
  self._time_handler = TimedRotatingFileHandler(
120
- filename,
121
+ path,
121
122
  when=when,
122
123
  interval=interval,
123
124
  backupCount=backupCount,
@@ -415,26 +416,53 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
415
416
 
416
417
  def basic_config(
417
418
  *,
418
- logger: LoggerOrName | None = None,
419
+ obj: LoggerOrName | Handler | None = None,
419
420
  format_: str = "{asctime} | {name} | {levelname:8} | {message}",
421
+ whenever: bool = False,
420
422
  level: LogLevel = "INFO",
423
+ plain: bool = False,
421
424
  ) -> None:
422
425
  """Do the basic config."""
426
+ if whenever:
427
+ format_ = format_.replace("{asctime}", "{zoned_datetime}")
423
428
  datefmt = maybe_sub_pct_y("%Y-%m-%d %H:%M:%S")
424
- if logger is None:
425
- basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
426
- else:
427
- logger_use = get_logger(logger=logger)
428
- logger_use.setLevel(level)
429
- logger_use.addHandler(handler := StreamHandler())
430
- handler.setLevel(level)
431
- try:
432
- from coloredlogs import ColoredFormatter
433
- except ModuleNotFoundError: # pragma: no cover
434
- formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
435
- else:
436
- formatter = ColoredFormatter(fmt=format_, datefmt=datefmt, style="{")
437
- handler.setFormatter(formatter)
429
+ match obj:
430
+ case None:
431
+ basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
432
+ case Logger() as logger:
433
+ logger.setLevel(level)
434
+ logger.addHandler(handler := StreamHandler())
435
+ basic_config(
436
+ obj=handler,
437
+ format_=format_,
438
+ whenever=whenever,
439
+ level=level,
440
+ plain=plain,
441
+ )
442
+ case str() as name:
443
+ basic_config(
444
+ obj=get_logger(logger=name),
445
+ format_=format_,
446
+ whenever=whenever,
447
+ level=level,
448
+ plain=plain,
449
+ )
450
+ case Handler() as handler:
451
+ handler.setLevel(level)
452
+ if plain:
453
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
454
+ else:
455
+ try:
456
+ from coloredlogs import ColoredFormatter
457
+ except ModuleNotFoundError: # pragma: no cover
458
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
459
+ else:
460
+ formatter = ColoredFormatter(
461
+ fmt=format_, datefmt=datefmt, style="{"
462
+ )
463
+ handler.setFormatter(formatter)
464
+ case _ as never:
465
+ assert_never(never)
438
466
 
439
467
 
440
468
  ##
utilities/traceback.py CHANGED
@@ -26,6 +26,7 @@ from typing import (
26
26
  runtime_checkable,
27
27
  )
28
28
 
29
+ from utilities.datetime import get_datetime, get_now
29
30
  from utilities.errors import ImpossibleCaseError
30
31
  from utilities.functions import (
31
32
  ensure_not_none,
@@ -45,8 +46,9 @@ from utilities.reprlib import (
45
46
  yield_call_args_repr,
46
47
  yield_mapping_repr,
47
48
  )
48
- from utilities.types import TBaseException, TCallable
49
+ from utilities.types import MaybeCallableDateTime, TBaseException, TCallable
49
50
  from utilities.version import get_version
51
+ from utilities.whenever import serialize_duration
50
52
 
51
53
  if TYPE_CHECKING:
52
54
  from collections.abc import Callable, Iterable, Iterator
@@ -60,6 +62,7 @@ if TYPE_CHECKING:
60
62
  _T = TypeVar("_T")
61
63
  _CALL_ARGS = "_CALL_ARGS"
62
64
  _INDENT = 4 * " "
65
+ _START = get_now()
63
66
 
64
67
 
65
68
  ##
@@ -78,6 +81,7 @@ class RichTracebackFormatter(Formatter):
78
81
  /,
79
82
  *,
80
83
  defaults: StrMapping | None = None,
84
+ start: MaybeCallableDateTime | None = _START,
81
85
  version: MaybeCallableVersionLike | None = None,
82
86
  max_width: int = RICH_MAX_WIDTH,
83
87
  indent_size: int = RICH_INDENT_SIZE,
@@ -89,7 +93,8 @@ class RichTracebackFormatter(Formatter):
89
93
  post: Callable[[str], str] | None = None,
90
94
  ) -> None:
91
95
  super().__init__(fmt, datefmt, style, validate, defaults=defaults)
92
- self._version = version
96
+ self._start = get_datetime(datetime=start)
97
+ self._version = get_version(version=version)
93
98
  self._max_width = max_width
94
99
  self._indent_size = indent_size
95
100
  self._max_length = max_length
@@ -110,6 +115,7 @@ class RichTracebackFormatter(Formatter):
110
115
  exc_value = ensure_not_none(exc_value, desc="exc_value")
111
116
  error = get_rich_traceback(
112
117
  exc_value,
118
+ start=self._start,
113
119
  version=self._version,
114
120
  max_width=self._max_width,
115
121
  indent_size=self._indent_size,
@@ -263,6 +269,7 @@ class ExcChainTB(Generic[TBaseException]):
263
269
  errors: list[
264
270
  ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
265
271
  ] = field(default_factory=list)
272
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
266
273
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
267
274
  max_width: int = RICH_MAX_WIDTH
268
275
  indent_size: int = RICH_INDENT_SIZE
@@ -292,7 +299,7 @@ class ExcChainTB(Generic[TBaseException]):
292
299
  """Format the traceback."""
293
300
  lines: list[str] = []
294
301
  if header: # pragma: no cover
295
- lines.extend(_yield_header_lines(version=self.version))
302
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
296
303
  total = len(self.errors)
297
304
  for i, errors in enumerate(self.errors, start=1):
298
305
  lines.append(f"Exception chain {i}/{total}:")
@@ -315,6 +322,7 @@ class ExcGroupTB(Generic[TBaseException]):
315
322
  errors: list[
316
323
  ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
317
324
  ] = field(default_factory=list)
325
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
318
326
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
319
327
  max_width: int = RICH_MAX_WIDTH
320
328
  indent_size: int = RICH_INDENT_SIZE
@@ -333,7 +341,7 @@ class ExcGroupTB(Generic[TBaseException]):
333
341
  """Format the traceback."""
334
342
  lines: list[str] = [] # skipif-ci
335
343
  if header: # pragma: no cover
336
- lines.extend(_yield_header_lines(version=self.version))
344
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
337
345
  lines.append("Exception group:") # skipif-ci
338
346
  match self.exc_group: # skipif-ci
339
347
  case ExcTB() as exc_tb:
@@ -363,6 +371,7 @@ class ExcTB(Generic[TBaseException]):
363
371
 
364
372
  frames: list[_Frame] = field(default_factory=list)
365
373
  error: TBaseException
374
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
366
375
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
367
376
  max_width: int = RICH_MAX_WIDTH
368
377
  indent_size: int = RICH_INDENT_SIZE
@@ -391,7 +400,7 @@ class ExcTB(Generic[TBaseException]):
391
400
  total = len(self)
392
401
  lines: list[str] = []
393
402
  if header: # pragma: no cover
394
- lines.extend(_yield_header_lines(version=self.version))
403
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
395
404
  for i, frame in enumerate(self.frames):
396
405
  is_head = i < total - 1
397
406
  lines.append(
@@ -485,6 +494,7 @@ def get_rich_traceback(
485
494
  error: TBaseException,
486
495
  /,
487
496
  *,
497
+ start: MaybeCallableDateTime | None = _START,
488
498
  version: MaybeCallableVersionLike | None = None,
489
499
  max_width: int = RICH_MAX_WIDTH,
490
500
  indent_size: int = RICH_INDENT_SIZE,
@@ -506,6 +516,7 @@ def get_rich_traceback(
506
516
  err_recast = cast("TBaseException", err)
507
517
  return _get_rich_traceback_non_chain(
508
518
  err_recast,
519
+ start=start,
509
520
  version=version,
510
521
  max_width=max_width,
511
522
  indent_size=indent_size,
@@ -520,6 +531,7 @@ def get_rich_traceback(
520
531
  errors=[
521
532
  _get_rich_traceback_non_chain(
522
533
  e,
534
+ start=start,
523
535
  version=version,
524
536
  max_width=max_width,
525
537
  indent_size=indent_size,
@@ -530,6 +542,7 @@ def get_rich_traceback(
530
542
  )
531
543
  for e in errs_recast
532
544
  ],
545
+ start=start,
533
546
  version=version,
534
547
  max_width=max_width,
535
548
  indent_size=indent_size,
@@ -544,6 +557,7 @@ def _get_rich_traceback_non_chain(
544
557
  error: ExceptionGroup[Any] | TBaseException,
545
558
  /,
546
559
  *,
560
+ start: MaybeCallableDateTime | None = _START,
547
561
  version: MaybeCallableVersionLike | None = None,
548
562
  max_width: int = RICH_MAX_WIDTH,
549
563
  indent_size: int = RICH_INDENT_SIZE,
@@ -567,6 +581,7 @@ def _get_rich_traceback_non_chain(
567
581
  errors = [
568
582
  _get_rich_traceback_non_chain(
569
583
  e,
584
+ start=start,
570
585
  version=version,
571
586
  max_width=max_width,
572
587
  indent_size=indent_size,
@@ -580,6 +595,7 @@ def _get_rich_traceback_non_chain(
580
595
  return ExcGroupTB(
581
596
  exc_group=exc_group_or_exc_tb,
582
597
  errors=errors,
598
+ start=start,
583
599
  version=version,
584
600
  max_width=max_width,
585
601
  indent_size=indent_size,
@@ -591,6 +607,7 @@ def _get_rich_traceback_non_chain(
591
607
  case BaseException() as base_exc:
592
608
  return _get_rich_traceback_base_one(
593
609
  base_exc,
610
+ start=start,
594
611
  version=version,
595
612
  max_width=max_width,
596
613
  indent_size=indent_size,
@@ -607,6 +624,7 @@ def _get_rich_traceback_base_one(
607
624
  error: TBaseException,
608
625
  /,
609
626
  *,
627
+ start: MaybeCallableDateTime | None = _START,
610
628
  version: MaybeCallableVersionLike | None = None,
611
629
  max_width: int = RICH_MAX_WIDTH,
612
630
  indent_size: int = RICH_INDENT_SIZE,
@@ -638,6 +656,7 @@ def _get_rich_traceback_base_one(
638
656
  return ExcTB(
639
657
  frames=frames,
640
658
  error=error,
659
+ start=start,
641
660
  version=version,
642
661
  max_width=max_width,
643
662
  indent_size=indent_size,
@@ -793,13 +812,25 @@ def _merge_frames(
793
812
 
794
813
 
795
814
  def _yield_header_lines(
796
- *, version: MaybeCallableVersionLike | None = None
815
+ *,
816
+ start: MaybeCallableDateTime | None = _START,
817
+ version: MaybeCallableVersionLike | None = None,
797
818
  ) -> Iterator[str]:
798
819
  """Yield the header lines."""
799
- from utilities.tzlocal import get_now_local
820
+ from utilities.tzlocal import get_local_time_zone, get_now_local
800
821
  from utilities.whenever import serialize_zoned_datetime
801
822
 
802
- yield f"Date/time | {serialize_zoned_datetime(get_now_local())}"
823
+ now = get_now_local()
824
+ start_use = get_datetime(datetime=start)
825
+ start_use = (
826
+ None if start_use is None else start_use.astimezone(get_local_time_zone())
827
+ )
828
+ yield f"Date/time | {serialize_zoned_datetime(now)}"
829
+ start_str = "" if start_use is None else serialize_zoned_datetime(start_use)
830
+ yield f"Started | {start_str}"
831
+ duration = None if start_use is None else (now - start_use)
832
+ duration_str = "" if duration is None else serialize_duration(duration)
833
+ yield f"Duration | {duration_str}"
803
834
  yield f"User | {getuser()}"
804
835
  yield f"Host | {gethostname()}"
805
836
  version_use = "" if version is None else get_version(version=version)
utilities/whenever.py CHANGED
@@ -4,7 +4,9 @@ import datetime as dt
4
4
  import re
5
5
  from contextlib import suppress
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, override
7
+ from functools import cache
8
+ from logging import LogRecord
9
+ from typing import TYPE_CHECKING, Any, override
8
10
 
9
11
  from whenever import (
10
12
  Date,
@@ -33,6 +35,8 @@ from utilities.re import (
33
35
  from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
34
36
 
35
37
  if TYPE_CHECKING:
38
+ from zoneinfo import ZoneInfo
39
+
36
40
  from utilities.types import (
37
41
  DateLike,
38
42
  DateTimeLike,
@@ -561,6 +565,64 @@ class SerializeZonedDateTimeError(Exception):
561
565
  ##
562
566
 
563
567
 
568
+ class WheneverLogRecord(LogRecord):
569
+ """Log record powered by `whenever`."""
570
+
571
+ zoned_datetime: str
572
+
573
+ @override
574
+ def __init__(
575
+ self,
576
+ name: str,
577
+ level: int,
578
+ pathname: str,
579
+ lineno: int,
580
+ msg: object,
581
+ args: Any,
582
+ exc_info: Any,
583
+ func: str | None = None,
584
+ sinfo: str | None = None,
585
+ ) -> None:
586
+ super().__init__(
587
+ name, level, pathname, lineno, msg, args, exc_info, func, sinfo
588
+ )
589
+ length = self._get_length()
590
+ plain = format(self._get_now().to_plain().format_common_iso(), f"{length}s")
591
+ time_zone = self._get_time_zone_key()
592
+ self.zoned_datetime = f"{plain}[{time_zone}]"
593
+
594
+ @classmethod
595
+ @cache
596
+ def _get_time_zone(cls) -> ZoneInfo:
597
+ """Get the local timezone."""
598
+ try:
599
+ from utilities.tzlocal import get_local_time_zone
600
+ except ModuleNotFoundError: # pragma: no cover
601
+ return UTC
602
+ return get_local_time_zone()
603
+
604
+ @classmethod
605
+ @cache
606
+ def _get_time_zone_key(cls) -> str:
607
+ """Get the local timezone as a string."""
608
+ return cls._get_time_zone().key
609
+
610
+ @classmethod
611
+ @cache
612
+ def _get_length(cls) -> int:
613
+ """Get maximum length of a formatted string."""
614
+ now = cls._get_now().replace(nanosecond=1000).to_plain()
615
+ return len(now.format_common_iso())
616
+
617
+ @classmethod
618
+ def _get_now(cls) -> ZonedDateTime:
619
+ """Get the current zoned datetime."""
620
+ return ZonedDateTime.now(cls._get_time_zone().key)
621
+
622
+
623
+ ##
624
+
625
+
564
626
  def _to_datetime_delta(timedelta: dt.timedelta, /) -> DateTimeDelta:
565
627
  """Serialize a timedelta."""
566
628
  total_microseconds = datetime_duration_to_microseconds(timedelta)
@@ -610,6 +672,7 @@ __all__ = [
610
672
  "SerializePlainDateTimeError",
611
673
  "SerializeTimeDeltaError",
612
674
  "SerializeZonedDateTimeError",
675
+ "WheneverLogRecord",
613
676
  "check_valid_zoned_datetime",
614
677
  "ensure_date",
615
678
  "ensure_datetime",