From 9290dc7e564fc9e857dbd534066c5a43441cbff6 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:19:34 -0700 Subject: [PATCH] Refactor username and project name validation to use standard end-points --- pyproject.toml | 3 - src/clabe/data_transfer/aind_watchdog.py | 13 +-- src/clabe/utils/aind_auth.py | 131 ++++++---------------- tests/data_transfer/test_data_transfer.py | 5 +- tests/utils/test_aind_auth.py | 107 ++++++++++-------- uv.lock | 98 ---------------- 6 files changed, 107 insertions(+), 250 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a7593ea1..320bb6bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,7 @@ aind-services = [ "requests", "pyyaml", "pykeepass", - "ms-active-directory", "cryptography", - "winkerberos; sys_platform == 'win32'", - "ldap3; sys_platform == 'win32'", "msal; sys_platform == 'win32'", "aind-data-schema>=2", "aind-data-transfer-service >= 1.22.0", diff --git a/src/clabe/data_transfer/aind_watchdog.py b/src/clabe/data_transfer/aind_watchdog.py index 85315d62..06b4fa12 100644 --- a/src/clabe/data_transfer/aind_watchdog.py +++ b/src/clabe/data_transfer/aind_watchdog.py @@ -430,26 +430,25 @@ def _find_modality_candidates(source: PathLike) -> Dict[str, List[Path]]: @staticmethod def _get_project_names( - end_point: str = "http://aind-metadata-service/project_names", timeout: int = 5 + end_point: str = "http://aind-metadata-service/api/v2/project_names", timeout: int = 5 ) -> list[str]: """ Fetches the list of valid project names from the metadata service. Args: - end_point: The endpoint URL for the metadata service - timeout: Timeout for the request in seconds + end_point: The endpoint URL for the metadata service. + timeout: Timeout for the request in seconds. Returns: - A list of valid project names + A list of valid project names. Raises: - HTTPError: If the request fails + HTTPError: If the request fails. """ response = requests.get(end_point, timeout=timeout) if response.ok: - return json.loads(response.content)["data"] + return response.json() else: - response.raise_for_status() raise HTTPError(f"Failed to fetch project names from endpoint. {response.content.decode('utf-8')}") def is_running(self) -> bool: diff --git a/src/clabe/utils/aind_auth.py b/src/clabe/utils/aind_auth.py index a72369d6..612e9f21 100644 --- a/src/clabe/utils/aind_auth.py +++ b/src/clabe/utils/aind_auth.py @@ -1,101 +1,40 @@ -import concurrent.futures import logging -import platform from typing import Optional +from urllib.parse import quote -logger = logging.getLogger(__name__) - -_ad_logger = logging.getLogger( - "ms_active_directory" -) # This library is annoyingly verbose at the INFO level. We will turn it off unless the root is at DEBUG level - - -if platform.system() == "Windows": - - def validate_aind_username( - username: str, - domain: str = "corp.alleninstitute.org", - domain_username: Optional[str] = None, - timeout: Optional[float] = 2, - ) -> bool: - """ - Validates if the given username exists in the AIND Active Directory. - - This function authenticates with the corporate Active Directory and searches - for the specified username to verify its existence within the organization. - See https://github.com/AllenNeuralDynamics/aind-watchdog-service/issues/110#issuecomment-2828869619 - - Args: - username: The username to validate against Active Directory - domain: The Active Directory domain to search. Defaults to Allen Institute domain - domain_username: Username for domain authentication. Defaults to current user - timeout: Timeout in seconds for the validation operation - - Returns: - bool: True if the username exists in Active Directory, False otherwise - - Raises: - concurrent.futures.TimeoutError: If the validation operation times out +import requests - Example: - ```python - # Validate a username in Active Directory - is_valid = validate_aind_username("j.doe") - ``` - """ - import getpass # type: ignore[import] - - import ldap3 # type: ignore[import] - import ms_active_directory # type: ignore[import] - - def _helper(username: str, domain: str, domain_username: Optional[str]) -> bool: - """A function submitted to a thread pool to validate the username.""" - if not logger.isEnabledFor(logging.DEBUG): - _ad_logger.disabled = True - if domain_username is None: - domain_username = getpass.getuser() - _domain = ms_active_directory.ADDomain(domain) - session = _domain.create_session_as_user( - domain_username, - authentication_mechanism=ldap3.SASL, - sasl_mechanism=ldap3.GSSAPI, - ) - return session.find_user_by_name(username) is not None - - try: - with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit(_helper, username, domain, domain_username) - result = future.result(timeout=timeout) - return result - except concurrent.futures.TimeoutError as e: - logger.error("Timeout occurred while validating username: %s", e) - e.add_note("Timeout occurred while validating username") - raise - -else: - - def validate_aind_username( - username: str, - domain: str = "corp.alleninstitute.org", - domain_username: Optional[str] = None, - timeout: Optional[float] = 2, - ) -> bool: - """ - Validates if the given username is in the AIND Active Directory. - - This function is a no-op on non-Windows platforms since Active Directory - authentication is not available. - - This function always returns True on non-Windows platforms. - - Args: - username: The username to validate (ignored on non-Windows platforms) - domain: The Active Directory domain (ignored on non-Windows platforms) - domain_username: Username for domain authentication (ignored on non-Windows platforms) - timeout: Timeout for the validation operation (ignored on non-Windows platforms) +logger = logging.getLogger(__name__) - Returns: - bool: Always returns True on non-Windows platforms - """ - logger.warning("Active Directory validation is not implemented for non-Windows platforms") - return True +_AD_ENDPOINT = "http://aind-metadata-service/api/v2/active_directory" + + +def validate_aind_username( + username: str, + timeout: Optional[float] = 2, +) -> bool: + """ + Validates if the given username exists in the AIND Active Directory. + + Queries the AIND metadata service to verify the username exists. + Returns False (instead of raising) on network errors so callers can + decide how to handle the degraded state. + + Args: + username: The username to validate. + timeout: Timeout in seconds for the HTTP request. Defaults to 2. + + Returns: + bool: True if the username was found, False otherwise. + + Example: + ```python + is_valid = validate_aind_username("j.doe") + ``` + """ + try: + response = requests.get(f"{_AD_ENDPOINT}/{quote(username, safe='')}", timeout=timeout) + return response.ok + except requests.RequestException as e: + logger.warning("Failed to validate username '%s': %s", username, e) + return False diff --git a/tests/data_transfer/test_data_transfer.py b/tests/data_transfer/test_data_transfer.py index c21ec279..c6e567cf 100644 --- a/tests/data_transfer/test_data_transfer.py +++ b/tests/data_transfer/test_data_transfer.py @@ -124,17 +124,18 @@ def test_is_not_running(self, mock_check_output, watchdog_service): def test_get_project_names(self, mock_get, watchdog_service): mock_response = MagicMock() mock_response.ok = True - mock_response.content = '{"data": ["test_project"]}' + mock_response.json.return_value = ["test_project", "other_project"] mock_get.return_value = mock_response project_names = watchdog_service._get_project_names() assert "test_project" in project_names + mock_get.assert_called_once_with("http://aind-metadata-service/api/v2/project_names", timeout=5) @patch("clabe.data_transfer.aind_watchdog.requests.get") def test_get_project_names_fail(self, mock_get, watchdog_service): mock_response = MagicMock() mock_response.ok = False mock_get.return_value = mock_response - with pytest.raises(Exception): + with pytest.raises(HTTPError): watchdog_service._get_project_names() @patch( diff --git a/tests/utils/test_aind_auth.py b/tests/utils/test_aind_auth.py index 44442b97..34545a40 100644 --- a/tests/utils/test_aind_auth.py +++ b/tests/utils/test_aind_auth.py @@ -1,50 +1,69 @@ -import platform from unittest.mock import MagicMock, patch -import pytest +import requests from clabe.utils import aind_auth -@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-only test") -def test_validate_aind_username_windows_valid(): - """Test validate_aind_username on Windows with a valid user.""" - with ( - patch("ms_active_directory.ADDomain") as mock_ad_domain, - patch("ldap3.SASL", "SASL"), - patch("ldap3.GSSAPI", "GSSAPI"), - ): - mock_session = MagicMock() - mock_session.find_user_by_name.return_value = True - mock_ad_domain.return_value.create_session_as_user.return_value = mock_session - - assert aind_auth.validate_aind_username("testuser") - - -@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-only test") -def test_validate_aind_username_windows_invalid(): - """Test validate_aind_username on Windows with an invalid user.""" - with ( - patch("ms_active_directory.ADDomain") as mock_ad_domain, - patch("ldap3.SASL", "SASL"), - patch("ldap3.GSSAPI", "GSSAPI"), - ): - mock_session = MagicMock() - mock_session.find_user_by_name.return_value = None - mock_ad_domain.return_value.create_session_as_user.return_value = mock_session - - assert not aind_auth.validate_aind_username("testuser") - - -@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-only test") -def test_validate_aind_username_windows_timeout(): - """Test validate_aind_username on Windows with a timeout.""" - with ( - patch("ms_active_directory.ADDomain"), - patch("ldap3.SASL", "SASL"), - patch("ldap3.GSSAPI", "GSSAPI"), - patch("concurrent.futures.ThreadPoolExecutor.submit") as mock_submit, - ): - mock_submit.side_effect = TimeoutError - with pytest.raises(TimeoutError): - aind_auth.validate_aind_username("testuser") +def test_validate_aind_username_valid(): + """Returns True when the metadata service finds the user.""" + with patch("clabe.utils.aind_auth.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.ok = True + mock_response.json.return_value = { + "username": "j.doe", + "full_name": "Jane Doe", + "email": "j.doe@alleninstitute.org", + } + mock_get.return_value = mock_response + + assert aind_auth.validate_aind_username("j.doe") is True + mock_get.assert_called_once_with( + "http://aind-metadata-service/api/v2/active_directory/j.doe", + timeout=2, + ) + + +def test_validate_aind_username_invalid(): + """Returns False when the metadata service does not find the user.""" + with patch("clabe.utils.aind_auth.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.ok = False + mock_get.return_value = mock_response + + assert aind_auth.validate_aind_username("no.one") is False + + +def test_validate_aind_username_request_exception(): + """Returns False (with a logged warning) on a network error.""" + with patch("clabe.utils.aind_auth.requests.get", side_effect=requests.RequestException("timeout")): + assert aind_auth.validate_aind_username("j.doe") is False + + +def test_validate_aind_username_custom_timeout(): + """Passes the timeout argument through to requests.get.""" + with patch("clabe.utils.aind_auth.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.ok = True + mock_response.json.return_value = {"username": "j.doe"} + mock_get.return_value = mock_response + + aind_auth.validate_aind_username("j.doe", timeout=10) + mock_get.assert_called_once_with( + "http://aind-metadata-service/api/v2/active_directory/j.doe", + timeout=10, + ) + + +def test_validate_aind_username_encodes_special_chars(): + """URL-encodes special characters in the username.""" + with patch("clabe.utils.aind_auth.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.ok = False + mock_get.return_value = mock_response + + aind_auth.validate_aind_username("../admin") + mock_get.assert_called_once_with( + "http://aind-metadata-service/api/v2/active_directory/..%2Fadmin", + timeout=2, + ) diff --git a/uv.lock b/uv.lock index c1c2b84f..f0658926 100644 --- a/uv.lock +++ b/uv.lock @@ -61,13 +61,10 @@ aind-services = [ { name = "aind-data-transfer-service" }, { name = "aind-watchdog-service" }, { name = "cryptography" }, - { name = "ldap3", marker = "sys_platform == 'win32'" }, - { name = "ms-active-directory" }, { name = "msal", marker = "sys_platform == 'win32'" }, { name = "pykeepass" }, { name = "pyyaml" }, { name = "requests" }, - { name = "winkerberos", marker = "sys_platform == 'win32'" }, ] [package.dev-dependencies] @@ -97,8 +94,6 @@ requires-dist = [ { name = "aind-watchdog-service", marker = "extra == 'aind-services'", specifier = ">=0.1.6" }, { name = "cryptography", marker = "extra == 'aind-services'" }, { name = "gitpython" }, - { name = "ldap3", marker = "sys_platform == 'win32' and extra == 'aind-services'" }, - { name = "ms-active-directory", marker = "extra == 'aind-services'" }, { name = "msal", marker = "sys_platform == 'win32' and extra == 'aind-services'" }, { name = "pydantic", specifier = ">=2.7" }, { name = "pydantic-settings" }, @@ -109,7 +104,6 @@ requires-dist = [ { name = "requests", marker = "extra == 'aind-services'" }, { name = "rich" }, { name = "semver" }, - { name = "winkerberos", marker = "sys_platform == 'win32' and extra == 'aind-services'" }, ] provides-extras = ["aind-services"] @@ -751,18 +745,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "ldap3" -version = "2.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/ac/96bd5464e3edbc61595d0d69989f5d9969ae411866427b2500a8e5b812c0/ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f", size = 398830, upload-time = "2021-07-18T06:34:21.786Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", size = 432192, upload-time = "2021-07-18T06:34:12.905Z" }, -] - [[package]] name = "lxml" version = "6.0.2" @@ -1097,20 +1079,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, ] -[[package]] -name = "ms-active-directory" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "ldap3" }, - { name = "pyasn1" }, - { name = "pycryptodome" }, - { name = "pytz" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/a0/da7aaa5c87d155f2af60db0db3ab12eec50bfdeeca6f6cd1559ca92375c0/ms_active_directory-1.14.1.tar.gz", hash = "sha256:86c3b9de8b8b5546104f0fe480db689b1a1d0a4109a4208603be4f981ce12040", size = 155782, upload-time = "2024-09-06T01:28:00.609Z" } - [[package]] name = "msal" version = "1.34.0" @@ -1330,15 +1298,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, ] -[[package]] -name = "pyasn1" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -1348,36 +1307,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] -[[package]] -name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, -] - [[package]] name = "pycryptodomex" version = "3.23.0" @@ -1641,15 +1570,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2007,21 +1927,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/64/6e/62daec357285b927e wheels = [ { url = "https://files.pythonhosted.org/packages/f2/3e/45583b67c2ff08ad5a582d316fcb2f11d6cf0a50c7707ac09d212d25bc98/wcwidth-0.5.0-py3-none-any.whl", hash = "sha256:1efe1361b83b0ff7877b81ba57c8562c99cf812158b778988ce17ec061095695", size = 93772, upload-time = "2026-01-27T01:31:43.432Z" }, ] - -[[package]] -name = "winkerberos" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/455f043bc28694a278125d1fc2ab7cbf0ce0953c97bbe1021f08fd19c7b8/winkerberos-0.13.0.tar.gz", hash = "sha256:f3fbb67346fe8ed697e125724b0699d5c2a15b9a5f9151d25a1be88df8dac427", size = 35677, upload-time = "2025-12-03T14:17:29.882Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/09/05c4d2fb93f5478fd1b6146c4fa3fbb80839576a34062e5677f2dec3a430/winkerberos-0.13.0-cp311-cp311-win32.whl", hash = "sha256:a23c83854650416545000c4630e94b16fa14c7b400bd5f08a79718e04eff9135", size = 25642, upload-time = "2025-12-03T14:17:17.42Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5b/bafa1cfb9f047be139ffae330f6eafa0487f8bf82164ead756e0bc2bc047/winkerberos-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bc03e66a737bfd11964e6cdc5f03a8cd0baed798f991b1467075c65980c4157", size = 27931, upload-time = "2025-12-03T14:17:18.708Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fa/02de79d7dbec9122a6778678ed432ebffb228c48b16cfba3007c45a6e8fd/winkerberos-0.13.0-cp312-cp312-win32.whl", hash = "sha256:3454b8bb9c11091e4775a8bd692dfbe45f2eab12f3a4837b820c2505088dfdd2", size = 25675, upload-time = "2025-12-03T14:17:20.052Z" }, - { url = "https://files.pythonhosted.org/packages/52/c2/ff9074cf423d82bdfb48ac89e64f360533ba4e2079e8485be8377a8c54fe/winkerberos-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:59f01879c62adcda5af857fd78d2b2dfdfd99cf6179b92d38e2f2bd12db75bf7", size = 27946, upload-time = "2025-12-03T14:17:21.22Z" }, - { url = "https://files.pythonhosted.org/packages/92/83/b1f52594cc2c3ce18c67a04aecb0cb4fb3f4769c268d194cc5f4863150fa/winkerberos-0.13.0-cp313-cp313-win32.whl", hash = "sha256:38fefdfc77a7f82c3cc9f83c7d1b6f242e6d3ea200bfde9b640f7dfe9fdf9bda", size = 25671, upload-time = "2025-12-03T14:17:23.235Z" }, - { url = "https://files.pythonhosted.org/packages/9c/26/b17649b0707e4d8cd9d0d4ceadcef06eff2fc76fcb444cb187763158ae63/winkerberos-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:c45e84a35a3b87b88d0e6d7b55d40712dc021f80af3cb9e81091651e6a73510d", size = 27941, upload-time = "2025-12-03T14:17:24.627Z" }, - { url = "https://files.pythonhosted.org/packages/80/d9/d12d310fdf9ace70f7469ecfd9f112dc39cb7e1f77348228c06a6bd72c57/winkerberos-0.13.0-cp314-cp314-win32.whl", hash = "sha256:46cc29fa95744076a0dd2a167158574826509a5e4aa052b81a2b535aab4af14a", size = 26185, upload-time = "2025-12-03T14:17:25.63Z" }, - { url = "https://files.pythonhosted.org/packages/97/7c/5a418e8d292e3fea1012ccf029b38fae430542fab1beaf6fc60cf138cc08/winkerberos-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:5d5add54d10e31671f7c28c90ccafe98b45cec6d7519949ba30add51e34aee9a", size = 28476, upload-time = "2025-12-03T14:17:26.629Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/cd8186479046b7a749cee8d4d9fd50e3ce3330d8ea611efe4b8b741f0c3b/winkerberos-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5bc5e40a816d94d4a5abd665fe62088c1ee91ee9a1f5d787032a63004842fedf", size = 26500, upload-time = "2025-12-03T14:17:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a6/cc5f24b3f1a46a826b7e30ef56fdc1fe22315fef96de8e22afbdd5d98e7a/winkerberos-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:441884c0bda4bee0125fdbd7fee6a232dab58b4a64be8950eb17a8a7404a5440", size = 28715, upload-time = "2025-12-03T14:17:28.813Z" }, -]