dycw-utilities 0.125.2__py3-none-any.whl → 0.125.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.125.2
3
+ Version: 0.125.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -10,7 +10,7 @@ Requires-Dist: hypothesis<6.132,>=6.131.27; extra == 'test'
10
10
  Requires-Dist: pytest-asyncio<0.27,>=0.26.0; extra == 'test'
11
11
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
12
12
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
13
- Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.2; extra == 'test'
13
+ Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.3; extra == 'test'
14
14
  Requires-Dist: pytest-only<2.2,>=2.1.2; extra == 'test'
15
15
  Requires-Dist: pytest-randomly<3.17,>=3.16.0; extra == 'test'
16
16
  Requires-Dist: pytest-regressions<2.8,>=2.7.0; extra == 'test'
@@ -174,7 +174,7 @@ Requires-Dist: scipy<1.16,>=1.15.3; extra == 'zzz-test-scipy'
174
174
  Provides-Extra: zzz-test-sentinel
175
175
  Provides-Extra: zzz-test-shelve
176
176
  Provides-Extra: zzz-test-slack-sdk
177
- Requires-Dist: aiohttp<3.12,>=3.11.16; extra == 'zzz-test-slack-sdk'
177
+ Requires-Dist: aiohttp<3.13,>=3.12.0; extra == 'zzz-test-slack-sdk'
178
178
  Requires-Dist: slack-sdk<3.36,>=3.35.0; extra == 'zzz-test-slack-sdk'
179
179
  Provides-Extra: zzz-test-socket
180
180
  Provides-Extra: zzz-test-sqlalchemy
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=7x7l5p4UMSL99Ss01VIGeMRAm8Ye9uEgOEkCcsohyRs,60
1
+ utilities/__init__.py,sha256=O9WCgPzIqylIlRxvuTI9h_FJzryWHiUMdYDZGV6Rhj8,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=gr2eUx0E6LiCup6VKgUGwh8lAUriGdX2TlK-PZdlvfo,28284
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -28,6 +28,7 @@ utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
28
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
29
29
  utilities/iterables.py,sha256=prKXBdF5QfLTGC-q4567DwO8xzUng_Z-2a4wBkMqyDo,45360
30
30
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
31
+ utilities/libcst.py,sha256=jGLm2vTwUFcCIghN66mDSUXesgZZ6Pw2BbixkUf-PHA,5062
31
32
  utilities/lightweight_charts.py,sha256=vyVOzarYhBIOZj2xDhqdbP85qbSKUjdc6Au91rc1W4M,2814
32
33
  utilities/logging.py,sha256=gwo3pusPjnWO1ollrtn1VKYyRAQJTue4SkCbMeNvec4,25715
33
34
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
@@ -35,7 +36,7 @@ utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
35
36
  utilities/math.py,sha256=-mQgbah-dPJwOEWf3SonrFoVZ2AVxMgpeQ3dfVa-oJA,26764
36
37
  utilities/memory_profiler.py,sha256=tf2C51P2lCujPGvRt2Rfc7VEw5LDXmVPCG3z_AvBmbU,962
37
38
  utilities/modules.py,sha256=iuvLluJya-hvl1Q25-Jk3dLgx2Es3ck4SjJiEkAlVTs,3195
38
- utilities/more_itertools.py,sha256=fYsGyIPB2s1KRWoH3AkV3CxJxnMLvmidqBf8l1VQnyE,7193
39
+ utilities/more_itertools.py,sha256=tBbjjKx8_Uv_TCjxhPwrGfAx_jRHtvLIZqXVWAsjzqA,8842
39
40
  utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
40
41
  utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
41
42
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
@@ -87,7 +88,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
88
  utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
88
89
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
90
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
90
- dycw_utilities-0.125.2.dist-info/METADATA,sha256=THq9zaven45RgPNzFE1TPKNJs-tvrIDGiPmekj1FdRM,12852
91
- dycw_utilities-0.125.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.125.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.125.2.dist-info/RECORD,,
91
+ dycw_utilities-0.125.4.dist-info/METADATA,sha256=dwOemW5k0Dz8lVM3bRKczDGMMkRNTW41Np8Bi_13dEU,12851
92
+ dycw_utilities-0.125.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.125.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
94
+ dycw_utilities-0.125.4.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.125.2"
3
+ __version__ = "0.125.4"
utilities/libcst.py ADDED
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from subprocess import check_output
6
+ from typing import assert_never, override
7
+
8
+ from libcst import (
9
+ AsName,
10
+ Attribute,
11
+ BaseExpression,
12
+ FormattedString,
13
+ FormattedStringExpression,
14
+ FormattedStringText,
15
+ Import,
16
+ ImportAlias,
17
+ ImportFrom,
18
+ ImportStar,
19
+ Module,
20
+ Name,
21
+ )
22
+
23
+
24
+ def generate_from_import(
25
+ module: str, name: str, /, *, asname: str | None = None
26
+ ) -> ImportFrom:
27
+ """Generate an `ImportFrom` object."""
28
+ alias = ImportAlias(
29
+ name=Name(name), asname=AsName(Name(asname)) if asname else None
30
+ )
31
+ return ImportFrom(module=split_dotted_str(module), names=[alias])
32
+
33
+
34
+ def generate_f_string(var: str, suffix: str, /) -> FormattedString:
35
+ """Generate an f-string."""
36
+ return FormattedString([
37
+ FormattedStringExpression(expression=Name(var)),
38
+ FormattedStringText(suffix),
39
+ ])
40
+
41
+
42
+ def generate_import(module: str, /, *, asname: str | None = None) -> Import:
43
+ """Generate an `Import` object."""
44
+ alias = ImportAlias(
45
+ name=split_dotted_str(module), asname=AsName(Name(asname)) if asname else None
46
+ )
47
+ return Import(names=[alias])
48
+
49
+
50
+ ##
51
+
52
+
53
+ @dataclass(kw_only=True, slots=True)
54
+ class _ParseImportOutput:
55
+ module: str
56
+ name: str | None = None
57
+
58
+
59
+ def parse_import(import_: Import | ImportFrom, /) -> Sequence[_ParseImportOutput]:
60
+ """Parse an import."""
61
+ match import_:
62
+ case Import():
63
+ return [_parse_import_one(n) for n in import_.names]
64
+ case ImportFrom():
65
+ if (attr_or_name := import_.module) is None:
66
+ raise _ParseImportEmptyModuleError(import_=import_)
67
+ module = join_dotted_str(attr_or_name)
68
+ match import_.names:
69
+ case Sequence() as names:
70
+ return [_parse_import_from_one(module, n) for n in names]
71
+ case ImportStar():
72
+ return [_ParseImportOutput(module=module, name="*")]
73
+ case _ as never:
74
+ assert_never(never)
75
+ case _ as never:
76
+ assert_never(never)
77
+
78
+
79
+ def _parse_import_one(alias: ImportAlias, /) -> _ParseImportOutput:
80
+ return _ParseImportOutput(module=join_dotted_str(alias.name))
81
+
82
+
83
+ def _parse_import_from_one(module: str, alias: ImportAlias, /) -> _ParseImportOutput:
84
+ match alias.name:
85
+ case Name(name):
86
+ return _ParseImportOutput(module=module, name=name)
87
+ case Attribute() as attr:
88
+ raise _ParseImportAliasError(module=module, attr=attr)
89
+ case _ as never:
90
+ assert_never(never)
91
+
92
+
93
+ @dataclass(kw_only=True, slots=True)
94
+ class ParseImportError(Exception): ...
95
+
96
+
97
+ @dataclass(kw_only=True, slots=True)
98
+ class _ParseImportEmptyModuleError(ParseImportError):
99
+ import_: ImportFrom
100
+
101
+ @override
102
+ def __str__(self) -> str:
103
+ return f"Module must not be None; got {self.import_}"
104
+
105
+
106
+ @dataclass(kw_only=True, slots=True)
107
+ class _ParseImportAliasError(ParseImportError):
108
+ module: str
109
+ attr: Attribute
110
+
111
+ @override
112
+ def __str__(self) -> str:
113
+ attr = self.attr
114
+ return f"Invalid alias name; got module {self.module!r} and attribute '{attr.value.value}.{attr.attr.value}'"
115
+
116
+
117
+ ##
118
+
119
+
120
+ def split_dotted_str(dotted: str, /) -> Name | Attribute:
121
+ """Split a dotted string into a name/attribute."""
122
+ parts = dotted.split(".")
123
+ node = Name(parts[0])
124
+ for part in parts[1:]:
125
+ node = Attribute(value=node, attr=Name(part))
126
+ return node
127
+
128
+
129
+ def join_dotted_str(name_or_attr: Name | Attribute, /) -> str:
130
+ """Join a dotted from from a name/attribute."""
131
+ parts: Sequence[str] = []
132
+ curr: BaseExpression | Name | Attribute = name_or_attr
133
+ while True:
134
+ match curr:
135
+ case Name(value=value):
136
+ parts.append(value)
137
+ break
138
+ case Attribute(value=value, attr=Name(value=attr_value)):
139
+ parts.append(attr_value)
140
+ curr = value
141
+ case BaseExpression() as expr:
142
+ raise JoinDottedStrError(name_or_attr=name_or_attr, expr=expr)
143
+ case _ as never:
144
+ assert_never(never)
145
+ return ".".join(reversed(parts))
146
+
147
+
148
+ @dataclass(kw_only=True, slots=True)
149
+ class JoinDottedStrError(Exception):
150
+ name_or_attr: Name | Attribute
151
+ expr: BaseExpression
152
+
153
+ @override
154
+ def __str__(self) -> str:
155
+ return f"Only names & attributes allowed; got {self.expr}"
156
+
157
+
158
+ ##
159
+
160
+
161
+ def render_module(source: str | Module, /) -> str:
162
+ """Render a module."""
163
+ match source: # skipif-ci
164
+ case str() as text:
165
+ return check_output(["ruff", "format", "-"], input=text, text=True)
166
+ case Module() as module:
167
+ return render_module(module.code)
168
+ case _ as never:
169
+ assert_never(never)
170
+
171
+
172
+ ##
173
+
174
+
175
+ __all__ = [
176
+ "ParseImportError",
177
+ "generate_f_string",
178
+ "generate_from_import",
179
+ "generate_import",
180
+ "join_dotted_str",
181
+ "parse_import",
182
+ "render_module",
183
+ "split_dotted_str",
184
+ ]
@@ -21,12 +21,14 @@ from more_itertools import bucket, partition, split_into
21
21
  from more_itertools import peekable as _peekable
22
22
 
23
23
  from utilities.functions import get_class_name
24
+ from utilities.iterables import OneNonUniqueError, one
25
+ from utilities.reprlib import get_repr
24
26
  from utilities.sentinel import Sentinel, sentinel
27
+ from utilities.types import THashable
25
28
 
26
29
  if TYPE_CHECKING:
27
30
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
28
31
 
29
- from utilities.types import THashable
30
32
 
31
33
  _T = TypeVar("_T")
32
34
  _U = TypeVar("_U")
@@ -35,6 +37,26 @@ _U = TypeVar("_U")
35
37
  ##
36
38
 
37
39
 
40
+ @overload
41
+ def bucket_mapping(
42
+ iterable: Iterable[_T],
43
+ func: Callable[[_T], THashable],
44
+ /,
45
+ *,
46
+ transform: Callable[[_T], _U],
47
+ list: bool = False,
48
+ unique: Literal[True],
49
+ ) -> Mapping[THashable, _U]: ...
50
+ @overload
51
+ def bucket_mapping(
52
+ iterable: Iterable[_T],
53
+ func: Callable[[_T], THashable],
54
+ /,
55
+ *,
56
+ transform: Callable[[_T], _U] | None = None,
57
+ list: bool = False,
58
+ unique: Literal[True],
59
+ ) -> Mapping[THashable, _T]: ...
38
60
  @overload
39
61
  def bucket_mapping(
40
62
  iterable: Iterable[_T],
@@ -79,11 +101,14 @@ def bucket_mapping(
79
101
  *,
80
102
  transform: Callable[[_T], _U] | None = None,
81
103
  list: bool = False,
104
+ unique: bool = False,
82
105
  ) -> (
83
106
  Mapping[THashable, Iterator[_T]]
84
107
  | Mapping[THashable, Iterator[_U]]
85
108
  | Mapping[THashable, Sequence[_T]]
86
109
  | Mapping[THashable, Sequence[_U]]
110
+ | Mapping[THashable, _T]
111
+ | Mapping[THashable, _U]
87
112
  ): ...
88
113
  def bucket_mapping(
89
114
  iterable: Iterable[_T],
@@ -92,26 +117,55 @@ def bucket_mapping(
92
117
  *,
93
118
  transform: Callable[[_T], _U] | None = None,
94
119
  list: bool = False, # noqa: A002
120
+ unique: bool = False,
95
121
  ) -> (
96
122
  Mapping[THashable, Iterator[_T]]
97
123
  | Mapping[THashable, Iterator[_U]]
98
124
  | Mapping[THashable, Sequence[_T]]
99
125
  | Mapping[THashable, Sequence[_U]]
126
+ | Mapping[THashable, _T]
127
+ | Mapping[THashable, _U]
100
128
  ):
101
129
  """Bucket the values of iterable into a mapping."""
102
130
  b = bucket(iterable, func)
103
131
  mapping = {key: b[key] for key in b}
104
132
  match transform, list:
105
133
  case None, False:
106
- return mapping
134
+ ...
107
135
  case None, True:
108
- return {k: builtins.list(v) for k, v in mapping.items()}
136
+ mapping = {k: builtins.list(v) for k, v in mapping.items()}
109
137
  case _, False:
110
- return {k: map(transform, v) for k, v in mapping.items()}
138
+ mapping = {k: map(transform, v) for k, v in mapping.items()}
111
139
  case _, True:
112
- return {k: builtins.list(map(transform, v)) for k, v in mapping.items()}
140
+ mapping = {k: builtins.list(map(transform, v)) for k, v in mapping.items()}
113
141
  case _ as never:
114
142
  assert_never(never)
143
+ if not unique:
144
+ return mapping
145
+ results = {}
146
+ error_no_transform: dict[THashable, tuple[_T, _T]] = {}
147
+ for key, value in mapping.items():
148
+ try:
149
+ results[key] = one(value)
150
+ except OneNonUniqueError as error:
151
+ error_no_transform[key] = (error.first, error.second)
152
+ if len(error_no_transform) >= 1:
153
+ raise BucketMappingError(errors=error_no_transform)
154
+ return results
155
+
156
+
157
+ @dataclass(kw_only=True, slots=True)
158
+ class BucketMappingError(Exception, Generic[THashable, _U]):
159
+ errors: Mapping[THashable, tuple[_U, _U]]
160
+
161
+ @override
162
+ def __str__(self) -> str:
163
+ parts = [
164
+ f"{get_repr(key)} (#1: {get_repr(first)}, #2: {get_repr(second)})"
165
+ for key, (first, second) in self.errors.items()
166
+ ]
167
+ desc = ", ".join(parts)
168
+ return f"Buckets must contain exactly one item each; got {desc}"
115
169
 
116
170
 
117
171
  ##
@@ -264,6 +318,7 @@ def _yield_splits3(
264
318
 
265
319
 
266
320
  __all__ = [
321
+ "BucketMappingError",
267
322
  "Split",
268
323
  "bucket_mapping",
269
324
  "partition_list",