dycw-utilities 0.135.0__py3-none-any.whl → 0.178.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +13 -10
- utilities/asyncio.py +312 -787
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +195 -77
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +28 -59
- utilities/fpdf2.py +2 -2
- utilities/functions.py +24 -269
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +513 -159
- utilities/importlib.py +17 -1
- utilities/inflect.py +12 -4
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +4 -7
- utilities/logging.py +136 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +43 -45
- utilities/operator.py +27 -27
- utilities/orjson.py +189 -36
- utilities/os.py +61 -4
- utilities/packaging.py +115 -0
- utilities/parse.py +8 -5
- utilities/pathlib.py +269 -40
- utilities/permissions.py +298 -0
- utilities/platform.py +7 -6
- utilities/polars.py +1205 -413
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +43 -19
- utilities/pqdm.py +3 -3
- utilities/psutil.py +5 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -52
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +7 -7
- utilities/pytest.py +104 -143
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +220 -343
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +35 -104
- utilities/sqlalchemy.py +496 -471
- utilities/sqlalchemy_polars.py +29 -54
- utilities/string.py +2 -3
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +70 -35
- utilities/types.py +102 -30
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1559 -361
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.135.0.dist-info/METADATA +0 -39
- dycw_utilities-0.135.0.dist-info/RECORD +0 -96
- dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
- dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/arq.py +0 -216
- utilities/eventkit.py +0 -388
- utilities/luigi.py +0 -183
- utilities/period.py +0 -152
- utilities/pudb.py +0 -62
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -123
utilities/throttle.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import partial, wraps
|
|
6
|
+
from inspect import iscoroutinefunction
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, NoReturn, assert_never, cast, override
|
|
9
|
+
|
|
10
|
+
from whenever import ZonedDateTime
|
|
11
|
+
|
|
12
|
+
from utilities.atomicwrites import writer
|
|
13
|
+
from utilities.os import get_env_var
|
|
14
|
+
from utilities.pathlib import to_path
|
|
15
|
+
from utilities.types import MaybeCallablePathLike, MaybeCoro
|
|
16
|
+
from utilities.whenever import SECOND, get_now_local
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from utilities.types import Coro, Delta
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def throttle[F: Callable[..., MaybeCoro[None]]](
|
|
23
|
+
*,
|
|
24
|
+
on_try: bool = False,
|
|
25
|
+
delta: Delta = SECOND,
|
|
26
|
+
path: MaybeCallablePathLike = Path.cwd,
|
|
27
|
+
raiser: Callable[[], NoReturn] | None = None,
|
|
28
|
+
) -> Callable[[F], F]:
|
|
29
|
+
"""Throttle a function. On success by default, on try otherwise."""
|
|
30
|
+
return cast(
|
|
31
|
+
"Any",
|
|
32
|
+
partial(_throttle_inner, on_try=on_try, delta=delta, path=path, raiser=raiser),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
|
|
37
|
+
func: F,
|
|
38
|
+
/,
|
|
39
|
+
*,
|
|
40
|
+
on_try: bool = False,
|
|
41
|
+
delta: Delta = SECOND,
|
|
42
|
+
path: MaybeCallablePathLike = Path.cwd,
|
|
43
|
+
raiser: Callable[[], NoReturn] | None = None,
|
|
44
|
+
) -> F:
|
|
45
|
+
match bool(iscoroutinefunction(func)), on_try:
|
|
46
|
+
case False, False:
|
|
47
|
+
|
|
48
|
+
@wraps(func)
|
|
49
|
+
def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
|
|
50
|
+
path_use = to_path(path)
|
|
51
|
+
if _is_throttle(path=path_use, delta=delta):
|
|
52
|
+
_try_raise(raiser=raiser)
|
|
53
|
+
else:
|
|
54
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
55
|
+
_write_throttle(path=path_use)
|
|
56
|
+
|
|
57
|
+
return cast("Any", throttle_sync_on_pass)
|
|
58
|
+
|
|
59
|
+
case False, True:
|
|
60
|
+
|
|
61
|
+
@wraps(func)
|
|
62
|
+
def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
|
|
63
|
+
path_use = to_path(path)
|
|
64
|
+
if _is_throttle(path=path_use, delta=delta):
|
|
65
|
+
_try_raise(raiser=raiser)
|
|
66
|
+
else:
|
|
67
|
+
_write_throttle(path=path_use)
|
|
68
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
return cast("Any", throttle_sync_on_try)
|
|
71
|
+
|
|
72
|
+
case True, False:
|
|
73
|
+
|
|
74
|
+
@wraps(func)
|
|
75
|
+
async def throttle_async_on_pass(*args: Any, **kwargs: Any) -> None:
|
|
76
|
+
path_use = to_path(path)
|
|
77
|
+
if _is_throttle(path=path_use, delta=delta):
|
|
78
|
+
_try_raise(raiser=raiser)
|
|
79
|
+
else:
|
|
80
|
+
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
81
|
+
_write_throttle(path=path_use)
|
|
82
|
+
|
|
83
|
+
return cast("Any", throttle_async_on_pass)
|
|
84
|
+
|
|
85
|
+
case True, True:
|
|
86
|
+
|
|
87
|
+
@wraps(func)
|
|
88
|
+
async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
|
|
89
|
+
path_use = to_path(path)
|
|
90
|
+
if _is_throttle(path=path_use, delta=delta):
|
|
91
|
+
_try_raise(raiser=raiser)
|
|
92
|
+
else:
|
|
93
|
+
_write_throttle(path=path_use)
|
|
94
|
+
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
return cast("Any", throttle_async_on_try)
|
|
97
|
+
|
|
98
|
+
case never:
|
|
99
|
+
assert_never(never)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_throttle(
|
|
103
|
+
*, path: MaybeCallablePathLike = Path.cwd, delta: Delta = SECOND
|
|
104
|
+
) -> bool:
|
|
105
|
+
if get_env_var("THROTTLE", nullable=True):
|
|
106
|
+
return False
|
|
107
|
+
path = to_path(path)
|
|
108
|
+
if path.is_file():
|
|
109
|
+
text = path.read_text()
|
|
110
|
+
if text == "":
|
|
111
|
+
path.unlink(missing_ok=True)
|
|
112
|
+
return False
|
|
113
|
+
try:
|
|
114
|
+
last = ZonedDateTime.parse_iso(text)
|
|
115
|
+
except ValueError:
|
|
116
|
+
raise _ThrottleParseZonedDateTimeError(path=path, text=text) from None
|
|
117
|
+
threshold = get_now_local() - delta
|
|
118
|
+
return threshold <= last
|
|
119
|
+
if not path.exists():
|
|
120
|
+
return False
|
|
121
|
+
raise _ThrottleMarkerFileError(path=path)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _try_raise(*, raiser: Callable[[], NoReturn] | None = None) -> None:
|
|
125
|
+
if raiser is not None:
|
|
126
|
+
raiser()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _write_throttle(*, path: MaybeCallablePathLike = Path.cwd) -> None:
|
|
130
|
+
path = to_path(path)
|
|
131
|
+
with writer(path, overwrite=True) as temp:
|
|
132
|
+
_ = temp.write_text(get_now_local().format_iso())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(kw_only=True, slots=True)
|
|
136
|
+
class ThrottleError(Exception): ...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(kw_only=True, slots=True)
|
|
140
|
+
class _ThrottleParseZonedDateTimeError(ThrottleError):
|
|
141
|
+
path: Path
|
|
142
|
+
text: str
|
|
143
|
+
|
|
144
|
+
@override
|
|
145
|
+
def __str__(self) -> str:
|
|
146
|
+
return f"Unable to parse the contents {self.text!r} of {str(self.path)!r} to a ZonedDateTime"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(kw_only=True, slots=True)
|
|
150
|
+
class _ThrottleMarkerFileError(ThrottleError):
|
|
151
|
+
path: Path
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
def __str__(self) -> str:
|
|
155
|
+
return f"Invalid marker file {str(self.path)!r}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = ["ThrottleError", "throttle"]
|
utilities/timer.py
CHANGED
|
@@ -56,11 +56,11 @@ class Timer:
|
|
|
56
56
|
|
|
57
57
|
@override
|
|
58
58
|
def __repr__(self) -> str:
|
|
59
|
-
return self.timedelta.
|
|
59
|
+
return self.timedelta.format_iso()
|
|
60
60
|
|
|
61
61
|
@override
|
|
62
62
|
def __str__(self) -> str:
|
|
63
|
-
return self.timedelta.
|
|
63
|
+
return self.timedelta.format_iso()
|
|
64
64
|
|
|
65
65
|
# comparison
|
|
66
66
|
|
utilities/traceback.py
CHANGED
|
@@ -2,21 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
5
|
-
from asyncio import run
|
|
6
|
-
from collections.abc import Callable
|
|
7
5
|
from dataclasses import dataclass
|
|
8
6
|
from functools import partial
|
|
9
7
|
from getpass import getuser
|
|
10
8
|
from itertools import repeat
|
|
9
|
+
from os import getpid
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from socket import gethostname
|
|
12
|
+
from sys import stderr
|
|
13
13
|
from traceback import TracebackException
|
|
14
14
|
from typing import TYPE_CHECKING, override
|
|
15
15
|
|
|
16
16
|
from utilities.atomicwrites import writer
|
|
17
17
|
from utilities.errors import repr_error
|
|
18
18
|
from utilities.iterables import OneEmptyError, one
|
|
19
|
-
from utilities.pathlib import
|
|
19
|
+
from utilities.pathlib import module_path, to_path
|
|
20
20
|
from utilities.reprlib import (
|
|
21
21
|
RICH_EXPAND_ALL,
|
|
22
22
|
RICH_INDENT_SIZE,
|
|
@@ -26,17 +26,26 @@ from utilities.reprlib import (
|
|
|
26
26
|
RICH_MAX_WIDTH,
|
|
27
27
|
yield_mapping_repr,
|
|
28
28
|
)
|
|
29
|
-
from utilities.
|
|
30
|
-
from utilities.
|
|
29
|
+
from utilities.text import to_bool
|
|
30
|
+
from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
|
|
31
|
+
from utilities.version import to_version
|
|
32
|
+
from utilities.whenever import (
|
|
33
|
+
format_compact,
|
|
34
|
+
get_now,
|
|
35
|
+
get_now_local,
|
|
36
|
+
to_zoned_date_time,
|
|
37
|
+
)
|
|
31
38
|
|
|
32
39
|
if TYPE_CHECKING:
|
|
33
|
-
from collections.abc import Callable, Iterator
|
|
40
|
+
from collections.abc import Callable, Iterator
|
|
34
41
|
from traceback import FrameSummary
|
|
35
42
|
from types import TracebackType
|
|
36
43
|
|
|
37
44
|
from utilities.types import (
|
|
45
|
+
Delta,
|
|
46
|
+
MaybeCallableBoolLike,
|
|
38
47
|
MaybeCallablePathLike,
|
|
39
|
-
|
|
48
|
+
MaybeCallableZonedDateTimeLike,
|
|
40
49
|
PathLike,
|
|
41
50
|
)
|
|
42
51
|
from utilities.version import MaybeCallableVersionLike
|
|
@@ -45,15 +54,12 @@ if TYPE_CHECKING:
|
|
|
45
54
|
##
|
|
46
55
|
|
|
47
56
|
|
|
48
|
-
_START = get_now()
|
|
49
|
-
|
|
50
|
-
|
|
51
57
|
def format_exception_stack(
|
|
52
58
|
error: BaseException,
|
|
53
59
|
/,
|
|
54
60
|
*,
|
|
55
61
|
header: bool = False,
|
|
56
|
-
start:
|
|
62
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
57
63
|
version: MaybeCallableVersionLike | None = None,
|
|
58
64
|
capture_locals: bool = False,
|
|
59
65
|
max_width: int = RICH_MAX_WIDTH,
|
|
@@ -64,7 +70,7 @@ def format_exception_stack(
|
|
|
64
70
|
expand_all: bool = RICH_EXPAND_ALL,
|
|
65
71
|
) -> str:
|
|
66
72
|
"""Format an exception stack."""
|
|
67
|
-
lines:
|
|
73
|
+
lines: list[str] = []
|
|
68
74
|
if header:
|
|
69
75
|
lines.extend(_yield_header_lines(start=start, version=version))
|
|
70
76
|
lines.extend(
|
|
@@ -84,22 +90,20 @@ def format_exception_stack(
|
|
|
84
90
|
|
|
85
91
|
def _yield_header_lines(
|
|
86
92
|
*,
|
|
87
|
-
start:
|
|
93
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
88
94
|
version: MaybeCallableVersionLike | None = None,
|
|
89
95
|
) -> Iterator[str]:
|
|
90
96
|
"""Yield the header lines."""
|
|
91
|
-
now =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
yield f"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
yield f"
|
|
99
|
-
|
|
100
|
-
yield f"
|
|
101
|
-
version_use = "" if version is None else get_version(version=version)
|
|
102
|
-
yield f"Version | {version_use}"
|
|
97
|
+
now = get_now_local()
|
|
98
|
+
yield f"Date/time | {format_compact(now)}"
|
|
99
|
+
start_use = to_zoned_date_time(start).to_tz(LOCAL_TIME_ZONE_NAME)
|
|
100
|
+
yield f"Started | {format_compact(start_use)}"
|
|
101
|
+
yield f"Duration | {(now - start_use).format_iso()}"
|
|
102
|
+
yield f"User | {getuser()}"
|
|
103
|
+
yield f"Host | {gethostname()}"
|
|
104
|
+
yield f"Process ID | {getpid()}"
|
|
105
|
+
version_use = "" if version is None else to_version(version)
|
|
106
|
+
yield f"Version | {version_use}"
|
|
103
107
|
yield ""
|
|
104
108
|
|
|
105
109
|
|
|
@@ -174,7 +178,7 @@ def _path_to_dots(path: PathLike, /) -> str:
|
|
|
174
178
|
if (new_path := _trim_path(path, pattern)) is not None:
|
|
175
179
|
break
|
|
176
180
|
path_use = Path(path) if new_path is None else new_path
|
|
177
|
-
return
|
|
181
|
+
return module_path(path_use)
|
|
178
182
|
|
|
179
183
|
|
|
180
184
|
def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
|
|
@@ -192,9 +196,10 @@ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
|
|
|
192
196
|
|
|
193
197
|
def make_except_hook(
|
|
194
198
|
*,
|
|
195
|
-
start:
|
|
199
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
196
200
|
version: MaybeCallableVersionLike | None = None,
|
|
197
201
|
path: MaybeCallablePathLike | None = None,
|
|
202
|
+
path_max_age: Delta | None = None,
|
|
198
203
|
max_width: int = RICH_MAX_WIDTH,
|
|
199
204
|
indent_size: int = RICH_INDENT_SIZE,
|
|
200
205
|
max_length: int | None = RICH_MAX_LENGTH,
|
|
@@ -202,6 +207,7 @@ def make_except_hook(
|
|
|
202
207
|
max_depth: int | None = RICH_MAX_DEPTH,
|
|
203
208
|
expand_all: bool = RICH_EXPAND_ALL,
|
|
204
209
|
slack_url: str | None = None,
|
|
210
|
+
pudb: MaybeCallableBoolLike = False,
|
|
205
211
|
) -> Callable[
|
|
206
212
|
[type[BaseException] | None, BaseException | None, TracebackType | None], None
|
|
207
213
|
]:
|
|
@@ -211,6 +217,7 @@ def make_except_hook(
|
|
|
211
217
|
start=start,
|
|
212
218
|
version=version,
|
|
213
219
|
path=path,
|
|
220
|
+
path_max_age=path_max_age,
|
|
214
221
|
max_width=max_width,
|
|
215
222
|
indent_size=indent_size,
|
|
216
223
|
max_length=max_length,
|
|
@@ -218,6 +225,7 @@ def make_except_hook(
|
|
|
218
225
|
max_depth=max_depth,
|
|
219
226
|
expand_all=expand_all,
|
|
220
227
|
slack_url=slack_url,
|
|
228
|
+
pudb=pudb,
|
|
221
229
|
)
|
|
222
230
|
|
|
223
231
|
|
|
@@ -227,9 +235,10 @@ def _make_except_hook_inner(
|
|
|
227
235
|
traceback: TracebackType | None,
|
|
228
236
|
/,
|
|
229
237
|
*,
|
|
230
|
-
start:
|
|
238
|
+
start: MaybeCallableZonedDateTimeLike = get_now,
|
|
231
239
|
version: MaybeCallableVersionLike | None = None,
|
|
232
240
|
path: MaybeCallablePathLike | None = None,
|
|
241
|
+
path_max_age: Delta | None = None,
|
|
233
242
|
max_width: int = RICH_MAX_WIDTH,
|
|
234
243
|
indent_size: int = RICH_INDENT_SIZE,
|
|
235
244
|
max_length: int | None = RICH_MAX_LENGTH,
|
|
@@ -237,6 +246,7 @@ def _make_except_hook_inner(
|
|
|
237
246
|
max_depth: int | None = RICH_MAX_DEPTH,
|
|
238
247
|
expand_all: bool = RICH_EXPAND_ALL,
|
|
239
248
|
slack_url: str | None = None,
|
|
249
|
+
pudb: MaybeCallableBoolLike = False,
|
|
240
250
|
) -> None:
|
|
241
251
|
"""Exception hook to log the traceback."""
|
|
242
252
|
_ = (exc_type, traceback)
|
|
@@ -245,9 +255,10 @@ def _make_except_hook_inner(
|
|
|
245
255
|
slim = format_exception_stack(exc_val, header=True, start=start, version=version)
|
|
246
256
|
_ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
|
|
247
257
|
if path is not None:
|
|
248
|
-
path = (
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
path = to_path(path)
|
|
259
|
+
path_log = path.joinpath(
|
|
260
|
+
format_compact(get_now_local(), path=True)
|
|
261
|
+
).with_suffix(".txt")
|
|
251
262
|
full = format_exception_stack(
|
|
252
263
|
exc_val,
|
|
253
264
|
header=True,
|
|
@@ -261,13 +272,37 @@ def _make_except_hook_inner(
|
|
|
261
272
|
max_depth=max_depth,
|
|
262
273
|
expand_all=expand_all,
|
|
263
274
|
)
|
|
264
|
-
with writer(
|
|
275
|
+
with writer(path_log, overwrite=True) as temp:
|
|
265
276
|
_ = temp.write_text(full)
|
|
277
|
+
if path_max_age is not None:
|
|
278
|
+
_make_except_hook_purge(path, path_max_age)
|
|
266
279
|
if slack_url is not None: # pragma: no cover
|
|
267
|
-
from utilities.slack_sdk import send_to_slack
|
|
280
|
+
from utilities.slack_sdk import SendToSlackError, send_to_slack
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
send_to_slack(slack_url, f"```{slim}```")
|
|
284
|
+
except SendToSlackError as error:
|
|
285
|
+
_ = stderr.write(f"{error}\n")
|
|
286
|
+
if to_bool(pudb): # pragma: no cover
|
|
287
|
+
from pudb import post_mortem # pyright: ignore[reportMissingImports]
|
|
288
|
+
|
|
289
|
+
post_mortem(tb=traceback, e_type=exc_type, e_value=exc_val)
|
|
290
|
+
|
|
268
291
|
|
|
269
|
-
|
|
270
|
-
|
|
292
|
+
def _make_except_hook_purge(path: PathLike, max_age: Delta, /) -> None:
|
|
293
|
+
threshold = get_now_local() - max_age
|
|
294
|
+
paths: set[Path] = set()
|
|
295
|
+
for p in Path(path).iterdir():
|
|
296
|
+
if p.is_file():
|
|
297
|
+
try:
|
|
298
|
+
date_time = to_zoned_date_time(p.stem)
|
|
299
|
+
except ValueError:
|
|
300
|
+
pass
|
|
301
|
+
else:
|
|
302
|
+
if date_time <= threshold:
|
|
303
|
+
paths.add(p)
|
|
304
|
+
for p in paths:
|
|
305
|
+
p.unlink(missing_ok=True)
|
|
271
306
|
|
|
272
307
|
|
|
273
308
|
@dataclass(kw_only=True, slots=True)
|