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