dycw-utilities 0.175.17__py3-none-any.whl → 0.185.8__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.
- dycw_utilities-0.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +2 -2
- utilities/__init__.py +1 -1
- utilities/altair.py +8 -6
- utilities/asyncio.py +40 -56
- utilities/atools.py +9 -11
- utilities/cachetools.py +8 -6
- utilities/click.py +4 -3
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +139 -45
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +33 -264
- utilities/http.py +2 -3
- utilities/hypothesis.py +48 -25
- utilities/iterables.py +39 -575
- utilities/jinja2.py +3 -6
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +17 -15
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +24 -25
- utilities/os.py +4 -185
- utilities/packaging.py +129 -0
- utilities/parse.py +33 -13
- utilities/pathlib.py +2 -136
- utilities/platform.py +8 -90
- utilities/polars.py +34 -31
- utilities/postgres.py +9 -4
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +18 -4
- utilities/pydantic_settings.py +7 -9
- utilities/pydantic_settings_sops.py +3 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +49 -108
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +8 -6
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +42 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +1166 -148
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -115
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +29 -12
- utilities/traceback.py +15 -22
- utilities/types.py +38 -3
- utilities/typing.py +18 -12
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +22 -150
- dycw_utilities-0.175.17.dist-info/METADATA +0 -34
- dycw_utilities-0.175.17.dist-info/RECORD +0 -103
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/grp.py +0 -28
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/permissions.py +0 -298
- utilities/pickle.py +0 -25
- utilities/pwd.py +0 -28
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -136
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- utilities/zoneinfo.py +0 -133
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +0 -0
utilities/permissions.py
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import gzip
|
|
4
|
-
from pickle import dump, load
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
-
|
|
7
|
-
from utilities.atomicwrites import writer
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from utilities.types import PathLike
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def read_pickle(path: PathLike, /) -> Any:
|
|
14
|
-
"""Read an object from disk."""
|
|
15
|
-
with gzip.open(path, mode="rb") as gz:
|
|
16
|
-
return load(gz) # noqa: S301
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def write_pickle(obj: Any, path: PathLike, /, *, overwrite: bool = False) -> None:
|
|
20
|
-
"""Write an object to disk."""
|
|
21
|
-
with writer(path, overwrite=overwrite) as temp, gzip.open(temp, mode="wb") as gz:
|
|
22
|
-
dump(obj, gz)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
__all__ = ["read_pickle", "write_pickle"]
|
utilities/pwd.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import assert_never
|
|
4
|
-
|
|
5
|
-
from utilities.os import EFFECTIVE_USER_ID
|
|
6
|
-
from utilities.platform import SYSTEM
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_uid_name(uid: int, /) -> str | None:
|
|
10
|
-
"""Get the name of a user ID."""
|
|
11
|
-
match SYSTEM:
|
|
12
|
-
case "windows": # skipif-not-windows
|
|
13
|
-
return None
|
|
14
|
-
case "mac" | "linux": # skipif-windows
|
|
15
|
-
from pwd import getpwuid
|
|
16
|
-
|
|
17
|
-
return getpwuid(uid).pw_name
|
|
18
|
-
case never:
|
|
19
|
-
assert_never(never)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ROOT_USER_NAME = get_uid_name(0)
|
|
23
|
-
EFFECTIVE_USER_NAME = (
|
|
24
|
-
None if EFFECTIVE_USER_ID is None else get_uid_name(EFFECTIVE_USER_ID)
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
__all__ = ["EFFECTIVE_USER_NAME", "ROOT_USER_NAME", "get_uid_name"]
|
utilities/re.py
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from re import Pattern
|
|
6
|
-
from typing import TYPE_CHECKING, assert_never, override
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from utilities.types import PatternLike
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def ensure_pattern(pattern: PatternLike, /, *, flags: int = 0) -> Pattern[str]:
|
|
13
|
-
"""Ensure a pattern is returned."""
|
|
14
|
-
match pattern:
|
|
15
|
-
case Pattern():
|
|
16
|
-
return pattern
|
|
17
|
-
case str():
|
|
18
|
-
return re.compile(pattern, flags=flags)
|
|
19
|
-
case never:
|
|
20
|
-
assert_never(never)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def extract_group(pattern: PatternLike, text: str, /, *, flags: int = 0) -> str:
|
|
27
|
-
"""Extract a group.
|
|
28
|
-
|
|
29
|
-
The regex must have 1 capture group, and this must match exactly once.
|
|
30
|
-
"""
|
|
31
|
-
pattern_use = ensure_pattern(pattern, flags=flags)
|
|
32
|
-
match pattern_use.groups:
|
|
33
|
-
case 0:
|
|
34
|
-
raise _ExtractGroupNoCaptureGroupsError(pattern=pattern_use, text=text)
|
|
35
|
-
case 1:
|
|
36
|
-
matches: list[str] = pattern_use.findall(text)
|
|
37
|
-
match len(matches):
|
|
38
|
-
case 0:
|
|
39
|
-
raise _ExtractGroupNoMatchesError(
|
|
40
|
-
pattern=pattern_use, text=text
|
|
41
|
-
) from None
|
|
42
|
-
case 1:
|
|
43
|
-
return matches[0]
|
|
44
|
-
case _:
|
|
45
|
-
raise _ExtractGroupMultipleMatchesError(
|
|
46
|
-
pattern=pattern_use, text=text, matches=matches
|
|
47
|
-
) from None
|
|
48
|
-
case _:
|
|
49
|
-
raise _ExtractGroupMultipleCaptureGroupsError(
|
|
50
|
-
pattern=pattern_use, text=text
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@dataclass(kw_only=True, slots=True)
|
|
55
|
-
class ExtractGroupError(Exception):
|
|
56
|
-
pattern: Pattern[str]
|
|
57
|
-
text: str
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass(kw_only=True, slots=True)
|
|
61
|
-
class _ExtractGroupMultipleCaptureGroupsError(ExtractGroupError):
|
|
62
|
-
@override
|
|
63
|
-
def __str__(self) -> str:
|
|
64
|
-
return f"Pattern {self.pattern} must contain exactly one capture group; it had multiple"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass(kw_only=True, slots=True)
|
|
68
|
-
class _ExtractGroupMultipleMatchesError(ExtractGroupError):
|
|
69
|
-
matches: list[str]
|
|
70
|
-
|
|
71
|
-
@override
|
|
72
|
-
def __str__(self) -> str:
|
|
73
|
-
return f"Pattern {self.pattern} must match against {self.text} exactly once; matches were {self.matches}"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass(kw_only=True, slots=True)
|
|
77
|
-
class _ExtractGroupNoCaptureGroupsError(ExtractGroupError):
|
|
78
|
-
@override
|
|
79
|
-
def __str__(self) -> str:
|
|
80
|
-
return f"Pattern {self.pattern} must contain exactly one capture group; it had none".format(
|
|
81
|
-
self.pattern
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@dataclass(kw_only=True, slots=True)
|
|
86
|
-
class _ExtractGroupNoMatchesError(ExtractGroupError):
|
|
87
|
-
@override
|
|
88
|
-
def __str__(self) -> str:
|
|
89
|
-
return f"Pattern {self.pattern} must match against {self.text}"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
##
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def extract_groups(pattern: PatternLike, text: str, /, *, flags: int = 0) -> list[str]:
|
|
96
|
-
"""Extract multiple groups.
|
|
97
|
-
|
|
98
|
-
The regex may have any number of capture groups, and they must collectively
|
|
99
|
-
match exactly once.
|
|
100
|
-
"""
|
|
101
|
-
pattern_use = ensure_pattern(pattern, flags=flags)
|
|
102
|
-
if (n_groups := pattern_use.groups) == 0:
|
|
103
|
-
raise _ExtractGroupsNoCaptureGroupsError(pattern=pattern_use, text=text)
|
|
104
|
-
matches: list[str] = pattern_use.findall(text)
|
|
105
|
-
match len(matches), n_groups:
|
|
106
|
-
case 0, _:
|
|
107
|
-
raise _ExtractGroupsNoMatchesError(pattern=pattern_use, text=text)
|
|
108
|
-
case 1, 1:
|
|
109
|
-
return matches
|
|
110
|
-
case 1, _:
|
|
111
|
-
return list(matches[0])
|
|
112
|
-
case _:
|
|
113
|
-
raise _ExtractGroupsMultipleMatchesError(
|
|
114
|
-
pattern=pattern_use, text=text, matches=matches
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
@dataclass(kw_only=True, slots=True)
|
|
119
|
-
class ExtractGroupsError(Exception):
|
|
120
|
-
pattern: Pattern[str]
|
|
121
|
-
text: str
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@dataclass(kw_only=True, slots=True)
|
|
125
|
-
class _ExtractGroupsMultipleMatchesError(ExtractGroupsError):
|
|
126
|
-
matches: list[str]
|
|
127
|
-
|
|
128
|
-
@override
|
|
129
|
-
def __str__(self) -> str:
|
|
130
|
-
return f"Pattern {self.pattern} must match against {self.text} exactly once; matches were {self.matches}"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@dataclass(kw_only=True, slots=True)
|
|
134
|
-
class _ExtractGroupsNoCaptureGroupsError(ExtractGroupsError):
|
|
135
|
-
pattern: Pattern[str]
|
|
136
|
-
text: str
|
|
137
|
-
|
|
138
|
-
@override
|
|
139
|
-
def __str__(self) -> str:
|
|
140
|
-
return f"Pattern {self.pattern} must contain at least one capture group"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@dataclass(kw_only=True, slots=True)
|
|
144
|
-
class _ExtractGroupsNoMatchesError(ExtractGroupsError):
|
|
145
|
-
@override
|
|
146
|
-
def __str__(self) -> str:
|
|
147
|
-
return f"Pattern {self.pattern} must match against {self.text}"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
__all__ = [
|
|
151
|
-
"ExtractGroupError",
|
|
152
|
-
"ExtractGroupsError",
|
|
153
|
-
"ensure_pattern",
|
|
154
|
-
"extract_group",
|
|
155
|
-
"extract_groups",
|
|
156
|
-
]
|
utilities/sentinel.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from re import IGNORECASE, search
|
|
5
|
-
from typing import Any, override
|
|
6
|
-
|
|
7
|
-
from typing_extensions import TypeIs
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class _Meta(type):
|
|
11
|
-
"""Metaclass for the sentinel."""
|
|
12
|
-
|
|
13
|
-
instance: Any = None
|
|
14
|
-
|
|
15
|
-
@override
|
|
16
|
-
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
17
|
-
if cls.instance is None:
|
|
18
|
-
cls.instance = super().__call__(*args, **kwargs)
|
|
19
|
-
return cls.instance
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
SENTINEL_REPR = "<sentinel>"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Sentinel(metaclass=_Meta):
|
|
26
|
-
"""Base class for the sentinel object."""
|
|
27
|
-
|
|
28
|
-
@override
|
|
29
|
-
def __repr__(self) -> str:
|
|
30
|
-
return SENTINEL_REPR
|
|
31
|
-
|
|
32
|
-
@override
|
|
33
|
-
def __str__(self) -> str:
|
|
34
|
-
return repr(self)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sentinel = Sentinel()
|
|
38
|
-
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]:
|
|
43
|
-
"""Check if an object is the sentinel."""
|
|
44
|
-
return obj is sentinel
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def parse_sentinel(text: str, /) -> Sentinel:
|
|
51
|
-
"""Parse text into the Sentinel value."""
|
|
52
|
-
if search("^(|sentinel|<sentinel>)$", text, flags=IGNORECASE):
|
|
53
|
-
return sentinel
|
|
54
|
-
raise ParseSentinelError(text=text)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass(kw_only=True, slots=True)
|
|
58
|
-
class ParseSentinelError(Exception):
|
|
59
|
-
text: str
|
|
60
|
-
|
|
61
|
-
@override
|
|
62
|
-
def __str__(self) -> str:
|
|
63
|
-
return f"Unable to parse sentinel value; got {self.text!r}"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
__all__ = [
|
|
67
|
-
"SENTINEL_REPR",
|
|
68
|
-
"ParseSentinelError",
|
|
69
|
-
"Sentinel",
|
|
70
|
-
"is_sentinel",
|
|
71
|
-
"parse_sentinel",
|
|
72
|
-
"sentinel",
|
|
73
|
-
]
|
utilities/socket.py
DELETED
utilities/string.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from os import environ
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from string import Template
|
|
6
|
-
from typing import Any, assert_never
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def substitute_environ(path_or_text: Path | str, /, **kwargs: Any) -> str:
|
|
10
|
-
"""Substitute the environment variables in a file."""
|
|
11
|
-
match path_or_text:
|
|
12
|
-
case Path() as path:
|
|
13
|
-
return substitute_environ(path.read_text(), **kwargs)
|
|
14
|
-
case str() as text:
|
|
15
|
-
return Template(text).substitute(environ, **kwargs)
|
|
16
|
-
case never:
|
|
17
|
-
assert_never(never)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
__all__ = ["substitute_environ"]
|