dycw-utilities 0.110.8__py3-none-any.whl → 0.111.1__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.110.8
3
+ Version: 0.111.1
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=USEDB4wi-U2inTRQAJuZ7L5i_LnX5kcGM17wMf2dOck,60
1
+ utilities/__init__.py,sha256=j1-gk7DzQOb2e2RhnLMwpl11AV5CfMKamy2OkF4La18,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
4
4
  utilities/asyncio.py,sha256=41oQUurWMvadFK5gFnaG21hMM0Vmfn2WS6OpC0R9mas,14757
@@ -12,13 +12,13 @@ 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
14
  utilities/dataclasses.py,sha256=Q197PVnE_vUMn_SNnqJBCo4eRy4bdHtgMHWRbSJPtFk,26670
15
- utilities/datetime.py,sha256=GOs-MIEW_A49kzqa1yhIoeNeSqqPVgGO-h2AThtgTDk,37326
15
+ utilities/datetime.py,sha256=XrVHvyHyYMBeU2ClC7xJKwB68l-F2ZCmDznyNXS4hT8,36862
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=BtSNP0JC3ik536ddPyTerLomCRJV9f6kdMe6POz0QHM,361
18
18
  utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
19
19
  utilities/fastapi.py,sha256=uwqOGbGwzIbP-lfm-ApG1ZEN3BA_TDsaiuTghhLmxb8,2413
20
20
  utilities/fpdf2.py,sha256=zM3gwOYcAfv7P4qhbyvzPmRY4PPAiAQ-ZnPC6I9SZ1M,1832
21
- utilities/functions.py,sha256=lAJeERNrmcpwon3drcTIlizLVRd8D-gdXojxtKFN0LM,28736
21
+ utilities/functions.py,sha256=jgt592voaHNtX56qX0SRvFveVCRmSIxCZmqvpLZCnY8,27305
22
22
  utilities/functools.py,sha256=WrpHt7NLNWSUn9A1Q_ZIWlNaYZOEI4IFKyBG9HO3BC4,1643
23
23
  utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
24
24
  utilities/git.py,sha256=wpt5dZ5Oi5931pN24_VLZYaQOvmR0OcQuVtgHzFUN1k,2359
@@ -39,14 +39,14 @@ utilities/more_itertools.py,sha256=CPUxrMAcTwRxbzbhiqPKi3Xx9hxqI0t6gkWjutaibGk,5
39
39
  utilities/numpy.py,sha256=cBgCBet8YfZP_rb4nkCJHZx9_03qPEinVENMk1dGVYQ,25683
40
40
  utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
41
41
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
42
- utilities/orjson.py,sha256=Wj5pzG_VdgoAy14a7Luhem-BgYrRtRFvvl_POiszRd0,36930
42
+ utilities/orjson.py,sha256=DBm2zPP04kcHpY3l1etL24ksNynu-R3duFyx3U-RjqQ,36948
43
43
  utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
44
- utilities/parse.py,sha256=fki2mnPGa1Ex-aeWiXDkUBHWb7FEk4F6AzMiHjqHXdw,19081
44
+ utilities/parse.py,sha256=hG-y8WAzpATakA61UOF6UlhfuJQ9XbMN-Uub1ZaiBRU,18780
45
45
  utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
46
- utilities/period.py,sha256=ikHXsWtDLr553cfH6p9mMaiCnIAP69B7q84ckWV3HaA,10884
46
+ utilities/period.py,sha256=RWfcNVoNlW07RNdU47g_zuLZMKbtgfK4bE6G-9tVjY8,11024
47
47
  utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
48
48
  utilities/platform.py,sha256=NU7ycTvAXAG-fdYmDXaM1m4EOml2cGiaYwaUzfzSqyU,1767
49
- utilities/polars.py,sha256=woTGhyzXNLN30SQgwXz54-I1aJp1oWATJ2rpmke7gKI,58419
49
+ utilities/polars.py,sha256=lr0l3JE2d5Rj1UbbFtagcv1z3Vw5YXjSfAwzVa9cFXw,58374
50
50
  utilities/polars_ols.py,sha256=efhXf0gjrHUpQrvS6a7g8yJQJWf_ATKtJnqqF2inCOU,5680
51
51
  utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
52
52
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -67,7 +67,7 @@ utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
67
67
  utilities/slack_sdk.py,sha256=SeDNMh24IPiEBWoGMdgvrflUaFa9TGlTS03H9-NKaQw,4132
68
68
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
69
69
  utilities/sqlalchemy.py,sha256=GWzp54TP3F2mGhxPTn0c56KxxDeN9VKLMagcRSELhf4,35453
70
- utilities/sqlalchemy_polars.py,sha256=oGyMX5gSxuLI3N8mtz_-ml3UdWKcZuj6aFRW6ifI0Kc,15617
70
+ utilities/sqlalchemy_polars.py,sha256=wjJpoUo-yO9E2ujpG_06vV5r2OdvBiQ4yvV6wKCa2Tk,15605
71
71
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
72
72
  utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
73
73
  utilities/sys.py,sha256=h0Xr7Vj86wNalvwJVP1wj5Y0kD_VWm1vzuXZ_jw94mE,2743
@@ -78,7 +78,7 @@ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
78
78
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
79
79
  utilities/traceback.py,sha256=KwHPLdEbdj0fFhXo8MBfxcvem8A-VXYDwFMNJ6f0cTM,27328
80
80
  utilities/types.py,sha256=kVY71hZkcnyYNIlYSse0mLm8yeP3OBkzhDPMME6jXxo,18126
81
- utilities/typing.py,sha256=WE3UWaVYO6nTkpEo8Ke3xUuxllsJtfOxsZ0-mSH69Nw,6668
81
+ utilities/typing.py,sha256=jtc6EiGZGG0E745jo3NeLqo_HdHt7Zdaco3kCAEWIYU,11177
82
82
  utilities/tzdata.py,sha256=2ZsPmhTVM9Ptrxb4QrWKtKOB9RiH8IOO-A1u7ULdVbg,176
83
83
  utilities/tzlocal.py,sha256=42BCquGF54oIqIKe5RGziP4K8Nbm3Ey7uqcNn6m5ge8,534
84
84
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
@@ -87,7 +87,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
87
  utilities/whenever.py,sha256=TjoTAJ1R27-rKXiXzdE4GzPidmYqm0W58XydDXp-QZM,17786
88
88
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
89
  utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
90
- dycw_utilities-0.110.8.dist-info/METADATA,sha256=ZVWP8hzsSisjUrY3uoJqCIoh5j7pVhSy0PwDigCaJRw,13004
91
- dycw_utilities-0.110.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.110.8.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.110.8.dist-info/RECORD,,
90
+ dycw_utilities-0.111.1.dist-info/METADATA,sha256=kXoTkJKsgrfBYD5eCwE45d4mGIX5CMZhVeNHh9SRiL8,13004
91
+ dycw_utilities-0.111.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.111.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.111.1.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.110.8"
3
+ __version__ = "0.111.1"
utilities/datetime.py CHANGED
@@ -22,6 +22,7 @@ from utilities.math import SafeRoundError, round_, safe_round
22
22
  from utilities.platform import SYSTEM
23
23
  from utilities.sentinel import Sentinel, sentinel
24
24
  from utilities.types import MaybeStr
25
+ from utilities.typing import is_instance_gen
25
26
  from utilities.zoneinfo import (
26
27
  UTC,
27
28
  HongKong,
@@ -140,9 +141,9 @@ def are_equal_dates_or_datetimes(
140
141
  x: DateOrDateTime, y: DateOrDateTime, /, *, strict: bool = False
141
142
  ) -> bool:
142
143
  """Check if x == y for dates/datetimes."""
143
- if is_instance_date_not_datetime(x) and is_instance_date_not_datetime(y):
144
+ if is_instance_gen(x, dt.date) and is_instance_gen(y, dt.date):
144
145
  return x == y
145
- if isinstance(x, dt.datetime) and isinstance(y, dt.datetime):
146
+ if is_instance_gen(x, dt.datetime) and is_instance_gen(y, dt.datetime):
146
147
  return are_equal_datetimes(x, y, strict=strict)
147
148
  raise AreEqualDatesOrDateTimesError(x=x, y=y)
148
149
 
@@ -210,7 +211,7 @@ def are_equal_months(x: DateOrMonth, y: DateOrMonth, /) -> bool:
210
211
 
211
212
  def check_date_not_datetime(date: dt.date, /) -> None:
212
213
  """Check if a date is not a datetime."""
213
- if not is_instance_date_not_datetime(date):
214
+ if not is_instance_gen(date, dt.date):
214
215
  raise CheckDateNotDateTimeError(date=date)
215
216
 
216
217
 
@@ -604,14 +605,6 @@ YEAR = get_years(n=1)
604
605
  ##
605
606
 
606
607
 
607
- def is_instance_date_not_datetime(obj: Any, /) -> TypeGuard[dt.date]:
608
- """Check if an object is a date, and not a datetime."""
609
- return isinstance(obj, dt.date) and not isinstance(obj, dt.datetime)
610
-
611
-
612
- ##
613
-
614
-
615
608
  def is_integral_timedelta(timedelta: dt.timedelta, /) -> bool:
616
609
  """Check if a timedelta is integral."""
617
610
  return (timedelta.seconds == 0) and (timedelta.microseconds == 0)
@@ -628,14 +621,6 @@ def is_local_datetime(obj: Any, /) -> TypeGuard[dt.datetime]:
628
621
  ##
629
622
 
630
623
 
631
- def is_subclass_date_not_datetime(cls: type[Any], /) -> TypeGuard[type[dt.date]]:
632
- """Check if a class is a date, and not a datetime."""
633
- return issubclass(cls, dt.date) and not issubclass(cls, dt.datetime)
634
-
635
-
636
- ##
637
-
638
-
639
624
  _FRIDAY = 5
640
625
 
641
626
 
@@ -1359,10 +1344,8 @@ __all__ = [
1359
1344
  "get_today_local",
1360
1345
  "get_today_tokyo",
1361
1346
  "get_years",
1362
- "is_instance_date_not_datetime",
1363
1347
  "is_integral_timedelta",
1364
1348
  "is_local_datetime",
1365
- "is_subclass_date_not_datetime",
1366
1349
  "is_weekday",
1367
1350
  "is_zero_time",
1368
1351
  "is_zoned_datetime",
utilities/functions.py CHANGED
@@ -22,7 +22,6 @@ from typing import (
22
22
  Literal,
23
23
  TypeGuard,
24
24
  TypeVar,
25
- assert_never,
26
25
  cast,
27
26
  overload,
28
27
  override,
@@ -660,28 +659,6 @@ def is_hashable(obj: Any, /) -> TypeGuard[Hashable]:
660
659
  ##
661
660
 
662
661
 
663
- def is_instance_not_bool_int(
664
- obj: Any, class_or_tuple: TypeLike[Any], /
665
- ) -> TypeGuard[int]:
666
- """Check if an instance relationship holds, except bool<int."""
667
- match class_or_tuple:
668
- case type() as type_:
669
- return _is_instance_not_bool_int(obj, type_)
670
- case tuple() as types:
671
- return any(_is_instance_not_bool_int(obj, p) for p in types)
672
- case _ as never:
673
- assert_never(never)
674
-
675
-
676
- def _is_instance_not_bool_int(obj: Any, type_: type[Any], /) -> bool:
677
- return isinstance(obj, type_) and not (
678
- isinstance(obj, bool) and issubclass(type_, int) and not issubclass(type_, bool)
679
- )
680
-
681
-
682
- ##
683
-
684
-
685
662
  @overload
686
663
  def is_iterable_of(obj: Any, cls: type[_T], /) -> TypeGuard[Iterable[_T]]: ...
687
664
  @overload
@@ -796,28 +773,6 @@ def is_string_mapping(obj: Any, /) -> TypeGuard[StrMapping]:
796
773
  ##
797
774
 
798
775
 
799
- def is_subclass_not_bool_int(cls: type[Any], class_or_tuple: TypeLike[Any], /) -> bool:
800
- """Check if a subclass relationship holds, except bool<int."""
801
- match class_or_tuple:
802
- case type() as parent:
803
- return _is_subclass_int_not_bool_one(cls, parent)
804
- case tuple() as parents:
805
- return any(_is_subclass_int_not_bool_one(cls, p) for p in parents)
806
- case _ as never:
807
- assert_never(never)
808
-
809
-
810
- def _is_subclass_int_not_bool_one(cls: type[Any], parent: type[Any], /) -> bool:
811
- return issubclass(cls, parent) and not (
812
- issubclass(cls, bool)
813
- and issubclass(parent, int)
814
- and not issubclass(parent, bool)
815
- )
816
-
817
-
818
- ##
819
-
820
-
821
776
  def is_tuple(obj: Any, /) -> TypeGuard[tuple[Any, ...]]:
822
777
  """Check if an object is a tuple or string mapping."""
823
778
  return make_isinstance(tuple)(obj)
@@ -1075,7 +1030,6 @@ __all__ = [
1075
1030
  "is_dataclass_class",
1076
1031
  "is_dataclass_instance",
1077
1032
  "is_hashable",
1078
- "is_instance_not_bool_int",
1079
1033
  "is_iterable_of",
1080
1034
  "is_none",
1081
1035
  "is_not_none",
@@ -1083,7 +1037,6 @@ __all__ = [
1083
1037
  "is_sized",
1084
1038
  "is_sized_not_str",
1085
1039
  "is_string_mapping",
1086
- "is_subclass_not_bool_int",
1087
1040
  "is_tuple",
1088
1041
  "is_tuple_or_str_mapping",
1089
1042
  "make_isinstance",
utilities/orjson.py CHANGED
@@ -692,6 +692,7 @@ _LOG_RECORD_DEFAULT_ATTRS = {
692
692
  "exc_text",
693
693
  "filename",
694
694
  "funcName",
695
+ "getMessage",
695
696
  "levelname",
696
697
  "levelno",
697
698
  "lineno",
utilities/parse.py CHANGED
@@ -9,12 +9,7 @@ from re import DOTALL
9
9
  from types import NoneType
10
10
  from typing import TYPE_CHECKING, Any, override
11
11
 
12
- from utilities.datetime import (
13
- is_instance_date_not_datetime,
14
- is_subclass_date_not_datetime,
15
- )
16
12
  from utilities.enum import ParseEnumError, parse_enum
17
- from utilities.functions import is_subclass_not_bool_int
18
13
  from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
19
14
  from utilities.math import ParseNumberError, parse_number
20
15
  from utilities.re import ExtractGroupError, extract_group
@@ -36,10 +31,12 @@ from utilities.typing import (
36
31
  get_args,
37
32
  is_dict_type,
38
33
  is_frozenset_type,
34
+ is_instance_gen,
39
35
  is_list_type,
40
36
  is_literal_type,
41
37
  is_optional_type,
42
38
  is_set_type,
39
+ is_subclass_gen,
43
40
  is_tuple_type,
44
41
  is_union_type,
45
42
  )
@@ -63,15 +60,15 @@ def parse_object(
63
60
  extra: ParseObjectExtra | None = None,
64
61
  ) -> Any:
65
62
  """Parse text."""
63
+ if extra is not None:
64
+ return _parse_object_extra(type_, text, extra)
66
65
  if type_ is None:
67
66
  try:
68
67
  return parse_none(text)
69
68
  except ParseNoneError:
70
69
  raise _ParseObjectParseError(type_=type_, text=text) from None
71
70
  if isinstance(type_, type):
72
- return _parse_object_type(
73
- type_, text, case_sensitive=case_sensitive, extra=extra
74
- )
71
+ return _parse_object_type(type_, text, case_sensitive=case_sensitive)
75
72
  if is_dict_type(type_):
76
73
  return _parse_object_dict_type(
77
74
  type_,
@@ -149,17 +146,12 @@ def parse_object(
149
146
  extra=extra,
150
147
  )
151
148
  if is_union_type(type_):
152
- return _parse_object_union_type(type_, text, extra=extra)
149
+ return _parse_object_union_type(type_, text)
153
150
  raise _ParseObjectParseError(type_=type_, text=text) from None
154
151
 
155
152
 
156
153
  def _parse_object_type(
157
- cls: type[Any],
158
- text: str,
159
- /,
160
- *,
161
- case_sensitive: bool = False,
162
- extra: ParseObjectExtra | None = None,
154
+ cls: type[Any], text: str, /, *, case_sensitive: bool = False
163
155
  ) -> Any:
164
156
  """Parse text."""
165
157
  if issubclass(cls, NoneType):
@@ -169,12 +161,12 @@ def _parse_object_type(
169
161
  raise _ParseObjectParseError(type_=cls, text=text) from None
170
162
  if issubclass(cls, str):
171
163
  return text
172
- if issubclass(cls, bool):
164
+ if is_subclass_gen(cls, bool):
173
165
  try:
174
166
  return parse_bool(text)
175
167
  except ParseBoolError:
176
168
  raise _ParseObjectParseError(type_=cls, text=text) from None
177
- if is_subclass_not_bool_int(cls, int):
169
+ if is_subclass_gen(cls, int):
178
170
  try:
179
171
  return int(text)
180
172
  except ValueError:
@@ -201,14 +193,14 @@ def _parse_object_type(
201
193
  return parse_version(text)
202
194
  except ParseVersionError:
203
195
  raise _ParseObjectParseError(type_=cls, text=text) from None
204
- if is_subclass_date_not_datetime(cls):
196
+ if is_subclass_gen(cls, dt.date):
205
197
  from utilities.whenever import ParseDateError, parse_date
206
198
 
207
199
  try:
208
200
  return parse_date(text)
209
201
  except ParseDateError:
210
202
  raise _ParseObjectParseError(type_=cls, text=text) from None
211
- if issubclass(cls, dt.datetime):
203
+ if is_subclass_gen(cls, dt.datetime):
212
204
  from utilities.whenever import ParseDateTimeError, parse_datetime
213
205
 
214
206
  try:
@@ -229,17 +221,6 @@ def _parse_object_type(
229
221
  return parse_timedelta(text)
230
222
  except ParseTimedeltaError:
231
223
  raise _ParseObjectParseError(type_=cls, text=text) from None
232
- if extra is not None:
233
- try:
234
- parser = one(p for c, p in extra.items() if issubclass(cls, c))
235
- except OneEmptyError:
236
- pass
237
- except OneNonUniqueError as error:
238
- raise _ParseObjectExtraNonUniqueError(
239
- type_=cls, text=text, first=error.first, second=error.second
240
- ) from None
241
- else:
242
- return parser(text)
243
224
  raise _ParseObjectParseError(type_=cls, text=text)
244
225
 
245
226
 
@@ -299,6 +280,21 @@ def _parse_object_dict_type(
299
280
  raise _ParseObjectParseError(type_=type_, text=text) from None
300
281
 
301
282
 
283
+ def _parse_object_extra(cls: Any, text: str, extra: ParseObjectExtra, /) -> Any:
284
+ try:
285
+ parser = one(
286
+ p for c, p in extra.items() if (cls is c) or is_subclass_gen(cls, c)
287
+ )
288
+ except OneEmptyError:
289
+ raise _ParseObjectParseError(type_=cls, text=text) from None
290
+ except OneNonUniqueError as error:
291
+ raise _ParseObjectExtraNonUniqueError(
292
+ type_=cls, text=text, first=error.first, second=error.second
293
+ ) from None
294
+ else:
295
+ return parser(text)
296
+
297
+
302
298
  def _parse_object_list_type(
303
299
  type_: Any,
304
300
  text: str,
@@ -371,9 +367,7 @@ def _parse_object_set_type(
371
367
  raise _ParseObjectParseError(type_=type_, text=text) from None
372
368
 
373
369
 
374
- def _parse_object_union_type(
375
- type_: Any, text: str, /, *, extra: ParseObjectExtra | None = None
376
- ) -> Any:
370
+ def _parse_object_union_type(type_: Any, text: str, /) -> Any:
377
371
  if type_ is Number:
378
372
  try:
379
373
  return parse_number(text)
@@ -386,13 +380,6 @@ def _parse_object_union_type(
386
380
  return parse_duration(text)
387
381
  except ParseDurationError:
388
382
  raise _ParseObjectParseError(type_=type_, text=text) from None
389
- if extra is not None:
390
- try:
391
- parser = one(p for c, p in extra.items() if c is type_)
392
- except OneEmptyError:
393
- pass
394
- else:
395
- return parser(text)
396
383
  raise _ParseObjectParseError(type_=type_, text=text) from None
397
384
 
398
385
 
@@ -469,15 +456,18 @@ def serialize_object(
469
456
  extra: SerializeObjectExtra | None = None,
470
457
  ) -> str:
471
458
  """Convert an object to text."""
459
+ if extra is not None:
460
+ with suppress(_SerializeObjectSerializeError):
461
+ return _serialize_object_extra(obj, extra)
472
462
  if (obj is None) or isinstance(
473
463
  obj, bool | int | float | str | Path | Sentinel | Version
474
464
  ):
475
465
  return str(obj)
476
- if is_instance_date_not_datetime(obj):
466
+ if is_instance_gen(obj, dt.date):
477
467
  from utilities.whenever import serialize_date
478
468
 
479
469
  return serialize_date(obj)
480
- if isinstance(obj, dt.datetime):
470
+ if is_instance_gen(obj, dt.datetime):
481
471
  from utilities.whenever import serialize_datetime
482
472
 
483
473
  return serialize_datetime(obj)
@@ -507,8 +497,6 @@ def serialize_object(
507
497
  return _serialize_object_set(
508
498
  obj, list_separator=list_separator, pair_separator=pair_separator
509
499
  )
510
- if extra is not None:
511
- return _serialize_object_extra(obj, extra)
512
500
  raise _SerializeObjectSerializeError(obj=obj)
513
501
 
514
502
 
@@ -540,10 +528,7 @@ def _serialize_object_dict(
540
528
  def _serialize_object_extra(obj: Any, extra: SerializeObjectExtra, /) -> str:
541
529
  try:
542
530
  serializer = one(
543
- s
544
- for c, s in extra.items()
545
- if (isinstance(c, type) and isinstance(obj, c))
546
- or isinstance(obj, get_args(c))
531
+ s for c, s in extra.items() if (obj is c) or is_instance_gen(obj, c)
547
532
  )
548
533
  except OneEmptyError:
549
534
  raise _SerializeObjectSerializeError(obj=obj) from None
utilities/period.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import datetime as dt
4
4
  from dataclasses import dataclass, field
5
5
  from functools import cached_property
6
+ from itertools import permutations
6
7
  from typing import (
7
8
  TYPE_CHECKING,
8
9
  Generic,
@@ -15,10 +16,11 @@ from typing import (
15
16
  override,
16
17
  )
17
18
 
18
- from utilities.datetime import ZERO_TIME, is_instance_date_not_datetime
19
+ from utilities.datetime import ZERO_TIME
19
20
  from utilities.functions import get_class_name
20
21
  from utilities.iterables import OneUniqueNonUniqueError, always_iterable, one_unique
21
22
  from utilities.sentinel import Sentinel, sentinel
23
+ from utilities.typing import is_instance_gen
22
24
  from utilities.whenever import (
23
25
  serialize_date,
24
26
  serialize_local_datetime,
@@ -32,6 +34,7 @@ if TYPE_CHECKING:
32
34
  from utilities.iterables import MaybeIterable
33
35
  from utilities.types import DateOrDateTime
34
36
 
37
+
35
38
  type _DateOrDateTime = Literal["date", "datetime"]
36
39
  _TPeriod = TypeVar("_TPeriod", dt.date, dt.datetime)
37
40
 
@@ -54,9 +57,11 @@ class Period(Generic[_TPeriod]):
54
57
  max_duration: dt.timedelta | None = field(default=None, repr=False, kw_only=True)
55
58
 
56
59
  def __post_init__(self) -> None:
57
- if is_instance_date_not_datetime(
58
- self.start
59
- ) is not is_instance_date_not_datetime(self.end):
60
+ if any(
61
+ is_instance_gen(left, cls) is not is_instance_gen(right, cls)
62
+ for left, right in permutations([self.start, self.end], 2)
63
+ for cls in [dt.date, dt.datetime]
64
+ ):
60
65
  raise _PeriodDateAndDateTimeMixedError(start=self.start, end=self.end)
61
66
  for date in [self.start, self.end]:
62
67
  if isinstance(date, dt.datetime):
@@ -166,7 +171,7 @@ class Period(Generic[_TPeriod]):
166
171
  @cached_property
167
172
  def kind(self) -> _DateOrDateTime:
168
173
  """The kind of the period."""
169
- return "date" if is_instance_date_not_datetime(self.start) else "datetime"
174
+ return "date" if is_instance_gen(self.start, dt.date) else "datetime"
170
175
 
171
176
  def replace(
172
177
  self,
utilities/polars.py CHANGED
@@ -56,7 +56,6 @@ from polars.exceptions import (
56
56
  from polars.testing import assert_frame_equal
57
57
 
58
58
  from utilities.dataclasses import _YieldFieldsInstance, yield_fields
59
- from utilities.datetime import is_instance_date_not_datetime
60
59
  from utilities.errors import ImpossibleCaseError
61
60
  from utilities.functions import (
62
61
  EnsureIntError,
@@ -95,6 +94,7 @@ from utilities.typing import (
95
94
  get_args,
96
95
  get_type_hints,
97
96
  is_frozenset_type,
97
+ is_instance_gen,
98
98
  is_list_type,
99
99
  is_literal_type,
100
100
  is_optional_type,
@@ -945,7 +945,7 @@ def dataclass_to_schema(
945
945
  dt.date,
946
946
  dt.datetime,
947
947
  }:
948
- if is_instance_date_not_datetime(field.value):
948
+ if is_instance_gen(field.value, dt.date):
949
949
  dtype = Date
950
950
  else:
951
951
  dtype = _dataclass_to_schema_datetime(field)
@@ -28,7 +28,6 @@ from sqlalchemy.exc import DuplicateColumnError
28
28
  from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
29
29
 
30
30
  from utilities.asyncio import timeout_dur
31
- from utilities.datetime import is_subclass_date_not_datetime
32
31
  from utilities.functions import identity
33
32
  from utilities.iterables import (
34
33
  CheckDuplicatesError,
@@ -49,6 +48,7 @@ from utilities.sqlalchemy import (
49
48
  upsert_items,
50
49
  )
51
50
  from utilities.text import snake_case
51
+ from utilities.typing import is_subclass_gen
52
52
  from utilities.zoneinfo import UTC
53
53
 
54
54
  if TYPE_CHECKING:
@@ -201,14 +201,14 @@ def _insert_dataframe_check_df_and_db_types(
201
201
  dtype: PolarsDataType, db_col_type: type, /
202
202
  ) -> bool:
203
203
  return (
204
- ((dtype == pl.Boolean) and issubclass(db_col_type, bool))
205
- or ((dtype == Date) and is_subclass_date_not_datetime(db_col_type))
206
- or ((dtype == Datetime) and issubclass(db_col_type, dt.datetime))
204
+ ((dtype == pl.Boolean) and is_subclass_gen(db_col_type, bool))
205
+ or ((dtype == Date) and is_subclass_gen(db_col_type, dt.date))
206
+ or ((dtype == Datetime) and is_subclass_gen(db_col_type, dt.datetime))
207
207
  or ((dtype == Float64) and issubclass(db_col_type, float))
208
- or ((dtype == Int32) and issubclass(db_col_type, int))
209
- or ((dtype == Int64) and issubclass(db_col_type, int))
210
- or ((dtype == UInt32) and issubclass(db_col_type, int))
211
- or ((dtype == UInt64) and issubclass(db_col_type, int))
208
+ or ((dtype == Int32) and is_subclass_gen(db_col_type, int))
209
+ or ((dtype == Int64) and is_subclass_gen(db_col_type, int))
210
+ or ((dtype == UInt32) and is_subclass_gen(db_col_type, int))
211
+ or ((dtype == UInt64) and is_subclass_gen(db_col_type, int))
212
212
  or ((dtype == String) and issubclass(db_col_type, str))
213
213
  )
214
214
 
@@ -414,15 +414,15 @@ def _select_to_dataframe_map_table_column_type_to_dtype(
414
414
  """Map a table column type to a polars type."""
415
415
  type_use = type_() if isinstance(type_, type) else type_
416
416
  py_type = type_use.python_type
417
- if issubclass(py_type, bool):
417
+ if is_subclass_gen(py_type, bool):
418
418
  return pl.Boolean
419
419
  if issubclass(py_type, bytes):
420
420
  return Binary
421
421
  if issubclass(py_type, decimal.Decimal):
422
422
  return pl.Decimal
423
- if issubclass(py_type, dt.date) and not issubclass(py_type, dt.datetime):
423
+ if is_subclass_gen(py_type, dt.date):
424
424
  return pl.Date
425
- if issubclass(py_type, dt.datetime):
425
+ if is_subclass_gen(py_type, dt.datetime):
426
426
  has_tz: bool = type_use.timezone
427
427
  return zoned_datetime(time_zone=time_zone) if has_tz else Datetime()
428
428
  if issubclass(py_type, dt.time):
@@ -431,7 +431,7 @@ def _select_to_dataframe_map_table_column_type_to_dtype(
431
431
  return pl.Duration
432
432
  if issubclass(py_type, float):
433
433
  return Float64
434
- if issubclass(py_type, int):
434
+ if is_subclass_gen(py_type, int):
435
435
  return Int64
436
436
  if issubclass(py_type, UUID | str):
437
437
  return String
utilities/typing.py CHANGED
@@ -14,8 +14,10 @@ from typing import (
14
14
  Self,
15
15
  TypeAliasType,
16
16
  TypeGuard,
17
+ TypeVar,
17
18
  Union, # pyright: ignore[reportDeprecated]
18
19
  get_origin,
20
+ overload,
19
21
  override,
20
22
  )
21
23
  from typing import get_args as _get_args
@@ -27,6 +29,16 @@ from utilities.iterables import unique_everseen
27
29
  from utilities.sentinel import Sentinel
28
30
  from utilities.types import StrMapping
29
31
 
32
+ _T = TypeVar("_T")
33
+ _T1 = TypeVar("_T1")
34
+ _T2 = TypeVar("_T2")
35
+ _T3 = TypeVar("_T3")
36
+ _T4 = TypeVar("_T4")
37
+ _T5 = TypeVar("_T5")
38
+
39
+
40
+ ##
41
+
30
42
 
31
43
  def contains_self(obj: Any, /) -> bool:
32
44
  """Check if an annotation contains `Self`."""
@@ -65,6 +77,52 @@ def _get_literal_elements_inner(obj: Any, /) -> list[Any]:
65
77
  ##
66
78
 
67
79
 
80
+ def get_type_classes(obj: Any, /) -> tuple[type[Any], ...]:
81
+ """Get the type classes from a type/tuple/Union type."""
82
+ types: Sequence[type[Any]] = []
83
+ if isinstance(obj, type):
84
+ types.append(obj)
85
+ elif isinstance(obj, tuple):
86
+ for arg in obj:
87
+ if isinstance(arg, type):
88
+ types.append(arg)
89
+ elif isinstance(arg, tuple):
90
+ types.extend(get_type_classes(arg))
91
+ elif is_union_type(arg):
92
+ types.extend(get_union_type_classes(arg))
93
+ else:
94
+ raise _GetTypeClassesTupleError(obj=obj, inner=arg)
95
+ elif is_union_type(obj):
96
+ types.extend(get_union_type_classes(obj))
97
+ else:
98
+ raise _GetTypeClassesTypeError(obj=obj)
99
+ return tuple(types)
100
+
101
+
102
+ @dataclass(kw_only=True, slots=True)
103
+ class GetTypeClassesError(Exception):
104
+ obj: Any
105
+
106
+
107
+ @dataclass(kw_only=True, slots=True)
108
+ class _GetTypeClassesTypeError(GetTypeClassesError):
109
+ @override
110
+ def __str__(self) -> str:
111
+ return f"Object must be a type, tuple or Union type; got {self.obj}"
112
+
113
+
114
+ @dataclass(kw_only=True, slots=True)
115
+ class _GetTypeClassesTupleError(GetTypeClassesError):
116
+ inner: Any
117
+
118
+ @override
119
+ def __str__(self) -> str:
120
+ return f"Tuple must contain types, tuples or Union types only; got {self.inner}"
121
+
122
+
123
+ ##
124
+
125
+
68
126
  def get_type_hints(
69
127
  obj: Any,
70
128
  /,
@@ -96,8 +154,9 @@ def get_type_hints(
96
154
 
97
155
 
98
156
  def get_union_type_classes(obj: Any, /) -> tuple[type[Any], ...]:
157
+ """Get the type classes from a Union type."""
99
158
  if not is_union_type(obj):
100
- raise _GetUnionTypeClassesNotAUnionTypeError(obj=obj)
159
+ raise _GetUnionTypeClassesUnionTypeError(obj=obj)
101
160
  types_: Sequence[type[Any]] = []
102
161
  for arg in get_args(obj):
103
162
  if isinstance(arg, type):
@@ -105,7 +164,7 @@ def get_union_type_classes(obj: Any, /) -> tuple[type[Any], ...]:
105
164
  elif is_union_type(arg):
106
165
  types_.extend(get_union_type_classes(arg))
107
166
  else:
108
- raise _GetUnionTypeClassesNotATypeError(obj=obj, inner=arg)
167
+ raise _GetUnionTypeClassesInternalTypeError(obj=obj, inner=arg)
109
168
  return tuple(types_)
110
169
 
111
170
 
@@ -115,14 +174,14 @@ class GetUnionTypeClassesError(Exception):
115
174
 
116
175
 
117
176
  @dataclass(kw_only=True, slots=True)
118
- class _GetUnionTypeClassesNotAUnionTypeError(GetUnionTypeClassesError):
177
+ class _GetUnionTypeClassesUnionTypeError(GetUnionTypeClassesError):
119
178
  @override
120
179
  def __str__(self) -> str:
121
180
  return f"Object must be a Union type; got {self.obj}"
122
181
 
123
182
 
124
183
  @dataclass(kw_only=True, slots=True)
125
- class _GetUnionTypeClassesNotATypeError(GetUnionTypeClassesError):
184
+ class _GetUnionTypeClassesInternalTypeError(GetUnionTypeClassesError):
126
185
  inner: Any
127
186
 
128
187
  @override
@@ -149,6 +208,50 @@ def is_frozenset_type(obj: Any, /) -> bool:
149
208
  ##
150
209
 
151
210
 
211
+ @overload
212
+ def is_instance_gen(obj: Any, type_: type[_T], /) -> TypeGuard[_T]: ...
213
+ @overload
214
+ def is_instance_gen(obj: Any, type_: tuple[_T1], /) -> TypeGuard[_T1]: ...
215
+ @overload
216
+ def is_instance_gen(obj: Any, type_: tuple[_T1, _T2], /) -> TypeGuard[_T1 | _T2]: ...
217
+ @overload
218
+ def is_instance_gen(
219
+ obj: Any, type_: tuple[_T1, _T2, _T3], /
220
+ ) -> TypeGuard[_T1 | _T2 | _T3]: ...
221
+ @overload
222
+ def is_instance_gen(
223
+ obj: Any, type_: tuple[_T1, _T2, _T3, _T4], /
224
+ ) -> TypeGuard[_T1 | _T2 | _T3 | _T4]: ...
225
+ @overload
226
+ def is_instance_gen(
227
+ obj: Any, type_: tuple[_T1, _T2, _T3, _T4, _T5], /
228
+ ) -> TypeGuard[_T1 | _T2 | _T3 | _T4 | _T5]: ...
229
+ @overload
230
+ def is_instance_gen(obj: Any, type_: Any, /) -> bool: ...
231
+ def is_instance_gen(obj: Any, type_: Any, /) -> bool:
232
+ """Check if an instance relationship holds, except bool<int."""
233
+ return any(_is_instance_gen_one(obj, t) for t in get_type_classes(type_))
234
+
235
+
236
+ def _is_instance_gen_one(obj: Any, type_: type[_T], /) -> TypeGuard[_T]:
237
+ return (
238
+ isinstance(obj, type_)
239
+ and not (
240
+ isinstance(obj, bool)
241
+ and issubclass(type_, int)
242
+ and not issubclass(type_, bool)
243
+ )
244
+ and not (
245
+ isinstance(obj, dt.datetime)
246
+ and issubclass(type_, dt.date)
247
+ and not issubclass(type_, dt.datetime)
248
+ )
249
+ )
250
+
251
+
252
+ ##
253
+
254
+
152
255
  def is_list_type(obj: Any, /) -> bool:
153
256
  """Check if an object is a list type annotation."""
154
257
  return _is_annotation_of_type(obj, list)
@@ -222,6 +325,56 @@ def is_set_type(obj: Any, /) -> bool:
222
325
  ##
223
326
 
224
327
 
328
+ @overload
329
+ def is_subclass_gen(cls: type[Any], parent: type[_T], /) -> TypeGuard[type[_T]]: ...
330
+ @overload
331
+ def is_subclass_gen(
332
+ cls: type[Any], parent: tuple[type[_T1]], /
333
+ ) -> TypeGuard[type[_T1]]: ...
334
+ @overload
335
+ def is_subclass_gen(
336
+ cls: type[Any], parent: tuple[type[_T1], type[_T2]], /
337
+ ) -> TypeGuard[type[_T1 | _T2]]: ...
338
+ @overload
339
+ def is_subclass_gen(
340
+ cls: type[Any], parent: tuple[type[_T1], type[_T2], type[_T3]], /
341
+ ) -> TypeGuard[type[_T1 | _T2 | _T3]]: ...
342
+ @overload
343
+ def is_subclass_gen(
344
+ cls: type[Any], parent: tuple[type[_T1], type[_T2], type[_T3], type[_T4]], /
345
+ ) -> TypeGuard[type[_T1 | _T2 | _T3 | _T4]]: ...
346
+ @overload
347
+ def is_subclass_gen(
348
+ cls: type[Any],
349
+ parent: tuple[type[_T1], type[_T2], type[_T3], type[_T4], type[_T5]],
350
+ /,
351
+ ) -> TypeGuard[type[_T1 | _T2 | _T3 | _T4 | _T5]]: ...
352
+ @overload
353
+ def is_subclass_gen(cls: type[Any], parent: Any, /) -> bool: ...
354
+ def is_subclass_gen(cls: type[Any], parent: Any, /) -> bool:
355
+ """Generalized `issubclass`."""
356
+ return any(_is_subclass_gen_one(cls, p) for p in get_type_classes(parent))
357
+
358
+
359
+ def _is_subclass_gen_one(cls: type[Any], parent: type[_T], /) -> TypeGuard[type[_T]]:
360
+ return (
361
+ issubclass(cls, parent)
362
+ and not (
363
+ issubclass(cls, bool)
364
+ and issubclass(parent, int)
365
+ and not issubclass(parent, bool)
366
+ )
367
+ and not (
368
+ issubclass(cls, dt.datetime)
369
+ and issubclass(parent, dt.date)
370
+ and not issubclass(parent, dt.datetime)
371
+ )
372
+ )
373
+
374
+
375
+ ##
376
+
377
+
225
378
  def is_tuple_type(obj: Any, /) -> bool:
226
379
  """Check if an object is a tuple type annotation."""
227
380
  return _is_annotation_of_type(obj, tuple)
@@ -247,13 +400,16 @@ def _is_annotation_of_type(obj: Any, origin: Any, /) -> bool:
247
400
 
248
401
 
249
402
  __all__ = [
403
+ "GetTypeClassesError",
250
404
  "GetUnionTypeClassesError",
251
405
  "contains_self",
252
406
  "get_literal_elements",
407
+ "get_type_classes",
253
408
  "get_type_hints",
254
409
  "get_union_type_classes",
255
410
  "is_dict_type",
256
411
  "is_frozenset_type",
412
+ "is_instance_gen",
257
413
  "is_list_type",
258
414
  "is_literal_type",
259
415
  "is_mapping_type",
@@ -262,6 +418,7 @@ __all__ = [
262
418
  "is_optional_type",
263
419
  "is_sequence_type",
264
420
  "is_set_type",
421
+ "is_subclass_gen",
265
422
  "is_tuple_type",
266
423
  "is_union_type",
267
424
  ]