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.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- utilities/tenacity.py +0 -145
utilities/tempfile.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import tempfile
|
|
4
|
+
from contextlib import contextmanager
|
|
3
5
|
from pathlib import Path
|
|
4
|
-
from
|
|
6
|
+
from shutil import move
|
|
7
|
+
from tempfile import NamedTemporaryFile as _NamedTemporaryFile
|
|
5
8
|
from tempfile import gettempdir as _gettempdir
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, override
|
|
10
|
+
|
|
11
|
+
from utilities.warnings import suppress_warnings
|
|
7
12
|
|
|
8
13
|
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterator
|
|
9
15
|
from types import TracebackType
|
|
10
16
|
|
|
11
17
|
from utilities.types import PathLike
|
|
@@ -21,13 +27,15 @@ class TemporaryDirectory:
|
|
|
21
27
|
prefix: str | None = None,
|
|
22
28
|
dir: PathLike | None = None, # noqa: A002
|
|
23
29
|
ignore_cleanup_errors: bool = False,
|
|
30
|
+
delete: bool = True,
|
|
24
31
|
) -> None:
|
|
25
32
|
super().__init__()
|
|
26
|
-
self._temp_dir =
|
|
33
|
+
self._temp_dir = _TemporaryDirectoryNoResourceWarning(
|
|
27
34
|
suffix=suffix,
|
|
28
35
|
prefix=prefix,
|
|
29
36
|
dir=dir,
|
|
30
37
|
ignore_cleanup_errors=ignore_cleanup_errors,
|
|
38
|
+
delete=delete,
|
|
31
39
|
)
|
|
32
40
|
self.path = Path(self._temp_dir.name)
|
|
33
41
|
|
|
@@ -43,6 +51,80 @@ class TemporaryDirectory:
|
|
|
43
51
|
self._temp_dir.__exit__(exc, val, tb)
|
|
44
52
|
|
|
45
53
|
|
|
54
|
+
class _TemporaryDirectoryNoResourceWarning(tempfile.TemporaryDirectory):
|
|
55
|
+
@classmethod
|
|
56
|
+
@override
|
|
57
|
+
def _cleanup( # pyright: ignore[reportGeneralTypeIssues]
|
|
58
|
+
cls,
|
|
59
|
+
name: str,
|
|
60
|
+
warn_message: str,
|
|
61
|
+
ignore_errors: bool = False,
|
|
62
|
+
delete: bool = True,
|
|
63
|
+
) -> None:
|
|
64
|
+
with suppress_warnings(category=ResourceWarning):
|
|
65
|
+
return super()._cleanup( # pyright: ignore[reportAttributeAccessIssue]
|
|
66
|
+
name, warn_message, ignore_errors=ignore_errors, delete=delete
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@contextmanager
|
|
74
|
+
def TemporaryFile( # noqa: N802
|
|
75
|
+
*,
|
|
76
|
+
suffix: str | None = None,
|
|
77
|
+
prefix: str | None = None,
|
|
78
|
+
dir: PathLike | None = None, # noqa: A002
|
|
79
|
+
ignore_cleanup_errors: bool = False,
|
|
80
|
+
delete: bool = True,
|
|
81
|
+
name: str | None = None,
|
|
82
|
+
text: str | None = None,
|
|
83
|
+
) -> Iterator[Path]:
|
|
84
|
+
"""Yield a temporary file."""
|
|
85
|
+
with _temporary_file_inner(
|
|
86
|
+
suffix=suffix,
|
|
87
|
+
prefix=prefix,
|
|
88
|
+
dir=dir,
|
|
89
|
+
ignore_cleanup_errors=ignore_cleanup_errors,
|
|
90
|
+
delete=delete,
|
|
91
|
+
name=name,
|
|
92
|
+
) as temp:
|
|
93
|
+
if text is not None:
|
|
94
|
+
_ = temp.write_text(text)
|
|
95
|
+
yield temp
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@contextmanager
|
|
99
|
+
def _temporary_file_inner(
|
|
100
|
+
*,
|
|
101
|
+
suffix: str | None = None,
|
|
102
|
+
prefix: str | None = None,
|
|
103
|
+
dir: PathLike | None = None, # noqa: A002
|
|
104
|
+
ignore_cleanup_errors: bool = False,
|
|
105
|
+
delete: bool = True,
|
|
106
|
+
name: str | None = None,
|
|
107
|
+
) -> Iterator[Path]:
|
|
108
|
+
with TemporaryDirectory(
|
|
109
|
+
suffix=suffix,
|
|
110
|
+
prefix=prefix,
|
|
111
|
+
dir=dir,
|
|
112
|
+
ignore_cleanup_errors=ignore_cleanup_errors,
|
|
113
|
+
delete=delete,
|
|
114
|
+
) as temp_dir:
|
|
115
|
+
temp_file = _NamedTemporaryFile( # noqa: SIM115
|
|
116
|
+
dir=temp_dir, delete=delete, delete_on_close=False
|
|
117
|
+
)
|
|
118
|
+
if name is None:
|
|
119
|
+
yield temp_dir / temp_file.name
|
|
120
|
+
else:
|
|
121
|
+
_ = move(temp_dir / temp_file.name, temp_dir / name)
|
|
122
|
+
yield temp_dir / name
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
|
|
127
|
+
|
|
46
128
|
def gettempdir() -> Path:
|
|
47
129
|
"""Get the name of the directory used for temporary files."""
|
|
48
130
|
return Path(_gettempdir())
|
|
@@ -51,4 +133,4 @@ def gettempdir() -> Path:
|
|
|
51
133
|
TEMP_DIR = gettempdir()
|
|
52
134
|
|
|
53
135
|
|
|
54
|
-
__all__ = ["TEMP_DIR", "TemporaryDirectory", "gettempdir"]
|
|
136
|
+
__all__ = ["TEMP_DIR", "TemporaryDirectory", "TemporaryFile", "gettempdir"]
|
utilities/testbook.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from testbook import testbook
|
|
7
|
+
|
|
8
|
+
from utilities.pytest import throttle
|
|
9
|
+
from utilities.text import pascal_case
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from utilities.types import Delta, PathLike
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_notebook_tester(
|
|
18
|
+
path: PathLike, /, *, throttle: Delta | None = None, on_try: bool = False
|
|
19
|
+
) -> type[Any]:
|
|
20
|
+
"""Build the notebook tester class."""
|
|
21
|
+
path = Path(path)
|
|
22
|
+
name = f"Test{pascal_case(path.stem)}"
|
|
23
|
+
notebooks = [
|
|
24
|
+
path_i
|
|
25
|
+
for path_i in path.rglob("**/*.ipynb")
|
|
26
|
+
if all(p != ".ipynb_checkpoints" for p in path_i.parts)
|
|
27
|
+
]
|
|
28
|
+
namespace = {
|
|
29
|
+
f"test_{p.stem.replace('-', '_')}": _build_test_method(
|
|
30
|
+
p, delta=throttle, on_try=on_try
|
|
31
|
+
)
|
|
32
|
+
for p in notebooks
|
|
33
|
+
}
|
|
34
|
+
return type(name, (), namespace)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_test_method(
|
|
38
|
+
path: Path, /, *, delta: Delta | None = None, on_try: bool = False
|
|
39
|
+
) -> Callable[..., Any]:
|
|
40
|
+
@testbook(path, execute=True)
|
|
41
|
+
def method(self: Any, tb: Any) -> None:
|
|
42
|
+
_ = (self, tb) # pragma: no cover
|
|
43
|
+
|
|
44
|
+
if delta is not None:
|
|
45
|
+
method = throttle(delta=delta, on_try=on_try)(method)
|
|
46
|
+
|
|
47
|
+
return method
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["build_notebook_tester"]
|
utilities/text.py
CHANGED
|
@@ -2,26 +2,44 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from collections import deque
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from itertools import chain
|
|
7
8
|
from os import getpid
|
|
8
|
-
from re import IGNORECASE,
|
|
9
|
+
from re import IGNORECASE, VERBOSE, escape, search
|
|
9
10
|
from textwrap import dedent
|
|
10
11
|
from threading import get_ident
|
|
11
12
|
from time import time_ns
|
|
12
|
-
from typing import
|
|
13
|
+
from typing import (
|
|
14
|
+
TYPE_CHECKING,
|
|
15
|
+
Any,
|
|
16
|
+
ClassVar,
|
|
17
|
+
Literal,
|
|
18
|
+
assert_never,
|
|
19
|
+
overload,
|
|
20
|
+
override,
|
|
21
|
+
)
|
|
13
22
|
from uuid import uuid4
|
|
14
23
|
|
|
15
24
|
from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose
|
|
16
25
|
from utilities.reprlib import get_repr
|
|
26
|
+
from utilities.sentinel import Sentinel
|
|
17
27
|
|
|
18
28
|
if TYPE_CHECKING:
|
|
19
29
|
from collections.abc import Iterable, Mapping, Sequence
|
|
20
30
|
|
|
21
|
-
from utilities.types import StrStrMapping
|
|
31
|
+
from utilities.types import MaybeCallableBoolLike, MaybeCallableStr, StrStrMapping
|
|
22
32
|
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
_DEFAULT_SEPARATOR = ","
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def kebab_case(text: str, /) -> str:
|
|
41
|
+
"""Convert text into kebab case."""
|
|
42
|
+
return _kebab_snake_case(text, "-")
|
|
25
43
|
|
|
26
44
|
|
|
27
45
|
##
|
|
@@ -67,6 +85,29 @@ class ParseNoneError(Exception):
|
|
|
67
85
|
##
|
|
68
86
|
|
|
69
87
|
|
|
88
|
+
def pascal_case(text: str, /) -> str:
|
|
89
|
+
"""Convert text to pascal case."""
|
|
90
|
+
parts = _SPLIT_TEXT.findall(text)
|
|
91
|
+
parts = [p for p in parts if len(p) >= 1]
|
|
92
|
+
parts = list(map(_pascal_case_one, parts))
|
|
93
|
+
return "".join(parts)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _pascal_case_one(text: str, /) -> str:
|
|
97
|
+
return text if text.isupper() else text.title()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def prompt_bool(prompt: object = "", /, *, confirm: bool = False) -> bool:
|
|
104
|
+
"""Prompt for a boolean."""
|
|
105
|
+
return True if confirm else parse_bool(input(prompt))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
|
|
110
|
+
|
|
70
111
|
def repr_encode(obj: Any, /) -> bytes:
|
|
71
112
|
"""Return the representation of the object encoded as bytes."""
|
|
72
113
|
return repr(obj).encode()
|
|
@@ -75,30 +116,15 @@ def repr_encode(obj: Any, /) -> bytes:
|
|
|
75
116
|
##
|
|
76
117
|
|
|
77
118
|
|
|
78
|
-
_ACRONYM_PATTERN = re.compile(r"([A-Z\d]+)(?=[A-Z\d]|$)")
|
|
79
|
-
_SPACES_PATTERN = re.compile(r"\s+")
|
|
80
|
-
_SPLIT_PATTERN = re.compile(r"([\-_]*[A-Z][^A-Z]*[\-_]*)")
|
|
81
|
-
|
|
82
|
-
|
|
83
119
|
def snake_case(text: str, /) -> str:
|
|
84
120
|
"""Convert text into snake case."""
|
|
85
|
-
text
|
|
86
|
-
if not text.isupper():
|
|
87
|
-
text = _ACRONYM_PATTERN.sub(_snake_case_title, text)
|
|
88
|
-
text = "_".join(s for s in _SPLIT_PATTERN.split(text) if s)
|
|
89
|
-
while search("__", text):
|
|
90
|
-
text = text.replace("__", "_")
|
|
91
|
-
return text.lower()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _snake_case_title(match: Match[str], /) -> str:
|
|
95
|
-
return match.group(0).title()
|
|
121
|
+
return _kebab_snake_case(text, "_")
|
|
96
122
|
|
|
97
123
|
|
|
98
124
|
##
|
|
99
125
|
|
|
100
126
|
|
|
101
|
-
LIST_SEPARATOR =
|
|
127
|
+
LIST_SEPARATOR = _DEFAULT_SEPARATOR
|
|
102
128
|
PAIR_SEPARATOR = "="
|
|
103
129
|
BRACKETS = [("(", ")"), ("[", "]"), ("{", "}")]
|
|
104
130
|
|
|
@@ -108,7 +134,7 @@ def split_key_value_pairs(
|
|
|
108
134
|
text: str,
|
|
109
135
|
/,
|
|
110
136
|
*,
|
|
111
|
-
list_separator: str =
|
|
137
|
+
list_separator: str = _DEFAULT_SEPARATOR,
|
|
112
138
|
pair_separator: str = PAIR_SEPARATOR,
|
|
113
139
|
brackets: Iterable[tuple[str, str]] | None = BRACKETS,
|
|
114
140
|
mapping: Literal[True],
|
|
@@ -118,7 +144,7 @@ def split_key_value_pairs(
|
|
|
118
144
|
text: str,
|
|
119
145
|
/,
|
|
120
146
|
*,
|
|
121
|
-
list_separator: str =
|
|
147
|
+
list_separator: str = _DEFAULT_SEPARATOR,
|
|
122
148
|
pair_separator: str = PAIR_SEPARATOR,
|
|
123
149
|
brackets: Iterable[tuple[str, str]] | None = BRACKETS,
|
|
124
150
|
mapping: Literal[False] = False,
|
|
@@ -128,7 +154,7 @@ def split_key_value_pairs(
|
|
|
128
154
|
text: str,
|
|
129
155
|
/,
|
|
130
156
|
*,
|
|
131
|
-
list_separator: str =
|
|
157
|
+
list_separator: str = _DEFAULT_SEPARATOR,
|
|
132
158
|
pair_separator: str = PAIR_SEPARATOR,
|
|
133
159
|
brackets: Iterable[tuple[str, str]] | None = BRACKETS,
|
|
134
160
|
mapping: bool = False,
|
|
@@ -137,7 +163,7 @@ def split_key_value_pairs(
|
|
|
137
163
|
text: str,
|
|
138
164
|
/,
|
|
139
165
|
*,
|
|
140
|
-
list_separator: str =
|
|
166
|
+
list_separator: str = _DEFAULT_SEPARATOR,
|
|
141
167
|
pair_separator: str = PAIR_SEPARATOR,
|
|
142
168
|
brackets: Iterable[tuple[str, str]] | None = BRACKETS,
|
|
143
169
|
mapping: bool = False,
|
|
@@ -196,7 +222,7 @@ def split_str(
|
|
|
196
222
|
text: str,
|
|
197
223
|
/,
|
|
198
224
|
*,
|
|
199
|
-
separator: str =
|
|
225
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
200
226
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
201
227
|
n: Literal[1],
|
|
202
228
|
) -> tuple[str]: ...
|
|
@@ -205,7 +231,7 @@ def split_str(
|
|
|
205
231
|
text: str,
|
|
206
232
|
/,
|
|
207
233
|
*,
|
|
208
|
-
separator: str =
|
|
234
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
209
235
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
210
236
|
n: Literal[2],
|
|
211
237
|
) -> tuple[str, str]: ...
|
|
@@ -214,7 +240,7 @@ def split_str(
|
|
|
214
240
|
text: str,
|
|
215
241
|
/,
|
|
216
242
|
*,
|
|
217
|
-
separator: str =
|
|
243
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
218
244
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
219
245
|
n: Literal[3],
|
|
220
246
|
) -> tuple[str, str, str]: ...
|
|
@@ -223,7 +249,7 @@ def split_str(
|
|
|
223
249
|
text: str,
|
|
224
250
|
/,
|
|
225
251
|
*,
|
|
226
|
-
separator: str =
|
|
252
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
227
253
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
228
254
|
n: Literal[4],
|
|
229
255
|
) -> tuple[str, str, str, str]: ...
|
|
@@ -232,7 +258,7 @@ def split_str(
|
|
|
232
258
|
text: str,
|
|
233
259
|
/,
|
|
234
260
|
*,
|
|
235
|
-
separator: str =
|
|
261
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
236
262
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
237
263
|
n: Literal[5],
|
|
238
264
|
) -> tuple[str, str, str, str, str]: ...
|
|
@@ -241,18 +267,18 @@ def split_str(
|
|
|
241
267
|
text: str,
|
|
242
268
|
/,
|
|
243
269
|
*,
|
|
244
|
-
separator: str =
|
|
270
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
245
271
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
246
272
|
n: int | None = None,
|
|
247
|
-
) ->
|
|
273
|
+
) -> tuple[str, ...]: ...
|
|
248
274
|
def split_str(
|
|
249
275
|
text: str,
|
|
250
276
|
/,
|
|
251
277
|
*,
|
|
252
|
-
separator: str =
|
|
278
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
253
279
|
brackets: Iterable[tuple[str, str]] | None = None,
|
|
254
280
|
n: int | None = None,
|
|
255
|
-
) ->
|
|
281
|
+
) -> tuple[str, ...]:
|
|
256
282
|
"""Split a string, with a special provision for the empty string."""
|
|
257
283
|
if text == "":
|
|
258
284
|
texts = []
|
|
@@ -263,7 +289,7 @@ def split_str(
|
|
|
263
289
|
else:
|
|
264
290
|
texts = _split_str_brackets(text, brackets, separator=separator)
|
|
265
291
|
if n is None:
|
|
266
|
-
return texts
|
|
292
|
+
return tuple(texts)
|
|
267
293
|
if len(texts) != n:
|
|
268
294
|
raise _SplitStrCountError(text=text, n=n, texts=texts)
|
|
269
295
|
return tuple(texts)
|
|
@@ -274,8 +300,8 @@ def _split_str_brackets(
|
|
|
274
300
|
brackets: Iterable[tuple[str, str]],
|
|
275
301
|
/,
|
|
276
302
|
*,
|
|
277
|
-
separator: str =
|
|
278
|
-
) ->
|
|
303
|
+
separator: str = _DEFAULT_SEPARATOR,
|
|
304
|
+
) -> list[str]:
|
|
279
305
|
brackets = list(brackets)
|
|
280
306
|
opens, closes = transpose(brackets)
|
|
281
307
|
close_to_open = {close: open_ for open_, close in brackets}
|
|
@@ -283,7 +309,7 @@ def _split_str_brackets(
|
|
|
283
309
|
escapes = map(escape, chain(chain.from_iterable(brackets), [separator]))
|
|
284
310
|
pattern = re.compile("|".join(escapes))
|
|
285
311
|
|
|
286
|
-
results:
|
|
312
|
+
results: list[str] = []
|
|
287
313
|
stack: deque[tuple[str, int]] = deque()
|
|
288
314
|
last = 0
|
|
289
315
|
|
|
@@ -325,7 +351,7 @@ class SplitStrError(Exception):
|
|
|
325
351
|
@dataclass(kw_only=True, slots=True)
|
|
326
352
|
class _SplitStrCountError(SplitStrError):
|
|
327
353
|
n: int
|
|
328
|
-
texts:
|
|
354
|
+
texts: list[str]
|
|
329
355
|
|
|
330
356
|
@override
|
|
331
357
|
def __str__(self) -> str:
|
|
@@ -365,7 +391,7 @@ class _SplitStrOpeningBracketUnmatchedError(SplitStrError):
|
|
|
365
391
|
|
|
366
392
|
|
|
367
393
|
def join_strs(
|
|
368
|
-
texts: Iterable[str], /, *, sort: bool = False, separator: str =
|
|
394
|
+
texts: Iterable[str], /, *, sort: bool = False, separator: str = _DEFAULT_SEPARATOR
|
|
369
395
|
) -> str:
|
|
370
396
|
"""Join a collection of strings, with a special provision for the empty list."""
|
|
371
397
|
texts = list(texts)
|
|
@@ -378,13 +404,39 @@ def join_strs(
|
|
|
378
404
|
return separator.join(texts)
|
|
379
405
|
|
|
380
406
|
|
|
381
|
-
def _escape_separator(*, separator: str =
|
|
407
|
+
def _escape_separator(*, separator: str = _DEFAULT_SEPARATOR) -> str:
|
|
382
408
|
return f"\\{separator}"
|
|
383
409
|
|
|
384
410
|
|
|
385
411
|
##
|
|
386
412
|
|
|
387
413
|
|
|
414
|
+
class secret_str(str): # noqa: N801
|
|
415
|
+
"""A string with an obfuscated representation."""
|
|
416
|
+
|
|
417
|
+
__slots__ = ("_text",)
|
|
418
|
+
_REPR: ClassVar[str] = "***"
|
|
419
|
+
|
|
420
|
+
def __init__(self, text: str, /) -> None:
|
|
421
|
+
super().__init__()
|
|
422
|
+
self._text = text
|
|
423
|
+
|
|
424
|
+
@override
|
|
425
|
+
def __repr__(self) -> str:
|
|
426
|
+
return self._REPR
|
|
427
|
+
|
|
428
|
+
@override
|
|
429
|
+
def __str__(self) -> str:
|
|
430
|
+
return self._REPR
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def str(self) -> str:
|
|
434
|
+
return self._text
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
##
|
|
438
|
+
|
|
439
|
+
|
|
388
440
|
def str_encode(obj: Any, /) -> bytes:
|
|
389
441
|
"""Return the string representation of the object encoded as bytes."""
|
|
390
442
|
return str(obj).encode()
|
|
@@ -402,6 +454,50 @@ def strip_and_dedent(text: str, /, *, trailing: bool = False) -> str:
|
|
|
402
454
|
##
|
|
403
455
|
|
|
404
456
|
|
|
457
|
+
@overload
|
|
458
|
+
def to_bool(bool_: MaybeCallableBoolLike, /) -> bool: ...
|
|
459
|
+
@overload
|
|
460
|
+
def to_bool(bool_: None, /) -> None: ...
|
|
461
|
+
@overload
|
|
462
|
+
def to_bool(bool_: Sentinel, /) -> Sentinel: ...
|
|
463
|
+
def to_bool(
|
|
464
|
+
bool_: MaybeCallableBoolLike | None | Sentinel, /
|
|
465
|
+
) -> bool | None | Sentinel:
|
|
466
|
+
"""Convert to a bool."""
|
|
467
|
+
match bool_:
|
|
468
|
+
case bool() | None | Sentinel():
|
|
469
|
+
return bool_
|
|
470
|
+
case str():
|
|
471
|
+
return parse_bool(bool_)
|
|
472
|
+
case Callable() as func:
|
|
473
|
+
return to_bool(func())
|
|
474
|
+
case never:
|
|
475
|
+
assert_never(never)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
##
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@overload
|
|
482
|
+
def to_str(text: MaybeCallableStr, /) -> str: ...
|
|
483
|
+
@overload
|
|
484
|
+
def to_str(text: None, /) -> None: ...
|
|
485
|
+
@overload
|
|
486
|
+
def to_str(text: Sentinel, /) -> Sentinel: ...
|
|
487
|
+
def to_str(text: MaybeCallableStr | None | Sentinel, /) -> str | None | Sentinel:
|
|
488
|
+
"""Convert to a string."""
|
|
489
|
+
match text:
|
|
490
|
+
case str() | None | Sentinel():
|
|
491
|
+
return text
|
|
492
|
+
case Callable() as func:
|
|
493
|
+
return to_str(func())
|
|
494
|
+
case never:
|
|
495
|
+
assert_never(never)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
##
|
|
499
|
+
|
|
500
|
+
|
|
405
501
|
def unique_str() -> str:
|
|
406
502
|
"""Generate at unique string."""
|
|
407
503
|
now = time_ns()
|
|
@@ -411,9 +507,30 @@ def unique_str() -> str:
|
|
|
411
507
|
return f"{now}_{pid}_{ident}_{key}"
|
|
412
508
|
|
|
413
509
|
|
|
510
|
+
##
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _kebab_snake_case(text: str, separator: str, /) -> str:
|
|
514
|
+
"""Convert text into kebab/snake case."""
|
|
515
|
+
leading = bool(search(r"^_", text))
|
|
516
|
+
trailing = bool(search(r"_$", text))
|
|
517
|
+
parts = _SPLIT_TEXT.findall(text)
|
|
518
|
+
parts = (p for p in parts if len(p) >= 1)
|
|
519
|
+
parts = chain([""] if leading else [], parts, [""] if trailing else [])
|
|
520
|
+
return separator.join(parts).lower()
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
_SPLIT_TEXT = re.compile(
|
|
524
|
+
r"""
|
|
525
|
+
[A-Z]+(?=[A-Z][a-z0-9]) | # all caps followed by Upper+lower or digit (API in APIResponse2)
|
|
526
|
+
[A-Z]?[a-z]+[0-9]* | # normal words with optional trailing digits (Text123)
|
|
527
|
+
[A-Z]+[0-9]* | # consecutive caps with optional trailing digits (ID2)
|
|
528
|
+
""",
|
|
529
|
+
flags=VERBOSE,
|
|
530
|
+
)
|
|
531
|
+
|
|
414
532
|
__all__ = [
|
|
415
533
|
"BRACKETS",
|
|
416
|
-
"DEFAULT_SEPARATOR",
|
|
417
534
|
"LIST_SEPARATOR",
|
|
418
535
|
"PAIR_SEPARATOR",
|
|
419
536
|
"ParseBoolError",
|
|
@@ -421,13 +538,19 @@ __all__ = [
|
|
|
421
538
|
"SplitKeyValuePairsError",
|
|
422
539
|
"SplitStrError",
|
|
423
540
|
"join_strs",
|
|
541
|
+
"kebab_case",
|
|
424
542
|
"parse_bool",
|
|
425
543
|
"parse_none",
|
|
544
|
+
"pascal_case",
|
|
545
|
+
"prompt_bool",
|
|
426
546
|
"repr_encode",
|
|
547
|
+
"secret_str",
|
|
427
548
|
"snake_case",
|
|
428
549
|
"split_key_value_pairs",
|
|
429
550
|
"split_str",
|
|
430
551
|
"str_encode",
|
|
431
552
|
"strip_and_dedent",
|
|
553
|
+
"to_bool",
|
|
554
|
+
"to_str",
|
|
432
555
|
"unique_str",
|
|
433
556
|
]
|