dycw-utilities 0.146.2__py3-none-any.whl → 0.178.1__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.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +129 -50
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -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 +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- 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 +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +131 -93
- 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/packaging.py +115 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +5 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +317 -153
- utilities/pottery.py +10 -86
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -51
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +100 -126
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- 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 +9 -4
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +59 -38
- utilities/types.py +68 -22
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +663 -178
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.146.2.dist-info/METADATA +0 -41
- dycw_utilities-0.146.2.dist-info/RECORD +0 -99
- dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
- dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -144
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,106 @@ 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
|
+
dir: PathLike | None = None, # noqa: A002
|
|
77
|
+
suffix: str | None = None,
|
|
78
|
+
prefix: str | None = None,
|
|
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
|
+
if dir is None:
|
|
86
|
+
with (
|
|
87
|
+
TemporaryDirectory(
|
|
88
|
+
suffix=suffix,
|
|
89
|
+
prefix=prefix,
|
|
90
|
+
dir=dir,
|
|
91
|
+
ignore_cleanup_errors=ignore_cleanup_errors,
|
|
92
|
+
delete=delete,
|
|
93
|
+
) as temp_dir,
|
|
94
|
+
_temporary_file_outer(
|
|
95
|
+
temp_dir,
|
|
96
|
+
suffix=suffix,
|
|
97
|
+
prefix=prefix,
|
|
98
|
+
delete=delete,
|
|
99
|
+
name=name,
|
|
100
|
+
text=text,
|
|
101
|
+
) as temp,
|
|
102
|
+
):
|
|
103
|
+
yield temp
|
|
104
|
+
else:
|
|
105
|
+
with _temporary_file_outer(
|
|
106
|
+
dir, suffix=suffix, prefix=prefix, delete=delete, name=name, text=text
|
|
107
|
+
) as temp:
|
|
108
|
+
yield temp
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@contextmanager
|
|
112
|
+
def _temporary_file_outer(
|
|
113
|
+
path: PathLike,
|
|
114
|
+
/,
|
|
115
|
+
*,
|
|
116
|
+
suffix: str | None = None,
|
|
117
|
+
prefix: str | None = None,
|
|
118
|
+
delete: bool = True,
|
|
119
|
+
name: str | None = None,
|
|
120
|
+
text: str | None = None,
|
|
121
|
+
) -> Iterator[Path]:
|
|
122
|
+
with _temporary_file_inner(
|
|
123
|
+
path, suffix=suffix, prefix=prefix, delete=delete, name=name
|
|
124
|
+
) as temp:
|
|
125
|
+
if text is not None:
|
|
126
|
+
_ = temp.write_text(text)
|
|
127
|
+
yield temp
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@contextmanager
|
|
131
|
+
def _temporary_file_inner(
|
|
132
|
+
path: PathLike,
|
|
133
|
+
/,
|
|
134
|
+
*,
|
|
135
|
+
suffix: str | None = None,
|
|
136
|
+
prefix: str | None = None,
|
|
137
|
+
delete: bool = True,
|
|
138
|
+
name: str | None = None,
|
|
139
|
+
) -> Iterator[Path]:
|
|
140
|
+
path = Path(path)
|
|
141
|
+
temp = _NamedTemporaryFile( # noqa: SIM115
|
|
142
|
+
suffix=suffix, prefix=prefix, dir=path, delete=delete, delete_on_close=False
|
|
143
|
+
)
|
|
144
|
+
if name is None:
|
|
145
|
+
yield path / temp.name
|
|
146
|
+
else:
|
|
147
|
+
_ = move(path / temp.name, path / name)
|
|
148
|
+
yield path / name
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
|
|
153
|
+
|
|
46
154
|
def gettempdir() -> Path:
|
|
47
155
|
"""Get the name of the directory used for temporary files."""
|
|
48
156
|
return Path(_gettempdir())
|
|
@@ -51,4 +159,4 @@ def gettempdir() -> Path:
|
|
|
51
159
|
TEMP_DIR = gettempdir()
|
|
52
160
|
|
|
53
161
|
|
|
54
|
-
__all__ = ["TEMP_DIR", "TemporaryDirectory", "gettempdir"]
|
|
162
|
+
__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_test
|
|
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_test(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,47 @@ 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
|
+
def repr_str(obj: Any, /) -> str:
|
|
415
|
+
"""Get the representation of the string of an object."""
|
|
416
|
+
return repr(str(obj))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
##
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class secret_str(str): # noqa: N801
|
|
423
|
+
"""A string with an obfuscated representation."""
|
|
424
|
+
|
|
425
|
+
__slots__ = ("_text",)
|
|
426
|
+
_REPR: ClassVar[str] = "***"
|
|
427
|
+
|
|
428
|
+
def __init__(self, text: str, /) -> None:
|
|
429
|
+
super().__init__()
|
|
430
|
+
self._text = text
|
|
431
|
+
|
|
432
|
+
@override
|
|
433
|
+
def __repr__(self) -> str:
|
|
434
|
+
return self._REPR
|
|
435
|
+
|
|
436
|
+
@override
|
|
437
|
+
def __str__(self) -> str:
|
|
438
|
+
return self._REPR
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
def str(self) -> str:
|
|
442
|
+
return self._text
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
##
|
|
446
|
+
|
|
447
|
+
|
|
388
448
|
def str_encode(obj: Any, /) -> bytes:
|
|
389
449
|
"""Return the string representation of the object encoded as bytes."""
|
|
390
450
|
return str(obj).encode()
|
|
@@ -402,6 +462,50 @@ def strip_and_dedent(text: str, /, *, trailing: bool = False) -> str:
|
|
|
402
462
|
##
|
|
403
463
|
|
|
404
464
|
|
|
465
|
+
@overload
|
|
466
|
+
def to_bool(bool_: MaybeCallableBoolLike, /) -> bool: ...
|
|
467
|
+
@overload
|
|
468
|
+
def to_bool(bool_: None, /) -> None: ...
|
|
469
|
+
@overload
|
|
470
|
+
def to_bool(bool_: Sentinel, /) -> Sentinel: ...
|
|
471
|
+
def to_bool(
|
|
472
|
+
bool_: MaybeCallableBoolLike | None | Sentinel, /
|
|
473
|
+
) -> bool | None | Sentinel:
|
|
474
|
+
"""Convert to a bool."""
|
|
475
|
+
match bool_:
|
|
476
|
+
case bool() | None | Sentinel():
|
|
477
|
+
return bool_
|
|
478
|
+
case str():
|
|
479
|
+
return parse_bool(bool_)
|
|
480
|
+
case Callable() as func:
|
|
481
|
+
return to_bool(func())
|
|
482
|
+
case never:
|
|
483
|
+
assert_never(never)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
##
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@overload
|
|
490
|
+
def to_str(text: MaybeCallableStr, /) -> str: ...
|
|
491
|
+
@overload
|
|
492
|
+
def to_str(text: None, /) -> None: ...
|
|
493
|
+
@overload
|
|
494
|
+
def to_str(text: Sentinel, /) -> Sentinel: ...
|
|
495
|
+
def to_str(text: MaybeCallableStr | None | Sentinel, /) -> str | None | Sentinel:
|
|
496
|
+
"""Convert to a string."""
|
|
497
|
+
match text:
|
|
498
|
+
case str() | None | Sentinel():
|
|
499
|
+
return text
|
|
500
|
+
case Callable() as func:
|
|
501
|
+
return to_str(func())
|
|
502
|
+
case never:
|
|
503
|
+
assert_never(never)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
##
|
|
507
|
+
|
|
508
|
+
|
|
405
509
|
def unique_str() -> str:
|
|
406
510
|
"""Generate at unique string."""
|
|
407
511
|
now = time_ns()
|
|
@@ -411,9 +515,30 @@ def unique_str() -> str:
|
|
|
411
515
|
return f"{now}_{pid}_{ident}_{key}"
|
|
412
516
|
|
|
413
517
|
|
|
518
|
+
##
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _kebab_snake_case(text: str, separator: str, /) -> str:
|
|
522
|
+
"""Convert text into kebab/snake case."""
|
|
523
|
+
leading = bool(search(r"^_", text))
|
|
524
|
+
trailing = bool(search(r"_$", text))
|
|
525
|
+
parts = _SPLIT_TEXT.findall(text)
|
|
526
|
+
parts = (p for p in parts if len(p) >= 1)
|
|
527
|
+
parts = chain([""] if leading else [], parts, [""] if trailing else [])
|
|
528
|
+
return separator.join(parts).lower()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
_SPLIT_TEXT = re.compile(
|
|
532
|
+
r"""
|
|
533
|
+
[A-Z]+(?=[A-Z][a-z0-9]) | # all caps followed by Upper+lower or digit (API in APIResponse2)
|
|
534
|
+
[A-Z]?[a-z]+[0-9]* | # normal words with optional trailing digits (Text123)
|
|
535
|
+
[A-Z]+[0-9]* | # consecutive caps with optional trailing digits (ID2)
|
|
536
|
+
""",
|
|
537
|
+
flags=VERBOSE,
|
|
538
|
+
)
|
|
539
|
+
|
|
414
540
|
__all__ = [
|
|
415
541
|
"BRACKETS",
|
|
416
|
-
"DEFAULT_SEPARATOR",
|
|
417
542
|
"LIST_SEPARATOR",
|
|
418
543
|
"PAIR_SEPARATOR",
|
|
419
544
|
"ParseBoolError",
|
|
@@ -421,13 +546,20 @@ __all__ = [
|
|
|
421
546
|
"SplitKeyValuePairsError",
|
|
422
547
|
"SplitStrError",
|
|
423
548
|
"join_strs",
|
|
549
|
+
"kebab_case",
|
|
424
550
|
"parse_bool",
|
|
425
551
|
"parse_none",
|
|
552
|
+
"pascal_case",
|
|
553
|
+
"prompt_bool",
|
|
426
554
|
"repr_encode",
|
|
555
|
+
"repr_str",
|
|
556
|
+
"secret_str",
|
|
427
557
|
"snake_case",
|
|
428
558
|
"split_key_value_pairs",
|
|
429
559
|
"split_str",
|
|
430
560
|
"str_encode",
|
|
431
561
|
"strip_and_dedent",
|
|
562
|
+
"to_bool",
|
|
563
|
+
"to_str",
|
|
432
564
|
"unique_str",
|
|
433
565
|
]
|