dycw-utilities 0.109.3__py3-none-any.whl → 0.109.5__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.109.3
3
+ Version: 0.109.5
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=XHS4BHXBA86Rjy07zPtJwGLkB2QC4JUa0pQjTRZS2zE,60
1
+ utilities/__init__.py,sha256=WUcAfLjgcNNeAm9q3alKWOcT4CjZ3liYuA1SFSky9Ng,60
2
2
  utilities/altair.py,sha256=NSyDsm8QlkAGmsGdxVwCkHnPxt_35yJBa9Lg7bz9Ays,9054
3
3
  utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
4
4
  utilities/asyncio.py,sha256=41oQUurWMvadFK5gFnaG21hMM0Vmfn2WS6OpC0R9mas,14757
@@ -11,7 +11,7 @@ utilities/contextlib.py,sha256=OOIIEa5lXKGzFAnauaul40nlQnQko6Na4ryiMJcHkIg,478
11
11
  utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
12
12
  utilities/cryptography.py,sha256=HyOewI20cl3uRXsKivhIaeLVDInQdzgXZGaly7hS5dE,771
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
- utilities/dataclasses.py,sha256=4siQUalzjYckhJ7R9Cn4Pmerxzx18_mjPIwUmeuwwKs,23049
14
+ utilities/dataclasses.py,sha256=Rh5QajjwyXGpQqndeOFRT-VnPJXF0jWI515GjCSprS0,23326
15
15
  utilities/datetime.py,sha256=GOs-MIEW_A49kzqa1yhIoeNeSqqPVgGO-h2AThtgTDk,37326
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
@@ -40,7 +40,7 @@ utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
40
40
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
41
41
  utilities/orjson.py,sha256=Wj5pzG_VdgoAy14a7Luhem-BgYrRtRFvvl_POiszRd0,36930
42
42
  utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
43
- utilities/parse.py,sha256=-rnx9qBKo9UZbE1HtvBwYOs4vF7YiJiciPAM6IKmfRc,5183
43
+ utilities/parse.py,sha256=q1A1-bsXltdAh8We5Acy-lPuVIClWTxsRXQWq5wYyKk,6382
44
44
  utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
45
45
  utilities/period.py,sha256=ikHXsWtDLr553cfH6p9mMaiCnIAP69B7q84ckWV3HaA,10884
46
46
  utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
@@ -53,7 +53,7 @@ utilities/pyinstrument.py,sha256=ROq2txPwbe2ZUuYJ2IDNbfT97lu2ca0v5_C_yn6sSlM,800
53
53
  utilities/pyrsistent.py,sha256=TLJfiiKO4cKNU_pCoM3zDqmSM421qpuoaeaBNnyC_Ac,2489
54
54
  utilities/pytest.py,sha256=85QUax4g2VBBAqAHtM9wekcSLB7_9O8AKFTaCshztL8,7989
55
55
  utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq3c,5088
56
- utilities/python_dotenv.py,sha256=10DHEB7AVeZqH7I4wr6nACdJQYQJanlEj6EsyLvCN9w,3059
56
+ utilities/python_dotenv.py,sha256=ZIz45FjpwoMs5fdYr9kRM6f4qEp2GNM5rKLnrRe8_7c,3218
57
57
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
58
58
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
59
59
  utilities/redis.py,sha256=CsDQqc9V6ASLzLQwtbQXZQEndyG9pJiCOhPlPeszt7Y,21203
@@ -84,7 +84,7 @@ utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
84
84
  utilities/whenever.py,sha256=5x2t47VJmJRWcd_NLFy54NkB3uom-XQYxEbLtEfL1bs,17775
85
85
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
86
86
  utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
87
- dycw_utilities-0.109.3.dist-info/METADATA,sha256=TF66WIpVFJnaJA80lxa_GKBZKZUzfz8VWd95FQW8v4A,13004
88
- dycw_utilities-0.109.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
- dycw_utilities-0.109.3.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
- dycw_utilities-0.109.3.dist-info/RECORD,,
87
+ dycw_utilities-0.109.5.dist-info/METADATA,sha256=MlQJX_ZNWyT78qMBJR7P1Evz8nzMaBTjTR3PYh2v07A,13004
88
+ dycw_utilities-0.109.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
+ dycw_utilities-0.109.5.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
+ dycw_utilities-0.109.5.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.109.3"
3
+ __version__ = "0.109.5"
utilities/dataclasses.py CHANGED
@@ -453,7 +453,8 @@ def text_to_dataclass(
453
453
  warn_name_errors: bool = False,
454
454
  head: bool = False,
455
455
  case_sensitive: bool = False,
456
- allow_extra: bool = False,
456
+ allow_extra_keys: bool = False,
457
+ extra_parsers: Mapping[type[_T], Callable[[str], _T]] | None = None,
457
458
  ) -> TDataclass:
458
459
  """Construct a dataclass from a string or a mapping or strings."""
459
460
  match text_or_mapping:
@@ -477,10 +478,12 @@ def text_to_dataclass(
477
478
  warn_name_errors=warn_name_errors,
478
479
  head=head,
479
480
  case_sensitive=case_sensitive,
480
- allow_extra=allow_extra,
481
+ allow_extra=allow_extra_keys,
481
482
  )
482
483
  field_names_to_values = {
483
- f.name: _text_to_dataclass_parse(f, t, cls, case_sensitive=case_sensitive)
484
+ f.name: _text_to_dataclass_parse(
485
+ f, t, cls, head=head, case_sensitive=case_sensitive, extra=extra_parsers
486
+ )
484
487
  for f, t in fields_to_serializes.items()
485
488
  }
486
489
  return mapping_to_dataclass(
@@ -492,7 +495,7 @@ def text_to_dataclass(
492
495
  warn_name_errors=warn_name_errors,
493
496
  head=head,
494
497
  case_sensitive=case_sensitive,
495
- allow_extra=allow_extra,
498
+ allow_extra=allow_extra_keys,
496
499
  )
497
500
 
498
501
 
@@ -519,10 +522,14 @@ def _text_to_dataclass_parse(
519
522
  cls: type[Dataclass],
520
523
  /,
521
524
  *,
525
+ head: bool = False,
522
526
  case_sensitive: bool = False,
527
+ extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
523
528
  ) -> Any:
524
529
  try:
525
- return parse_text(field.type_, text, case_sensitive=case_sensitive)
530
+ return parse_text(
531
+ field.type_, text, head=head, case_sensitive=case_sensitive, extra=extra
532
+ )
526
533
  except ParseTextError:
527
534
  raise _TextToDataClassParseValueError(cls=cls, field=field, text=text) from None
528
535
 
utilities/parse.py CHANGED
@@ -7,30 +7,42 @@ from enum import Enum
7
7
  from pathlib import Path
8
8
  from re import DOTALL
9
9
  from types import NoneType
10
- from typing import Any, override
10
+ from typing import TYPE_CHECKING, Any, TypeVar, override
11
11
 
12
12
  from utilities.datetime import is_subclass_date_not_datetime
13
13
  from utilities.enum import ParseEnumError, parse_enum
14
14
  from utilities.functions import is_subclass_int_not_bool
15
- from utilities.iterables import one, one_str
15
+ from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
16
16
  from utilities.re import ExtractGroupError, extract_group
17
17
  from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
18
18
  from utilities.text import ParseBoolError, ParseNoneError, parse_bool, parse_none
19
19
  from utilities.typing import get_args, is_literal_type, is_optional_type, is_tuple_type
20
20
  from utilities.version import ParseVersionError, Version, parse_version
21
21
 
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Callable, Mapping
24
+
25
+
26
+ _T = TypeVar("_T")
27
+
22
28
 
23
29
  def parse_text(
24
- obj: Any, text: str, /, *, case_sensitive: bool = False, head: bool = False
30
+ obj: Any,
31
+ text: str,
32
+ /,
33
+ *,
34
+ case_sensitive: bool = False,
35
+ head: bool = False,
36
+ extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
25
37
  ) -> Any:
26
38
  """Parse text."""
27
39
  if obj is None:
28
40
  try:
29
41
  return parse_none(text)
30
42
  except ParseNoneError:
31
- raise ParseTextError(obj=obj, text=text) from None
43
+ raise _ParseTextParseError(obj=obj, text=text) from None
32
44
  if isinstance(obj, type):
33
- return _parse_text_type(obj, text, case_sensitive=case_sensitive)
45
+ return _parse_text_type(obj, text, case_sensitive=case_sensitive, extra=extra)
34
46
  if is_literal_type(obj):
35
47
  return one_str(get_args(obj), text, head=head, case_sensitive=case_sensitive)
36
48
  if is_optional_type(obj):
@@ -42,95 +54,111 @@ def parse_text(
42
54
  ):
43
55
  try:
44
56
  return _parse_text_type(inner, text, case_sensitive=case_sensitive)
45
- except ParseTextError:
46
- raise ParseTextError(obj=obj, text=text) from None
57
+ except _ParseTextParseError:
58
+ raise _ParseTextParseError(obj=obj, text=text) from None
47
59
  if is_tuple_type(obj):
48
60
  args = get_args(obj)
49
61
  try:
50
62
  texts = extract_group(r"^\((.*)\)$", text, flags=DOTALL).split(", ")
51
63
  except ExtractGroupError:
52
- raise ParseTextError(obj=obj, text=text) from None
64
+ raise _ParseTextParseError(obj=obj, text=text) from None
53
65
  if len(args) != len(texts):
54
- raise ParseTextError(obj=obj, text=text)
66
+ raise _ParseTextParseError(obj=obj, text=text)
55
67
  return tuple(
56
68
  parse_text(arg, text, case_sensitive=case_sensitive, head=head)
57
69
  for arg, text in zip(args, texts, strict=True)
58
70
  )
59
- raise ParseTextError(obj=obj, text=text) from None
71
+ raise _ParseTextParseError(obj=obj, text=text) from None
60
72
 
61
73
 
62
74
  def _parse_text_type(
63
- cls: type[Any], text: str, /, *, case_sensitive: bool = False
75
+ cls: type[Any],
76
+ text: str,
77
+ /,
78
+ *,
79
+ case_sensitive: bool = False,
80
+ extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
64
81
  ) -> Any:
65
82
  """Parse text."""
66
83
  if issubclass(cls, NoneType):
67
84
  try:
68
85
  return parse_none(text)
69
86
  except ParseNoneError:
70
- raise ParseTextError(obj=cls, text=text) from None
87
+ raise _ParseTextParseError(obj=cls, text=text) from None
71
88
  if issubclass(cls, str):
72
89
  return text
73
90
  if issubclass(cls, bool):
74
91
  try:
75
92
  return parse_bool(text)
76
93
  except ParseBoolError:
77
- raise ParseTextError(obj=cls, text=text) from None
94
+ raise _ParseTextParseError(obj=cls, text=text) from None
78
95
  if is_subclass_int_not_bool(cls):
79
96
  try:
80
97
  return int(text)
81
98
  except ValueError:
82
- raise ParseTextError(obj=cls, text=text) from None
99
+ raise _ParseTextParseError(obj=cls, text=text) from None
83
100
  if issubclass(cls, float):
84
101
  try:
85
102
  return float(text)
86
103
  except ValueError:
87
- raise ParseTextError(obj=cls, text=text) from None
104
+ raise _ParseTextParseError(obj=cls, text=text) from None
88
105
  if issubclass(cls, Enum):
89
106
  try:
90
107
  return parse_enum(text, cls, case_sensitive=case_sensitive)
91
108
  except ParseEnumError:
92
- raise ParseTextError(obj=cls, text=text) from None
109
+ raise _ParseTextParseError(obj=cls, text=text) from None
93
110
  if issubclass(cls, Path):
94
111
  return Path(text).expanduser()
95
112
  if issubclass(cls, Sentinel):
96
113
  try:
97
114
  return parse_sentinel(text)
98
115
  except ParseSentinelError:
99
- raise ParseTextError(obj=cls, text=text) from None
116
+ raise _ParseTextParseError(obj=cls, text=text) from None
100
117
  if issubclass(cls, Version):
101
118
  try:
102
119
  return parse_version(text)
103
120
  except ParseVersionError:
104
- raise ParseTextError(obj=cls, text=text) from None
121
+ raise _ParseTextParseError(obj=cls, text=text) from None
105
122
  if is_subclass_date_not_datetime(cls):
106
123
  from utilities.whenever import ParseDateError, parse_date
107
124
 
108
125
  try:
109
126
  return parse_date(text)
110
127
  except ParseDateError:
111
- raise ParseTextError(obj=cls, text=text) from None
128
+ raise _ParseTextParseError(obj=cls, text=text) from None
112
129
  if issubclass(cls, dt.datetime):
113
130
  from utilities.whenever import ParseDateTimeError, parse_datetime
114
131
 
115
132
  try:
116
133
  return parse_datetime(text)
117
134
  except ParseDateTimeError:
118
- raise ParseTextError(obj=cls, text=text) from None
135
+ raise _ParseTextParseError(obj=cls, text=text) from None
119
136
  if issubclass(cls, dt.time):
120
137
  from utilities.whenever import ParseTimeError, parse_time
121
138
 
122
139
  try:
123
140
  return parse_time(text)
124
141
  except ParseTimeError:
125
- raise ParseTextError(obj=cls, text=text) from None
142
+ raise _ParseTextParseError(obj=cls, text=text) from None
126
143
  if issubclass(cls, dt.timedelta):
127
144
  from utilities.whenever import ParseTimedeltaError, parse_timedelta
128
145
 
129
146
  try:
130
147
  return parse_timedelta(text)
131
148
  except ParseTimedeltaError:
132
- raise ParseTextError(obj=cls, text=text) from None
133
- raise ParseTextError(obj=cls, text=text) from None
149
+ raise _ParseTextParseError(obj=cls, text=text) from None
150
+ if extra is not None:
151
+ try:
152
+ parser = one(p for c, p in extra.items() if issubclass(cls, c))
153
+ except OneEmptyError:
154
+ pass
155
+ except OneNonUniqueError as error:
156
+ raise _ParseTextExtraNonUniqueError(
157
+ obj=cls, text=text, first=error.first, second=error.second
158
+ ) from None
159
+ else:
160
+ return parser(text)
161
+ raise _ParseTextParseError(obj=cls, text=text) from None
134
162
 
135
163
 
136
164
  @dataclass
@@ -138,6 +166,19 @@ class ParseTextError(Exception):
138
166
  obj: Any
139
167
  text: str
140
168
 
169
+
170
+ @dataclass
171
+ class _ParseTextParseError(ParseTextError):
141
172
  @override
142
173
  def __str__(self) -> str:
143
174
  return f"Unable to parse {self.obj!r}; got {self.text!r}"
175
+
176
+
177
+ @dataclass
178
+ class _ParseTextExtraNonUniqueError(ParseTextError):
179
+ first: type[Any]
180
+ second: type[Any]
181
+
182
+ @override
183
+ def __str__(self) -> str:
184
+ return f"Unable to parse {self.obj!r} since `extra` must contain exactly one parent class; got {self.first!r}, {self.second!r} and perhaps more"
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from os import environ
5
- from typing import TYPE_CHECKING, override
5
+ from typing import TYPE_CHECKING, TypeVar, override
6
6
 
7
7
  from dotenv import dotenv_values
8
8
 
@@ -13,13 +13,16 @@ from utilities.pathlib import PWD
13
13
  from utilities.reprlib import get_repr
14
14
 
15
15
  if TYPE_CHECKING:
16
- from collections.abc import Mapping
16
+ from collections.abc import Callable, Mapping
17
17
  from collections.abc import Set as AbstractSet
18
18
  from pathlib import Path
19
19
 
20
20
  from utilities.types import PathLike, StrMapping, TDataclass
21
21
 
22
22
 
23
+ _T = TypeVar("_T")
24
+
25
+
23
26
  def load_settings(
24
27
  cls: type[TDataclass],
25
28
  /,
@@ -30,6 +33,7 @@ def load_settings(
30
33
  warn_name_errors: bool = False,
31
34
  head: bool = False,
32
35
  case_sensitive: bool = False,
36
+ extra_parsers: Mapping[type[_T], Callable[[str], _T]] | None = None,
33
37
  ) -> TDataclass:
34
38
  """Load a set of settings from the `.env` file."""
35
39
  path = get_repo_root(cwd=cwd).joinpath(".env")
@@ -57,7 +61,8 @@ def load_settings(
57
61
  warn_name_errors=warn_name_errors,
58
62
  head=head,
59
63
  case_sensitive=case_sensitive,
60
- allow_extra=True,
64
+ allow_extra_keys=True,
65
+ extra_parsers=extra_parsers,
61
66
  )
62
67
  except MappingToDataclassError as error:
63
68
  raise _LoadSettingsMissingKeysError(path=path, fields=error.fields) from None