diff --git a/pyproject.toml b/pyproject.toml index b6786acd..911a04f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dynamic = [ ] dependencies = [ "aiohttp>=3.10.11,<3.14", - "awsiot-credentialhelper>=0.6,<1.1", + "awscrt>=0.16.9,<0.32", "boto3>=1.34.35,<1.43", "grpcio>=1.70,<1.79", "protobuf>=4.25.8,<6.34", diff --git a/requirements.txt b/requirements.txt index f2bc5093..5ef25824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,29 +21,19 @@ attrs==25.3.0 ; python_full_version < '3.9' attrs==25.4.0 ; python_full_version >= '3.9' # via aiohttp awscrt==0.16.26 ; python_full_version < '3.8.1' - # via awsiot-credentialhelper -awscrt==0.31.1 ; python_full_version >= '3.8.1' - # via awsiot-credentialhelper -awsiot-credentialhelper==0.6.0 ; python_full_version < '3.8.1' # via otaclient-iot-logging-server -awsiot-credentialhelper==1.0.2 ; python_full_version >= '3.8.1' +awscrt==0.31.1 ; python_full_version >= '3.8.1' # via otaclient-iot-logging-server boto3==1.37.38 ; python_full_version < '3.9' - # via - # awsiot-credentialhelper - # otaclient-iot-logging-server + # via otaclient-iot-logging-server boto3==1.42.44 ; python_full_version >= '3.9' - # via - # awsiot-credentialhelper - # otaclient-iot-logging-server + # via otaclient-iot-logging-server botocore==1.37.38 ; python_full_version < '3.9' # via - # awsiot-credentialhelper # boto3 # s3transfer botocore==1.42.44 ; python_full_version >= '3.9' # via - # awsiot-credentialhelper # boto3 # s3transfer frozenlist==1.5.0 ; python_full_version < '3.9' @@ -121,7 +111,6 @@ six==1.17.0 typing-extensions==4.13.2 ; python_full_version < '3.9' # via # annotated-types - # awsiot-credentialhelper # multidict # otaclient-iot-logging-server # pydantic @@ -129,7 +118,6 @@ typing-extensions==4.13.2 ; python_full_version < '3.9' typing-extensions==4.15.0 ; python_full_version >= '3.9' # via # aiosignal - # awsiot-credentialhelper # grpcio # multidict # otaclient-iot-logging-server @@ -140,10 +128,10 @@ typing-inspection==0.4.2 ; python_full_version >= '3.9' # via # pydantic # pydantic-settings -urllib3==1.26.20 - # via - # awsiot-credentialhelper - # botocore +urllib3==1.26.20 ; python_full_version < '3.10' + # via botocore +urllib3==2.6.3 ; python_full_version >= '3.10' + # via botocore yarl==1.15.2 ; python_full_version < '3.9' # via aiohttp yarl==1.22.0 ; python_full_version >= '3.9' diff --git a/src/otaclient_iot_logging_server/boto3_session.py b/src/otaclient_iot_logging_server/boto3_session.py index 05462dcf..03202c7f 100644 --- a/src/otaclient_iot_logging_server/boto3_session.py +++ b/src/otaclient_iot_logging_server/boto3_session.py @@ -15,14 +15,26 @@ from __future__ import annotations +import json +import logging import ssl import subprocess +from http import HTTPStatus from pathlib import Path -from typing import Optional +from typing import Any, Dict, Optional -from awsiot_credentialhelper.boto3_session import Boto3SessionProvider -from awsiot_credentialhelper.boto3_session import Pkcs11Config as aws_PKcs11Config +from awscrt.http import HttpClientConnection, HttpRequest +from awscrt.io import ( + ClientBootstrap, + ClientTlsContext, + DefaultHostResolver, + EventLoopGroup, + Pkcs11Lib, + TlsContextOptions, +) from boto3 import Session +from botocore.credentials import DeferredRefreshableCredentials +from botocore.session import get_session as get_botocore_session from otaclient_iot_logging_server._utils import parse_pkcs11_uri from otaclient_iot_logging_server.greengrass_config import ( @@ -30,6 +42,10 @@ PKCS11Config, ) +logger = logging.getLogger(__name__) + +_AWSCRT_TIMEOUT_SEC = 10 + # # ------ certificate loading helpers ------ # # @@ -71,7 +87,7 @@ def _convert_to_pem(_data: bytes) -> bytes: def _load_certificate(cert_path: str, pkcs11_cfg: Optional[PKCS11Config]) -> bytes: """ - NOTE: Boto3SessionProvider only takes PEM format cert. + NOTE: Only PEM format cert is supported. """ if cert_path.startswith("pkcs11"): assert pkcs11_cfg, ( @@ -91,20 +107,200 @@ def _load_certificate(cert_path: str, pkcs11_cfg: Optional[PKCS11Config]) -> byt return _convert_to_pem(Path(cert_path).read_bytes()) +# +# ------ credential fetching ------ # +# +# AWS IoT Core Credential Provider API: +# https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html + + +def _parse_credentials_response( + response_status: int, + response_body: bytes, + credential_url: str, +) -> Dict[str, Any]: + """Parse credential provider response, raising on non-200 status.""" + if response_status != HTTPStatus.OK: + logger.error( + "Failed to get credentials from %s: status=%s, body=%s", + credential_url, + response_status, + response_body.decode(), + ) + raise ValueError( + f"Error getting credentials from IoT credential provider: " + f"status={response_status}" + ) + credentials = json.loads(response_body.decode())["credentials"] + return { + "access_key": credentials["accessKeyId"], + "secret_key": credentials["secretAccessKey"], + "token": credentials["sessionToken"], + "expiry_time": credentials["expiration"], + } + + +def _build_tls_context_from_path( + cert_path: str, + key_path: str, +) -> TlsContextOptions: + """Build TLS context options using plain certificate/key files.""" + return TlsContextOptions.create_client_with_mtls_from_path( + cert_filepath=cert_path, + pk_filepath=key_path, + ) + + +def _build_tls_context_pkcs11( + cert_pem: bytes, + pkcs11_cfg: PKCS11Config, + private_key_label: Optional[str] = None, +) -> TlsContextOptions: + """Build TLS context options using PKCS#11 for private key operations.""" + return TlsContextOptions.create_client_with_mtls_pkcs11( + pkcs11_lib=Pkcs11Lib(file=pkcs11_cfg.pkcs11_lib), + user_pin=pkcs11_cfg.user_pin, + slot_id=int(pkcs11_cfg.slot_id), + token_label=None, # type: ignore[arg-type] + private_key_label=private_key_label, # type: ignore[arg-type] + cert_file_path=None, # type: ignore[arg-type] + cert_file_contents=cert_pem, + ) + + +def _fetch_iot_credentials_via_awscrt( + endpoint: str, + role_alias: str, + thing_name: str, + tls_ctx_opt: TlsContextOptions, +) -> Dict[str, Any]: + """Fetch IAM credentials from AWS IoT Core Credential Provider via mTLS. + + Uses awscrt for the HTTP request with the given TLS context. + + Args: + endpoint: AWS IoT credential provider endpoint FQDN. + role_alias: IoT Role Alias name. + thing_name: IoT Thing Name. + tls_ctx_opt: TLS context options configured for mTLS. + + Returns: + Credentials dict with access_key, secret_key, token, expiry_time. + """ + path = f"/role-aliases/{role_alias}/credentials" + url = f"https://{endpoint}{path}" + + event_loop_group = EventLoopGroup() + host_resolver = DefaultHostResolver(event_loop_group) + bootstrap = ClientBootstrap(event_loop_group, host_resolver) + + tls_ctx = ClientTlsContext(tls_ctx_opt) + tls_conn_opt = tls_ctx.new_connection_options() + tls_conn_opt.set_server_name(endpoint) + + connection = HttpClientConnection.new( + host_name=endpoint, + port=443, + bootstrap=bootstrap, + tls_connection_options=tls_conn_opt, + ).result(_AWSCRT_TIMEOUT_SEC) + + request = HttpRequest("GET", path) + request.headers.add("host", endpoint) + request.headers.add("x-amzn-iot-thingname", thing_name) + + response_status_code: int = 0 + response_body = bytearray() + + def on_response( + http_stream: Any, status_code: int, headers: Any, **_kwargs: Any + ) -> None: + nonlocal response_status_code + response_status_code = status_code + + def on_body(http_stream: Any, chunk: bytes, **_kwargs: Any) -> None: + response_body.extend(chunk) + + stream = connection.request(request, on_response, on_body) + stream.activate() + stream.completion_future.result(_AWSCRT_TIMEOUT_SEC) + + return _parse_credentials_response(response_status_code, bytes(response_body), url) + + +def _fetch_iot_credentials( + endpoint: str, + role_alias: str, + thing_name: str, + cert_path: str, + key_path: str, +) -> Dict[str, Any]: + """Fetch IAM credentials using plain certificate/key files.""" + tls_ctx_opt = _build_tls_context_from_path(cert_path, key_path) + return _fetch_iot_credentials_via_awscrt( + endpoint=endpoint, + role_alias=role_alias, + thing_name=thing_name, + tls_ctx_opt=tls_ctx_opt, + ) + + +def _fetch_iot_credentials_pkcs11( + endpoint: str, + role_alias: str, + thing_name: str, + cert_pem: bytes, + pkcs11_cfg: PKCS11Config, + private_key_label: Optional[str] = None, +) -> Dict[str, Any]: + """Fetch IAM credentials using PKCS#11 for private key operations.""" + tls_ctx_opt = _build_tls_context_pkcs11(cert_pem, pkcs11_cfg, private_key_label) + return _fetch_iot_credentials_via_awscrt( + endpoint=endpoint, + role_alias=role_alias, + thing_name=thing_name, + tls_ctx_opt=tls_ctx_opt, + ) + + # # ------ session creating helpers ------ # # +def _create_boto3_session(region: str, refresh_func: Any) -> Session: + """Create a boto3 Session with auto-refreshing credentials. + + Args: + region: AWS region name. + refresh_func: Callable that returns credentials dict. + + Returns: + boto3 Session with refreshable credentials. + """ + botocore_session = get_botocore_session() + botocore_session._credentials = DeferredRefreshableCredentials( # type: ignore[attr-defined] + method="custom-iot-core-credential-provider", + refresh_using=refresh_func, + ) + botocore_session.set_config_variable("region", region) + + return Session(botocore_session=botocore_session) + + def _get_session(config: IoTSessionConfig) -> Session: """Get a session that using plain privkey.""" - return Boto3SessionProvider( - endpoint=config.aws_credential_provider_endpoint, - role_alias=config.aws_role_alias, - certificate=_load_certificate(config.certificate_path, config.pkcs11_config), - private_key=config.private_key_path, - thing_name=config.thing_name, - ).get_session() # type: ignore + + def _refresh(): + return _fetch_iot_credentials( + endpoint=config.aws_credential_provider_endpoint, + role_alias=config.aws_role_alias, + thing_name=config.thing_name, + cert_path=config.certificate_path, + key_path=config.private_key_path, + ) + + return _create_boto3_session(config.region, _refresh) def _get_session_pkcs11(config: IoTSessionConfig) -> Session: @@ -113,19 +309,20 @@ def _get_session_pkcs11(config: IoTSessionConfig) -> Session: "privkey is provided by pkcs11, but pkcs11_config is not available" ) + cert_pem = _load_certificate(config.certificate_path, config.pkcs11_config) _parsed_key_uri = parse_pkcs11_uri(config.private_key_path) - return Boto3SessionProvider( - endpoint=config.aws_credential_provider_endpoint, - role_alias=config.aws_role_alias, - certificate=_load_certificate(config.certificate_path, config.pkcs11_config), - thing_name=config.thing_name, - pkcs11=aws_PKcs11Config( - pkcs11_lib=pkcs11_cfg.pkcs11_lib, - slot_id=int(pkcs11_cfg.slot_id), - user_pin=pkcs11_cfg.user_pin, + + def _refresh(): + return _fetch_iot_credentials_pkcs11( + endpoint=config.aws_credential_provider_endpoint, + role_alias=config.aws_role_alias, + thing_name=config.thing_name, + cert_pem=cert_pem, + pkcs11_cfg=pkcs11_cfg, private_key_label=_parsed_key_uri.get("object"), - ), - ).get_session() # type: ignore + ) + + return _create_boto3_session(config.region, _refresh) # API diff --git a/tests/test_boto3_session.py b/tests/test_boto3_session.py index a49ab58a..2ee61403 100644 --- a/tests/test_boto3_session.py +++ b/tests/test_boto3_session.py @@ -16,16 +16,18 @@ from __future__ import annotations from typing import Any +from unittest.mock import MagicMock import pytest -from awsiot_credentialhelper.boto3_session import Boto3SessionProvider -from awsiot_credentialhelper.boto3_session import Pkcs11Config as aws_PKcs11Config from pytest_mock import MockerFixture import otaclient_iot_logging_server.boto3_session from otaclient_iot_logging_server._utils import parse_pkcs11_uri from otaclient_iot_logging_server.boto3_session import ( # type: ignore _convert_to_pem, + _create_boto3_session, + _fetch_iot_credentials, + _parse_credentials_response, get_session, ) from otaclient_iot_logging_server.greengrass_config import ( @@ -60,7 +62,7 @@ def test__convert_to_pem(_in: bytes, _expected: bytes): @pytest.mark.parametrize( - "_config, _expected_call", + "_config, _expected_fetch_target, _expected_call", [ # test#1: boto3 session without pkcs11 ( @@ -74,12 +76,13 @@ def test__convert_to_pem(_in: bytes, _expected: bytes): region="test_region", aws_credential_provider_endpoint="test_cred_endpoint", ), + "_fetch_iot_credentials", { "endpoint": test1_cfg.aws_credential_provider_endpoint, "role_alias": test1_cfg.aws_role_alias, - "certificate": _MOCKED_CERT, - "private_key": test1_cfg.private_key_path, "thing_name": test1_cfg.thing_name, + "cert_path": test1_cfg.certificate_path, + "key_path": test1_cfg.private_key_path, }, ), # test#2: boto3 session with pkcs11 @@ -101,34 +104,169 @@ def test__convert_to_pem(_in: bytes, _expected: bytes): ) ), ), + "_fetch_iot_credentials_pkcs11", { "endpoint": test2_cfg.aws_credential_provider_endpoint, "role_alias": test2_cfg.aws_role_alias, - "certificate": _MOCKED_CERT, "thing_name": test2_cfg.thing_name, - "pkcs11": aws_PKcs11Config( - pkcs11_lib=test2_pkcs11_cfg.pkcs11_lib, - slot_id=int(test2_pkcs11_cfg.slot_id), - user_pin=test2_pkcs11_cfg.user_pin, - private_key_label=_PARSED_PKCS11_PRIVKEY_URI["object"], - ), + "cert_pem": _MOCKED_CERT, + "pkcs11_cfg": test2_pkcs11_cfg, + "private_key_label": _PARSED_PKCS11_PRIVKEY_URI["object"], }, ), ], ) def test_get_session( - _config: IoTSessionConfig, _expected_call: dict[str, Any], mocker: MockerFixture + _config: IoTSessionConfig, + _expected_fetch_target: str, + _expected_call: dict[str, Any], + mocker: MockerFixture, ): """ - Confirm with specific input IoTSessionConfig, we get the expected Boto3Session being created. + Confirm with specific input IoTSessionConfig, we get the expected + credential fetch function being called with correct arguments. """ # ------ setup test ------ # - _boto3_session_provider_mock = mocker.MagicMock(spec=Boto3SessionProvider) - mocker.patch(f"{MODULE}.Boto3SessionProvider", _boto3_session_provider_mock) + _mock_credentials = { + "access_key": "test_access_key", + "secret_key": "test_secret_key", + "token": "test_token", + "expiry_time": "2099-01-01T00:00:00Z", + } + _fetch_mock = mocker.patch( + f"{MODULE}.{_expected_fetch_target}", + return_value=_mock_credentials, + ) mocker.patch( f"{MODULE}._load_certificate", mocker.MagicMock(return_value=_MOCKED_CERT) ) # ------ execution ------ # - get_session(_config) + session = get_session(_config) + # Call the refresh function directly to verify the arguments + _refresh_func = session._session._credentials._refresh_using # type: ignore + _refresh_func() # ------ check result ------ # - _boto3_session_provider_mock.assert_called_once_with(**_expected_call) + _fetch_mock.assert_called_once_with(**_expected_call) + + +class TestFetchIoTCredentials: + """Tests for _fetch_iot_credentials (awscrt-based).""" + + def _mock_awscrt(self, mocker: MockerFixture): + """Mock _fetch_iot_credentials_via_awscrt to return controlled responses.""" + mock_response = { + "access_key": "AKIAIOSFODNN7EXAMPLE", + "secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "token": "FwoGZXIvYXdzEBY...", + "expiry_time": "2099-01-01T00:00:00Z", + } + mock = mocker.patch( + f"{MODULE}._fetch_iot_credentials_via_awscrt", + return_value=mock_response, + ) + return mock + + def test_success(self, mocker: MockerFixture, tmp_path): + """Verify successful credential fetching returns correct format.""" + mock = self._mock_awscrt(mocker) + mock_build = mocker.patch(f"{MODULE}._build_tls_context_from_path") + + result = _fetch_iot_credentials( + endpoint="example.credentials.iot.ap-northeast-1.amazonaws.com", + role_alias="test-role-alias", + thing_name="test-thing", + cert_path=str(tmp_path / "cert.pem"), + key_path=str(tmp_path / "key.pem"), + ) + + assert result["access_key"] == "AKIAIOSFODNN7EXAMPLE" + assert result["secret_key"] == "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + assert result["token"] == "FwoGZXIvYXdzEBY..." + assert result["expiry_time"] == "2099-01-01T00:00:00Z" + # Verify tls_ctx_opt from _build_tls_context_from_path is passed through + mock.assert_called_once_with( + endpoint="example.credentials.iot.ap-northeast-1.amazonaws.com", + role_alias="test-role-alias", + thing_name="test-thing", + tls_ctx_opt=mock_build.return_value, + ) + + def test_tls_context_built_with_cert_and_key(self, mocker: MockerFixture): + """Verify cert and key file paths are passed to _build_tls_context_from_path.""" + mock_build = mocker.patch(f"{MODULE}._build_tls_context_from_path") + self._mock_awscrt(mocker) + + _fetch_iot_credentials( + endpoint="example.credentials.iot.ap-northeast-1.amazonaws.com", + role_alias="test-role-alias", + thing_name="test-thing", + cert_path="/path/to/cert.pem", + key_path="/path/to/key.pem", + ) + + mock_build.assert_called_once_with("/path/to/cert.pem", "/path/to/key.pem") + + def test_error_response_does_not_leak_body(self, mocker: MockerFixture): + """Verify non-200 response raises ValueError without leaking body.""" + mocker.patch(f"{MODULE}._build_tls_context_from_path") + mocker.patch( + f"{MODULE}._fetch_iot_credentials_via_awscrt", + side_effect=ValueError( + "Error getting credentials from IoT credential provider: status=403" + ), + ) + + with pytest.raises(ValueError, match="status=403") as exc_info: + _fetch_iot_credentials( + endpoint="example.credentials.iot.ap-northeast-1.amazonaws.com", + role_alias="test-role-alias", + thing_name="test-thing", + cert_path="/path/to/cert.pem", + key_path="/path/to/key.pem", + ) + + assert "secret" not in str(exc_info.value) + + +class TestParseCredentialsResponse: + """Tests for _parse_credentials_response.""" + + def test_success(self): + """Verify successful parsing of credential response.""" + body = ( + b'{"credentials": {' + b'"accessKeyId": "AKID", ' + b'"secretAccessKey": "SECRET", ' + b'"sessionToken": "TOKEN", ' + b'"expiration": "2099-01-01T00:00:00Z"}}' + ) + result = _parse_credentials_response(200, body, "https://example.com") + assert result == { + "access_key": "AKID", + "secret_key": "SECRET", + "token": "TOKEN", + "expiry_time": "2099-01-01T00:00:00Z", + } + + def test_error_does_not_leak_body(self): + """Verify non-200 response raises ValueError without leaking body.""" + with pytest.raises(ValueError, match="status=403") as exc_info: + _parse_credentials_response( + 403, b"secret error details", "https://example.com" + ) + assert "secret" not in str(exc_info.value) + + +class TestCreateBoto3Session: + """Tests for _create_boto3_session.""" + + @pytest.mark.parametrize( + "_region", + ["ap-northeast-1", "us-west-2", "eu-central-1"], + ) + def test_region_is_set(self, _region: str): + """Verify region is correctly set on the session.""" + mock_refresh = MagicMock() + session = _create_boto3_session(_region, mock_refresh) + + assert session.region_name == _region diff --git a/uv.lock b/uv.lock index eb564df8..fb4b6f0b 100644 --- a/uv.lock +++ b/uv.lock @@ -461,49 +461,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/6c/0f88e25d3b368e280fe2fc3016f66f89d44ea6d2e93325f16fccb99bffe3/awscrt-0.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:cc5cab9ceb8caeb82c524db92389ac74799522617485c75f4aa862a9b5ed75fb", size = 4102037, upload-time = "2026-01-15T02:21:24.402Z" }, ] -[[package]] -name = "awsiot-credentialhelper" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8.1'", -] -dependencies = [ - { name = "awscrt", version = "0.16.26", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, - { name = "boto3", version = "1.37.38", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, - { name = "botocore", version = "1.37.38", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, - { name = "urllib3", marker = "python_full_version < '3.8.1'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/e9/dc6aca29554ae90721086142059ce64205b536c5c2a395cdf00d82dfb60b/awsiot_credentialhelper-0.6.0.tar.gz", hash = "sha256:755b0974a80f16156cb04fe0fe31a07edc23678af2b7aeaf6211197c5d138f58", size = 14380, upload-time = "2023-11-22T19:51:13.985Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/66/c9df8a60d66a4db8d37ca69ddb8467d24f78e0540dc073cce78620e4b4b4/awsiot_credentialhelper-0.6.0-py3-none-any.whl", hash = "sha256:a1ca3775c5cdfd754cf41992506777b78e421ea46d39b59b356f560eebdd357d", size = 13410, upload-time = "2023-11-22T19:51:12.769Z" }, -] - -[[package]] -name = "awsiot-credentialhelper" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version >= '3.8.1' and python_full_version < '3.9'", -] -dependencies = [ - { name = "awscrt", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1'" }, - { name = "boto3", version = "1.37.38", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, - { name = "boto3", version = "1.42.44", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "botocore", version = "1.37.38", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, - { name = "botocore", version = "1.42.44", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "urllib3", marker = "python_full_version >= '3.8.1'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/ec/4bd284c92075bd513e2dd4cbae731634fc355b5bd314a0abc45f9d45b86f/awsiot_credentialhelper-1.0.2.tar.gz", hash = "sha256:4db2f4dd03f13be9387ef660cf30b6cf4b7dacbd63e0c8aaef54666a3f19aa18", size = 14370, upload-time = "2024-09-30T20:16:30.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/91/5861d666f872084180ddcd70dbe3c3f34bb5606be47bc008283301c5d5a3/awsiot_credentialhelper-1.0.2-py3-none-any.whl", hash = "sha256:b3d41064ea4a73726c0c489e12937c1493954eb5f50b2bcdd8cc6e5e57998084", size = 13397, upload-time = "2024-09-30T20:16:29.169Z" }, -] - [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -560,7 +517,7 @@ resolution-markers = [ dependencies = [ { name = "jmespath", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "python-dateutil", marker = "python_full_version < '3.9'" }, - { name = "urllib3", marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/79/4e072e614339727f79afef704e5993b5b4d2667c1671c757cc4deb954744/botocore-1.37.38.tar.gz", hash = "sha256:c3ea386177171f2259b284db6afc971c959ec103fa2115911c4368bea7cbbc5d", size = 13832365, upload-time = "2025-04-21T19:27:05.245Z" } wheels = [ @@ -578,7 +535,8 @@ resolution-markers = [ dependencies = [ { name = "jmespath", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "python-dateutil", marker = "python_full_version >= '3.9'" }, - { name = "urllib3", marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/ff/54cef2c5ff4e1c77fabc0ed68781e48eb36f33433f82bba3605e9c0e45ce/botocore-1.42.44.tar.gz", hash = "sha256:47ba27360f2afd2c2721545d8909217f7be05fdee16dd8fc0b09589535a0701c", size = 14936071, upload-time = "2026-02-06T20:27:53.654Z" } wheels = [ @@ -1742,8 +1700,8 @@ source = { editable = "." } dependencies = [ { name = "aiohttp", version = "3.10.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, - { name = "awsiot-credentialhelper", version = "0.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, - { name = "awsiot-credentialhelper", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1'" }, + { name = "awscrt", version = "0.16.26", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" }, + { name = "awscrt", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1'" }, { name = "boto3", version = "1.37.38", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "boto3", version = "1.42.44", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "grpcio", version = "1.70.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, @@ -1783,7 +1741,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.10.11,<3.14" }, - { name = "awsiot-credentialhelper", specifier = ">=0.6,<1.1" }, + { name = "awscrt", specifier = ">=0.16.9,<0.32" }, { name = "boto3", specifier = ">=1.34.35,<1.43" }, { name = "grpcio", specifier = ">=1.70,<1.79" }, { name = "protobuf", specifier = ">=4.25.8,<6.34" }, @@ -3000,11 +2958,28 @@ wheels = [ name = "urllib3" version = "1.26.20" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version >= '3.8.1' and python_full_version < '3.9'", + "python_full_version < '3.8.1'", +] sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, ] +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + [[package]] name = "virtualenv" version = "20.36.1"