dycw-utilities 0.131.0__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.0
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=uJKmD-jecfvyO-bClrlrEbRyiOqLmfntMZDawWbbSAk,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=UnUMJmeqwJuK7uyUqw_i3opUYzVKud4RMG0RMOSRBQY,44463
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
@@ -70,7 +70,7 @@ utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
70
70
  utilities/slack_sdk.py,sha256=ltmzv68aa73CJGqTDvt8L9vDm22YU9iOCo3NCiNd3vA,4347
71
71
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
72
72
  utilities/sqlalchemy.py,sha256=IuQ7CIVNl29TG6i81K6fam8NmTmPjtA6OiIN4nIM9W8,37935
73
- utilities/sqlalchemy_polars.py,sha256=OPrB_Aqh8KE3hfNqvvXSzqVH5CYgIYrDH13WoLdCzbw,15510
73
+ utilities/sqlalchemy_polars.py,sha256=hApbjQUY-XgKfAXcun8gDP2lGh5LxrudnCpbG_hrYa0,14968
74
74
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
75
75
  utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
76
76
  utilities/string.py,sha256=XmU-s04qIV_tODnKl2pQiwmHaxzgOqRKU-RyzdrfvSE,620
@@ -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.0.dist-info/METADATA,sha256=nFoGHW3OOG6CRJXkrM-qnnpeoyMTas8muYPqY7L5sSM,12989
94
- dycw_utilities-0.131.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.131.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.131.0.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.0"
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,11 +97,10 @@ if TYPE_CHECKING:
96
97
 
97
98
  from hypothesis.database import ExampleDatabase
98
99
  from numpy.random import RandomState
99
- from sqlalchemy.ext.asyncio import AsyncEngine
100
+ from whenever import PlainDateTime, ZonedDateTime
100
101
 
101
102
  from utilities.numpy import NDArrayB, NDArrayF, NDArrayI, NDArrayO
102
- from utilities.sqlalchemy import Dialect, TableOrORMInstOrClass
103
- from utilities.types import Duration, Number, RoundMode
103
+ from utilities.types import Duration, Number, RoundMode, TimeZoneLike
104
104
 
105
105
 
106
106
  _T = TypeVar("_T")
@@ -160,6 +160,45 @@ def bool_arrays(
160
160
  ##
161
161
 
162
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
+
163
202
  @composite
164
203
  def date_durations(
165
204
  draw: DrawFn,
@@ -240,6 +279,41 @@ def dates_two_digit_year(
240
279
  ##
241
280
 
242
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
+
243
317
  @composite
244
318
  def datetime_durations(
245
319
  draw: DrawFn,
@@ -922,7 +996,7 @@ def _pairs_map(elements: list[_T], /) -> tuple[_T, _T]:
922
996
 
923
997
  def paths() -> SearchStrategy[Path]:
924
998
  """Strategy for generating `Path`s."""
925
- reserved = {"NUL"}
999
+ reserved = {"AUX", "NUL"}
926
1000
  strategy = text_ascii(min_size=1, max_size=10).filter(lambda x: x not in reserved)
927
1001
  return lists(strategy, max_size=10).map(lambda parts: Path(*parts))
928
1002
 
@@ -967,6 +1041,45 @@ class PlainDateTimesError(Exception):
967
1041
  ##
968
1042
 
969
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
+
970
1083
  @composite
971
1084
  def random_states(
972
1085
  draw: DrawFn, /, *, seed: MaybeSearchStrategy[int | None] = None
@@ -1117,51 +1230,6 @@ def slices(
1117
1230
  ##
1118
1231
 
1119
1232
 
1120
- _STRATEGY_DIALECTS: list[Dialect] = ["sqlite", "postgresql"]
1121
- _SQLALCHEMY_ENGINE_DIALECTS = sampled_from(_STRATEGY_DIALECTS)
1122
-
1123
-
1124
- async def sqlalchemy_engines(
1125
- data: DataObject,
1126
- /,
1127
- *tables_or_orms: TableOrORMInstOrClass,
1128
- dialect: MaybeSearchStrategy[Dialect] = _SQLALCHEMY_ENGINE_DIALECTS,
1129
- ) -> AsyncEngine:
1130
- """Strategy for generating sqlalchemy engines."""
1131
- from utilities.sqlalchemy import create_async_engine
1132
-
1133
- dialect_: Dialect = draw2(data, dialect)
1134
- if "CI" in environ: # pragma: no cover
1135
- _ = assume(dialect_ == "sqlite")
1136
- match dialect_:
1137
- case "sqlite":
1138
- temp_path = data.draw(temp_paths())
1139
- path = Path(temp_path, "db.sqlite")
1140
- engine = create_async_engine("sqlite+aiosqlite", database=str(path))
1141
-
1142
- class EngineWithPath(type(engine)): ...
1143
-
1144
- engine_with_path = EngineWithPath(engine.sync_engine)
1145
- cast(
1146
- "Any", engine_with_path
1147
- ).temp_path = temp_path # keep `temp_path` alive
1148
- return engine_with_path
1149
- case "postgresql": # skipif-ci-and-not-linux
1150
- from utilities.sqlalchemy import ensure_tables_dropped
1151
-
1152
- engine = create_async_engine(
1153
- "postgresql+asyncpg", host="localhost", port=5432, database="testing"
1154
- )
1155
- with assume_does_not_raise(ConnectionRefusedError):
1156
- await ensure_tables_dropped(engine, *tables_or_orms)
1157
- return engine
1158
- case _: # pragma: no cover
1159
- raise NotImplementedError(dialect)
1160
-
1161
-
1162
- ##
1163
-
1164
-
1165
1233
  @composite
1166
1234
  def str_arrays(
1167
1235
  draw: DrawFn,
@@ -1474,6 +1542,46 @@ class ZonedDateTimesError(Exception):
1474
1542
  return "Rounding requires a timedelta; got None"
1475
1543
 
1476
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
+
1477
1585
  __all__ = [
1478
1586
  "Draw2Error",
1479
1587
  "MaybeSearchStrategy",
@@ -1482,8 +1590,10 @@ __all__ = [
1482
1590
  "ZonedDateTimesError",
1483
1591
  "assume_does_not_raise",
1484
1592
  "bool_arrays",
1593
+ "date_deltas_whenever",
1485
1594
  "date_durations",
1486
1595
  "dates_two_digit_year",
1596
+ "dates_whenever",
1487
1597
  "datetime_durations",
1488
1598
  "draw2",
1489
1599
  "float32s",
@@ -1507,12 +1617,12 @@ __all__ = [
1507
1617
  "paths",
1508
1618
  "plain_datetimes",
1509
1619
  "plain_datetimes",
1620
+ "plain_datetimes_whenever",
1510
1621
  "random_states",
1511
1622
  "sentinels",
1512
1623
  "sets_fixed_length",
1513
1624
  "setup_hypothesis_profiles",
1514
1625
  "slices",
1515
- "sqlalchemy_engines",
1516
1626
  "str_arrays",
1517
1627
  "temp_dirs",
1518
1628
  "temp_paths",
@@ -1528,4 +1638,5 @@ __all__ = [
1528
1638
  "uint64s",
1529
1639
  "versions",
1530
1640
  "zoned_datetimes",
1641
+ "zoned_datetimes_whenever",
1531
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}")
@@ -25,7 +25,6 @@ from polars import (
25
25
  )
26
26
  from sqlalchemy import Column, Select, select
27
27
  from sqlalchemy.exc import DuplicateColumnError
28
- from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
29
28
 
30
29
  from utilities.asyncio import timeout_dur
31
30
  from utilities.functions import identity
@@ -61,6 +60,7 @@ if TYPE_CHECKING:
61
60
  )
62
61
 
63
62
  from polars._typing import PolarsDataType, SchemaDict
63
+ from sqlalchemy.ext.asyncio import AsyncEngine
64
64
  from sqlalchemy.sql import ColumnCollection
65
65
  from sqlalchemy.sql.base import ReadOnlyColumnCollection
66
66
  from tenacity.retry import RetryBaseT
@@ -307,22 +307,6 @@ async def select_to_dataframe(
307
307
  **kwargs: Any,
308
308
  ) -> DataFrame | Iterable[DataFrame] | AsyncIterable[DataFrame]:
309
309
  """Read a table from a database into a DataFrame."""
310
- if not issubclass(AsyncEngine, type(engine)):
311
- # for handling testing
312
- engine = create_async_engine(engine.url)
313
- return await select_to_dataframe(
314
- sel,
315
- engine,
316
- snake=snake,
317
- time_zone=time_zone,
318
- batch_size=batch_size,
319
- in_clauses=in_clauses,
320
- in_clauses_chunk_size=in_clauses_chunk_size,
321
- chunk_size_frac=chunk_size_frac,
322
- timeout=timeout,
323
- error=error,
324
- **kwargs,
325
- )
326
310
  if snake:
327
311
  sel = _select_to_dataframe_apply_snake(sel)
328
312
  schema = _select_to_dataframe_map_select_to_df_schema(sel, time_zone=time_zone)
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
+ ]