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/period.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING, Self, TypedDict, override
|
|
5
|
-
from zoneinfo import ZoneInfo
|
|
6
|
-
|
|
7
|
-
from whenever import Date, DateDelta, TimeDelta, ZonedDateTime
|
|
8
|
-
|
|
9
|
-
from utilities.dataclasses import replace_non_sentinel
|
|
10
|
-
from utilities.functions import get_class_name
|
|
11
|
-
from utilities.sentinel import Sentinel, sentinel
|
|
12
|
-
from utilities.zoneinfo import get_time_zone_name
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from utilities.types import TimeZoneLike
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class _PeriodAsDict[T: (Date, ZonedDateTime)](TypedDict):
|
|
19
|
-
start: T
|
|
20
|
-
end: T
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
24
|
-
class DatePeriod:
|
|
25
|
-
"""A period of dates."""
|
|
26
|
-
|
|
27
|
-
start: Date
|
|
28
|
-
end: Date
|
|
29
|
-
|
|
30
|
-
def __post_init__(self) -> None:
|
|
31
|
-
if self.start > self.end:
|
|
32
|
-
raise _PeriodInvalidError(start=self.start, end=self.end)
|
|
33
|
-
|
|
34
|
-
def __add__(self, other: DateDelta, /) -> Self:
|
|
35
|
-
"""Offset the period."""
|
|
36
|
-
return self.replace(start=self.start + other, end=self.end + other)
|
|
37
|
-
|
|
38
|
-
def __contains__(self, other: Date, /) -> bool:
|
|
39
|
-
"""Check if a date/datetime lies in the period."""
|
|
40
|
-
return self.start <= other <= self.end
|
|
41
|
-
|
|
42
|
-
@override
|
|
43
|
-
def __repr__(self) -> str:
|
|
44
|
-
cls = get_class_name(self)
|
|
45
|
-
return f"{cls}({self.start}, {self.end})"
|
|
46
|
-
|
|
47
|
-
def __sub__(self, other: DateDelta, /) -> Self:
|
|
48
|
-
"""Offset the period."""
|
|
49
|
-
return self.replace(start=self.start - other, end=self.end - other)
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def delta(self) -> DateDelta:
|
|
53
|
-
"""The delta of the period."""
|
|
54
|
-
return self.end - self.start
|
|
55
|
-
|
|
56
|
-
def replace(
|
|
57
|
-
self, *, start: Date | Sentinel = sentinel, end: Date | Sentinel = sentinel
|
|
58
|
-
) -> Self:
|
|
59
|
-
"""Replace elements of the period."""
|
|
60
|
-
return replace_non_sentinel(self, start=start, end=end)
|
|
61
|
-
|
|
62
|
-
def to_dict(self) -> _PeriodAsDict[Date]:
|
|
63
|
-
"""Convert the period to a dictionary."""
|
|
64
|
-
return _PeriodAsDict(start=self.start, end=self.end)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@dataclass(repr=False, order=True, unsafe_hash=True, kw_only=False)
|
|
68
|
-
class ZonedDateTimePeriod:
|
|
69
|
-
"""A period of time."""
|
|
70
|
-
|
|
71
|
-
start: ZonedDateTime
|
|
72
|
-
end: ZonedDateTime
|
|
73
|
-
|
|
74
|
-
def __post_init__(self) -> None:
|
|
75
|
-
if self.start > self.end:
|
|
76
|
-
raise _PeriodInvalidError(start=self.start, end=self.end)
|
|
77
|
-
if self.start.tz != self.end.tz:
|
|
78
|
-
raise _PeriodTimeZoneError(
|
|
79
|
-
start=ZoneInfo(self.start.tz), end=ZoneInfo(self.end.tz)
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
def __add__(self, other: TimeDelta, /) -> Self:
|
|
83
|
-
"""Offset the period."""
|
|
84
|
-
return self.replace(start=self.start + other, end=self.end + other)
|
|
85
|
-
|
|
86
|
-
def __contains__(self, other: ZonedDateTime, /) -> bool:
|
|
87
|
-
"""Check if a date/datetime lies in the period."""
|
|
88
|
-
return self.start <= other <= self.end
|
|
89
|
-
|
|
90
|
-
@override
|
|
91
|
-
def __repr__(self) -> str:
|
|
92
|
-
cls = get_class_name(self)
|
|
93
|
-
return f"{cls}({self.start.to_plain()}, {self.end.to_plain()}[{self.time_zone.key}])"
|
|
94
|
-
|
|
95
|
-
def __sub__(self, other: TimeDelta, /) -> Self:
|
|
96
|
-
"""Offset the period."""
|
|
97
|
-
return self.replace(start=self.start - other, end=self.end - other)
|
|
98
|
-
|
|
99
|
-
@property
|
|
100
|
-
def delta(self) -> TimeDelta:
|
|
101
|
-
"""The duration of the period."""
|
|
102
|
-
return self.end - self.start
|
|
103
|
-
|
|
104
|
-
def replace(
|
|
105
|
-
self,
|
|
106
|
-
*,
|
|
107
|
-
start: ZonedDateTime | Sentinel = sentinel,
|
|
108
|
-
end: ZonedDateTime | Sentinel = sentinel,
|
|
109
|
-
) -> Self:
|
|
110
|
-
"""Replace elements of the period."""
|
|
111
|
-
return replace_non_sentinel(self, start=start, end=end)
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def time_zone(self) -> ZoneInfo:
|
|
115
|
-
"""The time zone of the period."""
|
|
116
|
-
return ZoneInfo(self.start.tz)
|
|
117
|
-
|
|
118
|
-
def to_dict(self) -> _PeriodAsDict[ZonedDateTime]:
|
|
119
|
-
"""Convert the period to a dictionary."""
|
|
120
|
-
return _PeriodAsDict(start=self.start, end=self.end)
|
|
121
|
-
|
|
122
|
-
def to_tz(self, time_zone: TimeZoneLike, /) -> Self:
|
|
123
|
-
"""Convert the time zone."""
|
|
124
|
-
tz = get_time_zone_name(time_zone)
|
|
125
|
-
return self.replace(start=self.start.to_tz(tz), end=self.end.to_tz(tz))
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@dataclass(kw_only=True, slots=True)
|
|
129
|
-
class PeriodError(Exception): ...
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@dataclass(kw_only=True, slots=True)
|
|
133
|
-
class _PeriodInvalidError[T: (Date, ZonedDateTime)](PeriodError):
|
|
134
|
-
start: T
|
|
135
|
-
end: T
|
|
136
|
-
|
|
137
|
-
@override
|
|
138
|
-
def __str__(self) -> str:
|
|
139
|
-
return f"Invalid period; got {self.start} > {self.end}"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@dataclass(kw_only=True, slots=True)
|
|
143
|
-
class _PeriodTimeZoneError(PeriodError):
|
|
144
|
-
start: ZoneInfo
|
|
145
|
-
end: ZoneInfo
|
|
146
|
-
|
|
147
|
-
@override
|
|
148
|
-
def __str__(self) -> str:
|
|
149
|
-
return f"Period must contain exactly one time zone; got {self.start} and {self.end}"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
__all__ = ["DatePeriod", "PeriodError", "ZonedDateTimePeriod"]
|
utilities/pudb.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from asyncio import iscoroutinefunction
|
|
4
|
-
from functools import partial, wraps
|
|
5
|
-
from typing import TYPE_CHECKING, Any, NoReturn, cast, overload
|
|
6
|
-
|
|
7
|
-
from utilities.os import GetEnvVarError, get_env_var
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from collections.abc import Callable
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
_ENV_VAR = "DEBUG"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@overload
|
|
17
|
-
def call_pudb[F: Callable](func: F, /, *, env_var: str = _ENV_VAR) -> F: ...
|
|
18
|
-
@overload
|
|
19
|
-
def call_pudb[F: Callable](
|
|
20
|
-
func: None = None, /, *, env_var: str = _ENV_VAR
|
|
21
|
-
) -> Callable[[F], F]: ...
|
|
22
|
-
def call_pudb[F: Callable](
|
|
23
|
-
func: F | None = None, /, *, env_var: str = _ENV_VAR
|
|
24
|
-
) -> F | Callable[[F], F]:
|
|
25
|
-
"""Call `pudb` upon failure, if the required environment variable is set."""
|
|
26
|
-
if func is None:
|
|
27
|
-
result = partial(call_pudb, env_var=env_var)
|
|
28
|
-
return cast("Callable[[F], F]", result)
|
|
29
|
-
|
|
30
|
-
if not iscoroutinefunction(func):
|
|
31
|
-
|
|
32
|
-
@wraps(func)
|
|
33
|
-
def wrapped_sync(*args: Any, **kwargs: Any) -> Any:
|
|
34
|
-
try:
|
|
35
|
-
return func(*args, **kwargs)
|
|
36
|
-
except Exception as error: # noqa: BLE001
|
|
37
|
-
_call_pudb(error, env_var=env_var)
|
|
38
|
-
|
|
39
|
-
return cast("F", wrapped_sync)
|
|
40
|
-
|
|
41
|
-
@wraps(func)
|
|
42
|
-
async def wrapped_async(*args: Any, **kwargs: Any) -> Any:
|
|
43
|
-
try:
|
|
44
|
-
return await func(*args, **kwargs)
|
|
45
|
-
except Exception as error: # noqa: BLE001
|
|
46
|
-
_call_pudb(error, env_var=env_var)
|
|
47
|
-
|
|
48
|
-
return cast("F", wrapped_async)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _call_pudb(error: Exception, /, *, env_var: str = _ENV_VAR) -> NoReturn:
|
|
52
|
-
try:
|
|
53
|
-
_ = get_env_var(env_var)
|
|
54
|
-
except GetEnvVarError:
|
|
55
|
-
raise error from None
|
|
56
|
-
from pudb import post_mortem # pragma: no cover
|
|
57
|
-
|
|
58
|
-
post_mortem() # pragma: no cover
|
|
59
|
-
raise error # pragma: no cover
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
__all__ = ["call_pudb"]
|
utilities/python_dotenv.py
DELETED
|
@@ -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"]
|
utilities/typed_settings.py
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from ipaddress import IPv4Address, IPv6Address
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from re import search
|
|
7
|
-
from typing import TYPE_CHECKING, Any, override
|
|
8
|
-
|
|
9
|
-
import typed_settings
|
|
10
|
-
from typed_settings import EnvLoader, FileLoader, find
|
|
11
|
-
from typed_settings.converters import TSConverter
|
|
12
|
-
from typed_settings.loaders import TomlFormat
|
|
13
|
-
from whenever import (
|
|
14
|
-
Date,
|
|
15
|
-
DateDelta,
|
|
16
|
-
DateTimeDelta,
|
|
17
|
-
PlainDateTime,
|
|
18
|
-
Time,
|
|
19
|
-
TimeDelta,
|
|
20
|
-
ZonedDateTime,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
from utilities.iterables import always_iterable
|
|
24
|
-
from utilities.whenever import Freq
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from collections.abc import Callable
|
|
28
|
-
|
|
29
|
-
from typed_settings.loaders import Loader
|
|
30
|
-
from typed_settings.processors import Processor
|
|
31
|
-
|
|
32
|
-
from utilities.types import MaybeIterable, PathLike
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ExtendedTSConverter(TSConverter):
|
|
39
|
-
"""An extension of the TSConverter for custom types."""
|
|
40
|
-
|
|
41
|
-
@override
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
*,
|
|
45
|
-
resolve_paths: bool = True,
|
|
46
|
-
strlist_sep: str | Callable[[str], list] | None = ":",
|
|
47
|
-
) -> None:
|
|
48
|
-
super().__init__(resolve_paths=resolve_paths, strlist_sep=strlist_sep)
|
|
49
|
-
cases: list[tuple[type[Any], Callable[..., Any]]] = [
|
|
50
|
-
(Date, Date.parse_common_iso),
|
|
51
|
-
(DateDelta, DateDelta.parse_common_iso),
|
|
52
|
-
(DateTimeDelta, DateTimeDelta.parse_common_iso),
|
|
53
|
-
(Freq, Freq.parse),
|
|
54
|
-
(IPv4Address, IPv4Address),
|
|
55
|
-
(IPv6Address, IPv6Address),
|
|
56
|
-
(PlainDateTime, PlainDateTime.parse_common_iso),
|
|
57
|
-
(Time, Time.parse_common_iso),
|
|
58
|
-
(TimeDelta, TimeDelta.parse_common_iso),
|
|
59
|
-
(ZonedDateTime, ZonedDateTime.parse_common_iso),
|
|
60
|
-
]
|
|
61
|
-
extras = {cls: _make_converter(cls, func) for cls, func in cases}
|
|
62
|
-
self.scalar_converters |= extras
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _make_converter[T](
|
|
66
|
-
cls: type[T], parser: Callable[[str], T], /
|
|
67
|
-
) -> Callable[[Any, type[Any]], Any]:
|
|
68
|
-
def hook(value: T | str, _: type[T] = cls, /) -> Any:
|
|
69
|
-
if not isinstance(value, (cls, str)): # pragma: no cover
|
|
70
|
-
msg = f"Invalid type {type(value).__name__!r}; expected '{cls.__name__}' or 'str'"
|
|
71
|
-
raise TypeError(msg)
|
|
72
|
-
if isinstance(value, str):
|
|
73
|
-
return parser(value)
|
|
74
|
-
return value
|
|
75
|
-
|
|
76
|
-
return hook
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
_BASE_DIR: Path = Path()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def load_settings[T](
|
|
85
|
-
cls: type[T],
|
|
86
|
-
app_name: str,
|
|
87
|
-
/,
|
|
88
|
-
*,
|
|
89
|
-
filenames: MaybeIterable[str] = "settings.toml",
|
|
90
|
-
start_dir: PathLike | None = None,
|
|
91
|
-
loaders: MaybeIterable[Loader] | None = None,
|
|
92
|
-
processors: MaybeIterable[Processor] = (),
|
|
93
|
-
base_dir: Path = _BASE_DIR,
|
|
94
|
-
) -> T:
|
|
95
|
-
if not search(r"^[A-Za-z]+(?:_[A-Za-z]+)*$", app_name):
|
|
96
|
-
raise LoadSettingsError(appname=app_name)
|
|
97
|
-
filenames_use = list(always_iterable(filenames))
|
|
98
|
-
start_dir_use = None if start_dir is None else Path(start_dir)
|
|
99
|
-
files = [find(filename, start_dir=start_dir_use) for filename in filenames_use]
|
|
100
|
-
file_loader = FileLoader(formats={"*.toml": TomlFormat(app_name)}, files=files)
|
|
101
|
-
env_loader = EnvLoader(f"{app_name.upper()}__", nested_delimiter="__")
|
|
102
|
-
loaders_use: list[Loader] = [file_loader, env_loader]
|
|
103
|
-
if loaders is not None:
|
|
104
|
-
loaders_use.extend(always_iterable(loaders))
|
|
105
|
-
return typed_settings.load_settings(
|
|
106
|
-
cls,
|
|
107
|
-
loaders_use,
|
|
108
|
-
processors=list(always_iterable(processors)),
|
|
109
|
-
converter=ExtendedTSConverter(),
|
|
110
|
-
base_dir=base_dir,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@dataclass(kw_only=True, slots=True)
|
|
115
|
-
class LoadSettingsError(Exception):
|
|
116
|
-
appname: str
|
|
117
|
-
|
|
118
|
-
@override
|
|
119
|
-
def __str__(self) -> str:
|
|
120
|
-
return f"Invalid app name; got {self.appname!r}"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
__all__ = ["ExtendedTSConverter", "LoadSettingsError", "load_settings"]
|