dycw-utilities 0.148.4__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.
- dycw_utilities-0.174.12.dist-info/METADATA +41 -0
- dycw_utilities-0.174.12.dist-info/RECORD +104 -0
- dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
- {dycw_utilities-0.148.4.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
- utilities/__init__.py +1 -1
- utilities/{eventkit.py → aeventkit.py} +12 -11
- utilities/altair.py +7 -6
- utilities/asyncio.py +113 -64
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +145 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +292 -0
- utilities/enum.py +2 -2
- utilities/errors.py +1 -1
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +360 -78
- utilities/inflect.py +1 -1
- utilities/iterables.py +12 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +74 -85
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +297 -0
- utilities/platform.py +5 -5
- utilities/polars.py +932 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +299 -174
- utilities/pottery.py +8 -73
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +155 -46
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +2 -3
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +8 -3
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +864 -0
- utilities/tempfile.py +62 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +2 -2
- utilities/traceback.py +46 -36
- utilities/types.py +62 -23
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +661 -151
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.148.4.dist-info/METADATA +0 -41
- dycw_utilities-0.148.4.dist-info/RECORD +0 -95
- dycw_utilities-0.148.4.dist-info/WHEEL +0 -4
- dycw_utilities-0.148.4.dist-info/licenses/LICENSE +0 -21
- utilities/period.py +0 -237
- utilities/typed_settings.py +0 -144
utilities/permissions.py
ADDED
|
@@ -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":
|
|
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": #
|
|
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
|
|
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
|
|
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
|
|
99
|
+
case never:
|
|
100
100
|
assert_never(never)
|
|
101
101
|
|
|
102
102
|
|