Skip to content

Bump botocore dependency specification #1391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changes
-------

2.24.1 (2025-08-09)
^^^^^^^^^^^^^^^^^^^
* bump botocore dependency specification

2.24.0 (2025-07-31)
^^^^^^^^^^^^^^^^^^^
* bump botocore dependency specification
Expand Down
2 changes: 1 addition & 1 deletion aiobotocore/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.24.0'
__version__ = '2.24.1'
14 changes: 12 additions & 2 deletions aiobotocore/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from copy import deepcopy

import botocore.compat
import dateutil.parser
from botocore import UNSIGNED
from botocore.compat import compat_shell_split
from botocore.compat import compat_shell_split, total_seconds
from botocore.config import Config
from botocore.credentials import (
_DEFAULT_ADVISORY_REFRESH_TIMEOUT,
Expand Down Expand Up @@ -1048,7 +1049,16 @@ async def _get_credentials(self):
initial_token_data = self._token_provider.load_token()
token = (await initial_token_data.get_frozen_token()).token
else:
token = self._token_loader(self._start_url)['accessToken']
token_dict = self._token_loader(self._start_url)
token = token_dict['accessToken']

# raise an UnauthorizedSSOTokenError if the loaded legacy token
# is expired to save a call to GetRoleCredentials with an
# expired token.
expiration = dateutil.parser.parse(token_dict['expiresAt'])
remaining = total_seconds(expiration - self._time_fetcher())
if remaining <= 0:
raise UnauthorizedSSOTokenError()

kwargs = {
'roleName': self._role_name,
Expand Down
3 changes: 2 additions & 1 deletion aiobotocore/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ async def describe_endpoint(self, **kwargs):
if not self._always_discover and not discovery_required:
# Discovery set to only run on required operations
logger.debug(
f'Optional discovery disabled. Skipping discovery for Operation: {operation}'
'Optional discovery disabled. Skipping discovery for Operation: %s',
operation,
)
return None

Expand Down
4 changes: 2 additions & 2 deletions aiobotocore/httpchecksum.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ async def handle_checksum_body(
return

logger.debug(
f'Skipping checksum validation. Response did not contain one of the '
f'following algorithms: {algorithms}.'
'Skipping checksum validation. Response did not contain one of the following algorithms: %s.',
algorithms,
)


Expand Down
4 changes: 2 additions & 2 deletions aiobotocore/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def construct_endpoint(
operation_model, call_args, request_context
)
LOG.debug(
f'Calling endpoint provider with parameters: {provider_params}'
'Calling endpoint provider with parameters: %s', provider_params
)
try:
provider_result = self._provider.resolve_endpoint(
Expand All @@ -41,7 +41,7 @@ async def construct_endpoint(
raise
else:
raise botocore_exception from ex
LOG.debug(f'Endpoint provider result: {provider_result.url}')
LOG.debug('Endpoint provider result: %s', provider_result.url)

# The endpoint provider does not support non-secure transport.
if not self._use_ssl and provider_result.url.startswith('https://'):
Expand Down
5 changes: 3 additions & 2 deletions aiobotocore/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,9 @@ async def _create_client(
aws_session_token, aws_account_id
):
logger.debug(
f"Ignoring the following credential-related values which were set without "
f"an access key id and secret key on the session or client: {ignored_credentials}"
"Ignoring the following credential-related values which were set without "
"an access key id and secret key on the session or client: %s",
ignored_credentials,
)
credentials = await self.get_credentials()
auth_token = self.get_auth_token()
Expand Down
3 changes: 2 additions & 1 deletion aiobotocore/signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import botocore
import botocore.auth
from botocore.compat import get_current_datetime
from botocore.exceptions import ParamValidationError, UnknownClientMethodError
from botocore.signers import (
RequestSigner,
Expand Down Expand Up @@ -368,7 +369,7 @@ async def generate_presigned_post(
policy = {}

# Create an expiration date for the policy
datetime_now = datetime.datetime.utcnow()
datetime_now = get_current_datetime()
expire_date = datetime_now + datetime.timedelta(seconds=expires_in)
policy['expiration'] = expire_date.strftime(botocore.auth.ISO8601)

Expand Down
6 changes: 3 additions & 3 deletions aiobotocore/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def _refresh_access_token(self, token):

expiry = dateutil.parser.parse(token["registrationExpiresAt"])
if total_seconds(expiry - self._now()) <= 0:
logger.info(f"SSO token registration expired at {expiry}")
logger.info("SSO token registration expired at %s", expiry)
return None

try:
Expand All @@ -137,10 +137,10 @@ async def _refresh_access_token(self, token):
async def _refresher(self):
start_url = self._sso_config["sso_start_url"]
session_name = self._sso_config["session_name"]
logger.info(f"Loading cached SSO token for {session_name}")
logger.info("Loading cached SSO token for %s", session_name)
token_dict = self._token_loader(start_url, session_name=session_name)
expiration = dateutil.parser.parse(token_dict["expiresAt"])
logger.debug(f"Cached SSO token expires at {expiration}")
logger.debug("Cached SSO token expires at %s", expiration)

remaining = total_seconds(expiration - self._now())
if remaining < self._REFRESH_WINDOW:
Expand Down
32 changes: 21 additions & 11 deletions aiobotocore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,16 +486,21 @@ async def redirect_from_error(

if new_region is None:
logger.debug(
f"S3 client configured for region {client_region} but the "
f"bucket {bucket} is not in that region and the proper region "
"could not be automatically determined."
"S3 client configured for region %s but the "
"bucket %s is not in that region and the proper region "
"could not be automatically determined.",
client_region,
bucket,
)
return

logger.debug(
f"S3 client configured for region {client_region} but the bucket {bucket} "
f"is in region {new_region}; Please configure the proper region to "
f"avoid multiple unnecessary redirects and signing attempts."
"S3 client configured for region %s but the bucket %s "
"is in region %s; Please configure the proper region to "
"avoid multiple unnecessary redirects and signing attempts.",
client_region,
bucket,
new_region,
)
# Adding the new region to _cache will make construct_endpoint() to
# use the new region as value for the AWS::Region builtin parameter.
Expand Down Expand Up @@ -622,16 +627,21 @@ async def redirect_from_error(

if new_region is None:
logger.debug(
f"S3 client configured for region {client_region} but the bucket {bucket} is not "
"S3 client configured for region %s but the bucket %s is not "
"in that region and the proper region could not be "
"automatically determined."
"automatically determined.",
client_region,
bucket,
)
return

logger.debug(
f"S3 client configured for region {client_region} but the bucket {bucket} is in region"
f" {new_region}; Please configure the proper region to avoid multiple "
"unnecessary redirects and signing attempts."
"S3 client configured for region %s but the bucket %s is in region"
" %s; Please configure the proper region to avoid multiple "
"unnecessary redirects and signing attempts.",
client_region,
bucket,
new_region,
)
endpoint = self._endpoint_resolver.resolve('s3', new_region)
endpoint = endpoint['endpoint_url']
Expand Down
17 changes: 12 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dynamic = ["version", "readme"]
dependencies = [
"aiohttp >= 3.9.2, < 4.0.0",
"aioitertools >= 0.5.1, < 1.0.0",
"botocore >= 1.39.9, < 1.39.12", # NOTE: When updating, always keep `project.optional-dependencies` aligned
"botocore >= 1.40.2, < 1.40.7", # NOTE: When updating, always keep `project.optional-dependencies` aligned
"python-dateutil >= 2.1, < 3.0.0",
"jmespath >= 0.7.1, < 2.0.0",
"multidict >= 6.0.0, < 7.0.0",
Expand All @@ -41,10 +41,10 @@ dependencies = [

[project.optional-dependencies]
awscli = [
"awscli >= 1.41.9, < 1.41.12",
"awscli >= 1.42.2, < 1.42.7",
]
boto3 = [
"boto3 >= 1.39.9, < 1.39.12",
"boto3 >= 1.40.2, < 1.40.7",
]
httpx = [
"httpx >= 0.25.1, < 0.29"
Expand Down Expand Up @@ -145,10 +145,17 @@ indent-width = 4
target-version = "py39"

[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F", "I", "UP"]
select = [
"E4", # pycodestyle
"E7", # pycodestyle
"E9", # pycodestyle
"F", # Pyflakes
"I", # Import sorting
"UP", # PyUpgrade
"G", # Log formatting
]
ignore = []

# Allow fix for all enabled rules (when `--fix`) is provided.
Expand Down
39 changes: 35 additions & 4 deletions tests/botocore_tests/unit/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,14 +1089,18 @@ async def ssl_credential_fetcher_setup():
self.start_url = 'https://d-92671207e4.awsapps.com/start'
self.role_name = 'test-role'
self.account_id = '1234567890'
self.access_token = 'some.sso.token'
self.access_token = {
'accessToken': 'some.sso.token',
'expiresAt': '2018-10-18T22:26:40Z',
}
# This is just an arbitrary point in time we can pin to
self.now = datetime(2008, 9, 23, 12, 26, 40, tzinfo=tzutc())
# The SSO endpoint uses ms whereas the OIDC endpoint uses seconds
self.now_timestamp = 1222172800000
self.mock_time_fetcher = mock.Mock(return_value=self.now)

self.loader = mock.Mock(spec=SSOTokenLoader)
self.loader.return_value = {'accessToken': self.access_token}
self.loader.return_value = self.access_token
self.fetcher = AioSSOCredentialFetcher(
self.start_url,
self.sso_region,
Expand All @@ -1105,11 +1109,13 @@ async def ssl_credential_fetcher_setup():
self.mock_session.create_client,
token_loader=self.loader,
cache=self.cache,
time_fetcher=self.mock_time_fetcher,
)

tc = TestCase()
self.assertEqual = tc.assertEqual
self.assertRaises = tc.assertRaises
self.assertFalse = tc.assertFalse
yield self


Expand Down Expand Up @@ -1292,7 +1298,7 @@ async def test_sso_credential_fetcher_can_fetch_credentials(
expected_params = {
'roleName': self.role_name,
'accountId': self.account_id,
'accessToken': self.access_token,
'accessToken': self.access_token['accessToken'],
}
expected_response = {
'roleCredentials': {
Expand Down Expand Up @@ -1334,7 +1340,7 @@ async def test_sso_cred_fetcher_raises_helpful_message_on_unauthorized_exception
expected_params = {
'roleName': self.role_name,
'accountId': self.account_id,
'accessToken': self.access_token,
'accessToken': self.access_token['accessToken'],
}
self.stubber.add_client_error(
'get_role_credentials',
Expand All @@ -1346,6 +1352,31 @@ async def test_sso_cred_fetcher_raises_helpful_message_on_unauthorized_exception
await self.fetcher.fetch_credentials()


async def test_sso_cred_fetcher_expired_legacy_token_has_expected_behavior(
ssl_credential_fetcher_setup,
):
self = ssl_credential_fetcher_setup
# Mock the current time to be in the future after the access token has expired
now = datetime(2018, 10, 19, 12, 26, 40, tzinfo=tzutc())
mock_client = mock.AsyncMock()
create_mock_client = mock.Mock(return_value=mock_client)
fetcher = AioSSOCredentialFetcher(
self.start_url,
self.sso_region,
self.role_name,
self.account_id,
create_mock_client,
token_loader=self.loader,
cache=self.cache,
time_fetcher=mock.Mock(return_value=now),
)
# since the cached token is expired, an UnauthorizedSSOTokenError should be
# raised and GetRoleCredentials should not be called.
with self.assertRaises(botocore.exceptions.UnauthorizedSSOTokenError):
await fetcher.fetch_credentials()
self.assertFalse(mock_client.get_role_credentials.called)


# from TestSSOProvider
@pytest.fixture
async def sso_provider_setup():
Expand Down
2 changes: 1 addition & 1 deletion tests/botocore_tests/unit/test_signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def test_signers_generate_db_auth_token(rds_client):
clock = datetime.datetime(2016, 11, 7, 17, 39, 33, tzinfo=timezone.utc)

with mock.patch('datetime.datetime') as dt:
dt.utcnow.return_value = clock
dt.now.return_value = clock
result = await aiobotocore.signers.generate_db_auth_token(
rds_client, hostname, port, username
)
Expand Down
Loading
Loading