dycw-utilities 0.110.7__py3-none-any.whl → 0.111.0__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.7
3
+ Version: 0.111.0
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=RH75kOPQZPuI3PzW-iy4R8ZfTSbKQHPjZwc14TPFLN8,60
1
+ utilities/__init__.py,sha256=fjbCo5KvSkqKWLmPeMS8U77JZlYaYPd9js1ZVmdD1FE,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=-sDMqA4nB4CFctc86uIlSoKmIUisTUd3qyBcuUnf1D0,18957
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=gLg4EbE1FX52fJ1d3ji4i08qolwu9qgWt8w_w_Y5DTk,5512
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.7.dist-info/METADATA,sha256=wFw9aZuOwiyPR1o4QtTmz5fPx9y5gBbKheuvV0XKhA8,13004
91
- dycw_utilities-0.110.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.110.7.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.110.7.dist-info/RECORD,,
90
+ dycw_utilities-0.111.0.dist-info/METADATA,sha256=9UDVxMivd6ye3O57NEI2wEddqVpW9OE_rc_uhtMXD-c,13004
91
+ dycw_utilities-0.111.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.111.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.111.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.110.7"
3
+ __version__ = "0.111.0"
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
  )
@@ -169,12 +166,12 @@ def _parse_object_type(
169
166
  raise _ParseObjectParseError(type_=cls, text=text) from None
170
167
  if issubclass(cls, str):
171
168
  return text
172
- if issubclass(cls, bool):
169
+ if is_subclass_gen(cls, bool):
173
170
  try:
174
171
  return parse_bool(text)
175
172
  except ParseBoolError:
176
173
  raise _ParseObjectParseError(type_=cls, text=text) from None
177
- if is_subclass_not_bool_int(cls, int):
174
+ if is_subclass_gen(cls, int):
178
175
  try:
179
176
  return int(text)
180
177
  except ValueError:
@@ -201,14 +198,14 @@ def _parse_object_type(
201
198
  return parse_version(text)
202
199
  except ParseVersionError:
203
200
  raise _ParseObjectParseError(type_=cls, text=text) from None
204
- if is_subclass_date_not_datetime(cls):
201
+ if is_subclass_gen(cls, dt.date):
205
202
  from utilities.whenever import ParseDateError, parse_date
206
203
 
207
204
  try:
208
205
  return parse_date(text)
209
206
  except ParseDateError:
210
207
  raise _ParseObjectParseError(type_=cls, text=text) from None
211
- if issubclass(cls, dt.datetime):
208
+ if is_subclass_gen(cls, dt.datetime):
212
209
  from utilities.whenever import ParseDateTimeError, parse_datetime
213
210
 
214
211
  try:
@@ -473,11 +470,11 @@ def serialize_object(
473
470
  obj, bool | int | float | str | Path | Sentinel | Version
474
471
  ):
475
472
  return str(obj)
476
- if is_instance_date_not_datetime(obj):
473
+ if is_instance_gen(obj, dt.date):
477
474
  from utilities.whenever import serialize_date
478
475
 
479
476
  return serialize_date(obj)
480
- if isinstance(obj, dt.datetime):
477
+ if is_instance_gen(obj, dt.datetime):
481
478
  from utilities.whenever import serialize_datetime
482
479
 
483
480
  return serialize_datetime(obj)
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
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import datetime as dt
4
4
  from collections.abc import Mapping, Sequence
5
+ from dataclasses import dataclass
5
6
  from itertools import chain
6
7
  from pathlib import Path
7
8
  from types import NoneType, UnionType
@@ -13,8 +14,11 @@ from typing import (
13
14
  Self,
14
15
  TypeAliasType,
15
16
  TypeGuard,
17
+ TypeVar,
16
18
  Union, # pyright: ignore[reportDeprecated]
17
19
  get_origin,
20
+ overload,
21
+ override,
18
22
  )
19
23
  from typing import get_args as _get_args
20
24
  from typing import get_type_hints as _get_type_hints
@@ -25,6 +29,16 @@ from utilities.iterables import unique_everseen
25
29
  from utilities.sentinel import Sentinel
26
30
  from utilities.types import StrMapping
27
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
+
28
42
 
29
43
  def contains_self(obj: Any, /) -> bool:
30
44
  """Check if an annotation contains `Self`."""
@@ -63,8 +77,54 @@ def _get_literal_elements_inner(obj: Any, /) -> list[Any]:
63
77
  ##
64
78
 
65
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
+
66
126
  def get_type_hints(
67
- cls: Any,
127
+ obj: Any,
68
128
  /,
69
129
  *,
70
130
  globalns: StrMapping | None = None,
@@ -72,15 +132,15 @@ def get_type_hints(
72
132
  warn_name_errors: bool = False,
73
133
  ) -> dict[str, Any]:
74
134
  """Get the type hints of an object."""
75
- result: dict[str, Any] = cls.__annotations__
135
+ result: dict[str, Any] = obj.__annotations__
76
136
  _ = {Literal, Path, Sentinel, StrMapping, UUID, dt}
77
137
  globalns_use = globals() | ({} if globalns is None else dict(globalns))
78
138
  localns_use = {} if localns is None else dict(localns)
79
139
  try:
80
- hints = _get_type_hints(cls, globalns=globalns_use, localns=localns_use)
140
+ hints = _get_type_hints(obj, globalns=globalns_use, localns=localns_use)
81
141
  except NameError as error:
82
142
  if warn_name_errors:
83
- warn(f"Error getting type hints for {cls!r}; {error}", stacklevel=2)
143
+ warn(f"Error getting type hints for {obj!r}; {error}", stacklevel=2)
84
144
  else:
85
145
  result.update({
86
146
  key: value
@@ -93,6 +153,45 @@ def get_type_hints(
93
153
  ##
94
154
 
95
155
 
156
+ def get_union_type_classes(obj: Any, /) -> tuple[type[Any], ...]:
157
+ """Get the type classes from a Union type."""
158
+ if not is_union_type(obj):
159
+ raise _GetUnionTypeClassesUnionTypeError(obj=obj)
160
+ types_: Sequence[type[Any]] = []
161
+ for arg in get_args(obj):
162
+ if isinstance(arg, type):
163
+ types_.append(arg)
164
+ elif is_union_type(arg):
165
+ types_.extend(get_union_type_classes(arg))
166
+ else:
167
+ raise _GetUnionTypeClassesInternalTypeError(obj=obj, inner=arg)
168
+ return tuple(types_)
169
+
170
+
171
+ @dataclass(kw_only=True, slots=True)
172
+ class GetUnionTypeClassesError(Exception):
173
+ obj: Any
174
+
175
+
176
+ @dataclass(kw_only=True, slots=True)
177
+ class _GetUnionTypeClassesUnionTypeError(GetUnionTypeClassesError):
178
+ @override
179
+ def __str__(self) -> str:
180
+ return f"Object must be a Union type; got {self.obj}"
181
+
182
+
183
+ @dataclass(kw_only=True, slots=True)
184
+ class _GetUnionTypeClassesInternalTypeError(GetUnionTypeClassesError):
185
+ inner: Any
186
+
187
+ @override
188
+ def __str__(self) -> str:
189
+ return f"Union type must contain types only; got {self.inner}"
190
+
191
+
192
+ ##
193
+
194
+
96
195
  def is_dict_type(obj: Any, /) -> bool:
97
196
  """Check if an object is a dict type annotation."""
98
197
  return _is_annotation_of_type(obj, dict)
@@ -109,6 +208,50 @@ def is_frozenset_type(obj: Any, /) -> bool:
109
208
  ##
110
209
 
111
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
+
112
255
  def is_list_type(obj: Any, /) -> bool:
113
256
  """Check if an object is a list type annotation."""
114
257
  return _is_annotation_of_type(obj, list)
@@ -182,6 +325,56 @@ def is_set_type(obj: Any, /) -> bool:
182
325
  ##
183
326
 
184
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
+
185
378
  def is_tuple_type(obj: Any, /) -> bool:
186
379
  """Check if an object is a tuple type annotation."""
187
380
  return _is_annotation_of_type(obj, tuple)
@@ -207,11 +400,16 @@ def _is_annotation_of_type(obj: Any, origin: Any, /) -> bool:
207
400
 
208
401
 
209
402
  __all__ = [
403
+ "GetTypeClassesError",
404
+ "GetUnionTypeClassesError",
210
405
  "contains_self",
211
406
  "get_literal_elements",
407
+ "get_type_classes",
212
408
  "get_type_hints",
409
+ "get_union_type_classes",
213
410
  "is_dict_type",
214
411
  "is_frozenset_type",
412
+ "is_instance_gen",
215
413
  "is_list_type",
216
414
  "is_literal_type",
217
415
  "is_mapping_type",
@@ -220,6 +418,7 @@ __all__ = [
220
418
  "is_optional_type",
221
419
  "is_sequence_type",
222
420
  "is_set_type",
421
+ "is_subclass_gen",
223
422
  "is_tuple_type",
224
423
  "is_union_type",
225
424
  ]