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.

Files changed (97) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +13 -10
  7. utilities/asyncio.py +312 -787
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +195 -77
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +28 -59
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +24 -269
  23. utilities/git.py +9 -30
  24. utilities/grp.py +28 -0
  25. utilities/gzip.py +31 -0
  26. utilities/http.py +3 -2
  27. utilities/hypothesis.py +513 -159
  28. utilities/importlib.py +17 -1
  29. utilities/inflect.py +12 -4
  30. utilities/iterables.py +33 -58
  31. utilities/jinja2.py +148 -0
  32. utilities/json.py +70 -0
  33. utilities/libcst.py +38 -17
  34. utilities/lightweight_charts.py +4 -7
  35. utilities/logging.py +136 -93
  36. utilities/math.py +8 -4
  37. utilities/more_itertools.py +43 -45
  38. utilities/operator.py +27 -27
  39. utilities/orjson.py +189 -36
  40. utilities/os.py +61 -4
  41. utilities/packaging.py +115 -0
  42. utilities/parse.py +8 -5
  43. utilities/pathlib.py +269 -40
  44. utilities/permissions.py +298 -0
  45. utilities/platform.py +7 -6
  46. utilities/polars.py +1205 -413
  47. utilities/polars_ols.py +1 -1
  48. utilities/postgres.py +408 -0
  49. utilities/pottery.py +43 -19
  50. utilities/pqdm.py +3 -3
  51. utilities/psutil.py +5 -57
  52. utilities/pwd.py +28 -0
  53. utilities/pydantic.py +4 -52
  54. utilities/pydantic_settings.py +240 -0
  55. utilities/pydantic_settings_sops.py +76 -0
  56. utilities/pyinstrument.py +7 -7
  57. utilities/pytest.py +104 -143
  58. utilities/pytest_plugins/__init__.py +1 -0
  59. utilities/pytest_plugins/pytest_randomly.py +23 -0
  60. utilities/pytest_plugins/pytest_regressions.py +56 -0
  61. utilities/pytest_regressions.py +26 -46
  62. utilities/random.py +11 -6
  63. utilities/re.py +1 -1
  64. utilities/redis.py +220 -343
  65. utilities/sentinel.py +10 -0
  66. utilities/shelve.py +4 -1
  67. utilities/shutil.py +25 -0
  68. utilities/slack_sdk.py +35 -104
  69. utilities/sqlalchemy.py +496 -471
  70. utilities/sqlalchemy_polars.py +29 -54
  71. utilities/string.py +2 -3
  72. utilities/subprocess.py +1977 -0
  73. utilities/tempfile.py +112 -4
  74. utilities/testbook.py +50 -0
  75. utilities/text.py +174 -42
  76. utilities/throttle.py +158 -0
  77. utilities/timer.py +2 -2
  78. utilities/traceback.py +70 -35
  79. utilities/types.py +102 -30
  80. utilities/typing.py +479 -19
  81. utilities/uuid.py +42 -5
  82. utilities/version.py +27 -26
  83. utilities/whenever.py +1559 -361
  84. utilities/zoneinfo.py +80 -22
  85. dycw_utilities-0.135.0.dist-info/METADATA +0 -39
  86. dycw_utilities-0.135.0.dist-info/RECORD +0 -96
  87. dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
  88. dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
  89. utilities/aiolimiter.py +0 -25
  90. utilities/arq.py +0 -216
  91. utilities/eventkit.py +0 -388
  92. utilities/luigi.py +0 -183
  93. utilities/period.py +0 -152
  94. utilities/pudb.py +0 -62
  95. utilities/python_dotenv.py +0 -101
  96. utilities/streamlit.py +0 -105
  97. 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.format_common_iso()
59
+ return self.timedelta.format_iso()
60
60
 
61
61
  @override
62
62
  def __str__(self) -> str:
63
- return self.timedelta.format_common_iso()
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 get_path
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.version import get_version
30
- from utilities.whenever import format_compact, get_now, to_zoned_date_time
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, Sequence
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
- MaybeCallableZonedDateTime,
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: MaybeCallableZonedDateTime | None = _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: Sequence[str] = []
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: MaybeCallableZonedDateTime | None = _START,
93
+ start: MaybeCallableZonedDateTimeLike = get_now,
88
94
  version: MaybeCallableVersionLike | None = None,
89
95
  ) -> Iterator[str]:
90
96
  """Yield the header lines."""
91
- now = get_now()
92
- start_use = to_zoned_date_time(date_time=start)
93
- yield f"Date/time | {format_compact(now)}"
94
- start_str = "" if start_use is None else format_compact(start_use)
95
- yield f"Started | {start_str}"
96
- delta = None if start_use is None else (now - start_use)
97
- delta_str = "" if delta is None else delta.format_common_iso()
98
- yield f"Duration | {delta_str}"
99
- yield f"User | {getuser()}"
100
- yield f"Host | {gethostname()}"
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 ".".join(path_use.with_suffix("").parts)
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: MaybeCallableZonedDateTime | None = _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: MaybeCallableZonedDateTime | None = _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
- get_path(path=path).joinpath(format_compact(get_now())).with_suffix(".txt")
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(path, overwrite=True) as temp:
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
- send = f"```{slim}```"
270
- run(send_to_slack(slack_url, send))
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)