dycw-utilities 0.131.1__py3-none-any.whl → 0.131.2__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.1
3
+ Version: 0.131.2
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=7cvfTXJSLM1ajMZOJ6keAbIOlGbRAUMdkqIQoooZ6Bk,60
1
+ utilities/__init__.py,sha256=ajN78bRkB5Im8iLJo5SoqSMwaBs05vhc-hxvF77jai4,60
2
2
  utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
3
3
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
4
4
  utilities/asyncio.py,sha256=lvdgBhuMtxq0dpiwF9g2WMMrit3kqXibN1V5NZ4xdbo,38046
@@ -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=y6a5Ilokrlvu7hNpPxUFWKy5p7vra1Oe6yEj1g-1Fng,42747
27
+ utilities/hypothesis.py,sha256=jiFJsS6rg4273BYDjrHT1iYH7D7ybROnH5bca9rBWqI,47372
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
@@ -32,7 +32,7 @@ utilities/iterables.py,sha256=mDqw2_0MUVp-P8FklgcaVTi2TXduH0MxbhTDzzhSBho,44915
32
32
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
33
33
  utilities/libcst.py,sha256=Jto5ppzRzsxn4AD32IS8n0lbgLYXwsVJB6EY8giNZyY,4974
34
34
  utilities/lightweight_charts.py,sha256=0xNfcsrgFI0R9xL25LtSm-W5yhfBI93qQNT6HyaXAhg,2769
35
- utilities/logging.py,sha256=0dUW0F0RISy9arU58M6WHn7ACSs3-S4GHDs8ZCkjyNk,18420
35
+ utilities/logging.py,sha256=DoLjy18w87fu6xDIBwiCtx3sAsNobm1QqQ4e2RRmp50,18421
36
36
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
37
37
  utilities/math.py,sha256=-mQgbah-dPJwOEWf3SonrFoVZ2AVxMgpeQ3dfVa-oJA,26764
38
38
  utilities/memory_profiler.py,sha256=tf2C51P2lCujPGvRt2Rfc7VEw5LDXmVPCG3z_AvBmbU,962
@@ -81,16 +81,17 @@ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
81
81
  utilities/timer.py,sha256=Rkc49KSpHuC8s7vUxGO9DU55U9I6yDKnchsQqrUCVBs,4075
82
82
  utilities/traceback.py,sha256=k3QhUca-643rl11S9uhibfNPlxjavOboCK56036KRcE,8859
83
83
  utilities/types.py,sha256=gP04CcCOyFrG7BgblVCsrrChiuO2x842NDVW-GF7odo,18370
84
- utilities/typing.py,sha256=H6ysJkI830aRwLsMKz0SZIw4cpcsm7d6KhQOwr-SDh0,13817
84
+ utilities/typing.py,sha256=kQWywPcRbFBKmvQBELmgbiqSHsnlo_D0ru53vl6KDeY,13846
85
85
  utilities/tzdata.py,sha256=yCf70NICwAeazN3_JcXhWvRqCy06XJNQ42j7r6gw3HY,1217
86
86
  utilities/tzlocal.py,sha256=3upDNFBvGh1l9njmLR2z2S6K6VxQSb7QizYGUbAH3JU,960
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
- utilities/whenever.py,sha256=QbXgFAPuUL7PCp2hajmIP-FFIfIR1J6Y0TxJbeoj60I,18434
90
+ utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
91
+ utilities/whenever2.py,sha256=uub90yQg2lUC8at7lnGR30qt5iuNlqPPTePwiKckhOE,3994
91
92
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
92
93
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
93
- dycw_utilities-0.131.1.dist-info/METADATA,sha256=wuztGM9vYp7gFU1LsLTuS2JxrKHKBH-AAMFyvjQ6Uo4,12989
94
- dycw_utilities-0.131.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.131.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.131.1.dist-info/RECORD,,
94
+ dycw_utilities-0.131.2.dist-info/METADATA,sha256=iP5y9JBuywFkkZML9iGcqagto8XgWUjD8sY4zCMWJv8,12989
95
+ dycw_utilities-0.131.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
+ dycw_utilities-0.131.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
+ dycw_utilities-0.131.2.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.131.1"
3
+ __version__ = "0.131.2"
utilities/hypothesis.py CHANGED
@@ -47,6 +47,7 @@ from hypothesis.strategies import (
47
47
  uuids,
48
48
  )
49
49
  from hypothesis.utils.conventions import not_set
50
+ from whenever import Date, DateDelta
50
51
 
51
52
  from utilities.datetime import (
52
53
  DATETIME_MAX_NAIVE,
@@ -88,7 +89,7 @@ from utilities.platform import IS_WINDOWS
88
89
  from utilities.sentinel import Sentinel, sentinel
89
90
  from utilities.tempfile import TEMP_DIR, TemporaryDirectory
90
91
  from utilities.version import Version
91
- from utilities.zoneinfo import UTC
92
+ from utilities.zoneinfo import UTC, ensure_time_zone
92
93
 
93
94
  if TYPE_CHECKING:
94
95
  from collections.abc import Collection, Hashable, Iterable, Iterator, Sequence
@@ -96,9 +97,10 @@ if TYPE_CHECKING:
96
97
 
97
98
  from hypothesis.database import ExampleDatabase
98
99
  from numpy.random import RandomState
100
+ from whenever import PlainDateTime, ZonedDateTime
99
101
 
100
102
  from utilities.numpy import NDArrayB, NDArrayF, NDArrayI, NDArrayO
101
- from utilities.types import Duration, Number, RoundMode
103
+ from utilities.types import Duration, Number, RoundMode, TimeZoneLike
102
104
 
103
105
 
104
106
  _T = TypeVar("_T")
@@ -158,6 +160,45 @@ def bool_arrays(
158
160
  ##
159
161
 
160
162
 
163
+ @composite
164
+ def date_deltas_whenever(
165
+ draw: DrawFn,
166
+ /,
167
+ *,
168
+ min_value: MaybeSearchStrategy[DateDelta | None] = None,
169
+ max_value: MaybeSearchStrategy[DateDelta | None] = None,
170
+ ) -> DateDelta:
171
+ """Strategy for generating date deltas."""
172
+ from utilities.whenever2 import DATE_DELTA_MAX, DATE_DELTA_MIN
173
+
174
+ min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
175
+ match min_value_:
176
+ case None:
177
+ min_value_ = DATE_DELTA_MIN
178
+ case DateDelta():
179
+ ...
180
+ case _ as never:
181
+ assert_never(never)
182
+ match max_value_:
183
+ case None:
184
+ max_value_ = DATE_DELTA_MAX
185
+ case DateDelta():
186
+ ...
187
+ case _ as never:
188
+ assert_never(never)
189
+ min_years, min_months, min_days = min_value_.in_years_months_days()
190
+ assert min_years == 0
191
+ assert min_months == 0
192
+ max_years, max_months, max_days = max_value_.in_years_months_days()
193
+ assert max_years == 0
194
+ assert max_months == 0
195
+ days = draw(integers(min_value=min_days, max_value=max_days))
196
+ return DateDelta(days=days)
197
+
198
+
199
+ ##
200
+
201
+
161
202
  @composite
162
203
  def date_durations(
163
204
  draw: DrawFn,
@@ -238,6 +279,41 @@ def dates_two_digit_year(
238
279
  ##
239
280
 
240
281
 
282
+ @composite
283
+ def dates_whenever(
284
+ draw: DrawFn,
285
+ /,
286
+ *,
287
+ min_value: MaybeSearchStrategy[Date | None] = None,
288
+ max_value: MaybeSearchStrategy[Date | None] = None,
289
+ ) -> Date:
290
+ """Strategy for generating dates."""
291
+ from utilities.whenever2 import DATE_MAX, DATE_MIN
292
+
293
+ min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
294
+ match min_value_:
295
+ case None:
296
+ min_value_ = DATE_MIN
297
+ case Date():
298
+ ...
299
+ case _ as never:
300
+ assert_never(never)
301
+ match max_value_:
302
+ case None:
303
+ max_value_ = DATE_MAX
304
+ case Date():
305
+ ...
306
+ case _ as never:
307
+ assert_never(never)
308
+ py_date = draw(
309
+ dates(min_value=min_value_.py_date(), max_value=max_value_.py_date())
310
+ )
311
+ return Date.from_py_date(py_date)
312
+
313
+
314
+ ##
315
+
316
+
241
317
  @composite
242
318
  def datetime_durations(
243
319
  draw: DrawFn,
@@ -920,7 +996,7 @@ def _pairs_map(elements: list[_T], /) -> tuple[_T, _T]:
920
996
 
921
997
  def paths() -> SearchStrategy[Path]:
922
998
  """Strategy for generating `Path`s."""
923
- reserved = {"NUL"}
999
+ reserved = {"AUX", "NUL"}
924
1000
  strategy = text_ascii(min_size=1, max_size=10).filter(lambda x: x not in reserved)
925
1001
  return lists(strategy, max_size=10).map(lambda parts: Path(*parts))
926
1002
 
@@ -965,6 +1041,45 @@ class PlainDateTimesError(Exception):
965
1041
  ##
966
1042
 
967
1043
 
1044
+ @composite
1045
+ def plain_datetimes_whenever(
1046
+ draw: DrawFn,
1047
+ /,
1048
+ *,
1049
+ min_value: MaybeSearchStrategy[PlainDateTime | None] = None,
1050
+ max_value: MaybeSearchStrategy[PlainDateTime | None] = None,
1051
+ ) -> PlainDateTime:
1052
+ """Strategy for generating plain datetimes."""
1053
+ from whenever import PlainDateTime
1054
+
1055
+ from utilities.whenever2 import PLAIN_DATE_TIME_MAX, PLAIN_DATE_TIME_MIN
1056
+
1057
+ min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
1058
+ match min_value_:
1059
+ case None:
1060
+ min_value_ = PLAIN_DATE_TIME_MIN
1061
+ case PlainDateTime():
1062
+ ...
1063
+ case _ as never:
1064
+ assert_never(never)
1065
+ match max_value_:
1066
+ case None:
1067
+ max_value_ = PLAIN_DATE_TIME_MAX
1068
+ case PlainDateTime():
1069
+ ...
1070
+ case _ as never:
1071
+ assert_never(never)
1072
+ py_datetime = draw(
1073
+ datetimes(
1074
+ min_value=min_value_.py_datetime(), max_value=max_value_.py_datetime()
1075
+ )
1076
+ )
1077
+ return PlainDateTime.from_py_datetime(py_datetime)
1078
+
1079
+
1080
+ ##
1081
+
1082
+
968
1083
  @composite
969
1084
  def random_states(
970
1085
  draw: DrawFn, /, *, seed: MaybeSearchStrategy[int | None] = None
@@ -1427,6 +1542,46 @@ class ZonedDateTimesError(Exception):
1427
1542
  return "Rounding requires a timedelta; got None"
1428
1543
 
1429
1544
 
1545
+ ##
1546
+
1547
+
1548
+ @composite
1549
+ def zoned_datetimes_whenever(
1550
+ draw: DrawFn,
1551
+ /,
1552
+ *,
1553
+ min_value: MaybeSearchStrategy[PlainDateTime | ZonedDateTime | None] = None,
1554
+ max_value: MaybeSearchStrategy[PlainDateTime | ZonedDateTime | None] = None,
1555
+ time_zone: MaybeSearchStrategy[TimeZoneLike] = UTC,
1556
+ ) -> ZonedDateTime:
1557
+ """Strategy for generating zoned datetimes."""
1558
+ from whenever import PlainDateTime, ZonedDateTime
1559
+
1560
+ min_value_, max_value_ = [draw2(draw, v) for v in [min_value, max_value]]
1561
+ time_zone_ = ensure_time_zone(draw2(draw, time_zone))
1562
+ match min_value_:
1563
+ case None | PlainDateTime():
1564
+ ...
1565
+ case ZonedDateTime():
1566
+ with assume_does_not_raise(ValueError):
1567
+ min_value_ = min_value_.to_tz(time_zone_.key).to_plain()
1568
+ case _ as never:
1569
+ assert_never(never)
1570
+ match max_value_:
1571
+ case None | PlainDateTime():
1572
+ ...
1573
+ case ZonedDateTime():
1574
+ with assume_does_not_raise(ValueError):
1575
+ max_value_ = max_value_.to_tz(time_zone_.key).to_plain()
1576
+ case _ as never:
1577
+ assert_never(never)
1578
+ plain_datetime = draw(
1579
+ plain_datetimes_whenever(min_value=min_value_, max_value=max_value_)
1580
+ )
1581
+ with assume_does_not_raise(ValueError):
1582
+ return plain_datetime.assume_tz(time_zone_.key, disambiguate="raise")
1583
+
1584
+
1430
1585
  __all__ = [
1431
1586
  "Draw2Error",
1432
1587
  "MaybeSearchStrategy",
@@ -1435,8 +1590,10 @@ __all__ = [
1435
1590
  "ZonedDateTimesError",
1436
1591
  "assume_does_not_raise",
1437
1592
  "bool_arrays",
1593
+ "date_deltas_whenever",
1438
1594
  "date_durations",
1439
1595
  "dates_two_digit_year",
1596
+ "dates_whenever",
1440
1597
  "datetime_durations",
1441
1598
  "draw2",
1442
1599
  "float32s",
@@ -1460,6 +1617,7 @@ __all__ = [
1460
1617
  "paths",
1461
1618
  "plain_datetimes",
1462
1619
  "plain_datetimes",
1620
+ "plain_datetimes_whenever",
1463
1621
  "random_states",
1464
1622
  "sentinels",
1465
1623
  "sets_fixed_length",
@@ -1480,4 +1638,5 @@ __all__ = [
1480
1638
  "uint64s",
1481
1639
  "versions",
1482
1640
  "zoned_datetimes",
1641
+ "zoned_datetimes_whenever",
1483
1642
  ]
utilities/logging.py CHANGED
@@ -190,7 +190,7 @@ def get_formatter(
190
190
  ) -> Formatter:
191
191
  """Get the formatter; colored if available."""
192
192
  if whenever:
193
- from utilities.whenever import WheneverLogRecord
193
+ from utilities.whenever2 import WheneverLogRecord
194
194
 
195
195
  setLogRecordFactory(WheneverLogRecord)
196
196
  format_ = format_.replace("{asctime}", "{zoned_datetime}")
utilities/typing.py CHANGED
@@ -234,7 +234,7 @@ def is_instance_gen(obj: Any, type_: Any, /) -> bool:
234
234
  """Check if an instance relationship holds, except bool<int."""
235
235
  # parent
236
236
  if isinstance(type_, tuple):
237
- return any(is_instance_gen(obj, t) for t in type_)
237
+ return any(is_instance_gen(obj, t) for t in type_) # skipif-ci-and-not-windows
238
238
  if is_literal_type(type_):
239
239
  return obj in get_args(type_)
240
240
  if is_union_type(type_):
utilities/whenever.py CHANGED
@@ -4,9 +4,7 @@ import datetime as dt
4
4
  import re
5
5
  from contextlib import suppress
6
6
  from dataclasses import dataclass
7
- from functools import cache
8
- from logging import LogRecord
9
- from typing import TYPE_CHECKING, Any, override
7
+ from typing import TYPE_CHECKING, override
10
8
 
11
9
  from whenever import (
12
10
  Date,
@@ -35,8 +33,6 @@ from utilities.re import (
35
33
  from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
36
34
 
37
35
  if TYPE_CHECKING:
38
- from zoneinfo import ZoneInfo
39
-
40
36
  from utilities.types import (
41
37
  DateLike,
42
38
  DateTimeLike,
@@ -565,64 +561,6 @@ class SerializeZonedDateTimeError(Exception):
565
561
  ##
566
562
 
567
563
 
568
- class WheneverLogRecord(LogRecord):
569
- """Log record powered by `whenever`."""
570
-
571
- zoned_datetime: str
572
-
573
- @override
574
- def __init__(
575
- self,
576
- name: str,
577
- level: int,
578
- pathname: str,
579
- lineno: int,
580
- msg: object,
581
- args: Any,
582
- exc_info: Any,
583
- func: str | None = None,
584
- sinfo: str | None = None,
585
- ) -> None:
586
- super().__init__(
587
- name, level, pathname, lineno, msg, args, exc_info, func, sinfo
588
- )
589
- length = self._get_length()
590
- plain = format(self._get_now().to_plain().format_common_iso(), f"{length}s")
591
- time_zone = self._get_time_zone_key()
592
- self.zoned_datetime = f"{plain}[{time_zone}]"
593
-
594
- @classmethod
595
- @cache
596
- def _get_time_zone(cls) -> ZoneInfo:
597
- """Get the local timezone."""
598
- try:
599
- from utilities.tzlocal import get_local_time_zone
600
- except ModuleNotFoundError: # pragma: no cover
601
- return UTC
602
- return get_local_time_zone()
603
-
604
- @classmethod
605
- @cache
606
- def _get_time_zone_key(cls) -> str:
607
- """Get the local timezone as a string."""
608
- return cls._get_time_zone().key
609
-
610
- @classmethod
611
- @cache
612
- def _get_length(cls) -> int:
613
- """Get maximum length of a formatted string."""
614
- now = cls._get_now().replace(nanosecond=1000).to_plain()
615
- return len(now.format_common_iso())
616
-
617
- @classmethod
618
- def _get_now(cls) -> ZonedDateTime:
619
- """Get the current zoned datetime."""
620
- return ZonedDateTime.now(cls._get_time_zone().key)
621
-
622
-
623
- ##
624
-
625
-
626
564
  def _to_datetime_delta(timedelta: dt.timedelta, /) -> DateTimeDelta:
627
565
  """Serialize a timedelta."""
628
566
  total_microseconds = datetime_duration_to_microseconds(timedelta)
@@ -672,7 +610,6 @@ __all__ = [
672
610
  "SerializePlainDateTimeError",
673
611
  "SerializeTimeDeltaError",
674
612
  "SerializeZonedDateTimeError",
675
- "WheneverLogRecord",
676
613
  "check_valid_zoned_datetime",
677
614
  "ensure_date",
678
615
  "ensure_datetime",
utilities/whenever2.py ADDED
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ from functools import cache
5
+ from logging import LogRecord
6
+ from typing import TYPE_CHECKING, Any, override
7
+
8
+ from whenever import Date, DateTimeDelta, PlainDateTime, ZonedDateTime
9
+
10
+ from utilities.zoneinfo import UTC, get_time_zone_name
11
+
12
+ if TYPE_CHECKING:
13
+ from zoneinfo import ZoneInfo
14
+
15
+ from utilities.types import TimeZoneLike
16
+
17
+
18
+ DATE_MIN = Date.from_py_date(dt.date.min)
19
+ DATE_MAX = Date.from_py_date(dt.date.max)
20
+ PLAIN_DATE_TIME_MIN = PlainDateTime.from_py_datetime(dt.datetime.min) # noqa: DTZ901
21
+ PLAIN_DATE_TIME_MAX = PlainDateTime.from_py_datetime(dt.datetime.max) # noqa: DTZ901
22
+ ZONED_DATE_TIME_MIN = PLAIN_DATE_TIME_MIN.assume_tz(UTC.key)
23
+ ZONED_DATE_TIME_MAX = PLAIN_DATE_TIME_MAX.assume_tz(UTC.key)
24
+ DATE_TIME_DELTA_MIN = DateTimeDelta(days=-3652059, seconds=-316192377600)
25
+ DATE_TIME_DELTA_MAX = DateTimeDelta(days=3652059, seconds=316192377600)
26
+ DATE_DELTA_MIN = DATE_TIME_DELTA_MIN.date_part()
27
+ DATE_DELTA_MAX = DATE_TIME_DELTA_MAX.date_part()
28
+ TIME_DELTA_MIN = DATE_TIME_DELTA_MIN.time_part()
29
+ TIME_DELTA_MAX = DATE_TIME_DELTA_MAX.time_part()
30
+
31
+
32
+ ##
33
+
34
+
35
+ def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
36
+ """Get a zoned datetime from a timestamp."""
37
+ return ZonedDateTime.from_timestamp(i, tz=get_time_zone_name(time_zone))
38
+
39
+
40
+ def from_timestamp_millis(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
41
+ """Get a zoned datetime from a timestamp (in milliseconds)."""
42
+ return ZonedDateTime.from_timestamp_millis(i, tz=get_time_zone_name(time_zone))
43
+
44
+
45
+ def from_timestamp_nanos(i: int, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
46
+ """Get a zoned datetime from a timestamp (in nanoseconds)."""
47
+ return ZonedDateTime.from_timestamp_nanos(i, tz=get_time_zone_name(time_zone))
48
+
49
+
50
+ ##
51
+
52
+
53
+ def get_now(*, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
54
+ """Get the current zoned datetime."""
55
+ return ZonedDateTime.now(get_time_zone_name(time_zone))
56
+
57
+
58
+ NOW_UTC = get_now(time_zone=UTC)
59
+
60
+
61
+ def get_now_local() -> ZonedDateTime:
62
+ """Get the current local time."""
63
+ return get_now(time_zone="local")
64
+
65
+
66
+ ##
67
+
68
+
69
+ class WheneverLogRecord(LogRecord):
70
+ """Log record powered by `whenever`."""
71
+
72
+ zoned_datetime: str
73
+
74
+ @override
75
+ def __init__(
76
+ self,
77
+ name: str,
78
+ level: int,
79
+ pathname: str,
80
+ lineno: int,
81
+ msg: object,
82
+ args: Any,
83
+ exc_info: Any,
84
+ func: str | None = None,
85
+ sinfo: str | None = None,
86
+ ) -> None:
87
+ super().__init__(
88
+ name, level, pathname, lineno, msg, args, exc_info, func, sinfo
89
+ )
90
+ length = self._get_length()
91
+ plain = format(get_now_local().to_plain().format_common_iso(), f"{length}s")
92
+ time_zone = self._get_time_zone_key()
93
+ self.zoned_datetime = f"{plain}[{time_zone}]"
94
+
95
+ @classmethod
96
+ @cache
97
+ def _get_time_zone(cls) -> ZoneInfo:
98
+ """Get the local timezone."""
99
+ try:
100
+ from utilities.tzlocal import get_local_time_zone
101
+ except ModuleNotFoundError: # pragma: no cover
102
+ return UTC
103
+ return get_local_time_zone()
104
+
105
+ @classmethod
106
+ @cache
107
+ def _get_time_zone_key(cls) -> str:
108
+ """Get the local timezone as a string."""
109
+ return cls._get_time_zone().key
110
+
111
+ @classmethod
112
+ @cache
113
+ def _get_length(cls) -> int:
114
+ """Get maximum length of a formatted string."""
115
+ now = get_now_local().replace(nanosecond=1000).to_plain()
116
+ return len(now.format_common_iso())
117
+
118
+
119
+ __all__ = [
120
+ "DATE_DELTA_MAX",
121
+ "DATE_DELTA_MIN",
122
+ "DATE_MAX",
123
+ "DATE_MIN",
124
+ "DATE_TIME_DELTA_MAX",
125
+ "DATE_TIME_DELTA_MIN",
126
+ "PLAIN_DATE_TIME_MAX",
127
+ "PLAIN_DATE_TIME_MIN",
128
+ "TIME_DELTA_MAX",
129
+ "TIME_DELTA_MIN",
130
+ "ZONED_DATE_TIME_MAX",
131
+ "ZONED_DATE_TIME_MIN",
132
+ "WheneverLogRecord",
133
+ "from_timestamp",
134
+ "from_timestamp_millis",
135
+ "from_timestamp_nanos",
136
+ "get_now",
137
+ "get_now",
138
+ "get_now_local",
139
+ ]