dycw-utilities 0.125.24__py3-none-any.whl → 0.125.26__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.24
3
+ Version: 0.125.26
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=WpvBRRRg9E0dopLOUcZu11h6cdS3FNrVVkS3L8r2LCU,61
1
+ utilities/__init__.py,sha256=Q_deKUGgT21jjkI5E7MKTAtXWcqWJotIcZmKdq_5394,61
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
- utilities/asyncio.py,sha256=qdhfPAKgeJEOKDN2xQbR35dpvYZSWUUqLhHNJEd-pv0,51733
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,9 +66,9 @@ 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
- utilities/sqlalchemy.py,sha256=p8vsHaNRoeq5zJouIKyp9piFM26wtm5yR4DkzCMFDSw,35471
71
+ utilities/sqlalchemy.py,sha256=KraI3PGrIs8ZpTQi0rZzBMjlcPbgWNipXlLySeu4aiY,36765
72
72
  utilities/sqlalchemy_polars.py,sha256=s7hQNep2O5DTgIRXyN_JRQma7a4DAtNd25tshaZW8iw,15490
73
73
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
74
74
  utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
@@ -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.24.dist-info/METADATA,sha256=PTMh4H329ZNF8vQUXJ4Z62OIxQ_NrfmL2jBxEhjvuS0,12852
94
- dycw_utilities-0.125.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.125.24.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.125.24.dist-info/RECORD,,
93
+ dycw_utilities-0.125.26.dist-info/METADATA,sha256=KvzlW9sc8xXbF0-RtEA4gzn8H_mkvGM1pgby4iC-u4A,12852
94
+ dycw_utilities-0.125.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
+ dycw_utilities-0.125.26.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
+ dycw_utilities-0.125.26.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.125.24"
3
+ __version__ = "0.125.26"
utilities/asyncio.py CHANGED
@@ -901,6 +901,7 @@ class Looper(Generic[_T]):
901
901
  timeout: Duration | None | Sentinel = sentinel,
902
902
  timeout_error: type[Exception] | Sentinel = sentinel,
903
903
  _debug: bool | Sentinel = sentinel,
904
+ **kwargs: Any,
904
905
  ) -> Self:
905
906
  """Replace elements of the looper."""
906
907
  return replace_non_sentinel(
@@ -913,6 +914,7 @@ class Looper(Generic[_T]):
913
914
  timeout=timeout,
914
915
  timeout_error=timeout_error,
915
916
  _debug=_debug,
917
+ **kwargs,
916
918
  )
917
919
 
918
920
  def request_restart(self) -> None:
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"]
utilities/sqlalchemy.py CHANGED
@@ -57,7 +57,8 @@ from sqlalchemy.orm import (
57
57
  from sqlalchemy.orm.exc import UnmappedClassError
58
58
  from sqlalchemy.pool import NullPool, Pool
59
59
 
60
- from utilities.asyncio import InfiniteQueueLooper, timeout_dur
60
+ from utilities.asyncio import InfiniteQueueLooper, Looper, timeout_dur
61
+ from utilities.datetime import SECOND
61
62
  from utilities.functions import (
62
63
  ensure_str,
63
64
  get_class_name,
@@ -654,6 +655,42 @@ class UpserterError(Exception):
654
655
  return f"Error running {get_class_name(self.upserter)!r}"
655
656
 
656
657
 
658
+ @dataclass(kw_only=True)
659
+ class UpsertService(Looper[_InsertItem]):
660
+ """Service to upsert items to a database."""
661
+
662
+ # base
663
+ freq: Duration = field(default=SECOND, repr=False)
664
+ backoff: Duration = field(default=SECOND, repr=False)
665
+ empty_upon_exit: bool = field(default=True, repr=False)
666
+ # self
667
+ engine: AsyncEngine
668
+ snake: bool = False
669
+ selected_or_all: _SelectedOrAll = "selected"
670
+ chunk_size_frac: float = CHUNK_SIZE_FRAC
671
+ assume_tables_exist: bool = False
672
+ timeout_create: Duration | None = None
673
+ error_create: type[Exception] = TimeoutError
674
+ timeout_insert: Duration | None = None
675
+ error_insert: type[Exception] = TimeoutError
676
+
677
+ @override
678
+ async def core(self) -> None:
679
+ await super().core()
680
+ await upsert_items(
681
+ self.engine,
682
+ *self.get_all_nowait(),
683
+ snake=self.snake,
684
+ selected_or_all=self.selected_or_all,
685
+ chunk_size_frac=self.chunk_size_frac,
686
+ assume_tables_exist=self.assume_tables_exist,
687
+ timeout_create=self.timeout_create,
688
+ error_create=self.error_create,
689
+ timeout_insert=self.timeout_insert,
690
+ error_insert=self.error_insert,
691
+ )
692
+
693
+
657
694
  ##
658
695
 
659
696
 
@@ -1109,6 +1146,7 @@ __all__ = [
1109
1146
  "InsertItemsError",
1110
1147
  "TablenameMixin",
1111
1148
  "UpsertItemsError",
1149
+ "UpsertService",
1112
1150
  "Upserter",
1113
1151
  "UpserterError",
1114
1152
  "check_engine",