dycw-utilities 0.131.12__py3-none-any.whl → 0.131.13__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.131.12
3
+ Version: 0.131.13
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,10 +1,10 @@
1
- utilities/__init__.py,sha256=Mewpkpu3DcrrhZzBvzBQpI4GkBsPPKRq0bdDX_sR7t4,61
1
+ utilities/__init__.py,sha256=3uDzFx7UMHsyNY8dbs9U0P66UUimaERvIAvdIFvrju8,61
2
2
  utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
3
3
  utilities/altair.py,sha256=HeZBVUocjkrTNwwKrClppsIqgNFF-ykv05HfZSoHYno,9104
4
4
  utilities/asyncio.py,sha256=yfKvAIDCRrWdyQMVZMo4DJQx4nVrXoAcqwhNuF95Ryo,38186
5
5
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
6
- utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
7
- utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
6
+ utilities/atools.py,sha256=-bFGIrwYMFR7xl39j02DZMsO_u5x5_Ph7bRlBUFVYyw,1048
7
+ utilities/cachetools.py,sha256=uBtEv4hD-TuCPX_cQy1lOpLF-QqfwnYGSf0o4Soqydc,2826
8
8
  utilities/click.py,sha256=8gRYeyu9KQ3uim0UpC8VnFnOOKD3DyGwMJ7k0Qns1lM,14659
9
9
  utilities/concurrent.py,sha256=s2scTEd2AhXVTW4hpASU2qxV_DiVLALfms55cCQzCvM,2886
10
10
  utilities/contextlib.py,sha256=lpaLJBy3X0UGLWjM98jkQZZq8so4fRmoK-Bheq0uOW4,1027
@@ -24,7 +24,7 @@ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
24
24
  utilities/git.py,sha256=oi7-_l5e9haSANSCvQw25ufYGoNahuUPHAZ6114s3JQ,1191
25
25
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
26
26
  utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
27
- utilities/hypothesis.py,sha256=spJCB3bKcSMFKuSwi184xfTbom_HEBLB0_-AiPnSR-A,49822
27
+ utilities/hypothesis.py,sha256=3mZbEhs5yTcigV2N8PSBqe8pdg2JOYf6e5XOwgGAwjQ,45206
28
28
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
29
29
  utilities/inflect.py,sha256=DbqB5Q9FbRGJ1NbvEiZBirRMxCxgrz91zy5jCO9ZIs0,347
30
30
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
@@ -41,11 +41,11 @@ utilities/more_itertools.py,sha256=tBbjjKx8_Uv_TCjxhPwrGfAx_jRHtvLIZqXVWAsjzqA,8
41
41
  utilities/numpy.py,sha256=Xn23sA2ZbVNqwUYEgNJD3XBYH6IbCri_WkHSNhg3NkY,26122
42
42
  utilities/operator.py,sha256=0M2yZJ0PODH47ogFEnkGMBe_cfxwZR02T_92LZVZvHo,3715
43
43
  utilities/optuna.py,sha256=loyJGWTzljgdJaoLhP09PT8Jz6o_pwBOwehY33lHkhw,1923
44
- utilities/orjson.py,sha256=oD9sQYd3bQKZi2dwpQsaRVwCSlSQ2sREl83UZP4LD7w,36962
44
+ utilities/orjson.py,sha256=lhBSO-QCm2g-uhUE0hKe-PVhAt1TKRRx0iZLSFPmew4,36970
45
45
  utilities/os.py,sha256=D_FyyT-6TtqiN9KSS7c9g1fnUtgxmyMtzAjmYLkk46A,3587
46
46
  utilities/parse.py,sha256=vsZ2jf_ceSI_Kta9titixufysJaVXh0Whjz1T4awJZw,18938
47
47
  utilities/pathlib.py,sha256=PK41rf1c9Wqv7h8f5R7H3_Lhq_gQZTUJD5tu3gMHVaU,3247
48
- utilities/period.py,sha256=o4wXYEXVlFomop4-Ra4L0yRP4i99NZFjIe_fa7NdZck,11024
48
+ utilities/period.py,sha256=opqpBevBGSGXbA7NYfRJjtthi1JPxdMaZ7QV3xosnTc,4774
49
49
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
50
50
  utilities/platform.py,sha256=48IOKx1IC6ZJXWG-b56ZQptITcNFhWRjELW72o2dGTA,2398
51
51
  utilities/polars.py,sha256=QlmUpYTqHNkcLnWOQh1TW22W2QyLzvifCvBcbsqhpdE,63272
@@ -82,16 +82,16 @@ utilities/timer.py,sha256=VeSl3ot8-f4D1d3HjjSsgKvjxHJGXd_sW4KcTExOR64,2475
82
82
  utilities/traceback.py,sha256=cMXrCD59CROnezAU8VW67CxZ8Igc5QmaxlV8qrBvNMs,8504
83
83
  utilities/types.py,sha256=CHQke10ETEpypxppYVhWp1G68S6mvifalrRLolYBcCg,19506
84
84
  utilities/typing.py,sha256=VuGuztLSkTicxgVwI5wrVOTcY70OlzwsTU7LcFVjGlY,14169
85
- utilities/tzdata.py,sha256=yCf70NICwAeazN3_JcXhWvRqCy06XJNQ42j7r6gw3HY,1217
85
+ utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
86
86
  utilities/tzlocal.py,sha256=xbBBzVIUKMk8AkhuIp1qxGRNBioIa5I09dpeoBnIOOU,662
87
87
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
88
88
  utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
89
89
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
90
90
  utilities/whenever.py,sha256=2NQ-0SnLNW2kFpefP9dVE8H0RbaeusXYLPmv282Jpto,16755
91
- utilities/whenever2.py,sha256=WiDVsgHA-4E-KiIJ25-R4qAWvtyBQk1EkhjMszFzQMM,7455
91
+ utilities/whenever2.py,sha256=iFVL4CjuIOpzsDU6li5smHnDEqam30-FtTgXWeHuWiE,7510
92
92
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
93
- utilities/zoneinfo.py,sha256=tvcgu3QzDxe2suTexi2QzRGpin7VK1TjHa0JYYxT69I,1862
94
- dycw_utilities-0.131.12.dist-info/METADATA,sha256=8G8r4jcsCl-iXqwXZ2vq8Kx7M4S2A6xCNxQA3Blrv64,1585
95
- dycw_utilities-0.131.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
- dycw_utilities-0.131.12.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
- dycw_utilities-0.131.12.dist-info/RECORD,,
93
+ utilities/zoneinfo.py,sha256=gJPr9l7V8s3Y7TXpCGYEM1S81Rplb9e4MoV9Nvy2VU8,1852
94
+ dycw_utilities-0.131.13.dist-info/METADATA,sha256=BSLseH0v8xkeiTFmvQ2ITtMLy6gHtk6aFRAmuNQEy_k,1585
95
+ dycw_utilities-0.131.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
+ dycw_utilities-0.131.13.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
+ dycw_utilities-0.131.13.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.131.12"
3
+ __version__ = "0.131.13"
utilities/atools.py CHANGED
@@ -5,25 +5,22 @@ from typing import TYPE_CHECKING, ParamSpec, TypeVar
5
5
 
6
6
  from atools import memoize
7
7
 
8
- from utilities.datetime import datetime_duration_to_timedelta
9
8
  from utilities.types import Coroutine1
10
9
 
11
10
  if TYPE_CHECKING:
12
- import datetime as dt
13
-
14
- from utilities.types import Duration
11
+ from whenever import TimeDelta
15
12
 
16
13
 
17
14
  _P = ParamSpec("_P")
18
15
  _R = TypeVar("_R")
19
16
  _AsyncFunc = Callable[_P, Coroutine1[_R]]
20
- type _Key = tuple[_AsyncFunc, dt.timedelta]
17
+ type _Key = tuple[_AsyncFunc, TimeDelta]
21
18
  _MEMOIZED_FUNCS: dict[_Key, _AsyncFunc] = {}
22
19
 
23
20
 
24
21
  async def call_memoized(
25
22
  func: _AsyncFunc[_P, _R],
26
- refresh: Duration | None = None,
23
+ refresh: TimeDelta | None = None,
27
24
  /,
28
25
  *args: _P.args,
29
26
  **kwargs: _P.kwargs,
@@ -31,13 +28,14 @@ async def call_memoized(
31
28
  """Call an asynchronous function, with possible memoization."""
32
29
  if refresh is None:
33
30
  return await func(*args, **kwargs)
34
- timedelta = datetime_duration_to_timedelta(refresh)
35
- key: _Key = (func, timedelta)
31
+ key: _Key = (func, refresh)
36
32
  memoized_func: _AsyncFunc[_P, _R]
37
33
  try:
38
34
  memoized_func = _MEMOIZED_FUNCS[key]
39
35
  except KeyError:
40
- memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=refresh)(func)
36
+ memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=refresh.in_seconds())(
37
+ func
38
+ )
41
39
  return await memoized_func(*args, **kwargs)
42
40
 
43
41
 
utilities/cachetools.py CHANGED
@@ -8,10 +8,10 @@ from typing import TYPE_CHECKING, Any, TypeVar, override
8
8
  import cachetools
9
9
  from cachetools.func import ttl_cache
10
10
 
11
- from utilities.datetime import datetime_duration_to_float
12
-
13
11
  if TYPE_CHECKING:
14
- from utilities.types import Duration, TCallable
12
+ from whenever import TimeDelta
13
+
14
+ from utilities.types import TCallable
15
15
 
16
16
  _K = TypeVar("_K")
17
17
  _T = TypeVar("_T")
@@ -25,15 +25,13 @@ class TTLCache(cachetools.TTLCache[_K, _V]):
25
25
  self,
26
26
  *,
27
27
  max_size: int | None = None,
28
- max_duration: Duration | None = None,
28
+ max_duration: TimeDelta | None = None,
29
29
  timer: Callable[[], float] = monotonic,
30
30
  get_size_of: Callable[[Any], int] | None = None,
31
31
  ) -> None:
32
32
  super().__init__(
33
33
  maxsize=inf if max_size is None else max_size,
34
- ttl=inf
35
- if max_duration is None
36
- else datetime_duration_to_float(max_duration),
34
+ ttl=inf if max_duration is None else max_duration.in_seconds(),
37
35
  timer=timer,
38
36
  getsizeof=get_size_of,
39
37
  )
@@ -54,7 +52,7 @@ class TTLSet(MutableSet[_T]):
54
52
  /,
55
53
  *,
56
54
  max_size: int | None = None,
57
- max_duration: Duration | None = None,
55
+ max_duration: TimeDelta | None = None,
58
56
  timer: Callable[[], float] = monotonic,
59
57
  get_size_of: Callable[[Any], int] | None = None,
60
58
  ) -> None:
@@ -103,14 +101,14 @@ class TTLSet(MutableSet[_T]):
103
101
  def cache(
104
102
  *,
105
103
  max_size: int | None = None,
106
- max_duration: Duration | None = None,
104
+ max_duration: TimeDelta | None = None,
107
105
  timer: Callable[[], float] = monotonic,
108
106
  typed_: bool = False,
109
107
  ) -> Callable[[TCallable], TCallable]:
110
108
  """Decorate a function with `max_size` and/or `ttl` settings."""
111
109
  return ttl_cache(
112
110
  maxsize=inf if max_size is None else max_size,
113
- ttl=inf if max_duration is None else datetime_duration_to_float(max_duration),
111
+ ttl=inf if max_duration is None else max_duration.in_seconds(),
114
112
  timer=timer,
115
113
  typed=typed_,
116
114
  )
utilities/hypothesis.py CHANGED
@@ -795,139 +795,6 @@ def min_and_max_datetimes(
795
795
  ##
796
796
 
797
797
 
798
- @composite
799
- def min_and_maybe_max_datetimes(
800
- draw: DrawFn,
801
- /,
802
- *,
803
- min_value: MaybeSearchStrategy[dt.datetime | None] = None,
804
- max_value: MaybeSearchStrategy[dt.datetime | None | Sentinel] = sentinel,
805
- time_zone: MaybeSearchStrategy[ZoneInfo | timezone] = UTC,
806
- round_: MathRoundMode | None = None,
807
- timedelta: dt.timedelta | None = None,
808
- rel_tol: float | None = None,
809
- abs_tol: float | None = None,
810
- valid: bool = False,
811
- ) -> tuple[dt.datetime, dt.datetime | None]:
812
- match min_value, max_value:
813
- case None, Sentinel():
814
- min_value_, max_value_ = draw(
815
- pairs(
816
- zoned_datetimes(
817
- time_zone=time_zone,
818
- round_=round_,
819
- timedelta=timedelta,
820
- rel_tol=rel_tol,
821
- abs_tol=abs_tol,
822
- valid=valid,
823
- ),
824
- sorted=True,
825
- )
826
- )
827
- return min_value_, draw(just(max_value_) | none())
828
- case None, None:
829
- min_value_ = draw(
830
- zoned_datetimes(
831
- time_zone=time_zone,
832
- round_=round_,
833
- timedelta=timedelta,
834
- rel_tol=rel_tol,
835
- abs_tol=abs_tol,
836
- valid=valid,
837
- )
838
- )
839
- return min_value_, None
840
- case None, dt.datetime():
841
- min_value_ = draw(
842
- zoned_datetimes(
843
- max_value=max_value,
844
- time_zone=time_zone,
845
- round_=round_,
846
- timedelta=timedelta,
847
- rel_tol=rel_tol,
848
- abs_tol=abs_tol,
849
- valid=valid,
850
- )
851
- )
852
- return min_value_, max_value
853
- case dt.datetime(), Sentinel():
854
- max_value_ = draw(
855
- zoned_datetimes(
856
- min_value=min_value,
857
- time_zone=time_zone,
858
- round_=round_,
859
- timedelta=timedelta,
860
- rel_tol=rel_tol,
861
- abs_tol=abs_tol,
862
- valid=valid,
863
- )
864
- | none()
865
- )
866
- return min_value, max_value_
867
- case dt.datetime(), None:
868
- return min_value, None
869
- case dt.datetime(), dt.datetime():
870
- _ = assume(min_value <= max_value)
871
- return min_value, max_value
872
- case _, _:
873
- strategy = zoned_datetimes(
874
- time_zone=time_zone,
875
- round_=round_,
876
- timedelta=timedelta,
877
- rel_tol=rel_tol,
878
- abs_tol=abs_tol,
879
- valid=valid,
880
- )
881
- min_value_ = draw2(draw, min_value, strategy)
882
- max_value_ = draw2(draw, max_value, strategy | none(), sentinel=True)
883
- _ = assume((max_value_ is None) or (min_value_ <= max_value_))
884
- return min_value_, max_value_
885
- case _ as never:
886
- assert_never(never)
887
-
888
-
889
- ##
890
-
891
-
892
- @composite
893
- def min_and_maybe_max_sizes(
894
- draw: DrawFn,
895
- /,
896
- *,
897
- min_value: MaybeSearchStrategy[int | None] = None,
898
- max_value: MaybeSearchStrategy[int | None | Sentinel] = sentinel,
899
- ) -> tuple[int, int | None]:
900
- match min_value, max_value:
901
- case None, Sentinel():
902
- min_value_, max_value_ = draw(pairs(integers(min_value=0), sorted=True))
903
- return min_value_, draw(just(max_value_) | none())
904
- case None, None:
905
- min_value_ = draw(integers(min_value=0))
906
- return min_value_, None
907
- case None, int():
908
- min_value_ = draw(integers(0, max_value))
909
- return min_value_, max_value
910
- case int(), Sentinel():
911
- max_value_ = draw(integers(min_value=min_value) | none())
912
- return min_value, max_value_
913
- case int(), None:
914
- return min_value, None
915
- case int(), int():
916
- _ = assume(min_value <= max_value)
917
- return min_value, max_value
918
- case _, _:
919
- strategy = integers(min_value=0)
920
- min_value_ = draw2(draw, min_value, strategy)
921
- max_value_ = draw2(draw, max_value, strategy | none(), sentinel=True)
922
- _ = assume((max_value_ is None) or (min_value_ <= max_value_))
923
- return min_value_, max_value_
924
- case _ as never:
925
- assert_never(never)
926
-
927
-
928
- ##
929
-
930
-
931
798
  @composite
932
799
  def months(
933
800
  draw: DrawFn,
@@ -1695,9 +1562,6 @@ __all__ = [
1695
1562
  "int_arrays",
1696
1563
  "lists_fixed_length",
1697
1564
  "min_and_max_datetimes",
1698
- "min_and_maybe_max_datetimes",
1699
- "min_and_maybe_max_sizes",
1700
- "min_and_maybe_max_sizes",
1701
1565
  "months",
1702
1566
  "namespace_mixins",
1703
1567
  "numbers",
utilities/orjson.py CHANGED
@@ -760,7 +760,7 @@ class OrjsonFormatter(Formatter):
760
760
  path_name=Path(record.pathname),
761
761
  line_num=record.lineno,
762
762
  message=record.getMessage(),
763
- datetime=from_timestamp(record.created, time_zone="local"),
763
+ datetime=from_timestamp(record.created, time_zone=LOCAL_TIME_ZONE),
764
764
  func_name=record.funcName,
765
765
  extra=extra if len(extra) >= 1 else None,
766
766
  )
utilities/period.py CHANGED
@@ -1,42 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
- from dataclasses import dataclass, field
5
- from functools import cached_property
6
- from itertools import permutations
7
- from typing import (
8
- TYPE_CHECKING,
9
- Generic,
10
- Literal,
11
- Self,
12
- TypedDict,
13
- TypeVar,
14
- assert_never,
15
- cast,
16
- override,
17
- )
18
-
19
- from utilities.datetime import ZERO_TIME
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Generic, Self, TypedDict, TypeVar, override
5
+ from zoneinfo import ZoneInfo
6
+
7
+ from whenever import Date, DateDelta, TimeDelta, ZonedDateTime
8
+
9
+ from utilities.dataclasses import replace_non_sentinel
20
10
  from utilities.functions import get_class_name
21
- from utilities.iterables import OneUniqueNonUniqueError, always_iterable, one_unique
22
11
  from utilities.sentinel import Sentinel, sentinel
23
- from utilities.typing import is_instance_gen
24
- from utilities.whenever import (
25
- serialize_date,
26
- serialize_plain_datetime,
27
- serialize_zoned_datetime,
28
- )
29
- from utilities.zoneinfo import EnsureTimeZoneError, ensure_time_zone
12
+ from utilities.zoneinfo import get_time_zone_name
30
13
 
31
14
  if TYPE_CHECKING:
32
- from zoneinfo import ZoneInfo
33
-
34
- from utilities.iterables import MaybeIterable
35
- from utilities.types import DateOrDateTime
15
+ from utilities.types import TimeZoneLike
36
16
 
37
-
38
- type _DateOrDateTime = Literal["date", "datetime"]
39
- _TPeriod = TypeVar("_TPeriod", dt.date, dt.datetime)
17
+ _TPeriod = TypeVar("_TPeriod", Date, ZonedDateTime)
40
18
 
41
19
 
42
20
  class _PeriodAsDict(TypedDict, Generic[_TPeriod]):
@@ -44,281 +22,133 @@ class _PeriodAsDict(TypedDict, Generic[_TPeriod]):
44
22
  end: _TPeriod
45
23
 
46
24
 
47
- @dataclass(repr=False, order=True, unsafe_hash=True)
48
- class Period(Generic[_TPeriod]):
49
- """A period of time."""
25
+ @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
26
+ class DatePeriod:
27
+ """A period of dates."""
50
28
 
51
- start: _TPeriod
52
- end: _TPeriod
53
- req_duration: MaybeIterable[dt.timedelta] | None = field(
54
- default=None, repr=False, kw_only=True
55
- )
56
- min_duration: dt.timedelta | None = field(default=None, repr=False, kw_only=True)
57
- max_duration: dt.timedelta | None = field(default=None, repr=False, kw_only=True)
29
+ start: Date
30
+ end: Date
58
31
 
59
32
  def __post_init__(self) -> None:
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
- ):
65
- raise _PeriodDateAndDateTimeMixedError(start=self.start, end=self.end)
66
- for date in [self.start, self.end]:
67
- if isinstance(date, dt.datetime):
68
- try:
69
- _ = ensure_time_zone(date)
70
- except EnsureTimeZoneError:
71
- raise _PeriodNaiveDateTimeError(
72
- start=self.start, end=self.end
73
- ) from None
74
- duration = self.end - self.start
75
- if duration < ZERO_TIME:
33
+ if self.start > self.end:
76
34
  raise _PeriodInvalidError(start=self.start, end=self.end)
77
- if (self.req_duration is not None) and (
78
- duration not in always_iterable(self.req_duration)
79
- ):
80
- raise _PeriodReqDurationError(
81
- start=self.start,
82
- end=self.end,
83
- duration=duration,
84
- req_duration=self.req_duration,
85
- )
86
- if (self.min_duration is not None) and (duration < self.min_duration):
87
- raise _PeriodMinDurationError(
88
- start=self.start,
89
- end=self.end,
90
- duration=duration,
91
- min_duration=self.min_duration,
92
- )
93
- if (self.max_duration is not None) and (duration > self.max_duration):
94
- raise _PeriodMaxDurationError(
95
- start=self.start,
96
- end=self.end,
97
- duration=duration,
98
- max_duration=self.max_duration,
99
- )
100
35
 
101
- def __add__(self, other: dt.timedelta, /) -> Self:
36
+ def __add__(self, other: DateDelta, /) -> Self:
102
37
  """Offset the period."""
103
38
  return self.replace(start=self.start + other, end=self.end + other)
104
39
 
105
- def __contains__(self, other: DateOrDateTime, /) -> bool:
40
+ def __contains__(self, other: Date, /) -> bool:
106
41
  """Check if a date/datetime lies in the period."""
107
- match self.kind:
108
- case "date":
109
- if isinstance(other, dt.datetime):
110
- raise _PeriodDateContainsDateTimeError(
111
- start=self.start, end=self.end
112
- )
113
- case "datetime":
114
- if not isinstance(other, dt.datetime):
115
- raise _PeriodDateTimeContainsDateError(
116
- start=self.start, end=self.end
117
- )
118
- case _ as never:
119
- assert_never(never)
120
42
  return self.start <= other <= self.end
121
43
 
122
44
  @override
123
45
  def __repr__(self) -> str:
124
46
  cls = get_class_name(self)
125
- match self.kind:
126
- case "date":
127
- result = cast("Period[dt.date]", self)
128
- start, end = map(serialize_date, [result.start, result.end])
129
- return f"{cls}({start}, {end})"
130
- case "datetime":
131
- result = cast("Period[dt.datetime]", self)
132
- try:
133
- time_zone = result.time_zone
134
- except _PeriodTimeZoneNonUniqueError:
135
- start, end = map(
136
- serialize_zoned_datetime, [result.start, result.end]
137
- )
138
- return f"{cls}({start}, {end})"
139
- start, end = (
140
- serialize_plain_datetime(t.replace(tzinfo=None))
141
- for t in [result.start, result.end]
142
- )
143
- return f"{cls}({start}, {end}, {time_zone})"
144
- case _ as never:
145
- assert_never(never)
146
-
147
- def __sub__(self, other: dt.timedelta, /) -> Self:
47
+ return f"{cls}({self.start}, {self.end})"
48
+
49
+ def __sub__(self, other: DateDelta, /) -> Self:
148
50
  """Offset the period."""
149
51
  return self.replace(start=self.start - other, end=self.end - other)
150
52
 
151
- def astimezone(self, time_zone: ZoneInfo, /) -> Self:
152
- """Convert the timezone of the period, if it is a datetime period."""
153
- match self.kind:
154
- case "date":
155
- raise _PeriodAsTimeZoneInapplicableError(start=self.start, end=self.end)
156
- case "datetime":
157
- result = cast("Period[dt.datetime]", self)
158
- result = result.replace(
159
- start=result.start.astimezone(time_zone),
160
- end=result.end.astimezone(time_zone),
161
- )
162
- return cast("Self", result)
163
- case _ as never:
164
- assert_never(never)
165
-
166
- @cached_property
167
- def duration(self) -> dt.timedelta:
168
- """The duration of the period."""
53
+ @property
54
+ def delta(self) -> DateDelta:
55
+ """The delta of the period."""
169
56
  return self.end - self.start
170
57
 
171
- @cached_property
172
- def kind(self) -> _DateOrDateTime:
173
- """The kind of the period."""
174
- return "date" if is_instance_gen(self.start, dt.date) else "datetime"
175
-
176
58
  def replace(
177
- self,
178
- *,
179
- start: _TPeriod | None = None,
180
- end: _TPeriod | None = None,
181
- req_duration: MaybeIterable[dt.timedelta] | None | Sentinel = sentinel,
182
- min_duration: dt.timedelta | None | Sentinel = sentinel,
183
- max_duration: dt.timedelta | None | Sentinel = sentinel,
59
+ self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
184
60
  ) -> Self:
185
61
  """Replace elements of the period."""
186
- return type(self)(
187
- self.start if start is None else start,
188
- self.end if end is None else end,
189
- req_duration=self.req_duration
190
- if isinstance(req_duration, Sentinel)
191
- else req_duration,
192
- min_duration=self.min_duration
193
- if isinstance(min_duration, Sentinel)
194
- else min_duration,
195
- max_duration=self.max_duration
196
- if isinstance(max_duration, Sentinel)
197
- else max_duration,
198
- )
199
-
200
- @cached_property
201
- def time_zone(self) -> ZoneInfo:
202
- """The time zone of the period."""
203
- match self.kind:
204
- case "date":
205
- raise _PeriodTimeZoneInapplicableError(
206
- start=self.start, end=self.end
207
- ) from None
208
- case "datetime":
209
- result = cast("Period[dt.datetime]", self)
210
- try:
211
- return one_unique(map(ensure_time_zone, [result.start, result.end]))
212
- except OneUniqueNonUniqueError as error:
213
- raise _PeriodTimeZoneNonUniqueError(
214
- start=self.start,
215
- end=self.end,
216
- first=error.first,
217
- second=error.second,
218
- ) from None
219
- case _ as never:
220
- assert_never(never)
221
-
222
- def to_dict(self) -> _PeriodAsDict:
223
- """Convert the period to a dictionary."""
224
- return {"start": self.start, "end": self.end}
225
-
226
-
227
- @dataclass(kw_only=True, slots=True)
228
- class PeriodError(Generic[_TPeriod], Exception):
229
- start: _TPeriod
230
- end: _TPeriod
62
+ return replace_non_sentinel(self, start=start, end=end)
231
63
 
232
-
233
- @dataclass(kw_only=True, slots=True)
234
- class _PeriodDateAndDateTimeMixedError(PeriodError[_TPeriod]):
235
- @override
236
- def __str__(self) -> str:
237
- return f"Invalid period; got date and datetime mix ({self.start}, {self.end})"
64
+ def to_dict(self) -> _PeriodAsDict[Date]:
65
+ """Convert the period to a dictionary."""
66
+ return _PeriodAsDict(start=self.start, end=self.end)
238
67
 
239
68
 
240
- @dataclass(kw_only=True, slots=True)
241
- class _PeriodNaiveDateTimeError(PeriodError[_TPeriod]):
242
- @override
243
- def __str__(self) -> str:
244
- return f"Invalid period; got naive datetime(s) ({self.start}, {self.end})"
69
+ @dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
70
+ class ZonedDateTimePeriod:
71
+ """A period of time."""
245
72
 
73
+ start: ZonedDateTime
74
+ end: ZonedDateTime
246
75
 
247
- @dataclass(kw_only=True, slots=True)
248
- class _PeriodInvalidError(PeriodError[_TPeriod]):
249
- @override
250
- def __str__(self) -> str:
251
- return f"Invalid period; got {self.start} > {self.end}"
76
+ def __post_init__(self) -> None:
77
+ if self.start > self.end:
78
+ raise _PeriodInvalidError(start=self.start, end=self.end)
79
+ if self.start.tz != self.end.tz:
80
+ raise _PeriodTimeZoneError(
81
+ start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
82
+ )
252
83
 
84
+ def __add__(self, other: TimeDelta, /) -> Self:
85
+ """Offset the period."""
86
+ return self.replace(start=self.start + other, end=self.end + other)
253
87
 
254
- @dataclass(kw_only=True, slots=True)
255
- class _PeriodReqDurationError(PeriodError[_TPeriod]):
256
- duration: dt.timedelta
257
- req_duration: MaybeIterable[dt.timedelta]
88
+ def __contains__(self, other: ZonedDateTime, /) -> bool:
89
+ """Check if a date/datetime lies in the period."""
90
+ return self.start <= other <= self.end
258
91
 
259
92
  @override
260
- def __str__(self) -> str:
261
- return f"Period must have duration {self.req_duration}; got {self.duration})"
262
-
263
-
264
- @dataclass(kw_only=True, slots=True)
265
- class _PeriodMinDurationError(PeriodError[_TPeriod]):
266
- duration: dt.timedelta
267
- min_duration: dt.timedelta
93
+ def __repr__(self) -> str:
94
+ cls = get_class_name(self)
95
+ return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
268
96
 
269
- @override
270
- def __str__(self) -> str:
271
- return (
272
- f"Period must have min duration {self.min_duration}; got {self.duration})"
273
- )
97
+ def __sub__(self, other: TimeDelta, /) -> Self:
98
+ """Offset the period."""
99
+ return self.replace(start=self.start - other, end=self.end - other)
274
100
 
101
+ @property
102
+ def delta(self) -> TimeDelta:
103
+ """The duration of the period."""
104
+ return self.end - self.start
275
105
 
276
- @dataclass(kw_only=True, slots=True)
277
- class _PeriodMaxDurationError(PeriodError[_TPeriod]):
278
- duration: dt.timedelta
279
- max_duration: dt.timedelta
106
+ def replace(
107
+ self,
108
+ *,
109
+ start: ZonedDateTime | Sentinel = sentinel,
110
+ end: ZonedDateTime | Sentinel = sentinel,
111
+ ) -> Self:
112
+ """Replace elements of the period."""
113
+ return replace_non_sentinel(self, start=start, end=end)
280
114
 
281
- @override
282
- def __str__(self) -> str:
283
- return f"Period must have duration at most {self.max_duration}; got {self.duration})"
115
+ @property
116
+ def time_zone(self) -> ZoneInfo:
117
+ """The time zone of the period."""
118
+ return ZoneInfo(self.start.tz)
284
119
 
120
+ def to_dict(self) -> _PeriodAsDict[ZonedDateTime]:
121
+ """Convert the period to a dictionary."""
122
+ return _PeriodAsDict(start=self.start, end=self.end)
285
123
 
286
- @dataclass(kw_only=True, slots=True)
287
- class _PeriodAsTimeZoneInapplicableError(PeriodError[_TPeriod]):
288
- @override
289
- def __str__(self) -> str:
290
- return "Period of dates does not have a timezone attribute"
124
+ def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
125
+ """Convert the time zone."""
126
+ tz = get_time_zone_name(time_zone)
127
+ return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
291
128
 
292
129
 
293
130
  @dataclass(kw_only=True, slots=True)
294
- class _PeriodDateContainsDateTimeError(PeriodError[_TPeriod]):
295
- @override
296
- def __str__(self) -> str:
297
- return "Period of dates cannot contain datetimes"
131
+ class PeriodError(Exception): ...
298
132
 
299
133
 
300
134
  @dataclass(kw_only=True, slots=True)
301
- class _PeriodDateTimeContainsDateError(PeriodError[_TPeriod]):
302
- @override
303
- def __str__(self) -> str:
304
- return "Period of datetimes cannot contain dates"
305
-
135
+ class _PeriodInvalidError(PeriodError, Generic[_TPeriod]):
136
+ start: _TPeriod
137
+ end: _TPeriod
306
138
 
307
- @dataclass(kw_only=True, slots=True)
308
- class _PeriodTimeZoneInapplicableError(PeriodError[_TPeriod]):
309
139
  @override
310
140
  def __str__(self) -> str:
311
- return "Period of dates does not have a timezone attribute"
141
+ return f"Invalid period; got {self.start} > {self.end}"
312
142
 
313
143
 
314
144
  @dataclass(kw_only=True, slots=True)
315
- class _PeriodTimeZoneNonUniqueError(PeriodError[_TPeriod]):
316
- first: ZoneInfo
317
- second: ZoneInfo
145
+ class _PeriodTimeZoneError(PeriodError):
146
+ start: ZoneInfo
147
+ end: ZoneInfo
318
148
 
319
149
  @override
320
150
  def __str__(self) -> str:
321
- return f"Period must contain exactly one time zone; got {self.first} and {self.second}"
151
+ return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
322
152
 
323
153
 
324
- __all__ = ["Period", "PeriodError"]
154
+ __all__ = ["DatePeriod", "PeriodError", "ZonedDateTimePeriod"]
utilities/tzdata.py CHANGED
@@ -1,63 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
4
3
  from zoneinfo import ZoneInfo
5
4
 
6
- from utilities.datetime import get_now, get_today
7
-
8
- if TYPE_CHECKING:
9
- import datetime as dt
10
-
11
-
12
5
  HongKong = ZoneInfo("Asia/Hong_Kong")
13
6
  Tokyo = ZoneInfo("Asia/Tokyo")
14
7
  USCentral = ZoneInfo("US/Central")
15
8
  USEastern = ZoneInfo("US/Eastern")
16
9
 
17
10
 
18
- def get_now_hong_kong() -> dt.datetime:
19
- """Get the current time in Hong Kong."""
20
- return get_now(time_zone=HongKong)
21
-
22
-
23
- NOW_HONG_KONG = get_now_hong_kong()
24
-
25
-
26
- def get_now_tokyo() -> dt.datetime:
27
- """Get the current time in Tokyo."""
28
- return get_now(time_zone=Tokyo)
29
-
30
-
31
- NOW_TOKYO = get_now_tokyo()
32
-
33
-
34
- def get_today_hong_kong() -> dt.date:
35
- """Get the current date in Hong Kong."""
36
- return get_today(time_zone=HongKong)
37
-
38
-
39
- TODAY_HONG_KONG = get_today_hong_kong()
40
-
41
-
42
- def get_today_tokyo() -> dt.date:
43
- """Get the current date in Tokyo."""
44
- return get_today(time_zone=Tokyo)
45
-
46
-
47
- TODAY_TOKYO = get_today_tokyo()
48
-
49
-
50
- __all__ = [
51
- "NOW_HONG_KONG",
52
- "NOW_TOKYO",
53
- "TODAY_HONG_KONG",
54
- "TODAY_TOKYO",
55
- "HongKong",
56
- "Tokyo",
57
- "USCentral",
58
- "USEastern",
59
- "get_now_hong_kong",
60
- "get_now_tokyo",
61
- "get_today_hong_kong",
62
- "get_today_tokyo",
63
- ]
11
+ __all__ = ["HongKong", "Tokyo", "USCentral", "USEastern"]
utilities/whenever2.py CHANGED
@@ -18,7 +18,7 @@ from whenever import (
18
18
 
19
19
  from utilities.datetime import maybe_sub_pct_y
20
20
  from utilities.sentinel import Sentinel, sentinel
21
- from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
21
+ from utilities.tzlocal import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME
22
22
  from utilities.zoneinfo import UTC, get_time_zone_name
23
23
 
24
24
  if TYPE_CHECKING:
@@ -111,7 +111,7 @@ NOW_UTC = get_now(time_zone=UTC)
111
111
 
112
112
  def get_now_local() -> ZonedDateTime:
113
113
  """Get the current local time."""
114
- return get_now(time_zone="local")
114
+ return get_now(time_zone=LOCAL_TIME_ZONE)
115
115
 
116
116
 
117
117
  NOW_LOCAL = get_now_local()
@@ -130,7 +130,7 @@ TODAY_UTC = get_today(time_zone=UTC)
130
130
 
131
131
  def get_today_local() -> Date:
132
132
  """Get the current, timezone-aware local date."""
133
- return get_today(time_zone="local")
133
+ return get_today(time_zone=LOCAL_TIME_ZONE)
134
134
 
135
135
 
136
136
  TODAY_LOCAL = get_today_local()
@@ -267,6 +267,7 @@ __all__ = [
267
267
  "ZONED_DATE_TIME_MIN",
268
268
  "WheneverLogRecord",
269
269
  "format_compact",
270
+ "format_compact",
270
271
  "from_timestamp",
271
272
  "from_timestamp_millis",
272
273
  "from_timestamp_nanos",
utilities/zoneinfo.py CHANGED
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, assert_never, cast, override
6
6
  from zoneinfo import ZoneInfo
7
7
 
8
- from utilities.tzlocal import get_local_time_zone
8
+ from utilities.tzlocal import LOCAL_TIME_ZONE
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from utilities.types import TimeZone, TimeZoneLike
@@ -23,7 +23,7 @@ def ensure_time_zone(obj: TimeZoneLike, /) -> ZoneInfo:
23
23
  case ZoneInfo() as zone_info:
24
24
  return zone_info
25
25
  case "local":
26
- return get_local_time_zone()
26
+ return LOCAL_TIME_ZONE
27
27
  case str() as key:
28
28
  return ZoneInfo(key)
29
29
  case dt.tzinfo() as tzinfo: