dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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.
Files changed (103) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.17.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +14 -14
  7. utilities/asyncio.py +350 -819
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +77 -22
  10. utilities/cachetools.py +24 -29
  11. utilities/click.py +393 -237
  12. utilities/concurrent.py +8 -11
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +83 -118
  17. utilities/docker.py +293 -0
  18. utilities/enum.py +26 -23
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +29 -65
  21. utilities/fpdf2.py +3 -3
  22. utilities/functions.py +169 -416
  23. utilities/functools.py +18 -19
  24. utilities/git.py +9 -30
  25. utilities/grp.py +28 -0
  26. utilities/gzip.py +31 -0
  27. utilities/http.py +3 -2
  28. utilities/hypothesis.py +738 -589
  29. utilities/importlib.py +17 -1
  30. utilities/inflect.py +25 -0
  31. utilities/iterables.py +194 -262
  32. utilities/jinja2.py +148 -0
  33. utilities/json.py +70 -0
  34. utilities/libcst.py +38 -17
  35. utilities/lightweight_charts.py +5 -9
  36. utilities/logging.py +345 -543
  37. utilities/math.py +18 -13
  38. utilities/memory_profiler.py +11 -15
  39. utilities/more_itertools.py +200 -131
  40. utilities/operator.py +33 -29
  41. utilities/optuna.py +6 -6
  42. utilities/orjson.py +272 -137
  43. utilities/os.py +61 -4
  44. utilities/parse.py +59 -61
  45. utilities/pathlib.py +281 -40
  46. utilities/permissions.py +298 -0
  47. utilities/pickle.py +2 -2
  48. utilities/platform.py +24 -5
  49. utilities/polars.py +1214 -430
  50. utilities/polars_ols.py +1 -1
  51. utilities/postgres.py +408 -0
  52. utilities/pottery.py +113 -26
  53. utilities/pqdm.py +10 -11
  54. utilities/psutil.py +6 -57
  55. utilities/pwd.py +28 -0
  56. utilities/pydantic.py +4 -54
  57. utilities/pydantic_settings.py +240 -0
  58. utilities/pydantic_settings_sops.py +76 -0
  59. utilities/pyinstrument.py +8 -10
  60. utilities/pytest.py +227 -121
  61. utilities/pytest_plugins/__init__.py +1 -0
  62. utilities/pytest_plugins/pytest_randomly.py +23 -0
  63. utilities/pytest_plugins/pytest_regressions.py +56 -0
  64. utilities/pytest_regressions.py +26 -46
  65. utilities/random.py +13 -9
  66. utilities/re.py +58 -28
  67. utilities/redis.py +401 -550
  68. utilities/scipy.py +1 -1
  69. utilities/sentinel.py +10 -0
  70. utilities/shelve.py +4 -1
  71. utilities/shutil.py +25 -0
  72. utilities/slack_sdk.py +36 -106
  73. utilities/sqlalchemy.py +502 -473
  74. utilities/sqlalchemy_polars.py +38 -94
  75. utilities/string.py +2 -3
  76. utilities/subprocess.py +1572 -0
  77. utilities/tempfile.py +86 -4
  78. utilities/testbook.py +50 -0
  79. utilities/text.py +165 -42
  80. utilities/timer.py +37 -65
  81. utilities/traceback.py +158 -929
  82. utilities/types.py +146 -116
  83. utilities/typing.py +531 -71
  84. utilities/tzdata.py +1 -53
  85. utilities/tzlocal.py +6 -23
  86. utilities/uuid.py +43 -5
  87. utilities/version.py +27 -26
  88. utilities/whenever.py +1776 -386
  89. utilities/zoneinfo.py +84 -22
  90. dycw_utilities-0.129.10.dist-info/METADATA +0 -241
  91. dycw_utilities-0.129.10.dist-info/RECORD +0 -96
  92. dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
  93. dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
  94. utilities/datetime.py +0 -1409
  95. utilities/eventkit.py +0 -402
  96. utilities/loguru.py +0 -144
  97. utilities/luigi.py +0 -228
  98. utilities/period.py +0 -324
  99. utilities/pyrsistent.py +0 -89
  100. utilities/python_dotenv.py +0 -105
  101. utilities/streamlit.py +0 -105
  102. utilities/sys.py +0 -87
  103. utilities/tenacity.py +0 -145
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import reduce
5
+ from operator import or_
6
+ from pathlib import Path
7
+ from stat import (
8
+ S_IMODE,
9
+ S_IRGRP,
10
+ S_IROTH,
11
+ S_IRUSR,
12
+ S_IWGRP,
13
+ S_IWOTH,
14
+ S_IWUSR,
15
+ S_IXGRP,
16
+ S_IXOTH,
17
+ S_IXUSR,
18
+ )
19
+ from typing import TYPE_CHECKING, Literal, Self, assert_never, override
20
+
21
+ from utilities.dataclasses import replace_non_sentinel
22
+ from utilities.re import ExtractGroupsError, extract_groups
23
+ from utilities.sentinel import Sentinel, sentinel
24
+
25
+ if TYPE_CHECKING:
26
+ from utilities.types import PathLike
27
+
28
+
29
+ type PermissionsLike = Permissions | int | str
30
+
31
+
32
+ ##
33
+
34
+
35
+ def ensure_perms(perms: PermissionsLike, /) -> Permissions:
36
+ """Ensure a set of file permissions."""
37
+ match perms:
38
+ case Permissions():
39
+ return perms
40
+ case int():
41
+ return Permissions.from_int(perms)
42
+ case str():
43
+ return Permissions.from_text(perms)
44
+ case never:
45
+ assert_never(never)
46
+
47
+
48
+ ##
49
+
50
+
51
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
52
+ class Permissions:
53
+ """A set of file permissions."""
54
+
55
+ user_read: bool = False
56
+ user_write: bool = False
57
+ user_execute: bool = False
58
+ group_read: bool = False
59
+ group_write: bool = False
60
+ group_execute: bool = False
61
+ others_read: bool = False
62
+ others_write: bool = False
63
+ others_execute: bool = False
64
+
65
+ def __int__(self) -> int:
66
+ flags: list[int] = [
67
+ S_IRUSR if self.user_read else 0,
68
+ S_IWUSR if self.user_write else 0,
69
+ S_IXUSR if self.user_execute else 0,
70
+ S_IRGRP if self.group_read else 0,
71
+ S_IWGRP if self.group_write else 0,
72
+ S_IXGRP if self.group_execute else 0,
73
+ S_IROTH if self.others_read else 0,
74
+ S_IWOTH if self.others_write else 0,
75
+ S_IXOTH if self.others_execute else 0,
76
+ ]
77
+ return reduce(or_, flags)
78
+
79
+ @override
80
+ def __repr__(self) -> str:
81
+ return ",".join([
82
+ self._repr_parts(
83
+ "u",
84
+ read=self.user_read,
85
+ write=self.user_write,
86
+ execute=self.user_execute,
87
+ ),
88
+ self._repr_parts(
89
+ "g",
90
+ read=self.group_read,
91
+ write=self.group_write,
92
+ execute=self.group_execute,
93
+ ),
94
+ self._repr_parts(
95
+ "o",
96
+ read=self.others_read,
97
+ write=self.others_write,
98
+ execute=self.others_execute,
99
+ ),
100
+ ])
101
+
102
+ def _repr_parts(
103
+ self,
104
+ prefix: Literal["u", "g", "o"],
105
+ /,
106
+ *,
107
+ read: bool = False,
108
+ write: bool = False,
109
+ execute: bool = False,
110
+ ) -> str:
111
+ parts: list[str] = [
112
+ "r" if read else "",
113
+ "w" if write else "",
114
+ "x" if execute else "",
115
+ ]
116
+ return f"{prefix}={''.join(parts)}"
117
+
118
+ @override
119
+ def __str__(self) -> str:
120
+ return repr(self)
121
+
122
+ @classmethod
123
+ def from_human_int(cls, n: int, /) -> Self:
124
+ if not (0 <= n <= 777):
125
+ raise PermissionsFromHumanIntRangeError(n=n)
126
+ user_read, user_write, user_execute = cls._from_human_int(n, (n // 100) % 10)
127
+ group_read, group_write, group_execute = cls._from_human_int(n, (n // 10) % 10)
128
+ others_read, others_write, others_execute = cls._from_human_int(n, n % 10)
129
+ return cls(
130
+ user_read=user_read,
131
+ user_write=user_write,
132
+ user_execute=user_execute,
133
+ group_read=group_read,
134
+ group_write=group_write,
135
+ group_execute=group_execute,
136
+ others_read=others_read,
137
+ others_write=others_write,
138
+ others_execute=others_execute,
139
+ )
140
+
141
+ @classmethod
142
+ def _from_human_int(cls, n: int, digit: int, /) -> tuple[bool, bool, bool]:
143
+ if not (0 <= digit <= 7):
144
+ raise PermissionsFromHumanIntDigitError(n=n, digit=digit)
145
+ return bool(4 & digit), bool(2 & digit), bool(1 & digit)
146
+
147
+ @classmethod
148
+ def from_int(cls, n: int, /) -> Self:
149
+ if 0o0 <= n <= 0o777:
150
+ return cls(
151
+ user_read=bool(n & S_IRUSR),
152
+ user_write=bool(n & S_IWUSR),
153
+ user_execute=bool(n & S_IXUSR),
154
+ group_read=bool(n & S_IRGRP),
155
+ group_write=bool(n & S_IWGRP),
156
+ group_execute=bool(n & S_IXGRP),
157
+ others_read=bool(n & S_IROTH),
158
+ others_write=bool(n & S_IWOTH),
159
+ others_execute=bool(n & S_IXOTH),
160
+ )
161
+ raise PermissionsFromIntError(n=n)
162
+
163
+ @classmethod
164
+ def from_path(cls, path: PathLike, /) -> Self:
165
+ return cls.from_int(S_IMODE(Path(path).stat().st_mode))
166
+
167
+ @classmethod
168
+ def from_text(cls, text: str, /) -> Self:
169
+ try:
170
+ user, group, others = extract_groups(
171
+ r"^u=(r?w?x?),g=(r?w?x?),o=(r?w?x?)$", text
172
+ )
173
+ except ExtractGroupsError:
174
+ raise PermissionsFromTextError(text=text) from None
175
+ user_read, user_write, user_execute = cls._from_text_part(user)
176
+ group_read, group_write, group_execute = cls._from_text_part(group)
177
+ others_read, others_write, others_execute = cls._from_text_part(others)
178
+ return cls(
179
+ user_read=user_read,
180
+ user_write=user_write,
181
+ user_execute=user_execute,
182
+ group_read=group_read,
183
+ group_write=group_write,
184
+ group_execute=group_execute,
185
+ others_read=others_read,
186
+ others_write=others_write,
187
+ others_execute=others_execute,
188
+ )
189
+
190
+ @classmethod
191
+ def _from_text_part(cls, text: str, /) -> tuple[bool, bool, bool]:
192
+ read, write, execute = extract_groups("^(r?)(w?)(x?)$", text)
193
+ return read != "", write != "", execute != ""
194
+
195
+ @property
196
+ def human_int(self) -> int:
197
+ return (
198
+ 100
199
+ * self._human_int(
200
+ read=self.user_read, write=self.user_write, execute=self.user_execute
201
+ )
202
+ + 10
203
+ * self._human_int(
204
+ read=self.group_read, write=self.group_write, execute=self.group_execute
205
+ )
206
+ + self._human_int(
207
+ read=self.others_read,
208
+ write=self.others_write,
209
+ execute=self.others_execute,
210
+ )
211
+ )
212
+
213
+ def _human_int(
214
+ self, *, read: bool = False, write: bool = False, execute: bool = False
215
+ ) -> int:
216
+ return (4 if read else 0) + (2 if write else 0) + (1 if execute else 0)
217
+
218
+ def replace(
219
+ self,
220
+ *,
221
+ user_read: bool | Sentinel = sentinel,
222
+ user_write: bool | Sentinel = sentinel,
223
+ user_execute: bool | Sentinel = sentinel,
224
+ group_read: bool | Sentinel = sentinel,
225
+ group_write: bool | Sentinel = sentinel,
226
+ group_execute: bool | Sentinel = sentinel,
227
+ others_read: bool | Sentinel = sentinel,
228
+ others_write: bool | Sentinel = sentinel,
229
+ others_execute: bool | Sentinel = sentinel,
230
+ ) -> Self:
231
+ return replace_non_sentinel(
232
+ self,
233
+ user_read=user_read,
234
+ user_write=user_write,
235
+ user_execute=user_execute,
236
+ group_read=group_read,
237
+ group_write=group_write,
238
+ group_execute=group_execute,
239
+ others_read=others_read,
240
+ others_write=others_write,
241
+ others_execute=others_execute,
242
+ )
243
+
244
+
245
+ @dataclass(kw_only=True, slots=True)
246
+ class PermissionsError(Exception): ...
247
+
248
+
249
+ @dataclass(kw_only=True, slots=True)
250
+ class PermissionsFromHumanIntError(PermissionsError):
251
+ n: int
252
+
253
+
254
+ @dataclass(kw_only=True, slots=True)
255
+ class PermissionsFromHumanIntRangeError(PermissionsFromHumanIntError):
256
+ @override
257
+ def __str__(self) -> str:
258
+ return f"Invalid human integer for permissions; got {self.n}"
259
+
260
+
261
+ @dataclass(kw_only=True, slots=True)
262
+ class PermissionsFromHumanIntDigitError(PermissionsFromHumanIntError):
263
+ digit: int
264
+
265
+ @override
266
+ def __str__(self) -> str:
267
+ return (
268
+ f"Invalid human integer for permissions; got digit {self.digit} in {self.n}"
269
+ )
270
+
271
+
272
+ @dataclass(kw_only=True, slots=True)
273
+ class PermissionsFromIntError(PermissionsError):
274
+ n: int
275
+
276
+ @override
277
+ def __str__(self) -> str:
278
+ return f"Invalid integer for permissions; got {self.n} = {oct(self.n)}"
279
+
280
+
281
+ @dataclass(kw_only=True, slots=True)
282
+ class PermissionsFromTextError(PermissionsError):
283
+ text: str
284
+
285
+ @override
286
+ def __str__(self) -> str:
287
+ return f"Invalid string for permissions; got {self.text!r}"
288
+
289
+
290
+ __all__ = [
291
+ "Permissions",
292
+ "PermissionsError",
293
+ "PermissionsFromHumanIntDigitError",
294
+ "PermissionsFromHumanIntError",
295
+ "PermissionsFromIntError",
296
+ "PermissionsFromTextError",
297
+ "ensure_perms",
298
+ ]
utilities/pickle.py CHANGED
@@ -4,6 +4,8 @@ import gzip
4
4
  from pickle import dump, load
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from utilities.atomicwrites import writer
8
+
7
9
  if TYPE_CHECKING:
8
10
  from utilities.types import PathLike
9
11
 
@@ -16,8 +18,6 @@ def read_pickle(path: PathLike, /) -> Any:
16
18
 
17
19
  def write_pickle(obj: Any, path: PathLike, /, *, overwrite: bool = False) -> None:
18
20
  """Write an object to disk."""
19
- from utilities.atomicwrites import writer
20
-
21
21
  with writer(path, overwrite=overwrite) as temp, gzip.open(temp, mode="wb") as gz:
22
22
  dump(obj, gz)
23
23
 
utilities/platform.py CHANGED
@@ -3,11 +3,13 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  from platform import system
6
+ from re import sub
6
7
  from typing import TYPE_CHECKING, Literal, assert_never, override
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from collections.abc import Iterable, Iterator
10
11
 
12
+
11
13
  System = Literal["windows", "mac", "linux"]
12
14
 
13
15
 
@@ -49,17 +51,17 @@ IS_NOT_LINUX = not IS_LINUX
49
51
  def get_max_pid() -> int | None:
50
52
  """Get the maximum process ID."""
51
53
  match SYSTEM:
52
- case "windows": # pragma: no cover
54
+ case "windows": # skipif-not-windows
53
55
  return None
54
56
  case "mac": # skipif-not-macos
55
57
  return 99999
56
58
  case "linux": # skipif-not-linux
59
+ path = Path("/proc/sys/kernel/pid_max")
57
60
  try:
58
- with Path("/proc/sys/kernel/pid_max").open() as fh:
59
- return int(fh.read())
61
+ return int(path.read_text())
60
62
  except FileNotFoundError: # pragma: no cover
61
63
  return None
62
- case _ as never:
64
+ case never:
63
65
  assert_never(never)
64
66
 
65
67
 
@@ -69,6 +71,22 @@ MAX_PID = get_max_pid()
69
71
  ##
70
72
 
71
73
 
74
+ def get_strftime(text: str, /) -> str:
75
+ """Get a platform-specific format string."""
76
+ match SYSTEM:
77
+ case "windows": # skipif-not-windows
78
+ return text
79
+ case "mac": # skipif-not-macos
80
+ return text
81
+ case "linux": # skipif-not-linux
82
+ return sub("%Y", "%4Y", text)
83
+ case never:
84
+ assert_never(never)
85
+
86
+
87
+ ##
88
+
89
+
72
90
  def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
73
91
  """Yield lower-cased text if the platform is case-insentive."""
74
92
  match SYSTEM:
@@ -78,7 +96,7 @@ def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
78
96
  yield from (t.lower() for t in text)
79
97
  case "linux": # skipif-not-linux
80
98
  yield from text
81
- case _ as never:
99
+ case never:
82
100
  assert_never(never)
83
101
 
84
102
 
@@ -94,6 +112,7 @@ __all__ = [
94
112
  "GetSystemError",
95
113
  "System",
96
114
  "get_max_pid",
115
+ "get_strftime",
97
116
  "get_system",
98
117
  "maybe_yield_lower_case",
99
118
  ]