dycw-utilities 0.148.5__py3-none-any.whl → 0.175.31__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 (84) hide show
  1. dycw_utilities-0.175.31.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.31.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.31.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.175.31.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +113 -64
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +381 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +1 -1
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  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 +298 -0
  41. utilities/platform.py +4 -4
  42. utilities/polars.py +934 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +296 -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 +27 -8
  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 +1947 -0
  67. utilities/tempfile.py +95 -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.5.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.5.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
  82. utilities/eventkit.py +0 -388
  83. utilities/period.py +0 -237
  84. utilities/typed_settings.py +0 -144
utilities/importlib.py CHANGED
@@ -1,7 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib.resources
3
4
  from importlib import import_module
4
5
  from importlib.util import find_spec
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from utilities.errors import ImpossibleCaseError
10
+
11
+ if TYPE_CHECKING:
12
+ from importlib.resources import Anchor
13
+
14
+
15
+ def files(*, anchor: Anchor | None = None) -> Path:
16
+ """Get the path for an anchor."""
17
+ path = importlib.resources.files(anchor)
18
+ if isinstance(path, Path):
19
+ return path
20
+ raise ImpossibleCaseError(case=[f"{path=}"]) # pragma: no cover
5
21
 
6
22
 
7
23
  def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
@@ -15,4 +31,4 @@ def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
15
31
  return hasattr(mod, name)
16
32
 
17
33
 
18
- __all__ = ["is_valid_import"]
34
+ __all__ = ["files", "is_valid_import"]
utilities/inflect.py CHANGED
@@ -15,7 +15,7 @@ def counted_noun(obj: int | Sized, noun: str, /) -> str:
15
15
  ...
16
16
  case Sized() as sized:
17
17
  count = len(sized)
18
- case _ as never:
18
+ case never:
19
19
  assert_never(never)
20
20
  word = cast("Word", noun)
21
21
  sin_or_plu = _ENGINE.plural_noun(word, count=count)
utilities/iterables.py CHANGED
@@ -18,7 +18,7 @@ from enum import Enum
18
18
  from functools import cmp_to_key, partial, reduce
19
19
  from itertools import accumulate, chain, groupby, islice, pairwise, product
20
20
  from math import isnan
21
- from operator import add, itemgetter, or_
21
+ from operator import add, or_
22
22
  from typing import (
23
23
  TYPE_CHECKING,
24
24
  Any,
@@ -31,7 +31,6 @@ from typing import (
31
31
  )
32
32
 
33
33
  from utilities.errors import ImpossibleCaseError
34
- from utilities.functions import ensure_hashable, ensure_str
35
34
  from utilities.math import (
36
35
  _CheckIntegerEqualError,
37
36
  _CheckIntegerEqualOrApproxError,
@@ -40,13 +39,13 @@ from utilities.math import (
40
39
  check_integer,
41
40
  )
42
41
  from utilities.reprlib import get_repr
43
- from utilities.sentinel import Sentinel, sentinel
42
+ from utilities.sentinel import Sentinel, is_sentinel, sentinel
44
43
  from utilities.types import SupportsAdd, SupportsLT
45
44
 
46
45
  if TYPE_CHECKING:
47
46
  from types import NoneType
48
47
 
49
- from utilities.types import MaybeIterable, MaybeIterableHashable, Sign, StrMapping
48
+ from utilities.types import MaybeIterable, Sign, StrMapping
50
49
 
51
50
 
52
51
  ##
@@ -66,16 +65,6 @@ def always_iterable[T](obj: MaybeIterable[T], /) -> Iterable[T]:
66
65
  ##
67
66
 
68
67
 
69
- def always_iterable_hashable[T](
70
- obj: MaybeIterable[T] | None, /
71
- ) -> MaybeIterableHashable[T] | None:
72
- """Ensure an object is always hashable."""
73
- return None if obj is None else tuple(always_iterable(obj))
74
-
75
-
76
- ##
77
-
78
-
79
68
  def apply_bijection[T, U](
80
69
  func: Callable[[T], U], iterable: Iterable[T], /
81
70
  ) -> Mapping[T, U]:
@@ -246,8 +235,7 @@ def check_iterables_equal(left: Iterable[Any], right: Iterable[Any], /) -> None:
246
235
  if lv != rv:
247
236
  errors.append((i, lv, rv))
248
237
  except ValueError as error:
249
- msg = ensure_str(one(error.args))
250
- match msg:
238
+ match one(error.args):
251
239
  case "zip() argument 2 is longer than argument 1":
252
240
  state = "right_longer"
253
241
  case "zip() argument 2 is shorter than argument 1":
@@ -295,7 +283,7 @@ class CheckIterablesEqualError[T](Exception):
295
283
  yield "right was longer"
296
284
  case None:
297
285
  pass
298
- case _ as never:
286
+ case never:
299
287
  assert_never(never)
300
288
 
301
289
 
@@ -690,7 +678,7 @@ def cmp_nullable[T: SupportsLT](x: T | None, y: T | None, /) -> Sign:
690
678
  return 1
691
679
  case _, _:
692
680
  return cast("Sign", (x > y) - (x < y))
693
- case _ as never:
681
+ case never:
694
682
  assert_never(never)
695
683
 
696
684
 
@@ -705,18 +693,6 @@ def chunked[T](iterable: Iterable[T], n: int, /) -> Iterator[Sequence[T]]:
705
693
  ##
706
694
 
707
695
 
708
- def ensure_hashables(
709
- *args: Any, **kwargs: Any
710
- ) -> tuple[list[Hashable], dict[str, Hashable]]:
711
- """Ensure a set of positional & keyword arguments are all hashable."""
712
- hash_args = list(map(ensure_hashable, args))
713
- hash_kwargs = {k: ensure_hashable(v) for k, v in kwargs.items()}
714
- return hash_args, hash_kwargs
715
-
716
-
717
- ##
718
-
719
-
720
696
  def ensure_iterable(obj: Any, /) -> Iterable[Any]:
721
697
  """Ensure an object is iterable."""
722
698
  if is_iterable(obj):
@@ -831,24 +807,6 @@ def filter_include_and_exclude[T, U](
831
807
  ##
832
808
 
833
809
 
834
- def group_consecutive_integers(iterable: Iterable[int], /) -> Iterable[tuple[int, int]]:
835
- """Group consecutive integers."""
836
- integers = sorted(iterable)
837
- for _, group in groupby(enumerate(integers), key=lambda x: x[1] - x[0]):
838
- as_list = list(map(itemgetter(1), group))
839
- yield as_list[0], as_list[-1]
840
-
841
-
842
- def ungroup_consecutive_integers(
843
- iterable: Iterable[tuple[int, int]], /
844
- ) -> Iterable[int]:
845
- """Ungroup consecutive integers."""
846
- return chain.from_iterable(range(start, end + 1) for start, end in iterable)
847
-
848
-
849
- ##
850
-
851
-
852
810
  @overload
853
811
  def groupby_lists[T](
854
812
  iterable: Iterable[T], /, *, key: None = None
@@ -975,7 +933,7 @@ class MergeStrMappingsError(Exception):
975
933
 
976
934
  def one[T](*iterables: Iterable[T]) -> T:
977
935
  """Return the unique value in a set of iterables."""
978
- it = iter(chain(*iterables))
936
+ it = chain(*iterables)
979
937
  try:
980
938
  first = next(it)
981
939
  except StopIteration:
@@ -1068,7 +1026,7 @@ def one_str(
1068
1026
  it = (t for t in as_list if t.startswith(text))
1069
1027
  case True, False:
1070
1028
  it = (t for t in as_list if t.lower().startswith(text.lower()))
1071
- case _ as never:
1029
+ case never:
1072
1030
  assert_never(never)
1073
1031
  try:
1074
1032
  return one(it)
@@ -1109,7 +1067,7 @@ class OneStrEmptyError(OneStrError):
1109
1067
  tail = f"any string starting with {self.text!r}"
1110
1068
  case True, False:
1111
1069
  tail = f"any string starting with {self.text!r} (modulo case)"
1112
- case _ as never:
1070
+ case never:
1113
1071
  assert_never(never)
1114
1072
  return f"{head} {tail}"
1115
1073
 
@@ -1131,7 +1089,7 @@ class OneStrNonUniqueError(OneStrError):
1131
1089
  mid = f"exactly one string starting with {self.text!r}"
1132
1090
  case True, False:
1133
1091
  mid = f"exactly one string starting with {self.text!r} (modulo case)"
1134
- case _ as never:
1092
+ case never:
1135
1093
  assert_never(never)
1136
1094
  return f"{head} {mid}; got {self.first!r}, {self.second!r} and perhaps more"
1137
1095
 
@@ -1265,7 +1223,7 @@ def reduce_mappings[K, V, W](
1265
1223
  ) -> Mapping[K, V | W]:
1266
1224
  """Reduce a function over the values of a set of mappings."""
1267
1225
  chained = chain_mappings(*sequence)
1268
- if isinstance(initial, Sentinel):
1226
+ if is_sentinel(initial):
1269
1227
  func2 = cast("Callable[[V, V], V]", func)
1270
1228
  return {k: reduce(func2, v) for k, v in chained.items()}
1271
1229
  func2 = cast("Callable[[W, V], W]", func)
@@ -1382,7 +1340,7 @@ def _sort_iterable_cmp_floats(x: float, y: float, /) -> Sign:
1382
1340
  return -1
1383
1341
  case False, False:
1384
1342
  return cast("Sign", (x > y) - (x < y))
1385
- case _ as never:
1343
+ case never:
1386
1344
  assert_never(never)
1387
1345
 
1388
1346
 
@@ -1489,7 +1447,6 @@ __all__ = [
1489
1447
  "ResolveIncludeAndExcludeError",
1490
1448
  "SortIterableError",
1491
1449
  "always_iterable",
1492
- "always_iterable_hashable",
1493
1450
  "apply_bijection",
1494
1451
  "apply_to_tuple",
1495
1452
  "apply_to_varargs",
@@ -1509,13 +1466,11 @@ __all__ = [
1509
1466
  "check_unique_modulo_case",
1510
1467
  "chunked",
1511
1468
  "cmp_nullable",
1512
- "ensure_hashables",
1513
1469
  "ensure_iterable",
1514
1470
  "ensure_iterable_not_str",
1515
1471
  "enumerate_with_edge",
1516
1472
  "expanding_window",
1517
1473
  "filter_include_and_exclude",
1518
- "group_consecutive_integers",
1519
1474
  "groupby_lists",
1520
1475
  "hashable_to_iterable",
1521
1476
  "is_iterable",
@@ -1538,6 +1493,5 @@ __all__ = [
1538
1493
  "sum_mappings",
1539
1494
  "take",
1540
1495
  "transpose",
1541
- "ungroup_consecutive_integers",
1542
1496
  "unique_everseen",
1543
1497
  ]
utilities/jinja2.py ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, Literal, assert_never, override
5
+
6
+ from jinja2 import BaseLoader, BytecodeCache, Environment, FileSystemLoader, Undefined
7
+ from jinja2.defaults import (
8
+ BLOCK_END_STRING,
9
+ BLOCK_START_STRING,
10
+ COMMENT_END_STRING,
11
+ COMMENT_START_STRING,
12
+ KEEP_TRAILING_NEWLINE,
13
+ LINE_COMMENT_PREFIX,
14
+ LINE_STATEMENT_PREFIX,
15
+ LSTRIP_BLOCKS,
16
+ NEWLINE_SEQUENCE,
17
+ TRIM_BLOCKS,
18
+ VARIABLE_END_STRING,
19
+ VARIABLE_START_STRING,
20
+ )
21
+
22
+ from utilities.atomicwrites import writer
23
+ from utilities.text import kebab_case, pascal_case, snake_case
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable, Sequence
27
+ from pathlib import Path
28
+
29
+ from jinja2.ext import Extension
30
+
31
+ from utilities.types import StrMapping
32
+
33
+
34
+ class EnhancedEnvironment(Environment):
35
+ """Environment with enhanced features."""
36
+
37
+ @override
38
+ def __init__(
39
+ self,
40
+ block_start_string: str = BLOCK_START_STRING,
41
+ block_end_string: str = BLOCK_END_STRING,
42
+ variable_start_string: str = VARIABLE_START_STRING,
43
+ variable_end_string: str = VARIABLE_END_STRING,
44
+ comment_start_string: str = COMMENT_START_STRING,
45
+ comment_end_string: str = COMMENT_END_STRING,
46
+ line_statement_prefix: str | None = LINE_STATEMENT_PREFIX,
47
+ line_comment_prefix: str | None = LINE_COMMENT_PREFIX,
48
+ trim_blocks: bool = TRIM_BLOCKS,
49
+ lstrip_blocks: bool = LSTRIP_BLOCKS,
50
+ newline_sequence: Literal["\n", "\r\n", "\r"] = NEWLINE_SEQUENCE,
51
+ keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE,
52
+ extensions: Sequence[str | type[Extension]] = (),
53
+ optimized: bool = True,
54
+ undefined: type[Undefined] = Undefined,
55
+ finalize: Callable[..., Any] | None = None,
56
+ autoescape: bool | Callable[[str | None], bool] = False,
57
+ loader: BaseLoader | None = None,
58
+ cache_size: int = 400,
59
+ auto_reload: bool = True,
60
+ bytecode_cache: BytecodeCache | None = None,
61
+ enable_async: bool = False,
62
+ ) -> None:
63
+ super().__init__(
64
+ block_start_string,
65
+ block_end_string,
66
+ variable_start_string,
67
+ variable_end_string,
68
+ comment_start_string,
69
+ comment_end_string,
70
+ line_statement_prefix,
71
+ line_comment_prefix,
72
+ trim_blocks,
73
+ lstrip_blocks,
74
+ newline_sequence,
75
+ keep_trailing_newline,
76
+ extensions,
77
+ optimized,
78
+ undefined,
79
+ finalize,
80
+ autoescape,
81
+ loader,
82
+ cache_size,
83
+ auto_reload,
84
+ bytecode_cache,
85
+ enable_async,
86
+ )
87
+ self.filters["kebab"] = kebab_case
88
+ self.filters["pascal"] = pascal_case
89
+ self.filters["snake"] = snake_case
90
+
91
+
92
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
93
+ class TemplateJob:
94
+ """A template with an associated rendering job."""
95
+
96
+ template: Path
97
+ kwargs: StrMapping
98
+ target: Path
99
+ mode: Literal["write", "append"] = "write"
100
+
101
+ def __post_init__(self) -> None:
102
+ if not self.template.exists():
103
+ raise _TemplateJobTemplateDoesNotExistError(path=self.template)
104
+ if (self.mode == "append") and not self.target.exists():
105
+ raise _TemplateJobTargetDoesNotExistError(path=self.template)
106
+
107
+ def run(self) -> None:
108
+ """Run the job."""
109
+ match self.mode:
110
+ case "write":
111
+ with writer(self.target, overwrite=True) as temp:
112
+ _ = temp.write_text(self.rendered)
113
+ case "append":
114
+ with self.target.open(mode="a") as fh:
115
+ _ = fh.write(self.rendered)
116
+ case never:
117
+ assert_never(never)
118
+
119
+ @property
120
+ def rendered(self) -> str:
121
+ """The template, rendered."""
122
+ env = EnhancedEnvironment(loader=FileSystemLoader(self.template.parent))
123
+ return env.get_template(self.template.name).render(self.kwargs)
124
+
125
+
126
+ @dataclass(kw_only=True, slots=True)
127
+ class TemplateJobError(Exception): ...
128
+
129
+
130
+ @dataclass(kw_only=True, slots=True)
131
+ class _TemplateJobTemplateDoesNotExistError(TemplateJobError):
132
+ path: Path
133
+
134
+ @override
135
+ def __str__(self) -> str:
136
+ return f"Template {str(self.path)!r} does not exist"
137
+
138
+
139
+ @dataclass(kw_only=True, slots=True)
140
+ class _TemplateJobTargetDoesNotExistError(TemplateJobError):
141
+ path: Path
142
+
143
+ @override
144
+ def __str__(self) -> str:
145
+ return f"Target {str(self.path)!r} does not exist"
146
+
147
+
148
+ __all__ = ["EnhancedEnvironment", "TemplateJob", "TemplateJobError"]
utilities/json.py CHANGED
@@ -36,7 +36,7 @@ def run_prettier(source: bytes | str | Path, /) -> bytes | str | None:
36
36
  with writer(path, overwrite=True) as temp:
37
37
  _ = temp.write_bytes(result)
38
38
  return None
39
- case _ as never:
39
+ case never:
40
40
  assert_never(never)
41
41
 
42
42
 
utilities/libcst.py CHANGED
@@ -54,7 +54,7 @@ def generate_import_from(
54
54
  case _, str():
55
55
  alias = ImportAlias(name=Name(name), asname=AsName(Name(asname)))
56
56
  names = [alias]
57
- case _ as never:
57
+ case never:
58
58
  assert_never(never)
59
59
  return ImportFrom(module=split_dotted_str(module), names=names)
60
60
 
@@ -92,9 +92,9 @@ def parse_import(import_: Import | ImportFrom, /) -> Sequence[_ParseImportOutput
92
92
  return [_parse_import_from_one(module, n) for n in names]
93
93
  case ImportStar():
94
94
  return [_ParseImportOutput(module=module, name="*")]
95
- case _ as never:
95
+ case never:
96
96
  assert_never(never)
97
- case _ as never:
97
+ case never:
98
98
  assert_never(never)
99
99
 
100
100
 
@@ -108,7 +108,7 @@ def _parse_import_from_one(module: str, alias: ImportAlias, /) -> _ParseImportOu
108
108
  return _ParseImportOutput(module=module, name=name)
109
109
  case Attribute() as attr:
110
110
  raise _ParseImportAliasError(module=module, attr=attr)
111
- case _ as never:
111
+ case never:
112
112
  assert_never(never)
113
113
 
114
114
 
@@ -150,7 +150,7 @@ def split_dotted_str(dotted: str, /) -> Name | Attribute:
150
150
 
151
151
  def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
152
152
  """Join a dotted from from a name/attribute."""
153
- parts: Sequence[str] = []
153
+ parts: list[str] = []
154
154
  curr: BaseExpression | Name | Attribute = name_or_attr
155
155
  while True:
156
156
  match curr:
@@ -162,7 +162,7 @@ def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
162
162
  curr = value
163
163
  case BaseExpression(): # pragma: no cover
164
164
  raise ImpossibleCaseError(case=[f"{curr=}"])
165
- case _ as never:
165
+ case never:
166
166
  assert_never(never)
167
167
  return ".".join(reversed(parts))
168
168
 
@@ -180,7 +180,7 @@ def render_module(source: str | Module, /) -> str:
180
180
  return text
181
181
  case Module() as module:
182
182
  return render_module(module.code)
183
- case _ as never:
183
+ case never:
184
184
  assert_never(never)
185
185
 
186
186