dycw-utilities 0.129.7__py3-none-any.whl → 0.129.8__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.129.7
3
+ Version: 0.129.8
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -98,7 +98,6 @@ Requires-Dist: polars-lts-cpu<1.31,>=1.30.0; extra == 'zzz-test-jupyter'
98
98
  Provides-Extra: zzz-test-logging
99
99
  Requires-Dist: atomicwrites<1.5,>=1.4.1; extra == 'zzz-test-logging'
100
100
  Requires-Dist: coloredlogs<15.1,>=15.0.1; extra == 'zzz-test-logging'
101
- Requires-Dist: concurrent-log-handler<0.10,>=0.9.26; extra == 'zzz-test-logging'
102
101
  Requires-Dist: rich<14.1,>=14.0.0; extra == 'zzz-test-logging'
103
102
  Requires-Dist: tomlkit<0.14,>=0.13.2; extra == 'zzz-test-logging'
104
103
  Requires-Dist: tzlocal<5.4,>=5.3.1; extra == 'zzz-test-logging'
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=V6BzEBm23oBvaBdsKWGFjnWWR1qGDDjldECqyydnokk,60
1
+ utilities/__init__.py,sha256=nud8TbHJzm02w0ycVV5vCV-F8CpfmCs3mB9e6p4DrdQ,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=3n5EIcSq2xtEF1i4oR0oY2JmBq3NyugeHKFK39Mt22s,37987
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -29,7 +29,7 @@ utilities/iterables.py,sha256=mDqw2_0MUVp-P8FklgcaVTi2TXduH0MxbhTDzzhSBho,44915
29
29
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
30
30
  utilities/libcst.py,sha256=Jto5ppzRzsxn4AD32IS8n0lbgLYXwsVJB6EY8giNZyY,4974
31
31
  utilities/lightweight_charts.py,sha256=0xNfcsrgFI0R9xL25LtSm-W5yhfBI93qQNT6HyaXAhg,2769
32
- utilities/logging.py,sha256=9vo6vz4sDMoF2nkc8eT-eWpymBb5QboIu0kMhEwPiqk,25206
32
+ utilities/logging.py,sha256=dA54i2gmULBLuEJ2roGYWt9pW2NvNBmx0YxlMns347M,26126
33
33
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
34
34
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
35
35
  utilities/math.py,sha256=-mQgbah-dPJwOEWf3SonrFoVZ2AVxMgpeQ3dfVa-oJA,26764
@@ -86,10 +86,10 @@ utilities/tzlocal.py,sha256=3upDNFBvGh1l9njmLR2z2S6K6VxQSb7QizYGUbAH3JU,960
86
86
  utilities/uuid.py,sha256=jJTFxz-CWgltqNuzmythB7iEQ-Q1mCwPevUfKthZT3c,611
87
87
  utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
88
88
  utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
89
- utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
89
+ utilities/whenever.py,sha256=QbXgFAPuUL7PCp2hajmIP-FFIfIR1J6Y0TxJbeoj60I,18434
90
90
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
91
91
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
92
- dycw_utilities-0.129.7.dist-info/METADATA,sha256=SnavcALe7H7k-aZL5l6A8TunsE1JBzlrep0hD7Y6fkw,12804
93
- dycw_utilities-0.129.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- dycw_utilities-0.129.7.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
- dycw_utilities-0.129.7.dist-info/RECORD,,
92
+ dycw_utilities-0.129.8.dist-info/METADATA,sha256=JaBYjbXepIOBDjYciDSC1_bSCE8VlzxhpOPAs22-nMI,12723
93
+ dycw_utilities-0.129.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ dycw_utilities-0.129.8.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.129.8.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.129.7"
3
+ __version__ = "0.129.8"
utilities/logging.py CHANGED
@@ -107,8 +107,9 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
107
107
  utc: bool = False,
108
108
  atTime: dt.time | None = None,
109
109
  ) -> None:
110
- filename = str(Path(filename))
111
- super().__init__(filename, mode, encoding=encoding, delay=delay, errors=errors)
110
+ path = Path(filename)
111
+ path.parent.mkdir(parents=True, exist_ok=True)
112
+ super().__init__(path, mode, encoding=encoding, delay=delay, errors=errors)
112
113
  self._max_bytes = maxBytes if maxBytes >= 1 else None
113
114
  self._backup_count = backupCount if backupCount >= 1 else None
114
115
  self._filename = Path(self.baseFilename)
@@ -117,7 +118,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
117
118
  self._suffix = self._filename.suffix
118
119
  self._patterns = _compute_rollover_patterns(self._stem, self._suffix)
119
120
  self._time_handler = TimedRotatingFileHandler(
120
- filename,
121
+ path,
121
122
  when=when,
122
123
  interval=interval,
123
124
  backupCount=backupCount,
@@ -415,26 +416,53 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
415
416
 
416
417
  def basic_config(
417
418
  *,
418
- logger: LoggerOrName | None = None,
419
+ obj: LoggerOrName | Handler | None = None,
419
420
  format_: str = "{asctime} | {name} | {levelname:8} | {message}",
421
+ whenever: bool = False,
420
422
  level: LogLevel = "INFO",
423
+ plain: bool = False,
421
424
  ) -> None:
422
425
  """Do the basic config."""
426
+ if whenever:
427
+ format_ = format_.replace("{asctime}", "{zoned_datetime}")
423
428
  datefmt = maybe_sub_pct_y("%Y-%m-%d %H:%M:%S")
424
- if logger is None:
425
- basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
426
- else:
427
- logger_use = get_logger(logger=logger)
428
- logger_use.setLevel(level)
429
- logger_use.addHandler(handler := StreamHandler())
430
- handler.setLevel(level)
431
- try:
432
- from coloredlogs import ColoredFormatter
433
- except ModuleNotFoundError: # pragma: no cover
434
- formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
435
- else:
436
- formatter = ColoredFormatter(fmt=format_, datefmt=datefmt, style="{")
437
- handler.setFormatter(formatter)
429
+ match obj:
430
+ case None:
431
+ basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
432
+ case Logger() as logger:
433
+ logger.setLevel(level)
434
+ logger.addHandler(handler := StreamHandler())
435
+ basic_config(
436
+ obj=handler,
437
+ format_=format_,
438
+ whenever=whenever,
439
+ level=level,
440
+ plain=plain,
441
+ )
442
+ case str() as name:
443
+ basic_config(
444
+ obj=get_logger(logger=name),
445
+ format_=format_,
446
+ whenever=whenever,
447
+ level=level,
448
+ plain=plain,
449
+ )
450
+ case Handler() as handler:
451
+ handler.setLevel(level)
452
+ if plain:
453
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
454
+ else:
455
+ try:
456
+ from coloredlogs import ColoredFormatter
457
+ except ModuleNotFoundError: # pragma: no cover
458
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
459
+ else:
460
+ formatter = ColoredFormatter(
461
+ fmt=format_, datefmt=datefmt, style="{"
462
+ )
463
+ handler.setFormatter(formatter)
464
+ case _ as never:
465
+ assert_never(never)
438
466
 
439
467
 
440
468
  ##
utilities/whenever.py CHANGED
@@ -4,7 +4,9 @@ import datetime as dt
4
4
  import re
5
5
  from contextlib import suppress
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, override
7
+ from functools import cache
8
+ from logging import LogRecord
9
+ from typing import TYPE_CHECKING, Any, override
8
10
 
9
11
  from whenever import (
10
12
  Date,
@@ -33,6 +35,8 @@ from utilities.re import (
33
35
  from utilities.zoneinfo import UTC, ensure_time_zone, get_time_zone_name
34
36
 
35
37
  if TYPE_CHECKING:
38
+ from zoneinfo import ZoneInfo
39
+
36
40
  from utilities.types import (
37
41
  DateLike,
38
42
  DateTimeLike,
@@ -561,6 +565,64 @@ class SerializeZonedDateTimeError(Exception):
561
565
  ##
562
566
 
563
567
 
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
+
564
626
  def _to_datetime_delta(timedelta: dt.timedelta, /) -> DateTimeDelta:
565
627
  """Serialize a timedelta."""
566
628
  total_microseconds = datetime_duration_to_microseconds(timedelta)
@@ -610,6 +672,7 @@ __all__ = [
610
672
  "SerializePlainDateTimeError",
611
673
  "SerializeTimeDeltaError",
612
674
  "SerializeZonedDateTimeError",
675
+ "WheneverLogRecord",
613
676
  "check_valid_zoned_datetime",
614
677
  "ensure_date",
615
678
  "ensure_datetime",