dycw-utilities 0.109.4__py3-none-any.whl → 0.109.6__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.4
3
+ Version: 0.109.6
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=TfQi7UYijWAmuZ8wsQ7kQKbgWiSQWgaXE80fBUn8qx8,60
1
+ utilities/__init__.py,sha256=w3npT3XHHPgayXlB2cRHRSo29zUPb2MDUTT0HCvkrhI,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
@@ -31,7 +31,7 @@ utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
31
31
  utilities/logging.py,sha256=opIwFjGKOYyMntVeCsFNXOmTY2z02hMf2UtCB76SaI4,25142
32
32
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
33
33
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
34
- utilities/math.py,sha256=AtUqrZ1Re9rwJjvp3QSlaa9GGIgBMKkslt77YxaNibw,26240
34
+ utilities/math.py,sha256=TexfvLCI12d9Sw5_W4pKVBZ3nRr3zk2iPkcEU7xdEWU,26771
35
35
  utilities/memory_profiler.py,sha256=tf2C51P2lCujPGvRt2Rfc7VEw5LDXmVPCG3z_AvBmbU,962
36
36
  utilities/modules.py,sha256=SnhsRHRUS1po_acejrINauihGQpPvVsp8RDNCei1OLQ,3173
37
37
  utilities/more_itertools.py,sha256=CPUxrMAcTwRxbzbhiqPKi3Xx9hxqI0t6gkWjutaibGk,5534
@@ -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=q1A1-bsXltdAh8We5Acy-lPuVIClWTxsRXQWq5wYyKk,6382
43
+ utilities/parse.py,sha256=hzMDMUA9h3IpgsR48-Dk4CaojXofLm-eAn_bvJ8o6k8,7081
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
@@ -81,10 +81,10 @@ utilities/tzlocal.py,sha256=42BCquGF54oIqIKe5RGziP4K8Nbm3Ey7uqcNn6m5ge8,534
81
81
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
82
82
  utilities/version.py,sha256=QFuyEeQA6jI0ruBEcmhqG36f-etg1AEiD1drBBqhQrs,5358
83
83
  utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
84
- utilities/whenever.py,sha256=5x2t47VJmJRWcd_NLFy54NkB3uom-XQYxEbLtEfL1bs,17775
84
+ utilities/whenever.py,sha256=TjoTAJ1R27-rKXiXzdE4GzPidmYqm0W58XydDXp-QZM,17786
85
85
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
86
86
  utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
87
- dycw_utilities-0.109.4.dist-info/METADATA,sha256=-fU_tSw6cCv59Fg6s0I0pUlV3YzXeuq15kfirsKUmik,13004
88
- dycw_utilities-0.109.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
- dycw_utilities-0.109.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
- dycw_utilities-0.109.4.dist-info/RECORD,,
87
+ dycw_utilities-0.109.6.dist-info/METADATA,sha256=QrUiasc-0kk8qhmb20T1rZsXPJMTTunfG4QMbba9F6E,13004
88
+ dycw_utilities-0.109.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
89
+ dycw_utilities-0.109.6.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
90
+ dycw_utilities-0.109.6.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.109.4"
3
+ __version__ = "0.109.6"
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/math.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from contextlib import suppress
4
5
  from dataclasses import dataclass
5
6
  from math import ceil, exp, floor, isclose, isfinite, isinf, isnan, log, log10, modf
6
7
  from re import Match, search
@@ -9,7 +10,8 @@ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
9
10
  from utilities.errors import ImpossibleCaseError
10
11
 
11
12
  if TYPE_CHECKING:
12
- from utilities.types import RoundMode
13
+ from utilities.types import Number, RoundMode
14
+
13
15
 
14
16
  MIN_FLOAT32, MAX_FLOAT32 = -3.4028234663852886e38, 3.4028234663852886e38
15
17
  MIN_FLOAT64, MAX_FLOAT64 = -1.7976931348623157e308, 1.7976931348623157e308
@@ -681,6 +683,27 @@ def order_of_magnitude(x: float, /, *, round_: bool = False) -> float:
681
683
  ##
682
684
 
683
685
 
686
+ def parse_number(number: str, /) -> Number:
687
+ """Convert text into a number."""
688
+ with suppress(ValueError):
689
+ return int(number)
690
+ with suppress(ValueError):
691
+ return float(number)
692
+ raise ParseNumberError(number=number)
693
+
694
+
695
+ @dataclass(kw_only=True, slots=True)
696
+ class ParseNumberError(Exception):
697
+ number: str
698
+
699
+ @override
700
+ def __str__(self) -> str:
701
+ return f"Unable to parse number; got {self.number!r}"
702
+
703
+
704
+ ##
705
+
706
+
684
707
  def round_(
685
708
  x: float,
686
709
  /,
@@ -887,6 +910,7 @@ __all__ = [
887
910
  "MIN_UINT64",
888
911
  "CheckIntegerError",
889
912
  "EWMParametersError",
913
+ "ParseNumberError",
890
914
  "SafeRoundError",
891
915
  "check_integer",
892
916
  "ewm_parameters",
@@ -934,6 +958,7 @@ __all__ = [
934
958
  "is_zero_or_non_micro_or_nan",
935
959
  "number_of_decimals",
936
960
  "order_of_magnitude",
961
+ "parse_number",
937
962
  "round_",
938
963
  "round_float_imprecisions",
939
964
  "round_to_float",
utilities/parse.py CHANGED
@@ -13,10 +13,18 @@ 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
15
  from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
16
+ from utilities.math import ParseNumberError, parse_number
16
17
  from utilities.re import ExtractGroupError, extract_group
17
18
  from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
18
19
  from utilities.text import ParseBoolError, ParseNoneError, parse_bool, parse_none
19
- from utilities.typing import get_args, is_literal_type, is_optional_type, is_tuple_type
20
+ from utilities.types import Duration, Number
21
+ from utilities.typing import (
22
+ get_args,
23
+ is_literal_type,
24
+ is_optional_type,
25
+ is_tuple_type,
26
+ is_union_type,
27
+ )
20
28
  from utilities.version import ParseVersionError, Version, parse_version
21
29
 
22
30
  if TYPE_CHECKING:
@@ -31,8 +39,8 @@ def parse_text(
31
39
  text: str,
32
40
  /,
33
41
  *,
34
- case_sensitive: bool = False,
35
42
  head: bool = False,
43
+ case_sensitive: bool = False,
36
44
  extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
37
45
  ) -> Any:
38
46
  """Parse text."""
@@ -49,13 +57,12 @@ def parse_text(
49
57
  with suppress(ParseNoneError):
50
58
  return parse_none(text)
51
59
  inner = one(arg for arg in get_args(obj) if arg is not NoneType)
52
- if isinstance(
53
- inner := one(arg for arg in get_args(obj) if arg is not NoneType), type
54
- ):
55
- try:
56
- return _parse_text_type(inner, text, case_sensitive=case_sensitive)
57
- except _ParseTextParseError:
58
- raise _ParseTextParseError(obj=obj, text=text) from None
60
+ try:
61
+ return parse_text(
62
+ inner, text, head=head, case_sensitive=case_sensitive, extra=extra
63
+ )
64
+ except _ParseTextParseError:
65
+ raise _ParseTextParseError(obj=obj, text=text) from None
59
66
  if is_tuple_type(obj):
60
67
  args = get_args(obj)
61
68
  try:
@@ -65,9 +72,11 @@ def parse_text(
65
72
  if len(args) != len(texts):
66
73
  raise _ParseTextParseError(obj=obj, text=text)
67
74
  return tuple(
68
- parse_text(arg, text, case_sensitive=case_sensitive, head=head)
75
+ parse_text(arg, text, head=head, case_sensitive=case_sensitive, extra=extra)
69
76
  for arg, text in zip(args, texts, strict=True)
70
77
  )
78
+ if is_union_type(obj):
79
+ return _parse_text_union_type(obj, text)
71
80
  raise _ParseTextParseError(obj=obj, text=text) from None
72
81
 
73
82
 
@@ -161,6 +170,22 @@ def _parse_text_type(
161
170
  raise _ParseTextParseError(obj=cls, text=text) from None
162
171
 
163
172
 
173
+ def _parse_text_union_type(obj: Any, text: str, /) -> Any:
174
+ if obj is Number:
175
+ try:
176
+ return parse_number(text)
177
+ except ParseNumberError:
178
+ raise _ParseTextParseError(obj=obj, text=text) from None
179
+ if obj is Duration:
180
+ from utilities.whenever import ParseDurationError, parse_duration
181
+
182
+ try:
183
+ return parse_duration(text)
184
+ except ParseDurationError:
185
+ raise _ParseTextParseError(obj=obj, text=text) from None
186
+ raise _ParseTextParseError(obj=obj, text=text) from None
187
+
188
+
164
189
  @dataclass
165
190
  class ParseTextError(Exception):
166
191
  obj: Any
@@ -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
utilities/whenever.py CHANGED
@@ -17,6 +17,7 @@ from utilities.datetime import (
17
17
  parse_two_digit_year,
18
18
  timedelta_to_microseconds,
19
19
  )
20
+ from utilities.math import ParseNumberError, parse_number
20
21
  from utilities.re import (
21
22
  ExtractGroupError,
22
23
  ExtractGroupsError,
@@ -298,10 +299,8 @@ class ParseDateTimeError(Exception):
298
299
 
299
300
  def parse_duration(duration: str, /) -> Duration:
300
301
  """Parse a string into a Duration."""
301
- with suppress(ValueError):
302
- return int(duration)
303
- with suppress(ValueError):
304
- return float(duration)
302
+ with suppress(ParseNumberError):
303
+ return parse_number(duration)
305
304
  try:
306
305
  return parse_timedelta(duration)
307
306
  except ParseTimedeltaError: