dycw-utilities 0.135.0__py3-none-any.whl → 0.178.1__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.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +13 -10
- utilities/asyncio.py +312 -787
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +195 -77
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +28 -59
- utilities/fpdf2.py +2 -2
- utilities/functions.py +24 -269
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +513 -159
- utilities/importlib.py +17 -1
- utilities/inflect.py +12 -4
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +4 -7
- utilities/logging.py +136 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +43 -45
- utilities/operator.py +27 -27
- utilities/orjson.py +189 -36
- utilities/os.py +61 -4
- utilities/packaging.py +115 -0
- utilities/parse.py +8 -5
- utilities/pathlib.py +269 -40
- utilities/permissions.py +298 -0
- utilities/platform.py +7 -6
- utilities/polars.py +1205 -413
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +43 -19
- utilities/pqdm.py +3 -3
- utilities/psutil.py +5 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -52
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +7 -7
- utilities/pytest.py +104 -143
- utilities/pytest_plugins/__init__.py +1 -0
- utilities/pytest_plugins/pytest_randomly.py +23 -0
- utilities/pytest_plugins/pytest_regressions.py +56 -0
- utilities/pytest_regressions.py +26 -46
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +220 -343
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +35 -104
- utilities/sqlalchemy.py +496 -471
- utilities/sqlalchemy_polars.py +29 -54
- utilities/string.py +2 -3
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +70 -35
- utilities/types.py +102 -30
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1559 -361
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.135.0.dist-info/METADATA +0 -39
- dycw_utilities-0.135.0.dist-info/RECORD +0 -96
- dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
- dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/arq.py +0 -216
- utilities/eventkit.py +0 -388
- utilities/luigi.py +0 -183
- utilities/period.py +0 -152
- utilities/pudb.py +0 -62
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -123
utilities/sentinel.py
CHANGED
|
@@ -4,6 +4,8 @@ from dataclasses import dataclass
|
|
|
4
4
|
from re import IGNORECASE, search
|
|
5
5
|
from typing import Any, override
|
|
6
6
|
|
|
7
|
+
from typing_extensions import TypeIs
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class _Meta(type):
|
|
9
11
|
"""Metaclass for the sentinel."""
|
|
@@ -34,6 +36,13 @@ class Sentinel(metaclass=_Meta):
|
|
|
34
36
|
|
|
35
37
|
sentinel = Sentinel()
|
|
36
38
|
|
|
39
|
+
##
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]:
|
|
43
|
+
"""Check if an object is the sentinel."""
|
|
44
|
+
return obj is sentinel
|
|
45
|
+
|
|
37
46
|
|
|
38
47
|
##
|
|
39
48
|
|
|
@@ -58,6 +67,7 @@ __all__ = [
|
|
|
58
67
|
"SENTINEL_REPR",
|
|
59
68
|
"ParseSentinelError",
|
|
60
69
|
"Sentinel",
|
|
70
|
+
"is_sentinel",
|
|
61
71
|
"parse_sentinel",
|
|
62
72
|
"sentinel",
|
|
63
73
|
]
|
utilities/shelve.py
CHANGED
|
@@ -12,12 +12,15 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from utilities.types import PathLike
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
type _Flag = Literal["r", "w", "c", "n"]
|
|
16
|
+
|
|
17
|
+
|
|
15
18
|
@contextmanager
|
|
16
19
|
def yield_shelf(
|
|
17
20
|
path: PathLike,
|
|
18
21
|
/,
|
|
19
22
|
*,
|
|
20
|
-
flag:
|
|
23
|
+
flag: _Flag = "c",
|
|
21
24
|
protocol: int | None = None,
|
|
22
25
|
writeback: bool = False,
|
|
23
26
|
) -> Iterator[Shelf[Any]]:
|
utilities/shutil.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import override
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def which(cmd: str, /) -> Path:
|
|
10
|
+
path = shutil.which(cmd)
|
|
11
|
+
if path is None:
|
|
12
|
+
raise WhichError(cmd=cmd)
|
|
13
|
+
return Path(path)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(kw_only=True, slots=True)
|
|
17
|
+
class WhichError(Exception):
|
|
18
|
+
cmd: str
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"{self.cmd!r} not found"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["WhichError", "which"]
|
utilities/slack_sdk.py
CHANGED
|
@@ -2,129 +2,66 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from http import HTTPStatus
|
|
5
|
-
from
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Self, override
|
|
5
|
+
from typing import TYPE_CHECKING, override
|
|
7
6
|
|
|
7
|
+
from slack_sdk.webhook import WebhookClient
|
|
8
8
|
from slack_sdk.webhook.async_client import AsyncWebhookClient
|
|
9
9
|
|
|
10
|
-
from utilities.asyncio import
|
|
10
|
+
from utilities.asyncio import timeout_td
|
|
11
11
|
from utilities.functools import cache
|
|
12
|
-
from utilities.
|
|
13
|
-
from utilities.whenever import MINUTE, SECOND
|
|
12
|
+
from utilities.whenever import MINUTE, to_seconds
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
|
-
from collections.abc import Callable
|
|
17
|
-
|
|
18
15
|
from slack_sdk.webhook import WebhookResponse
|
|
19
16
|
from whenever import TimeDelta
|
|
20
17
|
|
|
21
|
-
from utilities.types import
|
|
18
|
+
from utilities.types import Delta, MaybeType
|
|
22
19
|
|
|
23
20
|
|
|
24
|
-
_TIMEOUT:
|
|
21
|
+
_TIMEOUT: Delta = MINUTE
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
##
|
|
28
25
|
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"""Service to send messages to Slack."""
|
|
37
|
-
|
|
38
|
-
@override
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
*,
|
|
42
|
-
url: str,
|
|
43
|
-
auto_start: bool = False,
|
|
44
|
-
empty_upon_exit: bool = True,
|
|
45
|
-
freq: TimeDelta = SECOND,
|
|
46
|
-
backoff: TimeDelta = SECOND,
|
|
47
|
-
logger: str | None = None,
|
|
48
|
-
timeout: TimeDelta | None = None,
|
|
49
|
-
_debug: bool = False,
|
|
50
|
-
level: int = NOTSET,
|
|
51
|
-
sender: Callable[[str, str], Coro[None]] = _send_adapter,
|
|
52
|
-
send_timeout: TimeDelta = SECOND,
|
|
53
|
-
) -> None:
|
|
54
|
-
Looper.__init__( # Looper first
|
|
55
|
-
self,
|
|
56
|
-
auto_start=auto_start,
|
|
57
|
-
freq=freq,
|
|
58
|
-
empty_upon_exit=empty_upon_exit,
|
|
59
|
-
backoff=backoff,
|
|
60
|
-
logger=logger,
|
|
61
|
-
timeout=timeout,
|
|
62
|
-
_debug=_debug,
|
|
63
|
-
)
|
|
64
|
-
Looper.__post_init__(self)
|
|
65
|
-
Handler.__init__(self, level=level) # Handler next
|
|
66
|
-
self.url = url
|
|
67
|
-
self.sender = sender
|
|
68
|
-
self.send_timeout = send_timeout
|
|
69
|
-
|
|
70
|
-
@override
|
|
71
|
-
def emit(self, record: LogRecord) -> None:
|
|
72
|
-
fmtted = self.format(record)
|
|
73
|
-
try:
|
|
74
|
-
self.put_right_nowait(fmtted)
|
|
75
|
-
except Exception: # noqa: BLE001 # pragma: no cover
|
|
76
|
-
self.handleError(record)
|
|
77
|
-
|
|
78
|
-
@override
|
|
79
|
-
async def core(self) -> None:
|
|
80
|
-
await super().core()
|
|
81
|
-
if self.empty():
|
|
82
|
-
return
|
|
83
|
-
text = "\n".join(self.get_all_nowait())
|
|
84
|
-
async with timeout_td(self.send_timeout):
|
|
85
|
-
await self.sender(self.url, text)
|
|
86
|
-
|
|
87
|
-
@override
|
|
88
|
-
def replace(
|
|
89
|
-
self,
|
|
90
|
-
*,
|
|
91
|
-
auto_start: bool | Sentinel = sentinel,
|
|
92
|
-
empty_upon_exit: bool | Sentinel = sentinel,
|
|
93
|
-
freq: TimeDelta | Sentinel = sentinel,
|
|
94
|
-
backoff: TimeDelta | Sentinel = sentinel,
|
|
95
|
-
logger: str | None | Sentinel = sentinel,
|
|
96
|
-
timeout: TimeDelta | None | Sentinel = sentinel,
|
|
97
|
-
_debug: bool | Sentinel = sentinel,
|
|
98
|
-
**kwargs: Any,
|
|
99
|
-
) -> Self:
|
|
100
|
-
"""Replace elements of the looper."""
|
|
101
|
-
return super().replace(
|
|
102
|
-
url=self.url,
|
|
103
|
-
auto_start=auto_start,
|
|
104
|
-
empty_upon_exit=empty_upon_exit,
|
|
105
|
-
freq=freq,
|
|
106
|
-
backoff=backoff,
|
|
107
|
-
logger=logger,
|
|
108
|
-
timeout=timeout,
|
|
109
|
-
_debug=_debug,
|
|
110
|
-
**kwargs,
|
|
111
|
-
)
|
|
27
|
+
def send_to_slack(url: str, text: str, /, *, timeout: TimeDelta = _TIMEOUT) -> None:
|
|
28
|
+
"""Send a message via Slack synchronously."""
|
|
29
|
+
client = _get_client(url, timeout=timeout)
|
|
30
|
+
response = client.send(text=text)
|
|
31
|
+
if response.status_code != HTTPStatus.OK: # pragma: no cover
|
|
32
|
+
raise SendToSlackError(text=text, response=response)
|
|
112
33
|
|
|
113
34
|
|
|
114
|
-
|
|
35
|
+
@cache
|
|
36
|
+
def _get_client(url: str, /, *, timeout: Delta = _TIMEOUT) -> WebhookClient:
|
|
37
|
+
"""Get the Slack client."""
|
|
38
|
+
return WebhookClient(url, timeout=to_seconds(timeout))
|
|
115
39
|
|
|
116
40
|
|
|
117
|
-
async def
|
|
118
|
-
url: str,
|
|
41
|
+
async def send_to_slack_async(
|
|
42
|
+
url: str,
|
|
43
|
+
text: str,
|
|
44
|
+
/,
|
|
45
|
+
*,
|
|
46
|
+
timeout: TimeDelta = _TIMEOUT,
|
|
47
|
+
error: MaybeType[BaseException] = TimeoutError,
|
|
119
48
|
) -> None:
|
|
120
49
|
"""Send a message via Slack."""
|
|
121
|
-
client =
|
|
122
|
-
async with timeout_td(timeout):
|
|
50
|
+
client = _get_async_client(url, timeout=timeout)
|
|
51
|
+
async with timeout_td(timeout, error=error):
|
|
123
52
|
response = await client.send(text=text)
|
|
124
53
|
if response.status_code != HTTPStatus.OK: # pragma: no cover
|
|
125
54
|
raise SendToSlackError(text=text, response=response)
|
|
126
55
|
|
|
127
56
|
|
|
57
|
+
@cache
|
|
58
|
+
def _get_async_client(
|
|
59
|
+
url: str, /, *, timeout: TimeDelta = _TIMEOUT
|
|
60
|
+
) -> AsyncWebhookClient:
|
|
61
|
+
"""Get the Slack client."""
|
|
62
|
+
return AsyncWebhookClient(url, timeout=to_seconds(timeout))
|
|
63
|
+
|
|
64
|
+
|
|
128
65
|
@dataclass(kw_only=True, slots=True)
|
|
129
66
|
class SendToSlackError(Exception):
|
|
130
67
|
text: str
|
|
@@ -134,13 +71,7 @@ class SendToSlackError(Exception):
|
|
|
134
71
|
def __str__(self) -> str:
|
|
135
72
|
code = self.response.status_code # pragma: no cover
|
|
136
73
|
phrase = HTTPStatus(code).phrase # pragma: no cover
|
|
137
|
-
return f"Error sending to Slack
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@cache
|
|
141
|
-
def _get_client(url: str, /, *, timeout: TimeDelta = _TIMEOUT) -> AsyncWebhookClient:
|
|
142
|
-
"""Get the Slack client."""
|
|
143
|
-
return AsyncWebhookClient(url, timeout=round(timeout.in_seconds()))
|
|
74
|
+
return f"Error sending to Slack; got error code {code} ({phrase})" # pragma: no cover
|
|
144
75
|
|
|
145
76
|
|
|
146
|
-
__all__ = ["SendToSlackError", "
|
|
77
|
+
__all__ = ["SendToSlackError", "send_to_slack", "send_to_slack_async"]
|