dycw-utilities 0.125.23__py3-none-any.whl → 0.125.25__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.125.23
3
+ Version: 0.125.25
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
- utilities/__init__.py,sha256=wPKNBX6ygqFYs-IuHSilaZXm0vMIdcuqBGP9j1b_5O8,61
1
+ utilities/__init__.py,sha256=3MYZAqKLfaJb4ielcVRGJ7gJj-yObA5kgfPLoc-wN1Q,61
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
- utilities/asyncio.py,sha256=wtMOjHG85exqdwVBNH-DazTprkUCvMFv-JgY-9REhYs,51259
3
+ utilities/asyncio.py,sha256=K5Kj7rsM0nA17-b7d7mrNgPR1U_NbkfQmTruq5LBLRA,51778
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
5
5
  utilities/atools.py,sha256=IYMuFSFGSKyuQmqD6v5IUtDlz8PPw0Sr87Cub_gRU3M,1168
6
6
  utilities/cachetools.py,sha256=C1zqOg7BYz0IfQFK8e3qaDDgEZxDpo47F15RTfJM37Q,2910
@@ -66,7 +66,7 @@ utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
66
66
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
67
67
  utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
68
68
  utilities/shelve.py,sha256=HZsMwK4tcIfg3sh0gApx4-yjQnrY4o3V3ZRimvRhoW0,738
69
- utilities/slack_sdk.py,sha256=Q8UakiB7qo6SUfaBDB0j1N4b8MuFzaD9lG5HGq7rtuw,3200
69
+ utilities/slack_sdk.py,sha256=h2DiVkcFyYcT5zzZOAo6CSith5BBlHUbXeOJSL1neK8,5948
70
70
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
71
71
  utilities/sqlalchemy.py,sha256=p8vsHaNRoeq5zJouIKyp9piFM26wtm5yR4DkzCMFDSw,35471
72
72
  utilities/sqlalchemy_polars.py,sha256=s7hQNep2O5DTgIRXyN_JRQma7a4DAtNd25tshaZW8iw,15490
@@ -90,7 +90,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
90
90
  utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
91
91
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
92
92
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
93
- dycw_utilities-0.125.23.dist-info/METADATA,sha256=sTzpB8MuDKbN3vHMg9otGuFDqzQ6Ko-_iYSb_9W94QY,12852
94
- dycw_utilities-0.125.23.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.125.23.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.125.23.dist-info/RECORD,,
93
+ dycw_utilities-0.125.25.dist-info/METADATA,sha256=TYTHhscXtNic_ikhuiW7WQN30GUx2y-ay4RXYCt3qFI,12852
94
+ dycw_utilities-0.125.25.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
+ dycw_utilities-0.125.25.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
+ dycw_utilities-0.125.25.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.125.23"
3
+ __version__ = "0.125.25"
utilities/asyncio.py CHANGED
@@ -688,6 +688,7 @@ class Looper(Generic[_T]):
688
688
  auto_start: bool = field(default=False, repr=False)
689
689
  freq: Duration = field(default=SECOND, repr=False)
690
690
  backoff: Duration = field(default=10 * SECOND, repr=False)
691
+ empty_upon_exit: bool = field(default=False, repr=False)
691
692
  logger: str | None = field(default=None, repr=False)
692
693
  timeout: Duration | None = field(default=None, repr=False)
693
694
  timeout_error: type[Exception] = field(default=LooperTimeoutError, repr=False)
@@ -791,6 +792,8 @@ class Looper(Generic[_T]):
791
792
  )
792
793
  _ = await self._stack.__aexit__(exc_type, exc_value, traceback)
793
794
  await self.stop()
795
+ if self.empty_upon_exit:
796
+ await self.run_until_empty()
794
797
  case False:
795
798
  _ = self._debug and self._logger.debug("%s: already exited", self)
796
799
  case _ as never:
@@ -891,23 +894,27 @@ class Looper(Generic[_T]):
891
894
  self,
892
895
  *,
893
896
  auto_start: bool | Sentinel = sentinel,
897
+ empty_upon_exit: bool | Sentinel = sentinel,
894
898
  freq: Duration | Sentinel = sentinel,
895
899
  backoff: Duration | Sentinel = sentinel,
896
900
  logger: str | None | Sentinel = sentinel,
897
901
  timeout: Duration | None | Sentinel = sentinel,
898
902
  timeout_error: type[Exception] | Sentinel = sentinel,
899
903
  _debug: bool | Sentinel = sentinel,
904
+ **kwargs: Any,
900
905
  ) -> Self:
901
906
  """Replace elements of the looper."""
902
907
  return replace_non_sentinel(
903
908
  self,
904
909
  auto_start=auto_start,
910
+ empty_upon_exit=empty_upon_exit,
905
911
  freq=freq,
906
912
  backoff=backoff,
907
913
  logger=logger,
908
914
  timeout=timeout,
909
915
  timeout_error=timeout_error,
910
916
  _debug=_debug,
917
+ **kwargs,
911
918
  )
912
919
 
913
920
  def request_restart(self) -> None:
@@ -1061,6 +1068,13 @@ class Looper(Generic[_T]):
1061
1068
  self._core_successes += 1
1062
1069
  await sleep(self._freq)
1063
1070
 
1071
+ async def run_until_empty(self) -> None:
1072
+ """Run until the queue is empty."""
1073
+ while not self.empty():
1074
+ await self.core()
1075
+ if not self.empty():
1076
+ await sleep(self._freq)
1077
+
1064
1078
  @property
1065
1079
  def stats(self) -> _LooperStats:
1066
1080
  """Return the statistics."""
utilities/slack_sdk.py CHANGED
@@ -3,14 +3,20 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from http import HTTPStatus
5
5
  from logging import NOTSET, Handler, LogRecord
6
- from typing import TYPE_CHECKING, override
6
+ from typing import TYPE_CHECKING, Any, Self, override
7
7
 
8
8
  from slack_sdk.webhook.async_client import AsyncWebhookClient
9
9
 
10
- from utilities.asyncio import InfiniteQueueLooper, timeout_dur
10
+ from utilities.asyncio import (
11
+ InfiniteQueueLooper,
12
+ Looper,
13
+ LooperTimeoutError,
14
+ timeout_dur,
15
+ )
11
16
  from utilities.datetime import MINUTE, SECOND, datetime_duration_to_float
12
17
  from utilities.functools import cache
13
18
  from utilities.math import safe_round
19
+ from utilities.sentinel import Sentinel, sentinel
14
20
 
15
21
  if TYPE_CHECKING:
16
22
  from collections.abc import Callable
@@ -73,6 +79,90 @@ class SlackHandler(Handler, InfiniteQueueLooper[None, str]):
73
79
  await self.sender(self.url, text)
74
80
 
75
81
 
82
+ @dataclass(init=False, unsafe_hash=True)
83
+ class SlackHandlerService(Handler, Looper[str]):
84
+ """Service to send messages to Slack."""
85
+
86
+ @override
87
+ def __init__(
88
+ self,
89
+ *,
90
+ url: str,
91
+ auto_start: bool = False,
92
+ empty_upon_exit: bool = True,
93
+ freq: Duration = SECOND,
94
+ backoff: Duration = SECOND,
95
+ logger: str | None = None,
96
+ timeout: Duration | None = None,
97
+ timeout_error: type[Exception] = LooperTimeoutError,
98
+ _debug: bool = False,
99
+ level: int = NOTSET,
100
+ sender: Callable[[str, str], Coroutine1[None]] = _send_adapter,
101
+ send_timeout: Duration = SECOND,
102
+ ) -> None:
103
+ Looper.__init__( # Looper first
104
+ self,
105
+ auto_start=auto_start,
106
+ freq=freq,
107
+ empty_upon_exit=empty_upon_exit,
108
+ backoff=backoff,
109
+ logger=logger,
110
+ timeout=timeout,
111
+ timeout_error=timeout_error,
112
+ _debug=_debug,
113
+ )
114
+ Looper.__post_init__(self)
115
+ Handler.__init__(self, level=level) # Handler next
116
+ self.url = url
117
+ self.sender = sender
118
+ self.send_timeout = send_timeout
119
+
120
+ @override
121
+ def emit(self, record: LogRecord) -> None:
122
+ fmtted = self.format(record)
123
+ try:
124
+ self.put_right_nowait(fmtted)
125
+ except Exception: # noqa: BLE001 # pragma: no cover
126
+ self.handleError(record)
127
+
128
+ @override
129
+ async def core(self) -> None:
130
+ await super().core()
131
+ if self.empty():
132
+ return
133
+ text = "\n".join(self.get_all_nowait())
134
+ async with timeout_dur(duration=self.send_timeout):
135
+ await self.sender(self.url, text)
136
+
137
+ @override
138
+ def replace(
139
+ self,
140
+ *,
141
+ auto_start: bool | Sentinel = sentinel,
142
+ empty_upon_exit: bool | Sentinel = sentinel,
143
+ freq: Duration | Sentinel = sentinel,
144
+ backoff: Duration | Sentinel = sentinel,
145
+ logger: str | None | Sentinel = sentinel,
146
+ timeout: Duration | None | Sentinel = sentinel,
147
+ timeout_error: type[Exception] | Sentinel = sentinel,
148
+ _debug: bool | Sentinel = sentinel,
149
+ **kwargs: Any,
150
+ ) -> Self:
151
+ """Replace elements of the looper."""
152
+ return super().replace(
153
+ url=self.url,
154
+ auto_start=auto_start,
155
+ empty_upon_exit=empty_upon_exit,
156
+ freq=freq,
157
+ backoff=backoff,
158
+ logger=logger,
159
+ timeout=timeout,
160
+ timeout_error=timeout_error,
161
+ _debug=_debug,
162
+ **kwargs,
163
+ )
164
+
165
+
76
166
  ##
77
167
 
78
168
 
@@ -106,4 +196,4 @@ def _get_client(url: str, /, *, timeout: Duration = _TIMEOUT) -> AsyncWebhookCli
106
196
  return AsyncWebhookClient(url, timeout=timeout_use)
107
197
 
108
198
 
109
- __all__ = ["SendToSlackError", "SlackHandler", "send_to_slack"]
199
+ __all__ = ["SendToSlackError", "SlackHandler", "SlackHandlerService", "send_to_slack"]