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/contextlib.py CHANGED
@@ -1,28 +1,223 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from contextlib import contextmanager
5
- from typing import TYPE_CHECKING
4
+ from asyncio import create_task, get_event_loop
5
+ from contextlib import (
6
+ _AsyncGeneratorContextManager,
7
+ _GeneratorContextManager,
8
+ asynccontextmanager,
9
+ contextmanager,
10
+ )
11
+ from functools import partial, wraps
12
+ from signal import SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, getsignal, signal
13
+ from typing import TYPE_CHECKING, Any, assert_never, cast, overload
6
14
 
7
15
  if TYPE_CHECKING:
8
- from collections.abc import Iterator
9
- from types import TracebackType
16
+ from collections.abc import AsyncIterator, Callable, Iterator
17
+ from signal import _HANDLER, _SIGNUM
18
+ from types import FrameType
10
19
 
11
20
 
12
- class NoOpContextManager:
13
- """Context-manager for no-op."""
21
+ @overload
22
+ def enhanced_context_manager[**P, T_co](
23
+ func: Callable[P, Iterator[T_co]],
24
+ /,
25
+ *,
26
+ sigabrt: bool = True,
27
+ sigfpe: bool = True,
28
+ sigill: bool = True,
29
+ sigint: bool = True,
30
+ sigsegv: bool = True,
31
+ sigterm: bool = True,
32
+ ) -> Callable[P, _GeneratorContextManager[T_co]]: ...
33
+ @overload
34
+ def enhanced_context_manager[**P, T_co](
35
+ func: None = None,
36
+ /,
37
+ *,
38
+ sigabrt: bool = True,
39
+ sigfpe: bool = True,
40
+ sigill: bool = True,
41
+ sigint: bool = True,
42
+ sigsegv: bool = True,
43
+ sigterm: bool = True,
44
+ ) -> Callable[
45
+ [Callable[P, Iterator[T_co]]], Callable[P, _GeneratorContextManager[T_co]]
46
+ ]: ...
47
+ def enhanced_context_manager[**P, T_co](
48
+ func: Callable[P, Iterator[T_co]] | None = None,
49
+ /,
50
+ *,
51
+ sigabrt: bool = True,
52
+ sigfpe: bool = True,
53
+ sigill: bool = True,
54
+ sigint: bool = True,
55
+ sigsegv: bool = True,
56
+ sigterm: bool = True,
57
+ ) -> (
58
+ Callable[P, _GeneratorContextManager[T_co]]
59
+ | Callable[
60
+ [Callable[P, Iterator[T_co]]], Callable[P, _GeneratorContextManager[T_co]]
61
+ ]
62
+ ):
63
+ if func is None:
64
+ result = partial(
65
+ enhanced_context_manager,
66
+ sigabrt=sigabrt,
67
+ sigfpe=sigfpe,
68
+ sigill=sigill,
69
+ sigint=sigint,
70
+ sigsegv=sigsegv,
71
+ sigterm=sigterm,
72
+ )
73
+ return cast(
74
+ "Callable[[Callable[P, Iterator[T_co]]], Callable[P, _GeneratorContextManager[T_co]]]",
75
+ result,
76
+ )
77
+ make_gcm = contextmanager(func)
14
78
 
15
- def __enter__(self) -> None:
16
- return None
79
+ @contextmanager
80
+ @wraps(func)
81
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> Iterator[T_co]:
82
+ gcm = make_gcm(*args, **kwargs)
83
+ sigabrt0 = _swap_handler(SIGABRT, gcm) if sigabrt else None
84
+ sigfpe0 = _swap_handler(SIGFPE, gcm) if sigfpe else None
85
+ sigill0 = _swap_handler(SIGILL, gcm) if sigill else None
86
+ sigint0 = _swap_handler(SIGINT, gcm) if sigint else None
87
+ sigsegv0 = _swap_handler(SIGSEGV, gcm) if sigsegv else None
88
+ sigterm0 = _swap_handler(SIGTERM, gcm) if sigterm else None
89
+ try:
90
+ with gcm as value:
91
+ yield value
92
+ finally:
93
+ _ = signal(SIGABRT, sigabrt0) if sigabrt else None
94
+ _ = signal(SIGFPE, sigfpe0) if sigfpe else None
95
+ _ = signal(SIGILL, sigill0) if sigill else None
96
+ _ = signal(SIGINT, sigint0) if sigint else None
97
+ _ = signal(SIGSEGV, sigsegv0) if sigsegv else None
98
+ _ = signal(SIGTERM, sigterm0) if sigterm else None
17
99
 
18
- def __exit__(
19
- self,
20
- exc_type: type[BaseException] | None,
21
- exc_val: BaseException | None,
22
- traceback: TracebackType | None,
23
- ) -> bool:
24
- _ = (exc_type, exc_val, traceback)
25
- return False
100
+ return wrapped
101
+
102
+
103
+ @overload
104
+ def enhanced_async_context_manager[**P, T_co](
105
+ func: Callable[P, AsyncIterator[T_co]],
106
+ /,
107
+ *,
108
+ sigabrt: bool = True,
109
+ sigfpe: bool = True,
110
+ sigill: bool = True,
111
+ sigint: bool = True,
112
+ sigsegv: bool = True,
113
+ sigterm: bool = True,
114
+ ) -> Callable[P, _AsyncGeneratorContextManager[T_co]]: ...
115
+ @overload
116
+ def enhanced_async_context_manager[**P, T_co](
117
+ func: None = None,
118
+ /,
119
+ *,
120
+ sigabrt: bool = True,
121
+ sigfpe: bool = True,
122
+ sigill: bool = True,
123
+ sigint: bool = True,
124
+ sigsegv: bool = True,
125
+ sigterm: bool = True,
126
+ ) -> Callable[
127
+ [Callable[P, AsyncIterator[T_co]]], Callable[P, _AsyncGeneratorContextManager[T_co]]
128
+ ]: ...
129
+ def enhanced_async_context_manager[**P, T_co](
130
+ func: Callable[P, AsyncIterator[T_co]] | None = None,
131
+ /,
132
+ *,
133
+ sigabrt: bool = True,
134
+ sigfpe: bool = True,
135
+ sigill: bool = True,
136
+ sigint: bool = True,
137
+ sigsegv: bool = True,
138
+ sigterm: bool = True,
139
+ ) -> (
140
+ Callable[P, _AsyncGeneratorContextManager[T_co]]
141
+ | Callable[
142
+ [Callable[P, AsyncIterator[T_co]]],
143
+ Callable[P, _AsyncGeneratorContextManager[T_co]],
144
+ ]
145
+ ):
146
+ if func is None:
147
+ result = partial(
148
+ enhanced_async_context_manager,
149
+ sigabrt=sigabrt,
150
+ sigfpe=sigfpe,
151
+ sigill=sigill,
152
+ sigint=sigint,
153
+ sigsegv=sigsegv,
154
+ sigterm=sigterm,
155
+ )
156
+ return cast(
157
+ "Callable[[Callable[P, AsyncIterator[T_co]]], Callable[P, _AsyncGeneratorContextManager[T_co]]]",
158
+ result,
159
+ )
160
+ make_agcm = asynccontextmanager(func)
161
+
162
+ @asynccontextmanager
163
+ @wraps(func)
164
+ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncIterator[T_co]:
165
+ agcm = make_agcm(*args, **kwargs)
166
+ sigabrt0 = _swap_handler(SIGABRT, agcm) if sigabrt else None
167
+ sigfpe0 = _swap_handler(SIGFPE, agcm) if sigfpe else None
168
+ sigill0 = _swap_handler(SIGILL, agcm) if sigill else None
169
+ sigint0 = _swap_handler(SIGINT, agcm) if sigint else None
170
+ sigsegv0 = _swap_handler(SIGSEGV, agcm) if sigsegv else None
171
+ sigterm0 = _swap_handler(SIGTERM, agcm) if sigterm else None
172
+ try:
173
+ async with agcm as value:
174
+ yield value
175
+ finally:
176
+ _ = signal(SIGABRT, sigabrt0) if sigabrt else None
177
+ _ = signal(SIGFPE, sigfpe0) if sigfpe else None
178
+ _ = signal(SIGILL, sigill0) if sigill else None
179
+ _ = signal(SIGINT, sigint0) if sigint else None
180
+ _ = signal(SIGSEGV, sigsegv0) if sigsegv else None
181
+ _ = signal(SIGTERM, sigterm0) if sigterm else None
182
+
183
+ return wrapped
184
+
185
+
186
+ def _swap_handler(
187
+ signum: _SIGNUM,
188
+ obj: _GeneratorContextManager[Any, None, None]
189
+ | _AsyncGeneratorContextManager[Any, None],
190
+ /,
191
+ ) -> _HANDLER:
192
+ orig_handler = getsignal(signum)
193
+ new_handler = _make_handler(signum, obj)
194
+ _ = signal(signum, new_handler)
195
+ return orig_handler
196
+
197
+
198
+ def _make_handler(
199
+ signum: _SIGNUM,
200
+ obj: _GeneratorContextManager[Any, None, None]
201
+ | _AsyncGeneratorContextManager[Any, None],
202
+ /,
203
+ ) -> Callable[[int, FrameType | None], None]:
204
+ orig_handler = getsignal(signum)
205
+
206
+ def new_handler(signum: int, frame: FrameType | None) -> None:
207
+ match obj: # pragma: no cover
208
+ case _GeneratorContextManager() as gcm:
209
+ _ = gcm.__exit__(None, None, None)
210
+ case _AsyncGeneratorContextManager() as agcm:
211
+ loop = get_event_loop()
212
+ _ = loop.call_soon_threadsafe(
213
+ create_task, agcm.__aexit__(None, None, None)
214
+ )
215
+ case never:
216
+ assert_never(never)
217
+ if callable(orig_handler): # pragma: no cover
218
+ orig_handler(signum, frame)
219
+
220
+ return new_handler
26
221
 
27
222
 
28
223
  ##
@@ -41,4 +236,8 @@ def suppress_super_object_attribute_error() -> Iterator[None]:
41
236
  raise
42
237
 
43
238
 
44
- __all__ = ["NoOpContextManager", "suppress_super_object_attribute_error"]
239
+ __all__ = [
240
+ "enhanced_async_context_manager",
241
+ "enhanced_context_manager",
242
+ "suppress_super_object_attribute_error",
243
+ ]
utilities/contextvars.py CHANGED
@@ -1,6 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from contextlib import contextmanager
3
4
  from contextvars import ContextVar
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterator
9
+
4
10
 
5
11
  ##
6
12
 
@@ -19,4 +25,17 @@ def set_global_breakpoint() -> None:
19
25
  _ = _GLOBAL_BREAKPOINT.set(True)
20
26
 
21
27
 
22
- __all__ = ["global_breakpoint", "set_global_breakpoint"]
28
+ ##
29
+
30
+
31
+ @contextmanager
32
+ def yield_set_context(var: ContextVar[bool], /) -> Iterator[None]:
33
+ """Yield a context var as being set."""
34
+ token = var.set(True)
35
+ try:
36
+ yield
37
+ finally:
38
+ _ = var.reset(token)
39
+
40
+
41
+ __all__ = ["global_breakpoint", "set_global_breakpoint", "yield_set_context"]
utilities/cryptography.py CHANGED
@@ -11,18 +11,18 @@ _ENV_VAR = "FERNET_KEY"
11
11
 
12
12
  def encrypt(text: str, /, *, env_var: str = _ENV_VAR) -> bytes:
13
13
  """Encrypt a string."""
14
- return get_fernet(env_var=env_var).encrypt(text.encode())
14
+ return get_fernet(env_var).encrypt(text.encode())
15
15
 
16
16
 
17
17
  def decrypt(text: bytes, /, *, env_var: str = _ENV_VAR) -> str:
18
18
  """Encrypt a string."""
19
- return get_fernet(env_var=env_var).decrypt(text).decode()
19
+ return get_fernet(env_var).decrypt(text).decode()
20
20
 
21
21
 
22
22
  ##
23
23
 
24
24
 
25
- def get_fernet(*, env_var: str = _ENV_VAR) -> Fernet:
25
+ def get_fernet(env_var: str = _ENV_VAR, /) -> Fernet:
26
26
  """Get the Fernet key."""
27
27
  if (key := getenv(env_var)) is None:
28
28
  raise GetFernetError(env_var=env_var)
utilities/dataclasses.py CHANGED
@@ -6,11 +6,7 @@ from dataclasses import MISSING, dataclass, field, fields, replace
6
6
  from typing import TYPE_CHECKING, Any, Literal, assert_never, overload, override
7
7
 
8
8
  from utilities.errors import ImpossibleCaseError
9
- from utilities.functions import (
10
- get_class_name,
11
- is_dataclass_class,
12
- is_dataclass_instance,
13
- )
9
+ from utilities.functions import get_class_name
14
10
  from utilities.iterables import (
15
11
  OneStrEmptyError,
16
12
  OneStrNonUniqueError,
@@ -25,7 +21,7 @@ from utilities.parse import (
25
21
  serialize_object,
26
22
  )
27
23
  from utilities.re import ExtractGroupError, extract_group
28
- from utilities.sentinel import Sentinel, sentinel
24
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
29
25
  from utilities.text import (
30
26
  BRACKETS,
31
27
  LIST_SEPARATOR,
@@ -34,8 +30,8 @@ from utilities.text import (
34
30
  _SplitKeyValuePairsSplitError,
35
31
  split_key_value_pairs,
36
32
  )
37
- from utilities.types import SupportsLT
38
- from utilities.typing import get_type_hints
33
+ from utilities.types import MaybeType, SupportsLT
34
+ from utilities.typing import get_type_hints, is_dataclass_class, is_dataclass_instance
39
35
 
40
36
  if TYPE_CHECKING:
41
37
  from collections.abc import Callable, Iterable, Iterator
@@ -214,7 +210,7 @@ def is_nullable_lt[T: SupportsLT](x: T | None, y: T | None, /) -> bool | None:
214
210
  return True
215
211
  case 0:
216
212
  return None
217
- case _ as never:
213
+ case never:
218
214
  assert_never(never)
219
215
 
220
216
 
@@ -275,8 +271,7 @@ def mapping_to_dataclass[T: Dataclass](
275
271
  default = {
276
272
  f.name
277
273
  for f in fields_use
278
- if (not isinstance(f.default, Sentinel))
279
- or (not isinstance(f.default_factory, Sentinel))
274
+ if (not is_sentinel(f.default)) or (not is_sentinel(f.default_factory))
280
275
  }
281
276
  have = set(field_names_to_values) | default
282
277
  missing = {f.name for f in fields_use} - have
@@ -434,12 +429,10 @@ def replace_non_sentinel[T: Dataclass](
434
429
  """Replace attributes on a dataclass, filtering out sentinel values."""
435
430
  if in_place:
436
431
  for k, v in kwargs.items():
437
- if not isinstance(v, Sentinel):
432
+ if not is_sentinel(v):
438
433
  setattr(obj, k, v)
439
434
  return None
440
- return replace(
441
- obj, **{k: v for k, v in kwargs.items() if not isinstance(v, Sentinel)}
442
- )
435
+ return replace(obj, **{k: v for k, v in kwargs.items() if not is_sentinel(v)})
443
436
 
444
437
 
445
438
  ##
@@ -520,7 +513,7 @@ def parse_dataclass[T: Dataclass](
520
513
  )
521
514
  case Mapping() as keys_to_serializes:
522
515
  ...
523
- case _ as never:
516
+ case never:
524
517
  assert_never(never)
525
518
  fields = list(
526
519
  yield_fields(
@@ -833,7 +826,7 @@ def yield_fields(
833
826
  warn_name_errors: bool = False,
834
827
  ) -> Iterator[_YieldFieldsClass[Any]]: ...
835
828
  def yield_fields(
836
- obj: Dataclass | type[Dataclass],
829
+ obj: MaybeType[Dataclass],
837
830
  /,
838
831
  *,
839
832
  globalns: StrMapping | None = None,
@@ -912,17 +905,11 @@ class _YieldFieldsInstance[T]:
912
905
  extra: Mapping[type[U], Callable[[U, U], bool]] | None = None,
913
906
  ) -> bool:
914
907
  """Check if the field value equals its default."""
915
- if isinstance(self.default, Sentinel) and isinstance(
916
- self.default_factory, Sentinel
917
- ):
908
+ if is_sentinel(self.default) and is_sentinel(self.default_factory):
918
909
  return False
919
- if (not isinstance(self.default, Sentinel)) and isinstance(
920
- self.default_factory, Sentinel
921
- ):
910
+ if (not is_sentinel(self.default)) and is_sentinel(self.default_factory):
922
911
  expected = self.default
923
- elif isinstance(self.default, Sentinel) and (
924
- not isinstance(self.default_factory, Sentinel)
925
- ):
912
+ elif is_sentinel(self.default) and (not is_sentinel(self.default_factory)):
926
913
  expected = self.default_factory()
927
914
  else: # pragma: no cover
928
915
  raise ImpossibleCaseError(
@@ -1002,7 +989,7 @@ def _empty_error_str_core(
1002
989
  return f"any field starting with {key!r}"
1003
990
  case True, False:
1004
991
  return f"any field starting with {key!r} (modulo case)"
1005
- case _ as never:
992
+ case never:
1006
993
  assert_never(never)
1007
994
 
1008
995
 
@@ -1043,7 +1030,7 @@ def _non_unique_error_str_core(
1043
1030
  head_msg = f"exactly one field starting with {key!r}"
1044
1031
  case True, False:
1045
1032
  head_msg = f"exactly one field starting with {key!r} (modulo case)"
1046
- case _ as never:
1033
+ case never:
1047
1034
  assert_never(never)
1048
1035
  return f"{head_msg}; got {first!r}, {second!r} and perhaps more"
1049
1036