dycw-utilities 0.127.0__py3-none-any.whl → 0.129.13__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/traceback.py CHANGED
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
4
+ import sys
5
+ from asyncio import run
3
6
  from collections.abc import Callable, Iterable
4
7
  from dataclasses import dataclass, field, replace
5
- from functools import wraps
8
+ from functools import partial, wraps
6
9
  from getpass import getuser
7
10
  from inspect import iscoroutinefunction, signature
11
+ from itertools import repeat
8
12
  from logging import Formatter, Handler, LogRecord
9
13
  from pathlib import Path
10
14
  from socket import gethostname
@@ -26,7 +30,8 @@ from typing import (
26
30
  runtime_checkable,
27
31
  )
28
32
 
29
- from utilities.errors import ImpossibleCaseError
33
+ from utilities.datetime import get_datetime, get_now, serialize_compact
34
+ from utilities.errors import ImpossibleCaseError, repr_error
30
35
  from utilities.functions import (
31
36
  ensure_not_none,
32
37
  ensure_str,
@@ -34,7 +39,8 @@ from utilities.functions import (
34
39
  get_func_name,
35
40
  get_func_qualname,
36
41
  )
37
- from utilities.iterables import always_iterable, one
42
+ from utilities.iterables import OneEmptyError, always_iterable, one
43
+ from utilities.pathlib import get_path
38
44
  from utilities.reprlib import (
39
45
  RICH_EXPAND_ALL,
40
46
  RICH_INDENT_SIZE,
@@ -45,11 +51,18 @@ from utilities.reprlib import (
45
51
  yield_call_args_repr,
46
52
  yield_mapping_repr,
47
53
  )
48
- from utilities.types import TBaseException, TCallable
54
+ from utilities.types import (
55
+ MaybeCallableDateTime,
56
+ MaybeCallablePathLike,
57
+ PathLike,
58
+ TBaseException,
59
+ TCallable,
60
+ )
49
61
  from utilities.version import get_version
62
+ from utilities.whenever import serialize_duration
50
63
 
51
64
  if TYPE_CHECKING:
52
- from collections.abc import Callable, Iterable, Iterator
65
+ from collections.abc import Callable, Iterable, Iterator, Sequence
53
66
  from logging import _FormatStyle
54
67
  from types import FrameType, TracebackType
55
68
 
@@ -60,6 +73,132 @@ if TYPE_CHECKING:
60
73
  _T = TypeVar("_T")
61
74
  _CALL_ARGS = "_CALL_ARGS"
62
75
  _INDENT = 4 * " "
76
+ _START = get_now()
77
+
78
+
79
+ ##
80
+
81
+
82
+ def format_exception_stack(
83
+ error: BaseException,
84
+ /,
85
+ *,
86
+ header: bool = False,
87
+ start: MaybeCallableDateTime | None = _START,
88
+ version: MaybeCallableVersionLike | None = None,
89
+ capture_locals: bool = False,
90
+ max_width: int = RICH_MAX_WIDTH,
91
+ indent_size: int = RICH_INDENT_SIZE,
92
+ max_length: int | None = RICH_MAX_LENGTH,
93
+ max_string: int | None = RICH_MAX_STRING,
94
+ max_depth: int | None = RICH_MAX_DEPTH,
95
+ expand_all: bool = RICH_EXPAND_ALL,
96
+ ) -> str:
97
+ """Format an exception stack."""
98
+ lines: Sequence[str] = []
99
+ if header:
100
+ lines.extend(_yield_header_lines(start=start, version=version))
101
+ lines.extend(
102
+ _yield_formatted_frame_summary(
103
+ error,
104
+ capture_locals=capture_locals,
105
+ max_width=max_width,
106
+ indent_size=indent_size,
107
+ max_length=max_length,
108
+ max_string=max_string,
109
+ max_depth=max_depth,
110
+ expand_all=expand_all,
111
+ )
112
+ )
113
+ return "\n".join(lines)
114
+
115
+
116
+ ##
117
+
118
+
119
+ def make_except_hook(
120
+ *,
121
+ start: MaybeCallableDateTime | None = _START,
122
+ version: MaybeCallableVersionLike | None = None,
123
+ path: MaybeCallablePathLike | None = None,
124
+ max_width: int = RICH_MAX_WIDTH,
125
+ indent_size: int = RICH_INDENT_SIZE,
126
+ max_length: int | None = RICH_MAX_LENGTH,
127
+ max_string: int | None = RICH_MAX_STRING,
128
+ max_depth: int | None = RICH_MAX_DEPTH,
129
+ expand_all: bool = RICH_EXPAND_ALL,
130
+ slack_url: str | None = None,
131
+ ) -> Callable[
132
+ [type[BaseException] | None, BaseException | None, TracebackType | None], None
133
+ ]:
134
+ """Exception hook to log the traceback."""
135
+ return partial(
136
+ _make_except_hook_inner,
137
+ start=start,
138
+ version=version,
139
+ path=path,
140
+ max_width=max_width,
141
+ indent_size=indent_size,
142
+ max_length=max_length,
143
+ max_string=max_string,
144
+ max_depth=max_depth,
145
+ expand_all=expand_all,
146
+ slack_url=slack_url,
147
+ )
148
+
149
+
150
+ def _make_except_hook_inner(
151
+ exc_type: type[BaseException] | None,
152
+ exc_val: BaseException | None,
153
+ traceback: TracebackType | None,
154
+ /,
155
+ *,
156
+ start: MaybeCallableDateTime | None = _START,
157
+ version: MaybeCallableVersionLike | None = None,
158
+ path: MaybeCallablePathLike | None = None,
159
+ max_width: int = RICH_MAX_WIDTH,
160
+ indent_size: int = RICH_INDENT_SIZE,
161
+ max_length: int | None = RICH_MAX_LENGTH,
162
+ max_string: int | None = RICH_MAX_STRING,
163
+ max_depth: int | None = RICH_MAX_DEPTH,
164
+ expand_all: bool = RICH_EXPAND_ALL,
165
+ slack_url: str | None = None,
166
+ ) -> None:
167
+ """Exception hook to log the traceback."""
168
+ _ = (exc_type, traceback)
169
+ if exc_val is None:
170
+ raise MakeExceptHookError
171
+ slim = format_exception_stack(exc_val, header=True, start=start, version=version)
172
+ _ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
173
+ if path is not None:
174
+ from utilities.atomicwrites import writer
175
+ from utilities.tzlocal import get_now_local
176
+
177
+ path = (
178
+ get_path(path=path)
179
+ .joinpath(serialize_compact(get_now_local()))
180
+ .with_suffix(".txt")
181
+ )
182
+ full = format_exception_stack(
183
+ exc_val,
184
+ header=True,
185
+ start=start,
186
+ version=version,
187
+ capture_locals=True,
188
+ max_width=max_width,
189
+ indent_size=indent_size,
190
+ max_length=max_length,
191
+ max_string=max_string,
192
+ max_depth=max_depth,
193
+ expand_all=expand_all,
194
+ )
195
+ with writer(path, overwrite=True) as temp:
196
+ _ = temp.write_text(full)
197
+ if slack_url is not None: # pragma: no cover
198
+ from utilities.slack_sdk import send_to_slack
199
+
200
+ send = f"```{slim}```"
201
+ run(send_to_slack(slack_url, send))
63
202
 
64
203
 
65
204
  ##
@@ -78,6 +217,7 @@ class RichTracebackFormatter(Formatter):
78
217
  /,
79
218
  *,
80
219
  defaults: StrMapping | None = None,
220
+ start: MaybeCallableDateTime | None = _START,
81
221
  version: MaybeCallableVersionLike | None = None,
82
222
  max_width: int = RICH_MAX_WIDTH,
83
223
  indent_size: int = RICH_INDENT_SIZE,
@@ -89,7 +229,8 @@ class RichTracebackFormatter(Formatter):
89
229
  post: Callable[[str], str] | None = None,
90
230
  ) -> None:
91
231
  super().__init__(fmt, datefmt, style, validate, defaults=defaults)
92
- self._version = version
232
+ self._start = get_datetime(datetime=start)
233
+ self._version = get_version(version=version)
93
234
  self._max_width = max_width
94
235
  self._indent_size = indent_size
95
236
  self._max_length = max_length
@@ -110,6 +251,7 @@ class RichTracebackFormatter(Formatter):
110
251
  exc_value = ensure_not_none(exc_value, desc="exc_value")
111
252
  error = get_rich_traceback(
112
253
  exc_value,
254
+ start=self._start,
113
255
  version=self._version,
114
256
  max_width=self._max_width,
115
257
  indent_size=self._indent_size,
@@ -263,6 +405,7 @@ class ExcChainTB(Generic[TBaseException]):
263
405
  errors: list[
264
406
  ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
265
407
  ] = field(default_factory=list)
408
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
266
409
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
267
410
  max_width: int = RICH_MAX_WIDTH
268
411
  indent_size: int = RICH_INDENT_SIZE
@@ -292,7 +435,7 @@ class ExcChainTB(Generic[TBaseException]):
292
435
  """Format the traceback."""
293
436
  lines: list[str] = []
294
437
  if header: # pragma: no cover
295
- lines.extend(_yield_header_lines(version=self.version))
438
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
296
439
  total = len(self.errors)
297
440
  for i, errors in enumerate(self.errors, start=1):
298
441
  lines.append(f"Exception chain {i}/{total}:")
@@ -315,6 +458,7 @@ class ExcGroupTB(Generic[TBaseException]):
315
458
  errors: list[
316
459
  ExcGroupTB[TBaseException] | ExcTB[TBaseException] | TBaseException
317
460
  ] = field(default_factory=list)
461
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
318
462
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
319
463
  max_width: int = RICH_MAX_WIDTH
320
464
  indent_size: int = RICH_INDENT_SIZE
@@ -333,7 +477,7 @@ class ExcGroupTB(Generic[TBaseException]):
333
477
  """Format the traceback."""
334
478
  lines: list[str] = [] # skipif-ci
335
479
  if header: # pragma: no cover
336
- lines.extend(_yield_header_lines(version=self.version))
480
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
337
481
  lines.append("Exception group:") # skipif-ci
338
482
  match self.exc_group: # skipif-ci
339
483
  case ExcTB() as exc_tb:
@@ -363,6 +507,7 @@ class ExcTB(Generic[TBaseException]):
363
507
 
364
508
  frames: list[_Frame] = field(default_factory=list)
365
509
  error: TBaseException
510
+ start: MaybeCallableDateTime | None = field(default=_START, repr=False)
366
511
  version: MaybeCallableVersionLike | None = field(default=None, repr=False)
367
512
  max_width: int = RICH_MAX_WIDTH
368
513
  indent_size: int = RICH_INDENT_SIZE
@@ -391,7 +536,7 @@ class ExcTB(Generic[TBaseException]):
391
536
  total = len(self)
392
537
  lines: list[str] = []
393
538
  if header: # pragma: no cover
394
- lines.extend(_yield_header_lines(version=self.version))
539
+ lines.extend(_yield_header_lines(start=self.start, version=self.version))
395
540
  for i, frame in enumerate(self.frames):
396
541
  is_head = i < total - 1
397
542
  lines.append(
@@ -485,6 +630,7 @@ def get_rich_traceback(
485
630
  error: TBaseException,
486
631
  /,
487
632
  *,
633
+ start: MaybeCallableDateTime | None = _START,
488
634
  version: MaybeCallableVersionLike | None = None,
489
635
  max_width: int = RICH_MAX_WIDTH,
490
636
  indent_size: int = RICH_INDENT_SIZE,
@@ -506,6 +652,7 @@ def get_rich_traceback(
506
652
  err_recast = cast("TBaseException", err)
507
653
  return _get_rich_traceback_non_chain(
508
654
  err_recast,
655
+ start=start,
509
656
  version=version,
510
657
  max_width=max_width,
511
658
  indent_size=indent_size,
@@ -520,6 +667,7 @@ def get_rich_traceback(
520
667
  errors=[
521
668
  _get_rich_traceback_non_chain(
522
669
  e,
670
+ start=start,
523
671
  version=version,
524
672
  max_width=max_width,
525
673
  indent_size=indent_size,
@@ -530,6 +678,7 @@ def get_rich_traceback(
530
678
  )
531
679
  for e in errs_recast
532
680
  ],
681
+ start=start,
533
682
  version=version,
534
683
  max_width=max_width,
535
684
  indent_size=indent_size,
@@ -544,6 +693,7 @@ def _get_rich_traceback_non_chain(
544
693
  error: ExceptionGroup[Any] | TBaseException,
545
694
  /,
546
695
  *,
696
+ start: MaybeCallableDateTime | None = _START,
547
697
  version: MaybeCallableVersionLike | None = None,
548
698
  max_width: int = RICH_MAX_WIDTH,
549
699
  indent_size: int = RICH_INDENT_SIZE,
@@ -567,6 +717,7 @@ def _get_rich_traceback_non_chain(
567
717
  errors = [
568
718
  _get_rich_traceback_non_chain(
569
719
  e,
720
+ start=start,
570
721
  version=version,
571
722
  max_width=max_width,
572
723
  indent_size=indent_size,
@@ -580,6 +731,7 @@ def _get_rich_traceback_non_chain(
580
731
  return ExcGroupTB(
581
732
  exc_group=exc_group_or_exc_tb,
582
733
  errors=errors,
734
+ start=start,
583
735
  version=version,
584
736
  max_width=max_width,
585
737
  indent_size=indent_size,
@@ -591,6 +743,7 @@ def _get_rich_traceback_non_chain(
591
743
  case BaseException() as base_exc:
592
744
  return _get_rich_traceback_base_one(
593
745
  base_exc,
746
+ start=start,
594
747
  version=version,
595
748
  max_width=max_width,
596
749
  indent_size=indent_size,
@@ -607,6 +760,7 @@ def _get_rich_traceback_base_one(
607
760
  error: TBaseException,
608
761
  /,
609
762
  *,
763
+ start: MaybeCallableDateTime | None = _START,
610
764
  version: MaybeCallableVersionLike | None = None,
611
765
  max_width: int = RICH_MAX_WIDTH,
612
766
  indent_size: int = RICH_INDENT_SIZE,
@@ -638,6 +792,7 @@ def _get_rich_traceback_base_one(
638
792
  return ExcTB(
639
793
  frames=frames,
640
794
  error=error,
795
+ start=start,
641
796
  version=version,
642
797
  max_width=max_width,
643
798
  indent_size=indent_size,
@@ -792,14 +947,29 @@ def _merge_frames(
792
947
  return values[::-1]
793
948
 
794
949
 
950
+ ##
951
+
952
+
795
953
  def _yield_header_lines(
796
- *, version: MaybeCallableVersionLike | None = None
954
+ *,
955
+ start: MaybeCallableDateTime | None = _START,
956
+ version: MaybeCallableVersionLike | None = None,
797
957
  ) -> Iterator[str]:
798
958
  """Yield the header lines."""
799
- from utilities.tzlocal import get_now_local
959
+ from utilities.tzlocal import get_local_time_zone, get_now_local
800
960
  from utilities.whenever import serialize_zoned_datetime
801
961
 
802
- yield f"Date/time | {serialize_zoned_datetime(get_now_local())}"
962
+ now = get_now_local()
963
+ start_use = get_datetime(datetime=start)
964
+ start_use = (
965
+ None if start_use is None else start_use.astimezone(get_local_time_zone())
966
+ )
967
+ yield f"Date/time | {serialize_zoned_datetime(now)}"
968
+ start_str = "" if start_use is None else serialize_zoned_datetime(start_use)
969
+ yield f"Started | {start_str}"
970
+ duration = None if start_use is None else (now - start_use)
971
+ duration_str = "" if duration is None else serialize_duration(duration)
972
+ yield f"Duration | {duration_str}"
803
973
  yield f"User | {getuser()}"
804
974
  yield f"Host | {gethostname()}"
805
975
  version_use = "" if version is None else get_version(version=version)
@@ -807,12 +977,108 @@ def _yield_header_lines(
807
977
  yield ""
808
978
 
809
979
 
980
+ ##
981
+
982
+
983
+ def _yield_formatted_frame_summary(
984
+ error: BaseException,
985
+ /,
986
+ *,
987
+ capture_locals: bool = False,
988
+ max_width: int = RICH_MAX_WIDTH,
989
+ indent_size: int = RICH_INDENT_SIZE,
990
+ max_length: int | None = RICH_MAX_LENGTH,
991
+ max_string: int | None = RICH_MAX_STRING,
992
+ max_depth: int | None = RICH_MAX_DEPTH,
993
+ expand_all: bool = RICH_EXPAND_ALL,
994
+ ) -> Iterator[str]:
995
+ """Yield the formatted frame summary lines."""
996
+ stack = TracebackException.from_exception(
997
+ error, capture_locals=capture_locals
998
+ ).stack
999
+ n = len(stack)
1000
+ for i, frame in enumerate(stack, start=1):
1001
+ num = f"{i}/{n}"
1002
+ first, *rest = _yield_frame_summary_lines(
1003
+ frame,
1004
+ max_width=max_width,
1005
+ indent_size=indent_size,
1006
+ max_length=max_length,
1007
+ max_string=max_string,
1008
+ max_depth=max_depth,
1009
+ expand_all=expand_all,
1010
+ )
1011
+ yield f"{num} | {first}"
1012
+ blank = "".join(repeat(" ", len(num)))
1013
+ for rest_i in rest:
1014
+ yield f"{blank} | {rest_i}"
1015
+ yield repr_error(error)
1016
+
1017
+
1018
+ def _yield_frame_summary_lines(
1019
+ frame: FrameSummary,
1020
+ /,
1021
+ *,
1022
+ max_width: int = RICH_MAX_WIDTH,
1023
+ indent_size: int = RICH_INDENT_SIZE,
1024
+ max_length: int | None = RICH_MAX_LENGTH,
1025
+ max_string: int | None = RICH_MAX_STRING,
1026
+ max_depth: int | None = RICH_MAX_DEPTH,
1027
+ expand_all: bool = RICH_EXPAND_ALL,
1028
+ ) -> Iterator[str]:
1029
+ module = _path_to_dots(frame.filename)
1030
+ yield f"{module}:{frame.lineno} | {frame.name} | {frame.line}"
1031
+ if frame.locals is not None:
1032
+ yield from yield_mapping_repr(
1033
+ frame.locals,
1034
+ _max_width=max_width,
1035
+ _indent_size=indent_size,
1036
+ _max_length=max_length,
1037
+ _max_string=max_string,
1038
+ _max_depth=max_depth,
1039
+ _expand_all=expand_all,
1040
+ )
1041
+
1042
+
1043
+ def _path_to_dots(path: PathLike, /) -> str:
1044
+ new_path: Path | None = None
1045
+ for pattern in [
1046
+ "site-packages",
1047
+ ".venv", # after site-packages
1048
+ "src",
1049
+ r"python\d+\.\d+",
1050
+ ]:
1051
+ if (new_path := _trim_path(path, pattern)) is not None:
1052
+ break
1053
+ path_use = Path(path) if new_path is None else new_path
1054
+ return ".".join(path_use.with_suffix("").parts)
1055
+
1056
+
1057
+ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
1058
+ parts = Path(path).parts
1059
+ compiled = re.compile(f"^{pattern}$")
1060
+ try:
1061
+ i = one(i for i, p in enumerate(parts) if compiled.search(p))
1062
+ except OneEmptyError:
1063
+ return None
1064
+ return Path(*parts[i + 1 :])
1065
+
1066
+
1067
+ @dataclass(kw_only=True, slots=True)
1068
+ class MakeExceptHookError(Exception):
1069
+ @override
1070
+ def __str__(self) -> str:
1071
+ return "No exception to log"
1072
+
1073
+
810
1074
  __all__ = [
811
1075
  "ExcChainTB",
812
1076
  "ExcGroupTB",
813
1077
  "ExcTB",
814
1078
  "RichTracebackFormatter",
1079
+ "format_exception_stack",
815
1080
  "get_rich_traceback",
1081
+ "make_except_hook",
816
1082
  "trace",
817
1083
  "yield_exceptions",
818
1084
  "yield_extended_frame_summaries",
utilities/types.py CHANGED
@@ -241,8 +241,8 @@ type SerializeObjectExtra = Mapping[Any, Callable[[Any], str]]
241
241
 
242
242
 
243
243
  # pathlib
244
+ type MaybeCallablePathLike = MaybeCallable[PathLike]
244
245
  type PathLike = MaybeStr[Path]
245
- type PathLikeOrCallable = PathLike | Callable[[], PathLike]
246
246
 
247
247
 
248
248
  # random
@@ -282,6 +282,7 @@ __all__ = [
282
282
  "MaybeCallableDate",
283
283
  "MaybeCallableDateTime",
284
284
  "MaybeCallableEvent",
285
+ "MaybeCallablePathLike",
285
286
  "MaybeCoroutine1",
286
287
  "MaybeIterable",
287
288
  "MaybeIterableHashable",
@@ -293,7 +294,6 @@ __all__ = [
293
294
  "Parallelism",
294
295
  "ParseObjectExtra",
295
296
  "PathLike",
296
- "PathLikeOrCallable",
297
297
  "RoundMode",
298
298
  "Seed",
299
299
  "SerializeObjectExtra",
utilities/version.py CHANGED
@@ -137,14 +137,6 @@ def get_version(*, version: MaybeCallableVersionLike) -> Version: ...
137
137
  def get_version(*, version: None) -> None: ...
138
138
  @overload
139
139
  def get_version(*, version: Sentinel) -> Sentinel: ...
140
- @overload
141
- def get_version(
142
- *, version: MaybeCallableVersionLike | Sentinel
143
- ) -> Version | Sentinel: ...
144
- @overload
145
- def get_version(
146
- *, version: MaybeCallableVersionLike | None | Sentinel = sentinel
147
- ) -> Version | None | Sentinel: ...
148
140
  def get_version(
149
141
  *, version: MaybeCallableVersionLike | None | Sentinel = sentinel
150
142
  ) -> Version | None | Sentinel:
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",