dycw-utilities 0.146.9__py3-none-any.whl → 0.147.0__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.146.9
3
+ Version: 0.147.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,5 +1,4 @@
1
- utilities/__init__.py,sha256=TuChRb5OgNhC1Cunka6x7TS6wN_8B5wyF2sdqO2Ugew,60
2
- utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
1
+ utilities/__init__.py,sha256=0p37XbV8mEhPdZc0CQpeIny7GldujNef8ClFcakI1lk,60
3
2
  utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
4
3
  utilities/asyncio.py,sha256=aB0EtUbUJ5ZKQ5ET-Xfyx6wfUJG2G4vihEX0blK4TGE,14964
5
4
  utilities/atomicwrites.py,sha256=xcOWenTBRS0oat3kg7Sqe51AohNThMQ2ixPL7QCG8hw,5795
@@ -54,11 +53,9 @@ utilities/pottery.py,sha256=doqA59CMGvErUTK3vubTUe1sYqY90VvdnIjws4W5Ta0,6953
54
53
  utilities/pqdm.py,sha256=BTsYPtbKQWwX-iXF4qCkfPG7DPxIB54J989n83bXrIo,3092
55
54
  utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
56
55
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- utilities/pydantic.py,sha256=RlsFoRMzMAMHEf7Gp-0wqfUvOG_GYeB3ZAqGu4boT_E,1722
58
56
  utilities/pyinstrument.py,sha256=E3U4T6qzTGVcGzAiShl9BL3acO-uu_dHZVgSbkSBd7A,864
59
57
  utilities/pytest.py,sha256=TUguQEeUiftUWW-aLJEQmZr-J6W2bwpkUt9Y-q2ua14,7904
60
58
  utilities/pytest_regressions.py,sha256=50h6hqX60U6dkYaB6tW-XEDhSH4y_FQ5LgJ0pGF1Wo4,4102
61
- utilities/python_dotenv.py,sha256=dYooRYwqrvhSoZWuiVbCiKUWiS-M5b5yv2zDWGYPEvI,3209
62
59
  utilities/random.py,sha256=YWYzWxQDeyJRiuHGnO1OxF6dDucpq7qc1tH_ealwCRg,4130
63
60
  utilities/re.py,sha256=6qxeV0rQZaBDKWcB7apSBmxtg_XzoGY-EdegTkMn-ZY,4578
64
61
  utilities/redis.py,sha256=MNxDTbTQTkUOtIikJY9UbfozTC3zuJpAUBsw02LLXTA,28377
@@ -71,7 +68,6 @@ utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
71
68
  utilities/sqlalchemy.py,sha256=q2aYUDAC3SE88Lt6XaKa3CLzT_ePaWvQu_OuRk19x9g,35520
72
69
  utilities/sqlalchemy_polars.py,sha256=18AoEbeNJUKF3-5hroNy9J5LQwS_QJAXbMfKc9sChtk,14250
73
70
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
74
- utilities/streamlit.py,sha256=U9PJBaKP1IdSykKhPZhIzSPTZsmLsnwbEPZWzNhJPKk,2955
75
71
  utilities/string.py,sha256=MB0X6UPTUc06JdAdj-PctZ238IXeCjE5dAJibNw6ZrU,587
76
72
  utilities/tempfile.py,sha256=VqmZJAhTJ1OaVywFzk5eqROV8iJbW9XQ_QYAV0bpdRo,1384
77
73
  utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
@@ -92,8 +88,8 @@ utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
92
88
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
93
89
  utilities/pytest_plugins/pytest_randomly.py,sha256=NXzCcGKbpgYouz5yehKb4jmxmi2SexKKpgF4M65bi10,414
94
90
  utilities/pytest_plugins/pytest_regressions.py,sha256=Iwhfv_OJH7UCPZCfoh7ugZ2Xjqjil-BBBsOb8sDwiGI,1471
95
- dycw_utilities-0.146.9.dist-info/METADATA,sha256=g3UwlF7SBgSmylm4_hxRi7glD9L-M9RgeN-DkAxVdK0,1697
96
- dycw_utilities-0.146.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
97
- dycw_utilities-0.146.9.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
98
- dycw_utilities-0.146.9.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
99
- dycw_utilities-0.146.9.dist-info/RECORD,,
91
+ dycw_utilities-0.147.0.dist-info/METADATA,sha256=qzQQx8-fSVY0l6ALS7i4IUgXVYYiznXEYzp5lReRqR4,1697
92
+ dycw_utilities-0.147.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.147.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
94
+ dycw_utilities-0.147.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.147.0.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.146.9"
3
+ __version__ = "0.147.0"
utilities/aiolimiter.py DELETED
@@ -1,25 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from asyncio import get_running_loop
4
- from typing import TYPE_CHECKING
5
-
6
- from aiolimiter import AsyncLimiter
7
-
8
- if TYPE_CHECKING:
9
- from collections.abc import Hashable
10
-
11
- _LIMITERS: dict[tuple[int, Hashable], AsyncLimiter] = {}
12
-
13
-
14
- def get_async_limiter(key: Hashable, /, *, rate: float = 1.0) -> AsyncLimiter:
15
- """Get a loop-aware rate limiter."""
16
- id_ = id(get_running_loop())
17
- full = (id_, key)
18
- try:
19
- return _LIMITERS[full]
20
- except KeyError:
21
- limiter = _LIMITERS[full] = AsyncLimiter(1.0, time_period=rate)
22
- return limiter
23
-
24
-
25
- __all__ = ["get_async_limiter"]
utilities/pydantic.py DELETED
@@ -1,58 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- from typing import TYPE_CHECKING, override
6
-
7
- from pydantic import BaseModel
8
-
9
- from utilities.atomicwrites import writer
10
-
11
- if TYPE_CHECKING:
12
- from utilities.types import PathLike
13
-
14
-
15
- class HashableBaseModel(BaseModel):
16
- """Subclass of BaseModel which is hashable."""
17
-
18
- @override
19
- def __hash__(self) -> int:
20
- return hash((type(self), *self.__dict__.values()))
21
-
22
-
23
- def load_model[T: BaseModel](model: type[T], path: PathLike, /) -> T:
24
- path = Path(path)
25
- try:
26
- return model.model_validate_json(path.read_text())
27
- except FileNotFoundError:
28
- raise _LoadModelFileNotFoundError(model=model, path=path) from None
29
- except IsADirectoryError: # skipif-not-windows
30
- raise _LoadModelIsADirectoryError(model=model, path=path) from None
31
-
32
-
33
- @dataclass(kw_only=True, slots=True)
34
- class LoadModelError(Exception):
35
- model: type[BaseModel]
36
- path: Path
37
-
38
-
39
- @dataclass(kw_only=True, slots=True)
40
- class _LoadModelFileNotFoundError(LoadModelError):
41
- @override
42
- def __str__(self) -> str:
43
- return f"Unable to load {self.model}; path {str(self.path)!r} must exist."
44
-
45
-
46
- @dataclass(kw_only=True, slots=True)
47
- class _LoadModelIsADirectoryError(LoadModelError):
48
- @override
49
- def __str__(self) -> str:
50
- return f"Unable to load {self.model}; path {str(self.path)!r} must not be a directory." # skipif-not-windows
51
-
52
-
53
- def save_model(model: BaseModel, path: PathLike, /, *, overwrite: bool = False) -> None:
54
- with writer(path, overwrite=overwrite) as temp:
55
- _ = temp.write_text(model.model_dump_json())
56
-
57
-
58
- __all__ = ["HashableBaseModel", "LoadModelError", "load_model", "save_model"]
@@ -1,101 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from os import environ
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING, override
7
-
8
- from dotenv import dotenv_values
9
-
10
- from utilities.dataclasses import _ParseDataClassMissingValuesError, parse_dataclass
11
- from utilities.iterables import MergeStrMappingsError, merge_str_mappings
12
- from utilities.pathlib import get_root
13
- from utilities.reprlib import get_repr
14
- from utilities.types import Dataclass
15
-
16
- if TYPE_CHECKING:
17
- from collections.abc import Mapping
18
- from collections.abc import Set as AbstractSet
19
-
20
- from utilities.types import MaybeCallablePathLike, ParseObjectExtra, StrMapping
21
-
22
-
23
- def load_settings[T: Dataclass](
24
- cls: type[T],
25
- /,
26
- *,
27
- path: MaybeCallablePathLike | None = Path.cwd,
28
- globalns: StrMapping | None = None,
29
- localns: StrMapping | None = None,
30
- warn_name_errors: bool = False,
31
- head: bool = False,
32
- case_sensitive: bool = False,
33
- extra_parsers: ParseObjectExtra | None = None,
34
- ) -> T:
35
- """Load a set of settings from the `.env` file."""
36
- path = get_root(path=path).joinpath(".env")
37
- if not path.exists():
38
- raise _LoadSettingsFileNotFoundError(path=path) from None
39
- maybe_values_dotenv = dotenv_values(path)
40
- try:
41
- maybe_values: Mapping[str, str | None] = merge_str_mappings(
42
- maybe_values_dotenv, environ, case_sensitive=case_sensitive
43
- )
44
- except MergeStrMappingsError as error:
45
- raise _LoadSettingsDuplicateKeysError(
46
- path=path,
47
- values=error.mapping,
48
- counts=error.counts,
49
- case_sensitive=case_sensitive,
50
- ) from None
51
- values = {k: v for k, v in maybe_values.items() if v is not None}
52
- try:
53
- return parse_dataclass(
54
- values,
55
- cls,
56
- globalns=globalns,
57
- localns=localns,
58
- warn_name_errors=warn_name_errors,
59
- head=head,
60
- case_sensitive=case_sensitive,
61
- allow_extra_keys=True,
62
- extra_parsers=extra_parsers,
63
- )
64
- except _ParseDataClassMissingValuesError as error:
65
- raise _LoadSettingsMissingKeysError(path=path, fields=error.fields) from None
66
-
67
-
68
- @dataclass(kw_only=True, slots=True)
69
- class LoadSettingsError(Exception):
70
- path: Path
71
-
72
-
73
- @dataclass(kw_only=True, slots=True)
74
- class _LoadSettingsDuplicateKeysError(LoadSettingsError):
75
- values: StrMapping
76
- counts: Mapping[str, int]
77
- case_sensitive: bool = False
78
-
79
- @override
80
- def __str__(self) -> str:
81
- return f"Mapping {get_repr(dict(self.values))} keys must not contain duplicates (modulo case); got {get_repr(self.counts)}"
82
-
83
-
84
- @dataclass(kw_only=True, slots=True)
85
- class _LoadSettingsFileNotFoundError(LoadSettingsError):
86
- @override
87
- def __str__(self) -> str:
88
- return f"Path {str(self.path)!r} must exist"
89
-
90
-
91
- @dataclass(kw_only=True, slots=True)
92
- class _LoadSettingsMissingKeysError(LoadSettingsError):
93
- fields: AbstractSet[str]
94
-
95
- @override
96
- def __str__(self) -> str:
97
- desc = ", ".join(map(repr, sorted(self.fields)))
98
- return f"Unable to load {str(self.path)!r}; missing value(s) for {desc}"
99
-
100
-
101
- __all__ = ["LoadSettingsError", "load_settings"]
utilities/streamlit.py DELETED
@@ -1,105 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from hmac import compare_digest
4
- from typing import TYPE_CHECKING, Literal
5
-
6
- from streamlit import (
7
- button,
8
- empty,
9
- error,
10
- form,
11
- form_submit_button,
12
- markdown,
13
- secrets,
14
- session_state,
15
- stop,
16
- text_input,
17
- )
18
-
19
- if TYPE_CHECKING:
20
- from collections.abc import Callable
21
-
22
- from streamlit.elements.lib.utils import Key
23
- from streamlit.runtime.state import WidgetArgs, WidgetCallback, WidgetKwargs
24
-
25
-
26
- def centered_button(
27
- label: str,
28
- /,
29
- *,
30
- key: Key | None = None,
31
- help: str | None = None, # noqa: A002
32
- on_click: WidgetCallback | None = None,
33
- args: WidgetArgs | None = None,
34
- kwargs: WidgetKwargs | None = None,
35
- type: Literal["primary", "secondary"] = "secondary", # noqa: A002
36
- disabled: bool = False,
37
- use_container_width: bool = False,
38
- ) -> bool:
39
- """Create a centered button."""
40
- style = r"<style>.row-widget.stButton {text-align: center;}</style>"
41
- _ = markdown(style, unsafe_allow_html=True)
42
- with empty():
43
- return button(
44
- label,
45
- key=key,
46
- help=help,
47
- on_click=on_click,
48
- args=args,
49
- kwargs=kwargs,
50
- type=type,
51
- disabled=disabled,
52
- use_container_width=use_container_width,
53
- )
54
-
55
-
56
- _USERNAME = "username"
57
- _PASSWORD = "password" # noqa: S105
58
- _PASSWORD_CORRECT = "password_correct" # noqa: S105
59
-
60
-
61
- def ensure_logged_in(
62
- *,
63
- skip: bool = False,
64
- before_form: Callable[..., None] | None = None,
65
- after_form: Callable[..., None] | None = None,
66
- ) -> None:
67
- """Ensure the user is logged in."""
68
- if not (skip or _check_password(before_form=before_form, after_form=after_form)):
69
- stop()
70
-
71
-
72
- def _check_password(
73
- *,
74
- before_form: Callable[..., None] | None = None,
75
- after_form: Callable[..., None] | None = None,
76
- ) -> bool:
77
- """Return `True` if the user had a correct password."""
78
- if session_state.get("password_correct", False):
79
- return True
80
- if before_form is not None:
81
- before_form()
82
- with form("Credentials"):
83
- _ = text_input("Username", key=_USERNAME)
84
- _ = text_input("Password", type="password", key=_PASSWORD)
85
- _ = form_submit_button("Log in", on_click=_password_entered)
86
- if after_form is not None:
87
- after_form()
88
- if _PASSWORD_CORRECT in session_state:
89
- _ = error("Username/password combination invalid or incorrect")
90
- return False
91
-
92
-
93
- def _password_entered() -> None:
94
- """Check whether a password entered by the user is correct."""
95
- if (session_state[_USERNAME] in secrets["passwords"]) and compare_digest(
96
- session_state[_PASSWORD], secrets.passwords[session_state[_USERNAME]]
97
- ):
98
- session_state[_PASSWORD_CORRECT] = True
99
- del session_state[_PASSWORD]
100
- del session_state[_USERNAME]
101
- else:
102
- session_state[_PASSWORD_CORRECT] = False
103
-
104
-
105
- __all__ = ["ensure_logged_in"]