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.

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.4.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 +299 -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.4.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.4.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.4.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.4.dist-info/licenses/LICENSE +0 -21
  82. utilities/period.py +0 -237
  83. 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 tempfile import TemporaryDirectory as _TemporaryDirectory
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 = _TemporaryDirectory(
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,56 @@ 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
+ ) -> Iterator[Path]:
83
+ """Yield a temporary file."""
84
+ with TemporaryDirectory(
85
+ suffix=suffix,
86
+ prefix=prefix,
87
+ dir=dir,
88
+ ignore_cleanup_errors=ignore_cleanup_errors,
89
+ delete=delete,
90
+ ) as temp_dir:
91
+ temp_file = _NamedTemporaryFile( # noqa: SIM115
92
+ dir=temp_dir, delete=delete, delete_on_close=False
93
+ )
94
+ if name is None:
95
+ yield temp_dir / temp_file.name
96
+ else:
97
+ _ = move(temp_dir / temp_file.name, temp_dir / name)
98
+ yield temp_dir / name
99
+
100
+
101
+ ##
102
+
103
+
46
104
  def gettempdir() -> Path:
47
105
  """Get the name of the directory used for temporary files."""
48
106
  return Path(_gettempdir())
@@ -51,4 +109,4 @@ def gettempdir() -> Path:
51
109
  TEMP_DIR = gettempdir()
52
110
 
53
111
 
54
- __all__ = ["TEMP_DIR", "TemporaryDirectory", "gettempdir"]
112
+ __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, Match, escape, search
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 TYPE_CHECKING, Any, Literal, overload, override
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
- DEFAULT_SEPARATOR = ","
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 = _SPACES_PATTERN.sub("", 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 = DEFAULT_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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
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 = DEFAULT_SEPARATOR,
270
+ separator: str = _DEFAULT_SEPARATOR,
245
271
  brackets: Iterable[tuple[str, str]] | None = None,
246
272
  n: int | None = None,
247
- ) -> Sequence[str]: ...
273
+ ) -> tuple[str, ...]: ...
248
274
  def split_str(
249
275
  text: str,
250
276
  /,
251
277
  *,
252
- separator: str = DEFAULT_SEPARATOR,
278
+ separator: str = _DEFAULT_SEPARATOR,
253
279
  brackets: Iterable[tuple[str, str]] | None = None,
254
280
  n: int | None = None,
255
- ) -> Sequence[str]:
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 = DEFAULT_SEPARATOR,
278
- ) -> Sequence[str]:
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: Sequence[str] = []
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: Sequence[str]
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 = DEFAULT_SEPARATOR
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 = DEFAULT_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
  ]
utilities/timer.py CHANGED
@@ -56,11 +56,11 @@ class Timer:
56
56
 
57
57
  @override
58
58
  def __repr__(self) -> str:
59
- return self.timedelta.format_common_iso()
59
+ return self.timedelta.format_iso()
60
60
 
61
61
  @override
62
62
  def __str__(self) -> str:
63
- return self.timedelta.format_common_iso()
63
+ return self.timedelta.format_iso()
64
64
 
65
65
  # comparison
66
66