dycw-utilities 0.131.11__py3-none-any.whl → 0.131.12__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.11
3
+ Version: 0.131.12
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=wP5J8Mq8g6P6zyTVUVaa3X4tWb427OMAAyoOjt4J0qQ,61
1
+ utilities/__init__.py,sha256=Mewpkpu3DcrrhZzBvzBQpI4GkBsPPKRq0bdDX_sR7t4,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
@@ -12,12 +12,12 @@ utilities/contextvars.py,sha256=RsSGGrbQqqZ67rOydnM7WWIsM2lIE31UHJLejnHJPWY,505
12
12
  utilities/cryptography.py,sha256=_CiK_K6c_-uQuUhsUNjNjTL-nqxAh4_1zTfS11Xe120,972
13
13
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
14
  utilities/dataclasses.py,sha256=iiC1wpGXWhaocIikzwBt8bbLWyImoUlOlcDZJGejaIg,33011
15
- utilities/datetime.py,sha256=cDb0-fE7rRt3Gwl5vJ1HW0ZWf-wW7PHsQruD6or9hUo,38770
15
+ utilities/datetime.py,sha256=uPQdUgJJ9KuF-pogjYRbI9lOK-i5UBzPHexWe4pOVEo,38713
16
16
  utilities/enum.py,sha256=HoRwVCWzsnH0vpO9ZEcAAIZLMv0Sn2vJxxA4sYMQgDs,5793
17
17
  utilities/errors.py,sha256=nC7ZYtxxDBMfrTHtT_MByBfup_wfGQFRo3eDt-0ZPe8,1045
18
18
  utilities/eventkit.py,sha256=6M5Xu1SzN-juk9PqBHwy5dS-ta7T0qA6SMpDsakOJ0E,13039
19
- utilities/fastapi.py,sha256=8ABDSOH99j7H8cpKLZhUT2JzU18wiBqcTQuBUlTe-QE,2937
20
- utilities/fpdf2.py,sha256=lqizPXpzdwfJk3ChcbbWVa7WNXO2uvy7KDzWQtVTnXA,1831
19
+ utilities/fastapi.py,sha256=zDNPgfYNTZWQfS87y8ekgoJvTvmq07gv5m5kOyMYoX8,2796
20
+ utilities/fpdf2.py,sha256=PQysO4BcSMRpg-h-t2Pm7yatyk2yFl1inkvtKs1NG4M,1853
21
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
@@ -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=JrkrAZMo6JID2Eoc9QCc05Y_pK4l2zsApIhmii1z2Ig,2764
35
- utilities/logging.py,sha256=GP2BqpUlb9T7v90sdavsweJnOxfSdK7cJKFdgKURJZo,17892
35
+ utilities/logging.py,sha256=zm5k0Cduxtx2H2o7odxUTJtPNkJS85mqHYN1cS5Kc1w,17863
36
36
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
37
37
  utilities/math.py,sha256=_6vrDyjtaqE_OFE-F2DNWrDG_J_kMl3nFAJsok9v_bY,26862
38
38
  utilities/memory_profiler.py,sha256=tf2C51P2lCujPGvRt2Rfc7VEw5LDXmVPCG3z_AvBmbU,962
@@ -41,7 +41,7 @@ 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=vmPsuOOxrQBAg6aEVVKHfOX9A04QlSa162El5HrIT9E,36889
44
+ utilities/orjson.py,sha256=oD9sQYd3bQKZi2dwpQsaRVwCSlSQ2sREl83UZP4LD7w,36962
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
@@ -55,7 +55,7 @@ utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
55
55
  utilities/psutil.py,sha256=RtbLKOoIJhqrJmEoHDBVeSD-KPzshtS0FtRXBP9_w2s,3751
56
56
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  utilities/pydantic.py,sha256=aP6OKowg2Md4rgQuQ5qTSF4bTbBuq7WtRb7zS3JSRGY,1841
58
- utilities/pyinstrument.py,sha256=KU7_wPX63TY_8kSps0WZ0ijpN7vXQUkp175vdg-CIdE,899
58
+ utilities/pyinstrument.py,sha256=3MPNDbZW_1Aj1aA1_f-yqPStgmjIFxPwIafYq2dsaTs,853
59
59
  utilities/pyrsistent.py,sha256=wVOVIe_68AAaa-lUE9y-TEzDawVp1uEIc_zfoDgr5ww,2287
60
60
  utilities/pytest.py,sha256=zP4CWKXpRVk4aRDRxolUAvqQwX7wgDO8lzmkQfuZaZo,7832
61
61
  utilities/pytest_regressions.py,sha256=YI55B7EtLjhz7zPJZ6NK9bWrxrKCKabWZJe1cwcbA5o,5082
@@ -79,19 +79,19 @@ utilities/tenacity.py,sha256=1PUvODiBVgeqIh7G5TRt5WWMSqjLYkEqP53itT97WQc,4914
79
79
  utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
80
80
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
81
81
  utilities/timer.py,sha256=VeSl3ot8-f4D1d3HjjSsgKvjxHJGXd_sW4KcTExOR64,2475
82
- utilities/traceback.py,sha256=U8Du13z0qFo7sntup2x8xLw49KNb-QtEv7-UgziZ_Cw,8769
82
+ utilities/traceback.py,sha256=cMXrCD59CROnezAU8VW67CxZ8Igc5QmaxlV8qrBvNMs,8504
83
83
  utilities/types.py,sha256=CHQke10ETEpypxppYVhWp1G68S6mvifalrRLolYBcCg,19506
84
- utilities/typing.py,sha256=kQWywPcRbFBKmvQBELmgbiqSHsnlo_D0ru53vl6KDeY,13846
84
+ utilities/typing.py,sha256=VuGuztLSkTicxgVwI5wrVOTcY70OlzwsTU7LcFVjGlY,14169
85
85
  utilities/tzdata.py,sha256=yCf70NICwAeazN3_JcXhWvRqCy06XJNQ42j7r6gw3HY,1217
86
- utilities/tzlocal.py,sha256=P5BjqTiYskeCwjE7i9zycCFXO4MWdZgYCh4jut-LpzA,1042
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=76tFaAVX5CfXOjjMJ1Sg3LW85RoYARNZIALseCLG81k,7047
91
+ utilities/whenever2.py,sha256=WiDVsgHA-4E-KiIJ25-R4qAWvtyBQk1EkhjMszFzQMM,7455
92
92
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
93
93
  utilities/zoneinfo.py,sha256=tvcgu3QzDxe2suTexi2QzRGpin7VK1TjHa0JYYxT69I,1862
94
- dycw_utilities-0.131.11.dist-info/METADATA,sha256=m2QhrLfCIpPGxtKXK8nVbiyX5ldnlLwNHjQnHy9UWkk,1585
95
- dycw_utilities-0.131.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
- dycw_utilities-0.131.11.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
- dycw_utilities-0.131.11.dist-info/RECORD,,
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,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.131.11"
3
+ __version__ = "0.131.12"
utilities/datetime.py CHANGED
@@ -21,21 +21,14 @@ from utilities.iterables import OneEmptyError, one
21
21
  from utilities.math import SafeRoundError, round_, safe_round
22
22
  from utilities.platform import SYSTEM
23
23
  from utilities.sentinel import Sentinel, sentinel
24
- from utilities.types import MaybeStr
24
+ from utilities.types import MaybeCallablePyDate, MaybeCallablePyDateTime, MaybeStr
25
25
  from utilities.typing import is_instance_gen
26
26
  from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from collections.abc import Iterator
30
30
 
31
- from utilities.types import (
32
- DateOrDateTime,
33
- Duration,
34
- MathRoundMode,
35
- MaybeCallablePyDate,
36
- MaybeCallablePyDateTime,
37
- TimeZoneLike,
38
- )
31
+ from utilities.types import DateOrDateTime, Duration, MathRoundMode, TimeZoneLike
39
32
 
40
33
 
41
34
  _DAYS_PER_YEAR = 365.25
utilities/fastapi.py CHANGED
@@ -9,8 +9,7 @@ from uvicorn import Config, Server
9
9
 
10
10
  from utilities.asyncio import Looper
11
11
  from utilities.datetime import SECOND, datetime_duration_to_float
12
- from utilities.tzlocal import get_now_local # skipif-ci
13
- from utilities.whenever import serialize_zoned_datetime # skipif-ci
12
+ from utilities.whenever2 import get_now_local
14
13
 
15
14
  if TYPE_CHECKING:
16
15
  from types import TracebackType
@@ -31,8 +30,7 @@ class _PingerReceiverApp(FastAPI):
31
30
 
32
31
  @self.get("/ping") # skipif-ci
33
32
  def ping() -> str:
34
- now = serialize_zoned_datetime(get_now_local()) # skipif-ci
35
- return f"pong @ {now}" # skipif-ci
33
+ return f"pong @ {get_now_local()}" # skipif-ci
36
34
 
37
35
  _ = ping # skipif-ci
38
36
 
utilities/fpdf2.py CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, override
6
6
  from fpdf import FPDF
7
7
  from fpdf.enums import XPos, YPos
8
8
 
9
- from utilities.tzlocal import get_now_local
9
+ from utilities.whenever2 import format_compact, get_now
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Iterator
@@ -47,7 +47,7 @@ def yield_pdf(*, header: str | None = None) -> Iterator[_BasePDF]:
47
47
  def footer(self) -> None:
48
48
  self.set_y(-15)
49
49
  self.set_font(family="Helvetica", style="I", size=8)
50
- page_no, now = self.page_no(), get_now_local()
50
+ page_no, now = self.page_no(), format_compact(get_now())
51
51
  text = f"page {page_no}/{{}}; {now}"
52
52
  _ = self.cell(
53
53
  w=0,
utilities/logging.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
3
  import re
5
4
  from dataclasses import dataclass, field
6
5
  from functools import cached_property
@@ -19,7 +18,6 @@ from logging import (
19
18
  from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
20
19
  from pathlib import Path
21
20
  from re import Pattern
22
- from time import time
23
21
  from typing import (
24
22
  TYPE_CHECKING,
25
23
  Any,
@@ -32,17 +30,31 @@ from typing import (
32
30
  override,
33
31
  )
34
32
 
33
+ from whenever import PlainDateTime, ZonedDateTime
34
+
35
35
  from utilities.atomicwrites import move_many
36
36
  from utilities.dataclasses import replace_non_sentinel
37
- from utilities.datetime import parse_datetime_compact, serialize_compact
38
37
  from utilities.errors import ImpossibleCaseError
39
38
  from utilities.iterables import OneEmptyError, always_iterable, one
40
39
  from utilities.pathlib import ensure_suffix, get_path
40
+ from utilities.re import (
41
+ ExtractGroupError,
42
+ ExtractGroupsError,
43
+ extract_group,
44
+ extract_groups,
45
+ )
41
46
  from utilities.sentinel import Sentinel, sentinel
42
- from utilities.tzlocal import get_now_local
47
+ from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
48
+ from utilities.whenever2 import (
49
+ WheneverLogRecord,
50
+ format_compact,
51
+ get_now,
52
+ get_now_local,
53
+ )
43
54
 
44
55
  if TYPE_CHECKING:
45
56
  from collections.abc import Callable, Iterable, Mapping
57
+ from datetime import time
46
58
  from logging import _FilterType
47
59
 
48
60
  from utilities.types import (
@@ -54,7 +66,9 @@ if TYPE_CHECKING:
54
66
  )
55
67
 
56
68
 
57
- _DEFAULT_FORMAT = "{asctime} | {name}:{funcName}:{lineno} | {levelname:8} | {message}"
69
+ _DEFAULT_FORMAT = (
70
+ "{zoned_datetime} | {name}:{funcName}:{lineno} | {levelname:8} | {message}"
71
+ )
58
72
  _DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S"
59
73
  _DEFAULT_BACKUP_COUNT: int = 100
60
74
  _DEFAULT_MAX_BYTES: int = 10 * 1024**2
@@ -76,7 +90,6 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
76
90
  def basic_config(
77
91
  *,
78
92
  obj: LoggerOrName | Handler | None = None,
79
- whenever: bool = False,
80
93
  format_: str = _DEFAULT_FORMAT,
81
94
  datefmt: str = _DEFAULT_DATEFMT,
82
95
  level: LogLevel = "INFO",
@@ -93,7 +106,6 @@ def basic_config(
93
106
  logger.addHandler(handler := StreamHandler())
94
107
  basic_config(
95
108
  obj=handler,
96
- whenever=whenever,
97
109
  format_=format_,
98
110
  datefmt=datefmt,
99
111
  level=level,
@@ -104,7 +116,6 @@ def basic_config(
104
116
  case str() as name:
105
117
  basic_config(
106
118
  obj=get_logger(logger=name),
107
- whenever=whenever,
108
119
  format_=format_,
109
120
  datefmt=datefmt,
110
121
  level=level,
@@ -117,7 +128,6 @@ def basic_config(
117
128
  if filters is not None:
118
129
  add_filters(handler, *always_iterable(filters))
119
130
  formatter = get_formatter(
120
- whenever=whenever,
121
131
  format_=format_,
122
132
  datefmt=datefmt,
123
133
  plain=plain,
@@ -179,18 +189,13 @@ class _FieldStyleDict(TypedDict):
179
189
 
180
190
  def get_formatter(
181
191
  *,
182
- whenever: bool = False,
183
192
  format_: str = _DEFAULT_FORMAT,
184
193
  datefmt: str = _DEFAULT_DATEFMT,
185
194
  plain: bool = False,
186
195
  color_field_styles: Mapping[str, _FieldStyleKeys] | None = None,
187
196
  ) -> Formatter:
188
197
  """Get the formatter; colored if available."""
189
- if whenever:
190
- from utilities.whenever2 import WheneverLogRecord
191
-
192
- setLogRecordFactory(WheneverLogRecord)
193
- format_ = format_.replace("{asctime}", "{zoned_datetime}")
198
+ setLogRecordFactory(WheneverLogRecord)
194
199
  if plain:
195
200
  return _get_plain_formatter(format_=format_, datefmt=datefmt)
196
201
  try:
@@ -199,8 +204,7 @@ def get_formatter(
199
204
  return _get_plain_formatter(format_=format_, datefmt=datefmt)
200
205
  default = cast("dict[_FieldStyleKeys, _FieldStyleDict]", DEFAULT_FIELD_STYLES)
201
206
  field_styles = {cast("str", k): v for k, v in default.items()}
202
- if whenever:
203
- field_styles["zoned_datetime"] = default["asctime"]
207
+ field_styles["zoned_datetime"] = default["asctime"]
204
208
  if color_field_styles is not None:
205
209
  field_styles.update({k: default[v] for k, v in color_field_styles.items()})
206
210
  return ColoredFormatter(
@@ -256,7 +260,6 @@ class GetLoggingLevelNumberError(Exception):
256
260
  def setup_logging(
257
261
  *,
258
262
  logger: LoggerOrName | None = None,
259
- whenever: bool = False,
260
263
  format_: str = _DEFAULT_FORMAT,
261
264
  datefmt: str = _DEFAULT_DATEFMT,
262
265
  console_level: LogLevel = "INFO",
@@ -272,7 +275,6 @@ def setup_logging(
272
275
  """Set up logger."""
273
276
  basic_config(
274
277
  obj=logger,
275
- whenever=whenever,
276
278
  format_=f"{console_prefix} {format_}",
277
279
  datefmt=datefmt,
278
280
  level=console_level,
@@ -295,7 +297,6 @@ def setup_logging(
295
297
  logger_use.addHandler(handler)
296
298
  basic_config(
297
299
  obj=handler,
298
- whenever=whenever,
299
300
  format_=format_,
300
301
  datefmt=datefmt,
301
302
  level=level,
@@ -330,7 +331,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
330
331
  interval: int = 1,
331
332
  backupCount: int = _DEFAULT_BACKUP_COUNT,
332
333
  utc: bool = False,
333
- atTime: dt.time | None = None,
334
+ atTime: time | None = None,
334
335
  ) -> None:
335
336
  path = Path(filename)
336
337
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -382,7 +383,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
382
383
  if not self.delay: # pragma: no cover
383
384
  self.stream = self._open()
384
385
  self._time_handler.rolloverAt = ( # skipif-ci-and-windows
385
- self._time_handler.computeRollover(int(time()))
386
+ self._time_handler.computeRollover(get_now().timestamp())
386
387
  )
387
388
 
388
389
  def _should_rollover(self, record: LogRecord, /) -> bool:
@@ -400,10 +401,8 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
400
401
  def _compute_rollover_patterns(stem: str, suffix: str, /) -> _RolloverPatterns:
401
402
  return _RolloverPatterns(
402
403
  pattern1=re.compile(rf"^{stem}\.(\d+){suffix}$"),
403
- pattern2=re.compile(rf"^{stem}\.(\d+)__(\d{{8}}T\d{{6}}){suffix}$"),
404
- pattern3=re.compile(
405
- rf"^{stem}\.(\d+)__(\d{{8}}T\d{{6}})__(\d{{8}}T\d{{6}}){suffix}$"
406
- ),
404
+ pattern2=re.compile(rf"^{stem}\.(\d+)__([\dT]+?){suffix}$"),
405
+ pattern3=re.compile(rf"^{stem}\.(\d+)__([\dT]+?)__([\dT]+?){suffix}$"),
407
406
  )
408
407
 
409
408
 
@@ -452,7 +451,7 @@ def _compute_rollover_actions(
452
451
  rotations.add(
453
452
  _Rotation(file=file, index=curr + 1, start=start, end=end)
454
453
  )
455
- case int() as index, dt.datetime(), dt.datetime():
454
+ case int() as index, ZonedDateTime(), ZonedDateTime():
456
455
  rotations.add(_Rotation(file=file, index=index + 1))
457
456
  case _: # pragma: no cover
458
457
  raise NotImplementedError
@@ -476,8 +475,8 @@ class _RotatingLogFile:
476
475
  stem: str
477
476
  suffix: str
478
477
  index: int | None = None
479
- start: dt.datetime | None = None
480
- end: dt.datetime | None = None
478
+ start: ZonedDateTime | None = None
479
+ end: ZonedDateTime | None = None
481
480
 
482
481
  @classmethod
483
482
  def from_path(
@@ -494,16 +493,23 @@ class _RotatingLogFile:
494
493
  if patterns is None:
495
494
  patterns = _compute_rollover_patterns(stem, suffix)
496
495
  try:
497
- (index,) = patterns.pattern1.findall(path.name)
498
- except ValueError:
496
+ index, start, end = extract_groups(patterns.pattern3, path.name)
497
+ except ExtractGroupsError:
499
498
  pass
500
499
  else:
501
500
  return cls(
502
- directory=path.parent, stem=stem, suffix=suffix, index=int(index)
501
+ directory=path.parent,
502
+ stem=stem,
503
+ suffix=suffix,
504
+ index=int(index),
505
+ start=PlainDateTime.parse_common_iso(start).assume_tz(
506
+ LOCAL_TIME_ZONE_NAME
507
+ ),
508
+ end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
503
509
  )
504
510
  try:
505
- ((index, end),) = patterns.pattern2.findall(path.name)
506
- except ValueError:
511
+ index, end = extract_groups(patterns.pattern2, path.name)
512
+ except ExtractGroupsError:
507
513
  pass
508
514
  else:
509
515
  return cls(
@@ -511,21 +517,17 @@ class _RotatingLogFile:
511
517
  stem=stem,
512
518
  suffix=suffix,
513
519
  index=int(index),
514
- end=parse_datetime_compact(end),
520
+ end=PlainDateTime.parse_common_iso(end).assume_tz(LOCAL_TIME_ZONE_NAME),
515
521
  )
516
522
  try:
517
- ((index, start, end),) = patterns.pattern3.findall(path.name)
518
- except ValueError:
519
- return cls(directory=path.parent, stem=stem, suffix=suffix)
523
+ index = extract_group(patterns.pattern1, path.name)
524
+ except ExtractGroupError:
525
+ pass
520
526
  else:
521
527
  return cls(
522
- directory=path.parent,
523
- stem=stem,
524
- suffix=suffix,
525
- index=int(index),
526
- start=parse_datetime_compact(start),
527
- end=parse_datetime_compact(end),
528
+ directory=path.parent, stem=stem, suffix=suffix, index=int(index)
528
529
  )
530
+ return cls(directory=path.parent, stem=stem, suffix=suffix)
529
531
 
530
532
  @cached_property
531
533
  def path(self) -> Path:
@@ -535,10 +537,10 @@ class _RotatingLogFile:
535
537
  tail = None
536
538
  case int() as index, None, None:
537
539
  tail = str(index)
538
- case int() as index, None, dt.datetime() as end:
539
- tail = f"{index}__{serialize_compact(end)}"
540
- case int() as index, dt.datetime() as start, dt.datetime() as end:
541
- tail = f"{index}__{serialize_compact(start)}__{serialize_compact(end)}"
540
+ case int() as index, None, ZonedDateTime() as end:
541
+ tail = f"{index}__{format_compact(end)}"
542
+ case int() as index, ZonedDateTime() as start, ZonedDateTime() as end:
543
+ tail = f"{index}__{format_compact(start)}__{format_compact(end)}"
542
544
  case _: # pragma: no cover
543
545
  raise ImpossibleCaseError(
544
546
  case=[f"{self.index=}", f"{self.start=}", f"{self.end=}"]
@@ -550,8 +552,8 @@ class _RotatingLogFile:
550
552
  self,
551
553
  *,
552
554
  index: int | None | Sentinel = sentinel,
553
- start: dt.datetime | None | Sentinel = sentinel,
554
- end: dt.datetime | None | Sentinel = sentinel,
555
+ start: ZonedDateTime | None | Sentinel = sentinel,
556
+ end: ZonedDateTime | None | Sentinel = sentinel,
555
557
  ) -> Self:
556
558
  return replace_non_sentinel(self, index=index, start=start, end=end)
557
559
 
@@ -568,8 +570,8 @@ class _Deletion:
568
570
  class _Rotation:
569
571
  file: _RotatingLogFile
570
572
  index: int = 0
571
- start: dt.datetime | None | Sentinel = sentinel
572
- end: dt.datetime | Sentinel = sentinel
573
+ start: ZonedDateTime | None | Sentinel = sentinel
574
+ end: ZonedDateTime | Sentinel = sentinel
573
575
 
574
576
  @cached_property
575
577
  def destination(self) -> Path:
utilities/orjson.py CHANGED
@@ -14,6 +14,7 @@ from pathlib import Path
14
14
  from re import Pattern, search
15
15
  from typing import TYPE_CHECKING, Any, Literal, Self, assert_never, overload, override
16
16
  from uuid import UUID
17
+ from zoneinfo import ZoneInfo
17
18
 
18
19
  from orjson import (
19
20
  OPT_PASSTHROUGH_DATACLASS,
@@ -22,6 +23,7 @@ from orjson import (
22
23
  dumps,
23
24
  loads,
24
25
  )
26
+ from whenever import ZonedDateTime
25
27
 
26
28
  from utilities.concurrent import concurrent_map
27
29
  from utilities.dataclasses import dataclass_to_dict
@@ -35,15 +37,8 @@ from utilities.iterables import (
35
37
  )
36
38
  from utilities.logging import get_logging_level_number
37
39
  from utilities.math import MAX_INT64, MIN_INT64
38
- from utilities.types import (
39
- Dataclass,
40
- DateOrDateTime,
41
- LogLevel,
42
- MaybeIterable,
43
- PathLike,
44
- StrMapping,
45
- )
46
- from utilities.tzlocal import get_local_time_zone
40
+ from utilities.types import Dataclass, LogLevel, MaybeIterable, PathLike, StrMapping
41
+ from utilities.tzlocal import LOCAL_TIME_ZONE
47
42
  from utilities.uuid import UUID_PATTERN
48
43
  from utilities.version import Version, parse_version
49
44
  from utilities.whenever import (
@@ -57,12 +52,14 @@ from utilities.whenever import (
57
52
  serialize_time,
58
53
  serialize_timedelta,
59
54
  )
60
- from utilities.zoneinfo import ensure_time_zone
55
+ from utilities.whenever2 import from_timestamp
61
56
 
62
57
  if TYPE_CHECKING:
63
58
  from collections.abc import Set as AbstractSet
64
59
  from logging import _FormatStyle
65
60
 
61
+ from whenever import Date
62
+
66
63
  from utilities.types import Parallelism
67
64
 
68
65
 
@@ -92,6 +89,7 @@ class _Prefixes(Enum):
92
89
  unserializable = "un"
93
90
  uuid = "uu"
94
91
  version = "v"
92
+ zoned_datetime = "zd"
95
93
 
96
94
 
97
95
  type _DataclassHook = Callable[[type[Dataclass], StrMapping], StrMapping]
@@ -194,6 +192,8 @@ def _pre_process(
194
192
  return f"[{_Prefixes.exception_class.value}|{error_cls.__qualname__}]"
195
193
  case Version() as version:
196
194
  return f"[{_Prefixes.version.value}]{version!s}"
195
+ case ZonedDateTime() as datetime:
196
+ return f"[{_Prefixes.zoned_datetime.value}]{datetime}"
197
197
  # contains
198
198
  case Dataclass() as dataclass:
199
199
  asdict = dataclass_to_dict(
@@ -350,6 +350,11 @@ _ZONED_DATETIME_PATTERN = re.compile(
350
350
  + _Prefixes.datetime.value
351
351
  + r"\](\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?[\+\-]\d{2}:\d{2}(?::\d{2})?\[(?!(?:dt\.)).+?\])$"
352
352
  )
353
+ _ZONED_DATETIME_PATTERN2 = re.compile(
354
+ r"^\["
355
+ + _Prefixes.zoned_datetime.value
356
+ + r"\](\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?[\+\-]\d{2}:\d{2}(?::\d{2})?\[(?!(?:dt\.)).+?\])$"
357
+ )
353
358
 
354
359
 
355
360
  def _make_unit_pattern(prefix: _Prefixes, /) -> Pattern[str]:
@@ -437,6 +442,8 @@ def _object_hook(
437
442
  return parse_version(match.group(1))
438
443
  if match := _ZONED_DATETIME_PATTERN.search(text):
439
444
  return parse_zoned_datetime(match.group(1))
445
+ if match := _ZONED_DATETIME_PATTERN2.search(text):
446
+ return ZonedDateTime.parse_common_iso(match.group(1))
440
447
  if (
441
448
  exc_class := _object_hook_exception_class(
442
449
  text, data=data, objects=objects, redirects=redirects
@@ -753,9 +760,7 @@ class OrjsonFormatter(Formatter):
753
760
  path_name=Path(record.pathname),
754
761
  line_num=record.lineno,
755
762
  message=record.getMessage(),
756
- datetime=dt.datetime.fromtimestamp(
757
- record.created, tz=get_local_time_zone()
758
- ),
763
+ datetime=from_timestamp(record.created, time_zone="local"),
759
764
  func_name=record.funcName,
760
765
  extra=extra if len(extra) >= 1 else None,
761
766
  )
@@ -859,11 +864,16 @@ class GetLogRecordsOutput:
859
864
  for r in self.records
860
865
  ]
861
866
  if len(records) >= 1:
862
- time_zone = one_unique(ensure_time_zone(r.datetime) for r in records)
867
+ time_zone = one_unique(ZoneInfo(r.datetime.tz) for r in records)
863
868
  else:
864
- time_zone = get_local_time_zone()
869
+ time_zone = LOCAL_TIME_ZONE
865
870
  return DataFrame(
866
- data=[dataclass_to_dict(r, recursive=False) for r in records],
871
+ data=[
872
+ dataclass_to_dict(
873
+ replace(r, datetime=r.datetime.py_datetime()), recursive=False
874
+ )
875
+ for r in records
876
+ ],
867
877
  schema={
868
878
  "index": UInt64,
869
879
  "name": String,
@@ -891,9 +901,12 @@ class GetLogRecordsOutput:
891
901
  level: LogLevel | None = None,
892
902
  min_level: LogLevel | None = None,
893
903
  max_level: LogLevel | None = None,
894
- date_or_datetime: DateOrDateTime | None = None,
895
- min_date_or_datetime: DateOrDateTime | None = None,
896
- max_date_or_datetime: DateOrDateTime | None = None,
904
+ date: Date | None = None,
905
+ min_date: Date | None = None,
906
+ max_date: Date | None = None,
907
+ datetime: ZonedDateTime | None = None,
908
+ min_datetime: ZonedDateTime | None = None,
909
+ max_datetime: ZonedDateTime | None = None,
897
910
  func_name: bool | str | None = None,
898
911
  extra: bool | MaybeIterable[str] | None = None,
899
912
  log_file: bool | PathLike | None = None,
@@ -932,30 +945,18 @@ class GetLogRecordsOutput:
932
945
  records = [
933
946
  r for r in records if r.level <= get_logging_level_number(max_level)
934
947
  ]
935
- if date_or_datetime is not None:
936
- match date_or_datetime:
937
- case dt.datetime() as datetime:
938
- records = [r for r in records if r.datetime == datetime]
939
- case dt.date() as date:
940
- records = [r for r in records if r.date == date]
941
- case _ as never:
942
- assert_never(never)
943
- if min_date_or_datetime is not None:
944
- match min_date_or_datetime:
945
- case dt.datetime() as min_datetime:
946
- records = [r for r in records if r.datetime >= min_datetime]
947
- case dt.date() as min_date:
948
- records = [r for r in records if r.date >= min_date]
949
- case _ as never:
950
- assert_never(never)
951
- if max_date_or_datetime is not None:
952
- match max_date_or_datetime:
953
- case dt.datetime() as max_datetime:
954
- records = [r for r in records if r.datetime <= max_datetime]
955
- case dt.date() as max_date:
956
- records = [r for r in records if r.date <= max_date]
957
- case _ as never:
958
- assert_never(never)
948
+ if date is not None:
949
+ records = [r for r in records if r.date == date]
950
+ if min_date is not None:
951
+ records = [r for r in records if r.date >= min_date]
952
+ if max_date is not None:
953
+ records = [r for r in records if r.date <= max_date]
954
+ if datetime is not None:
955
+ records = [r for r in records if r.datetime == datetime]
956
+ if min_datetime is not None:
957
+ records = [r for r in records if r.datetime >= min_datetime]
958
+ if max_datetime is not None:
959
+ records = [r for r in records if r.datetime <= max_datetime]
959
960
  if func_name is not None:
960
961
  match func_name:
961
962
  case bool() as has_func_name:
@@ -1058,7 +1059,7 @@ class OrjsonLogRecord:
1058
1059
  level: int
1059
1060
  path_name: Path
1060
1061
  line_num: int
1061
- datetime: dt.datetime
1062
+ datetime: ZonedDateTime
1062
1063
  func_name: str | None = None
1063
1064
  stack_info: str | None = None
1064
1065
  extra: StrMapping | None = None
@@ -1066,7 +1067,7 @@ class OrjsonLogRecord:
1066
1067
  log_file_line_num: int | None = None
1067
1068
 
1068
1069
  @cached_property
1069
- def date(self) -> dt.date:
1070
+ def date(self) -> Date:
1070
1071
  return self.datetime.date()
1071
1072
 
1072
1073
 
utilities/pyinstrument.py CHANGED
@@ -7,9 +7,8 @@ from typing import TYPE_CHECKING
7
7
  from pyinstrument.profiler import Profiler
8
8
 
9
9
  from utilities.atomicwrites import writer
10
- from utilities.datetime import serialize_compact
11
10
  from utilities.pathlib import get_path
12
- from utilities.tzlocal import get_now_local
11
+ from utilities.whenever2 import format_compact, get_now
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from collections.abc import Iterator
@@ -23,7 +22,7 @@ def profile(*, path: MaybeCallablePathLike | None = Path.cwd) -> Iterator[None]:
23
22
  with Profiler() as profiler:
24
23
  yield
25
24
  filename = get_path(path=path).joinpath(
26
- f"profile__{serialize_compact(get_now_local())}.html"
25
+ f"profile__{format_compact(get_now())}.html"
27
26
  )
28
27
  with writer(filename) as temp, temp.open(mode="w") as fh:
29
28
  _ = fh.write(profiler.output_html())
utilities/traceback.py CHANGED
@@ -14,7 +14,6 @@ from traceback import TracebackException
14
14
  from typing import TYPE_CHECKING, override
15
15
 
16
16
  from utilities.atomicwrites import writer
17
- from utilities.datetime import get_datetime, get_now, serialize_compact
18
17
  from utilities.errors import repr_error
19
18
  from utilities.iterables import OneEmptyError, one
20
19
  from utilities.pathlib import get_path
@@ -27,16 +26,19 @@ from utilities.reprlib import (
27
26
  RICH_MAX_WIDTH,
28
27
  yield_mapping_repr,
29
28
  )
30
- from utilities.tzlocal import get_local_time_zone, get_now_local
31
29
  from utilities.version import get_version
32
- from utilities.whenever import serialize_duration, serialize_zoned_datetime
30
+ from utilities.whenever2 import format_compact, get_now, to_zoned_date_time
33
31
 
34
32
  if TYPE_CHECKING:
35
33
  from collections.abc import Callable, Iterator, Sequence
36
34
  from traceback import FrameSummary
37
35
  from types import TracebackType
38
36
 
39
- from utilities.types import MaybeCallablePathLike, MaybeCallablePyDateTime, PathLike
37
+ from utilities.types import (
38
+ MaybeCallablePathLike,
39
+ MaybeCallableZonedDateTime,
40
+ PathLike,
41
+ )
40
42
  from utilities.version import MaybeCallableVersionLike
41
43
 
42
44
 
@@ -51,7 +53,7 @@ def format_exception_stack(
51
53
  /,
52
54
  *,
53
55
  header: bool = False,
54
- start: MaybeCallablePyDateTime | None = _START,
56
+ start: MaybeCallableZonedDateTime | None = _START,
55
57
  version: MaybeCallableVersionLike | None = None,
56
58
  capture_locals: bool = False,
57
59
  max_width: int = RICH_MAX_WIDTH,
@@ -82,21 +84,18 @@ def format_exception_stack(
82
84
 
83
85
  def _yield_header_lines(
84
86
  *,
85
- start: MaybeCallablePyDateTime | None = _START,
87
+ start: MaybeCallableZonedDateTime | None = _START,
86
88
  version: MaybeCallableVersionLike | None = None,
87
89
  ) -> Iterator[str]:
88
90
  """Yield the header lines."""
89
- now = get_now_local()
90
- start_use = get_datetime(datetime=start)
91
- start_use = (
92
- None if start_use is None else start_use.astimezone(get_local_time_zone())
93
- )
94
- yield f"Date/time | {serialize_zoned_datetime(now)}"
95
- start_str = "" if start_use is None else serialize_zoned_datetime(start_use)
91
+ now = get_now()
92
+ start_use = to_zoned_date_time(date_time=start)
93
+ yield f"Date/time | {format_compact(now)}"
94
+ start_str = "" if start_use is None else format_compact(start_use)
96
95
  yield f"Started | {start_str}"
97
- duration = None if start_use is None else (now - start_use)
98
- duration_str = "" if duration is None else serialize_duration(duration)
99
- yield f"Duration | {duration_str}"
96
+ delta = None if start_use is None else (now - start_use)
97
+ delta_str = "" if delta is None else delta.format_common_iso()
98
+ yield f"Duration | {delta_str}"
100
99
  yield f"User | {getuser()}"
101
100
  yield f"Host | {gethostname()}"
102
101
  version_use = "" if version is None else get_version(version=version)
@@ -193,7 +192,7 @@ def _trim_path(path: PathLike, pattern: str, /) -> Path | None:
193
192
 
194
193
  def make_except_hook(
195
194
  *,
196
- start: MaybeCallablePyDateTime | None = _START,
195
+ start: MaybeCallableZonedDateTime | None = _START,
197
196
  version: MaybeCallableVersionLike | None = None,
198
197
  path: MaybeCallablePathLike | None = None,
199
198
  max_width: int = RICH_MAX_WIDTH,
@@ -228,7 +227,7 @@ def _make_except_hook_inner(
228
227
  traceback: TracebackType | None,
229
228
  /,
230
229
  *,
231
- start: MaybeCallablePyDateTime | None = _START,
230
+ start: MaybeCallableZonedDateTime | None = _START,
232
231
  version: MaybeCallableVersionLike | None = None,
233
232
  path: MaybeCallablePathLike | None = None,
234
233
  max_width: int = RICH_MAX_WIDTH,
@@ -247,9 +246,7 @@ def _make_except_hook_inner(
247
246
  _ = sys.stderr.write(f"{slim}\n") # don't 'from sys import stderr'
248
247
  if path is not None:
249
248
  path = (
250
- get_path(path=path)
251
- .joinpath(serialize_compact(get_now_local()))
252
- .with_suffix(".txt")
249
+ get_path(path=path).joinpath(format_compact(get_now())).with_suffix(".txt")
253
250
  )
254
251
  full = format_exception_stack(
255
252
  exc_val,
utilities/typing.py CHANGED
@@ -25,6 +25,16 @@ from typing import get_type_hints as _get_type_hints
25
25
  from uuid import UUID
26
26
  from warnings import warn
27
27
 
28
+ from whenever import (
29
+ Date,
30
+ DateDelta,
31
+ DateTimeDelta,
32
+ PlainDateTime,
33
+ Time,
34
+ TimeDelta,
35
+ ZonedDateTime,
36
+ )
37
+
28
38
  from utilities.iterables import unique_everseen
29
39
  from utilities.sentinel import Sentinel
30
40
  from utilities.types import StrMapping
@@ -133,7 +143,21 @@ def get_type_hints(
133
143
  ) -> dict[str, Any]:
134
144
  """Get the type hints of an object."""
135
145
  result: dict[str, Any] = obj.__annotations__
136
- _ = {Literal, Path, Sentinel, StrMapping, UUID, dt}
146
+ _ = {
147
+ Date,
148
+ DateDelta,
149
+ DateTimeDelta,
150
+ Literal,
151
+ Path,
152
+ PlainDateTime,
153
+ Sentinel,
154
+ StrMapping,
155
+ Time,
156
+ TimeDelta,
157
+ UUID,
158
+ ZonedDateTime,
159
+ dt,
160
+ }
137
161
  globalns_use = globals() | ({} if globalns is None else dict(globalns))
138
162
  localns_use = {} if localns is None else dict(localns)
139
163
  try:
utilities/tzlocal.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
3
  from logging import getLogger
5
4
  from typing import TYPE_CHECKING
6
5
 
@@ -21,30 +20,7 @@ def get_local_time_zone() -> ZoneInfo:
21
20
 
22
21
 
23
22
  LOCAL_TIME_ZONE = get_local_time_zone()
23
+ LOCAL_TIME_ZONE_NAME = LOCAL_TIME_ZONE.key
24
24
 
25
25
 
26
- def get_now_local() -> dt.datetime:
27
- """Get the current local time."""
28
- return dt.datetime.now(tz=LOCAL_TIME_ZONE)
29
-
30
-
31
- NOW_LOCAL = get_now_local()
32
-
33
-
34
- def get_today_local() -> dt.date:
35
- """Get the current, timezone-aware local date."""
36
- return get_now_local().date()
37
-
38
-
39
- TODAY_LOCAL = get_today_local()
40
-
41
-
42
- __all__ = [
43
- "LOCAL_TIME_ZONE",
44
- "LOCAL_TIME_ZONE",
45
- "NOW_LOCAL",
46
- "TODAY_LOCAL",
47
- "get_local_time_zone",
48
- "get_now_local",
49
- "get_today_local",
50
- ]
26
+ __all__ = ["LOCAL_TIME_ZONE", "LOCAL_TIME_ZONE_NAME", "get_local_time_zone"]
utilities/whenever2.py CHANGED
@@ -16,7 +16,9 @@ from whenever import (
16
16
  ZonedDateTime,
17
17
  )
18
18
 
19
+ from utilities.datetime import maybe_sub_pct_y
19
20
  from utilities.sentinel import Sentinel, sentinel
21
+ from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
20
22
  from utilities.zoneinfo import UTC, get_time_zone_name
21
23
 
22
24
  if TYPE_CHECKING:
@@ -72,6 +74,15 @@ WEEK = DateDelta(weeks=1)
72
74
  ##
73
75
 
74
76
 
77
+ def format_compact(datetime: ZonedDateTime, /) -> str:
78
+ """Convert a zoned datetime to the local time zone, then format."""
79
+ py_datetime = datetime.round().to_tz(LOCAL_TIME_ZONE_NAME).to_plain().py_datetime()
80
+ return py_datetime.strftime(maybe_sub_pct_y("%Y%m%dT%H%M%S"))
81
+
82
+
83
+ ##
84
+
85
+
75
86
  def from_timestamp(i: float, /, *, time_zone: TimeZoneLike = UTC) -> ZonedDateTime:
76
87
  """Get a zoned datetime from a timestamp."""
77
88
  return ZonedDateTime.from_timestamp(i, tz=get_time_zone_name(time_zone))
@@ -255,6 +266,7 @@ __all__ = [
255
266
  "ZONED_DATE_TIME_MAX",
256
267
  "ZONED_DATE_TIME_MIN",
257
268
  "WheneverLogRecord",
269
+ "format_compact",
258
270
  "from_timestamp",
259
271
  "from_timestamp_millis",
260
272
  "from_timestamp_nanos",