dycw-utilities 0.129.7__py3-none-any.whl → 0.129.9__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.
- {dycw_utilities-0.129.7.dist-info → dycw_utilities-0.129.9.dist-info}/METADATA +1 -2
- {dycw_utilities-0.129.7.dist-info → dycw_utilities-0.129.9.dist-info}/RECORD +8 -8
- utilities/__init__.py +1 -1
- utilities/logging.py +46 -18
- utilities/traceback.py +241 -6
- utilities/whenever.py +64 -1
- {dycw_utilities-0.129.7.dist-info → dycw_utilities-0.129.9.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.129.7.dist-info → dycw_utilities-0.129.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.129.
|
3
|
+
Version: 0.129.9
|
4
4
|
Author-email: Derek Wan <d.wan@icloud.com>
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.12
|
@@ -98,7 +98,6 @@ 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'
|
@@ -1,4 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
1
|
+
utilities/__init__.py,sha256=uFYFepkZNArEi_sHiw7tlOn88WgLGjxnMihyD4GzfOQ,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=
|
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=
|
81
|
+
utilities/traceback.py,sha256=EUm-5jhH1RICBmsG_H77-CXvD-IVErfkzGRSqnvR0dI,35782
|
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=
|
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.
|
93
|
-
dycw_utilities-0.129.
|
94
|
-
dycw_utilities-0.129.
|
95
|
-
dycw_utilities-0.129.
|
92
|
+
dycw_utilities-0.129.9.dist-info/METADATA,sha256=c0KdCG0ORHKUnqbJ4eaGnDoZNUDbRtkjupd_8Oqg22c,12723
|
93
|
+
dycw_utilities-0.129.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
94
|
+
dycw_utilities-0.129.9.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
|
95
|
+
dycw_utilities-0.129.9.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
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
|
-
|
111
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
@@ -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,8 +30,8 @@ from typing import (
|
|
26
30
|
runtime_checkable,
|
27
31
|
)
|
28
32
|
|
29
|
-
from utilities.datetime import get_datetime, get_now
|
30
|
-
from utilities.errors import ImpossibleCaseError
|
33
|
+
from utilities.datetime import get_datetime, get_now, serialize_compact
|
34
|
+
from utilities.errors import ImpossibleCaseError, repr_error
|
31
35
|
from utilities.functions import (
|
32
36
|
ensure_not_none,
|
33
37
|
ensure_str,
|
@@ -35,7 +39,8 @@ from utilities.functions import (
|
|
35
39
|
get_func_name,
|
36
40
|
get_func_qualname,
|
37
41
|
)
|
38
|
-
from utilities.iterables import always_iterable, one
|
42
|
+
from utilities.iterables import OneEmptyError, always_iterable, one
|
43
|
+
from utilities.pathlib import get_path
|
39
44
|
from utilities.reprlib import (
|
40
45
|
RICH_EXPAND_ALL,
|
41
46
|
RICH_INDENT_SIZE,
|
@@ -46,12 +51,18 @@ from utilities.reprlib import (
|
|
46
51
|
yield_call_args_repr,
|
47
52
|
yield_mapping_repr,
|
48
53
|
)
|
49
|
-
from utilities.types import
|
54
|
+
from utilities.types import (
|
55
|
+
MaybeCallableDateTime,
|
56
|
+
MaybeCallablePathLike,
|
57
|
+
PathLike,
|
58
|
+
TBaseException,
|
59
|
+
TCallable,
|
60
|
+
)
|
50
61
|
from utilities.version import get_version
|
51
62
|
from utilities.whenever import serialize_duration
|
52
63
|
|
53
64
|
if TYPE_CHECKING:
|
54
|
-
from collections.abc import Callable, Iterable, Iterator
|
65
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
55
66
|
from logging import _FormatStyle
|
56
67
|
from types import FrameType, TracebackType
|
57
68
|
|
@@ -68,6 +79,131 @@ _START = get_now()
|
|
68
79
|
##
|
69
80
|
|
70
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))
|
202
|
+
|
203
|
+
|
204
|
+
##
|
205
|
+
|
206
|
+
|
71
207
|
class RichTracebackFormatter(Formatter):
|
72
208
|
"""Formatter for rich tracebacks."""
|
73
209
|
|
@@ -811,6 +947,9 @@ def _merge_frames(
|
|
811
947
|
return values[::-1]
|
812
948
|
|
813
949
|
|
950
|
+
##
|
951
|
+
|
952
|
+
|
814
953
|
def _yield_header_lines(
|
815
954
|
*,
|
816
955
|
start: MaybeCallableDateTime | None = _START,
|
@@ -838,12 +977,108 @@ def _yield_header_lines(
|
|
838
977
|
yield ""
|
839
978
|
|
840
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
|
+
|
841
1074
|
__all__ = [
|
842
1075
|
"ExcChainTB",
|
843
1076
|
"ExcGroupTB",
|
844
1077
|
"ExcTB",
|
845
1078
|
"RichTracebackFormatter",
|
1079
|
+
"format_exception_stack",
|
846
1080
|
"get_rich_traceback",
|
1081
|
+
"make_except_hook",
|
847
1082
|
"trace",
|
848
1083
|
"yield_exceptions",
|
849
1084
|
"yield_extended_frame_summaries",
|
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
|
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",
|
File without changes
|
File without changes
|