dycw-utilities 0.146.9__tar.gz → 0.147.1__tar.gz
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.9 → dycw_utilities-0.147.1}/PKG-INFO +1 -1
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/pyproject.toml +2 -6
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_traceback.py +13 -6
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/__init__.py +1 -1
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/slack_sdk.py +1 -1
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/traceback.py +13 -3
- dycw_utilities-0.146.9/src/tests/test_aiolimiter.py +0 -40
- dycw_utilities-0.146.9/src/tests/test_pydantic.py +0 -57
- dycw_utilities-0.146.9/src/tests/test_python_dotenv.py +0 -125
- dycw_utilities-0.146.9/src/tests/test_streamlit.py +0 -6
- dycw_utilities-0.146.9/src/utilities/aiolimiter.py +0 -25
- dycw_utilities-0.146.9/src/utilities/pydantic.py +0 -58
- dycw_utilities-0.146.9/src/utilities/python_dotenv.py +0 -101
- dycw_utilities-0.146.9/src/utilities/streamlit.py +0 -105
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/.gitignore +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/LICENSE +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/README.md +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/conftest.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_missing/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_missing/module.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/outer_1.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/outer_2.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_without/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_without/module_1.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/package_without/module_2.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/standalone.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/modules/with_imports.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_altair.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_asyncio.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_atomicwrites.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_atools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_cachetools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_click.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_concurrent.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_contextlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_contextvars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_cryptography.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_cvxpy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_dataclasses.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_enum.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_errors.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_eventkit.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_fastapi.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_fpdf2.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_functions.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_functools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_getpass.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_gzip.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_hashlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_http.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_hypothesis.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_importlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_inflect.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_ipython.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_iterables.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_json.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_jupyter.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_libcst.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_lightweight_charts.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_logging.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_math.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_memory_profiler.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_modules.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_more_itertools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_numpy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_objects/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_objects/objects.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_operator.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_optuna.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_orjson.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_os.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_parse.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pathlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_period.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pickle.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_platform.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_polars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_polars_ols.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_postgres.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pottery.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pqdm.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_psutil.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pyinstrument.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pytest.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pytest_randomly.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_pytest_regressions.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_random.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_re.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_redis.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_reprlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_scipy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_sentinel.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_shelve.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_slack_sdk.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_socket.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_sqlalchemy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_sqlalchemy_polars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_statsmodels.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_string.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_tempfile.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_text.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_threading.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_timer.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_typed_settings.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_types.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_typing.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_typing_funcs/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_typing_funcs/no_future.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_typing_funcs/with_future.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_tzdata.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_tzlocal.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_uuid.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_version.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_warnings.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_whenever.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_zipfile.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/tests/test_zoneinfo.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/altair.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/asyncio.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/atomicwrites.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/atools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/cachetools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/click.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/concurrent.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/contextlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/contextvars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/cryptography.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/cvxpy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/dataclasses.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/enum.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/errors.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/eventkit.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/fastapi.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/fpdf2.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/functions.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/functools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/getpass.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/gzip.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/hashlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/http.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/hypothesis.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/importlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/inflect.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/ipython.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/iterables.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/json.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/jupyter.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/libcst.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/lightweight_charts.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/logging.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/math.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/memory_profiler.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/modules.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/more_itertools.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/numpy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/operator.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/optuna.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/orjson.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/os.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/parse.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pathlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/period.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pickle.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/platform.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/polars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/polars_ols.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/postgres.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pottery.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pqdm.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/psutil.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/py.typed +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pyinstrument.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pytest.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pytest_plugins/__init__.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/pytest_regressions.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/random.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/re.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/redis.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/reprlib.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/scipy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/sentinel.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/shelve.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/socket.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/sqlalchemy.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/sqlalchemy_polars.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/statsmodels.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/string.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/tempfile.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/text.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/threading.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/timer.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/typed_settings.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/types.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/typing.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/tzdata.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/tzlocal.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/uuid.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/version.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/warnings.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/whenever.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/zipfile.py +0 -0
- {dycw_utilities-0.146.9 → dycw_utilities-0.147.1}/src/utilities/zoneinfo.py +0 -0
@@ -7,7 +7,6 @@ requires = ["hatchling"]
|
|
7
7
|
|
8
8
|
# dependency groups
|
9
9
|
[dependency-groups]
|
10
|
-
aiolimiter = ["aiolimiter >= 1.2.1, < 1.3"]
|
11
10
|
altair = ["altair >= 5.5.0, < 5.6"]
|
12
11
|
altair-test = [
|
13
12
|
"polars",
|
@@ -64,7 +63,6 @@ pottery = ["pottery >= 3.0.1, < 3.1"]
|
|
64
63
|
pottery-test = ["orjson", "pytest-rerunfailures"]
|
65
64
|
pqdm = ["pqdm >= 0.2.0, < 0.3"]
|
66
65
|
psutil = ["psutil >= 7.0.0, < 7.1"]
|
67
|
-
pydantic = ["pydantic >= 2.11.4, < 2.12"]
|
68
66
|
pyinstrument = ["pyinstrument >= 5.0.3, < 5.1"]
|
69
67
|
pytest = [
|
70
68
|
"pudb >= 2025.1, < 2025.2",
|
@@ -77,7 +75,6 @@ pytest = [
|
|
77
75
|
pytest-regressions = ["pytest-regressions >= 2.8.1, < 2.9"]
|
78
76
|
pytest-regressions-test = ["orjson", "polars"]
|
79
77
|
pytest-test = ["orjson", "pytest-rng", "pytest-rerunfailures"]
|
80
|
-
python-dotenv = ["python-dotenv >= 1.1.1, < 1.2"]
|
81
78
|
redis = ["redis >= 6.2.0, < 6.3", "orjson"]
|
82
79
|
redis-test = ["pytest-rerunfailures"]
|
83
80
|
reprlib-test = ["rich"]
|
@@ -90,7 +87,6 @@ sqlalchemy-polars = ["sqlalchemy", "polars"]
|
|
90
87
|
sqlalchemy-polars-test = ["aiosqlite", "asyncpg", "greenlet"]
|
91
88
|
sqlalchemy-test = ["aiosqlite", "asyncpg", "greenlet"]
|
92
89
|
statsmodels = ["statsmodels >= 0.14.4, < 0.15"]
|
93
|
-
streamlit = ["streamlit >= 1.46.0, < 1.47"]
|
94
90
|
typed-settings = ["typed-settings >= 24.6.0, < 24.7"]
|
95
91
|
tzdata = ["tzdata >= 2025.2, < 2025.3"]
|
96
92
|
|
@@ -106,7 +102,7 @@ dependencies = [
|
|
106
102
|
name = "dycw-utilities"
|
107
103
|
readme = "README.md"
|
108
104
|
requires-python = ">= 3.12"
|
109
|
-
version = "0.
|
105
|
+
version = "0.147.1"
|
110
106
|
|
111
107
|
[project.entry-points.pytest11]
|
112
108
|
pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
|
@@ -139,7 +135,7 @@ test = [
|
|
139
135
|
# bump-my-version
|
140
136
|
[tool.bumpversion]
|
141
137
|
allow_dirty = true
|
142
|
-
current_version = "0.
|
138
|
+
current_version = "0.147.1"
|
143
139
|
|
144
140
|
[[tool.bumpversion.files]]
|
145
141
|
filename = "src/utilities/__init__.py"
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|
8
8
|
|
9
9
|
from hypothesis import given
|
10
10
|
from hypothesis.strategies import sampled_from
|
11
|
-
from pytest import CaptureFixture, raises
|
11
|
+
from pytest import CaptureFixture, mark, param, raises
|
12
12
|
|
13
13
|
from utilities.iterables import one
|
14
14
|
from utilities.traceback import (
|
@@ -17,10 +17,14 @@ from utilities.traceback import (
|
|
17
17
|
format_exception_stack,
|
18
18
|
make_except_hook,
|
19
19
|
)
|
20
|
+
from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
|
21
|
+
from utilities.whenever import get_now
|
20
22
|
|
21
23
|
if TYPE_CHECKING:
|
22
24
|
from collections.abc import Iterable
|
23
25
|
|
26
|
+
from utilities.types import MaybeCallableZonedDateTime
|
27
|
+
|
24
28
|
|
25
29
|
class TestFormatExceptionStack:
|
26
30
|
@classmethod
|
@@ -41,15 +45,18 @@ class TestFormatExceptionStack:
|
|
41
45
|
result = format_exception_stack(error).splitlines()
|
42
46
|
self._assert_lines(result)
|
43
47
|
|
44
|
-
|
48
|
+
@mark.parametrize("start", [param(get_now), param(None)])
|
49
|
+
def test_header(self, *, start: MaybeCallableZonedDateTime) -> None:
|
45
50
|
try:
|
46
51
|
_ = self.func(1, 2, 3, 4, c=5, d=6, e=7)
|
47
52
|
except AssertionError as error:
|
48
|
-
result = format_exception_stack(
|
53
|
+
result = format_exception_stack(
|
54
|
+
error, header=True, start=start
|
55
|
+
).splitlines()
|
49
56
|
patterns = [
|
50
|
-
|
51
|
-
|
52
|
-
r"^Duration \|
|
57
|
+
rf"^Date/time \| \d{{8}}T\d{{6}}\[{LOCAL_TIME_ZONE_NAME}\]$",
|
58
|
+
rf"^Started \| (\d{{8}}T\d{{6}}\[{LOCAL_TIME_ZONE_NAME}\]|)$",
|
59
|
+
r"^Duration \| (-?PT\d+\.\d+S|)$",
|
53
60
|
r"^User \| .+$",
|
54
61
|
r"^Host \| .+$",
|
55
62
|
r"^Process ID \| \d+$",
|
@@ -66,7 +66,7 @@ class SendToSlackError(Exception):
|
|
66
66
|
def __str__(self) -> str:
|
67
67
|
code = self.response.status_code # pragma: no cover
|
68
68
|
phrase = HTTPStatus(code).phrase # pragma: no cover
|
69
|
-
return f"Error sending to Slack
|
69
|
+
return f"Error sending to Slack; got error code {code} ({phrase})" # pragma: no cover
|
70
70
|
|
71
71
|
|
72
72
|
__all__ = ["SendToSlackError", "send_to_slack", "send_to_slack_async"]
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
6
6
|
from functools import partial
|
7
7
|
from getpass import getuser
|
8
8
|
from itertools import repeat
|
9
|
+
from logging import exception
|
9
10
|
from os import getpid
|
10
11
|
from pathlib import Path
|
11
12
|
from socket import gethostname
|
@@ -26,6 +27,7 @@ from utilities.reprlib import (
|
|
26
27
|
RICH_MAX_WIDTH,
|
27
28
|
yield_mapping_repr,
|
28
29
|
)
|
30
|
+
from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
|
29
31
|
from utilities.version import get_version
|
30
32
|
from utilities.whenever import (
|
31
33
|
format_compact,
|
@@ -98,7 +100,10 @@ def _yield_header_lines(
|
|
98
100
|
now = get_now_local()
|
99
101
|
start_use = to_zoned_date_time(date_time=start)
|
100
102
|
yield f"Date/time | {format_compact(now)}"
|
101
|
-
|
103
|
+
if start_use is None:
|
104
|
+
start_str = ""
|
105
|
+
else:
|
106
|
+
start_str = format_compact(start_use.to_tz(LOCAL_TIME_ZONE_NAME))
|
102
107
|
yield f"Started | {start_str}"
|
103
108
|
delta = None if start_use is None else (now - start_use)
|
104
109
|
delta_str = "" if delta is None else delta.format_common_iso()
|
@@ -277,9 +282,14 @@ def _make_except_hook_inner(
|
|
277
282
|
with writer(path, overwrite=True) as temp:
|
278
283
|
_ = temp.write_text(full)
|
279
284
|
if slack_url is not None: # pragma: no cover
|
280
|
-
from utilities.slack_sdk import send_to_slack
|
285
|
+
from utilities.slack_sdk import SendToSlackError, send_to_slack
|
286
|
+
|
287
|
+
try:
|
288
|
+
send_to_slack(slack_url, f"```{slim}```")
|
289
|
+
except SendToSlackError as error:
|
290
|
+
msg = str(error)
|
291
|
+
exception(msg) # noqa: LOG015
|
281
292
|
|
282
|
-
send_to_slack(slack_url, f"```{slim}```")
|
283
293
|
if to_bool(bool_=pudb): # pragma: no cover
|
284
294
|
from pudb import post_mortem
|
285
295
|
|
@@ -1,40 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from asyncio import sleep
|
4
|
-
from typing import ClassVar
|
5
|
-
|
6
|
-
from utilities.aiolimiter import get_async_limiter
|
7
|
-
from utilities.text import unique_str
|
8
|
-
from utilities.timer import Timer
|
9
|
-
from utilities.whenever import SECOND
|
10
|
-
|
11
|
-
|
12
|
-
class TestGetAsyncLimiter:
|
13
|
-
async def test_main(self) -> None:
|
14
|
-
counter = 0
|
15
|
-
|
16
|
-
async def increment() -> None:
|
17
|
-
nonlocal counter
|
18
|
-
counter += 1
|
19
|
-
await sleep(0.01)
|
20
|
-
|
21
|
-
name = unique_str()
|
22
|
-
with Timer() as timer:
|
23
|
-
for _ in range(2):
|
24
|
-
async with get_async_limiter(name, rate=0.5):
|
25
|
-
await increment()
|
26
|
-
assert timer >= 0.48 * SECOND
|
27
|
-
|
28
|
-
shared: ClassVar[str] = unique_str()
|
29
|
-
|
30
|
-
async def test_shared1(self) -> None:
|
31
|
-
async with get_async_limiter(self.shared):
|
32
|
-
await sleep(0.01)
|
33
|
-
|
34
|
-
async def test_shared2(self) -> None:
|
35
|
-
async with get_async_limiter(self.shared):
|
36
|
-
await sleep(0.01)
|
37
|
-
|
38
|
-
async def test_shared3(self) -> None:
|
39
|
-
async with get_async_limiter(self.shared):
|
40
|
-
await sleep(0.01)
|
@@ -1,57 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
|
-
from hypothesis import given
|
6
|
-
from hypothesis.strategies import integers
|
7
|
-
from pydantic import BaseModel
|
8
|
-
from pytest import raises
|
9
|
-
|
10
|
-
from utilities.hypothesis import temp_paths
|
11
|
-
from utilities.pydantic import HashableBaseModel, LoadModelError, load_model, save_model
|
12
|
-
from utilities.pytest import skipif_windows
|
13
|
-
|
14
|
-
|
15
|
-
class TestHashableBaseModel:
|
16
|
-
@given(x=integers())
|
17
|
-
def test_main(self, *, x: int) -> None:
|
18
|
-
class Example(HashableBaseModel):
|
19
|
-
x: int
|
20
|
-
|
21
|
-
example = Example(x=x)
|
22
|
-
assert isinstance(hash(example), int)
|
23
|
-
|
24
|
-
|
25
|
-
class TestSaveAndLoadModel:
|
26
|
-
@given(x=integers(), root=temp_paths())
|
27
|
-
def test_main(self, *, x: int, root: Path) -> None:
|
28
|
-
path = Path(root, "model.json")
|
29
|
-
|
30
|
-
class Example(BaseModel):
|
31
|
-
x: int
|
32
|
-
|
33
|
-
example = Example(x=x)
|
34
|
-
save_model(example, path)
|
35
|
-
loaded = load_model(Example, path)
|
36
|
-
assert loaded == example
|
37
|
-
|
38
|
-
@skipif_windows
|
39
|
-
def test_load_model_error_dir(self, *, tmp_path: Path) -> None:
|
40
|
-
path = tmp_path.joinpath("dir")
|
41
|
-
path.mkdir()
|
42
|
-
|
43
|
-
class Example(BaseModel):
|
44
|
-
x: int
|
45
|
-
|
46
|
-
with raises(
|
47
|
-
LoadModelError,
|
48
|
-
match=r"Unable to load .*; path '.*' must not be a directory\.",
|
49
|
-
):
|
50
|
-
_ = load_model(Example, path)
|
51
|
-
|
52
|
-
def test_load_model_error_file(self, *, tmp_path: Path) -> None:
|
53
|
-
class Example(BaseModel):
|
54
|
-
x: int
|
55
|
-
|
56
|
-
with raises(LoadModelError, match=r"Unable to load .*; path '.*' must exist\."):
|
57
|
-
_ = load_model(Example, tmp_path.joinpath("model.json"))
|
@@ -1,125 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import re
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from re import DOTALL
|
6
|
-
from typing import TYPE_CHECKING
|
7
|
-
|
8
|
-
from hypothesis import given
|
9
|
-
from hypothesis.strategies import DataObject, booleans, data, integers, sampled_from
|
10
|
-
from pytest import raises
|
11
|
-
|
12
|
-
from utilities.errors import ImpossibleCaseError
|
13
|
-
from utilities.hypothesis import git_repos, settings_with_reduced_examples, text_ascii
|
14
|
-
from utilities.os import temp_environ
|
15
|
-
from utilities.python_dotenv import (
|
16
|
-
_LoadSettingsDuplicateKeysError,
|
17
|
-
_LoadSettingsFileNotFoundError,
|
18
|
-
_LoadSettingsMissingKeysError,
|
19
|
-
load_settings,
|
20
|
-
)
|
21
|
-
|
22
|
-
if TYPE_CHECKING:
|
23
|
-
from pathlib import Path
|
24
|
-
|
25
|
-
|
26
|
-
class TestLoadSettings:
|
27
|
-
@given(
|
28
|
-
data=data(),
|
29
|
-
root=git_repos(),
|
30
|
-
key_file=sampled_from(["key", "KEY"]),
|
31
|
-
value_file=text_ascii(),
|
32
|
-
use_env=booleans(),
|
33
|
-
)
|
34
|
-
@settings_with_reduced_examples()
|
35
|
-
def test_main(
|
36
|
-
self,
|
37
|
-
*,
|
38
|
-
data: DataObject,
|
39
|
-
root: Path,
|
40
|
-
key_file: str,
|
41
|
-
value_file: str,
|
42
|
-
use_env: bool,
|
43
|
-
) -> None:
|
44
|
-
_ = root.joinpath(".env").write_text(f"{key_file} = {value_file}\n")
|
45
|
-
|
46
|
-
@dataclass(kw_only=True, slots=True)
|
47
|
-
class SettingsLower:
|
48
|
-
key: str
|
49
|
-
|
50
|
-
@dataclass(kw_only=True, slots=True)
|
51
|
-
class SettingsUpper:
|
52
|
-
KEY: str
|
53
|
-
|
54
|
-
SettingsUse = data.draw(sampled_from([SettingsLower, SettingsUpper])) # noqa: N806
|
55
|
-
if use_env:
|
56
|
-
key_env = data.draw(sampled_from(["key", "KEY"]))
|
57
|
-
value_env = data.draw(text_ascii())
|
58
|
-
with temp_environ({key_env: value_env}):
|
59
|
-
settings = load_settings(SettingsUse, path=root)
|
60
|
-
exp_value = value_env
|
61
|
-
else:
|
62
|
-
settings = load_settings(SettingsUse, path=root)
|
63
|
-
exp_value = value_file
|
64
|
-
|
65
|
-
if SettingsUse is SettingsLower:
|
66
|
-
expected = SettingsLower(key=exp_value)
|
67
|
-
elif SettingsUse is SettingsUpper:
|
68
|
-
expected = SettingsUpper(KEY=exp_value)
|
69
|
-
else:
|
70
|
-
raise ImpossibleCaseError(case=[f"{SettingsUse=}"])
|
71
|
-
assert settings == expected
|
72
|
-
|
73
|
-
@given(root=git_repos(), value=text_ascii())
|
74
|
-
@settings_with_reduced_examples()
|
75
|
-
def test_file_extra_key(self, *, root: Path, value: str) -> None:
|
76
|
-
@dataclass(kw_only=True, slots=True)
|
77
|
-
class Settings:
|
78
|
-
key: str
|
79
|
-
|
80
|
-
_ = root.joinpath(".env").write_text(f"key = {value}\nother = {value}\n")
|
81
|
-
settings = load_settings(Settings, path=root)
|
82
|
-
expected = Settings(key=value)
|
83
|
-
assert settings == expected
|
84
|
-
|
85
|
-
@given(root=git_repos())
|
86
|
-
@settings_with_reduced_examples()
|
87
|
-
def test_error_file_not_found(self, *, root: Path) -> None:
|
88
|
-
@dataclass(kw_only=True, slots=True)
|
89
|
-
class Settings:
|
90
|
-
KEY: str
|
91
|
-
|
92
|
-
with raises(_LoadSettingsFileNotFoundError, match=r"Path '.*' must exist"):
|
93
|
-
_ = load_settings(Settings, path=root)
|
94
|
-
|
95
|
-
@given(root=git_repos(), value=integers())
|
96
|
-
@settings_with_reduced_examples()
|
97
|
-
def test_error_duplicate_keys(self, *, root: Path, value: int) -> None:
|
98
|
-
@dataclass(kw_only=True, slots=True)
|
99
|
-
class Settings:
|
100
|
-
key: str
|
101
|
-
|
102
|
-
_ = root.joinpath(".env").write_text(f"key = {value}\nKEY = {value}\n")
|
103
|
-
with raises(
|
104
|
-
_LoadSettingsDuplicateKeysError,
|
105
|
-
match=re.compile(
|
106
|
-
r"Mapping .* keys must not contain duplicates \(modulo case\); got .*",
|
107
|
-
flags=DOTALL,
|
108
|
-
),
|
109
|
-
):
|
110
|
-
_ = load_settings(Settings, path=root)
|
111
|
-
|
112
|
-
@given(root=git_repos())
|
113
|
-
@settings_with_reduced_examples()
|
114
|
-
def test_error_missing_keys(self, *, root: Path) -> None:
|
115
|
-
@dataclass(kw_only=True, slots=True)
|
116
|
-
class Settings:
|
117
|
-
key: str
|
118
|
-
|
119
|
-
root.joinpath(".env").touch()
|
120
|
-
|
121
|
-
with raises(
|
122
|
-
_LoadSettingsMissingKeysError,
|
123
|
-
match=r"Unable to load '.*'; missing value\(s\) for 'key'",
|
124
|
-
):
|
125
|
-
_ = load_settings(Settings, path=root)
|
@@ -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"]
|
@@ -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"]
|
@@ -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
|