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.

Files changed (97) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +13 -10
  7. utilities/asyncio.py +312 -787
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +195 -77
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +28 -59
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +24 -269
  23. utilities/git.py +9 -30
  24. utilities/grp.py +28 -0
  25. utilities/gzip.py +31 -0
  26. utilities/http.py +3 -2
  27. utilities/hypothesis.py +513 -159
  28. utilities/importlib.py +17 -1
  29. utilities/inflect.py +12 -4
  30. utilities/iterables.py +33 -58
  31. utilities/jinja2.py +148 -0
  32. utilities/json.py +70 -0
  33. utilities/libcst.py +38 -17
  34. utilities/lightweight_charts.py +4 -7
  35. utilities/logging.py +136 -93
  36. utilities/math.py +8 -4
  37. utilities/more_itertools.py +43 -45
  38. utilities/operator.py +27 -27
  39. utilities/orjson.py +189 -36
  40. utilities/os.py +61 -4
  41. utilities/packaging.py +115 -0
  42. utilities/parse.py +8 -5
  43. utilities/pathlib.py +269 -40
  44. utilities/permissions.py +298 -0
  45. utilities/platform.py +7 -6
  46. utilities/polars.py +1205 -413
  47. utilities/polars_ols.py +1 -1
  48. utilities/postgres.py +408 -0
  49. utilities/pottery.py +43 -19
  50. utilities/pqdm.py +3 -3
  51. utilities/psutil.py +5 -57
  52. utilities/pwd.py +28 -0
  53. utilities/pydantic.py +4 -52
  54. utilities/pydantic_settings.py +240 -0
  55. utilities/pydantic_settings_sops.py +76 -0
  56. utilities/pyinstrument.py +7 -7
  57. utilities/pytest.py +104 -143
  58. utilities/pytest_plugins/__init__.py +1 -0
  59. utilities/pytest_plugins/pytest_randomly.py +23 -0
  60. utilities/pytest_plugins/pytest_regressions.py +56 -0
  61. utilities/pytest_regressions.py +26 -46
  62. utilities/random.py +11 -6
  63. utilities/re.py +1 -1
  64. utilities/redis.py +220 -343
  65. utilities/sentinel.py +10 -0
  66. utilities/shelve.py +4 -1
  67. utilities/shutil.py +25 -0
  68. utilities/slack_sdk.py +35 -104
  69. utilities/sqlalchemy.py +496 -471
  70. utilities/sqlalchemy_polars.py +29 -54
  71. utilities/string.py +2 -3
  72. utilities/subprocess.py +1977 -0
  73. utilities/tempfile.py +112 -4
  74. utilities/testbook.py +50 -0
  75. utilities/text.py +174 -42
  76. utilities/throttle.py +158 -0
  77. utilities/timer.py +2 -2
  78. utilities/traceback.py +70 -35
  79. utilities/types.py +102 -30
  80. utilities/typing.py +479 -19
  81. utilities/uuid.py +42 -5
  82. utilities/version.py +27 -26
  83. utilities/whenever.py +1559 -361
  84. utilities/zoneinfo.py +80 -22
  85. dycw_utilities-0.135.0.dist-info/METADATA +0 -39
  86. dycw_utilities-0.135.0.dist-info/RECORD +0 -96
  87. dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
  88. dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
  89. utilities/aiolimiter.py +0 -25
  90. utilities/arq.py +0 -216
  91. utilities/eventkit.py +0 -388
  92. utilities/luigi.py +0 -183
  93. utilities/period.py +0 -152
  94. utilities/pudb.py +0 -62
  95. utilities/python_dotenv.py +0 -101
  96. utilities/streamlit.py +0 -105
  97. 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: Literal["r", "w", "c", "n"] = "c",
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 logging import NOTSET, Handler, LogRecord
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 Looper, timeout_td
10
+ from utilities.asyncio import timeout_td
11
11
  from utilities.functools import cache
12
- from utilities.sentinel import Sentinel, sentinel
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 Coro
18
+ from utilities.types import Delta, MaybeType
22
19
 
23
20
 
24
- _TIMEOUT: TimeDelta = MINUTE
21
+ _TIMEOUT: Delta = MINUTE
25
22
 
26
23
 
27
24
  ##
28
25
 
29
26
 
30
- async def _send_adapter(url: str, text: str, /) -> None:
31
- await send_to_slack(url, text) # pragma: no cover
32
-
33
-
34
- @dataclass(init=False, unsafe_hash=True)
35
- class SlackHandlerService(Handler, Looper[str]):
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 send_to_slack(
118
- url: str, text: str, /, *, timeout: TimeDelta = _TIMEOUT
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 = _get_client(url, timeout=timeout)
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:\n\n{self.text}\n\n{code}: {phrase}" # pragma: no cover
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", "SlackHandlerService", "send_to_slack"]
77
+ __all__ = ["SendToSlackError", "send_to_slack", "send_to_slack_async"]