dycw-utilities 0.146.8__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.
- {dycw_utilities-0.146.8.dist-info → dycw_utilities-0.147.0.dist-info}/METADATA +1 -1
- {dycw_utilities-0.146.8.dist-info → dycw_utilities-0.147.0.dist-info}/RECORD +7 -11
- utilities/__init__.py +1 -1
- utilities/whenever.py +2 -27
- utilities/aiolimiter.py +0 -25
- utilities/pydantic.py +0 -58
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- {dycw_utilities-0.146.8.dist-info → dycw_utilities-0.147.0.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.146.8.dist-info → dycw_utilities-0.147.0.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.146.8.dist-info → dycw_utilities-0.147.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,4 @@
|
|
1
|
-
utilities/__init__.py,sha256=
|
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
|
@@ -86,14 +82,14 @@ utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
|
|
86
82
|
utilities/uuid.py,sha256=32p7DGHGM2Btx6PcBvCZvERSWbpupMXqx6FppPoSoTU,612
|
87
83
|
utilities/version.py,sha256=ufhJMmI6KPs1-3wBI71aj5wCukd3sP_m11usLe88DNA,5117
|
88
84
|
utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
89
|
-
utilities/whenever.py,sha256=
|
85
|
+
utilities/whenever.py,sha256=nG9IbFcJTKQJuRxoS35I7ww7pRwKHc1MHxff7vVmJls,44131
|
90
86
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
91
87
|
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.
|
96
|
-
dycw_utilities-0.
|
97
|
-
dycw_utilities-0.
|
98
|
-
dycw_utilities-0.
|
99
|
-
dycw_utilities-0.
|
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
utilities/whenever.py
CHANGED
@@ -316,20 +316,16 @@ def min_max_date(
|
|
316
316
|
max_age: DateDelta | None = None,
|
317
317
|
time_zone: TimeZoneLike = UTC,
|
318
318
|
) -> tuple[Date | None, Date | None]:
|
319
|
-
"""
|
319
|
+
"""Compute the min/max date given a combination of dates/ages."""
|
320
320
|
today = get_today(time_zone=time_zone)
|
321
321
|
min_parts: list[Date] = []
|
322
322
|
if min_date is not None:
|
323
|
-
if min_date > today:
|
324
|
-
raise _MinMaxDateMinDateError(min_date=min_date, today=today)
|
325
323
|
min_parts.append(min_date)
|
326
324
|
if max_age is not None:
|
327
325
|
min_parts.append(today - max_age)
|
328
326
|
min_date_use = max(min_parts, default=None)
|
329
327
|
max_parts: list[Date] = []
|
330
328
|
if max_date is not None:
|
331
|
-
if max_date > today:
|
332
|
-
raise _MinMaxDateMaxDateError(max_date=max_date, today=today)
|
333
329
|
max_parts.append(max_date)
|
334
330
|
if min_age is not None:
|
335
331
|
max_parts.append(today - min_age)
|
@@ -344,34 +340,13 @@ def min_max_date(
|
|
344
340
|
|
345
341
|
|
346
342
|
@dataclass(kw_only=True, slots=True)
|
347
|
-
class MinMaxDateError(Exception):
|
348
|
-
|
349
|
-
|
350
|
-
@dataclass(kw_only=True, slots=True)
|
351
|
-
class _MinMaxDateMinDateError(MinMaxDateError):
|
343
|
+
class MinMaxDateError(Exception):
|
352
344
|
min_date: Date
|
353
|
-
today: Date
|
354
|
-
|
355
|
-
@override
|
356
|
-
def __str__(self) -> str:
|
357
|
-
return f"Min date must be at most today; got {self.min_date} > {self.today}"
|
358
|
-
|
359
|
-
|
360
|
-
@dataclass(kw_only=True, slots=True)
|
361
|
-
class _MinMaxDateMaxDateError(MinMaxDateError):
|
362
345
|
max_date: Date
|
363
|
-
today: Date
|
364
|
-
|
365
|
-
@override
|
366
|
-
def __str__(self) -> str:
|
367
|
-
return f"Max date must be at most today; got {self.max_date} > {self.today}"
|
368
346
|
|
369
347
|
|
370
348
|
@dataclass(kw_only=True, slots=True)
|
371
349
|
class _MinMaxDatePeriodError(MinMaxDateError):
|
372
|
-
min_date: Date
|
373
|
-
max_date: Date
|
374
|
-
|
375
350
|
@override
|
376
351
|
def __str__(self) -> str:
|
377
352
|
return (
|
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"]
|
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"]
|
File without changes
|
File without changes
|
File without changes
|