From e14b0cc5e5013f0d883f34b7e47b73195d92df48 Mon Sep 17 00:00:00 2001 From: Kseniia Nivnia Date: Mon, 13 Apr 2026 16:20:42 +0100 Subject: [PATCH] Update to v2.33.0 Resolves: RHEL-157870 Signed-off-by: Kseniia Nivnia --- .gitignore | 1 + ...-for-botocore-memory-leak-tests-to-1.patch | 38 + assertions.patch | 41 + awscli2.spec | 114 ++- prompt-toolkit.patch | 76 ++ python312.patch | 882 ++++++++++++++++++ python314.patch | 140 +++ ruamel-yaml-0.17.32.patch | 113 +++ sources | 2 +- urllib3-v2.patch | 37 + 10 files changed, 1397 insertions(+), 47 deletions(-) create mode 100644 0001-Bump-the-ceiling-for-botocore-memory-leak-tests-to-1.patch create mode 100644 assertions.patch create mode 100644 prompt-toolkit.patch create mode 100644 python312.patch create mode 100644 python314.patch create mode 100644 ruamel-yaml-0.17.32.patch create mode 100644 urllib3-v2.patch diff --git a/.gitignore b/.gitignore index 2223f05..4982a15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /aws-cli-2.15.31.tar.gz /aws-cli-2.15.47.tar.gz +/aws-cli-2.33.0.tar.gz diff --git a/0001-Bump-the-ceiling-for-botocore-memory-leak-tests-to-1.patch b/0001-Bump-the-ceiling-for-botocore-memory-leak-tests-to-1.patch new file mode 100644 index 0000000..6560c5d --- /dev/null +++ b/0001-Bump-the-ceiling-for-botocore-memory-leak-tests-to-1.patch @@ -0,0 +1,38 @@ +From 4b5762bb17f172d9f9e058df8908651856ff4a69 Mon Sep 17 00:00:00 2001 +From: Adam Williamson +Date: Thu, 13 Jun 2024 10:33:42 -0700 +Subject: [PATCH] Bump the ceiling for botocore memory leak tests to 15 MiB + +See https://github.com/boto/botocore/issues/3205 for the +background on this. In rebuilding awscli2 for Python 3.13 in +Fedora Rawhide, we found that two of these tests fail because +they now top out around 11MiB of memory usage, rather than +around 1.6MiB. We don't understand why this is yet, but it's not +a memory *leak*, so bumping the ceiling seems appropriate. I'm +sending this upstream so I have a reference for the downstream +package and to raise awareness of the issue, but the correct fix +may be something else. + +Signed-off-by: Adam Williamson +--- + tests/functional/botocore/leak/test_resource_leaks.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/tests/functional/botocore/leak/test_resource_leaks.py b/tests/functional/botocore/leak/test_resource_leaks.py +index df1c4fe7f..73027725f 100644 +--- a/tests/functional/botocore/leak/test_resource_leaks.py ++++ b/tests/functional/botocore/leak/test_resource_leaks.py +@@ -22,8 +22,8 @@ class TestDoesNotLeakMemory(BaseClientDriverTest): + # a substantial amount of time to the total test run time. + INJECT_DUMMY_CREDS = True + # We're making up numbers here, but let's say arbitrarily +- # that the memory can't increase by more than 10MB. +- MAX_GROWTH_BYTES = 10 * 1024 * 1024 ++ # that the memory can't increase by more than 15MB. ++ MAX_GROWTH_BYTES = 15 * 1024 * 1024 + + def test_create_single_client_memory_constant(self): + self.cmd('create_client', 's3') +-- +2.45.2 + diff --git a/assertions.patch b/assertions.patch new file mode 100644 index 0000000..95cff44 --- /dev/null +++ b/assertions.patch @@ -0,0 +1,41 @@ +diff --git a/tests/functional/eks/test_kubeconfig.py b/tests/functional/eks/test_kubeconfig.py +index d5dda2d..6c642aa 100644 +--- a/tests/functional/eks/test_kubeconfig.py ++++ b/tests/functional/eks/test_kubeconfig.py +@@ -127,9 +127,9 @@ class TestKubeconfigLoader(unittest.TestCase): + ) + loaded_config = self._loader.load_kubeconfig(simple_path) + self.assertEqual(loaded_config.content, content) +- self._validator.validate_config.assert_called_with( +- Kubeconfig(simple_path, content) +- ) ++ validated_config = self._validator.validate_config.call_args.args[0] ++ self.assertEqual(validated_config.path, simple_path) ++ self.assertEqual(validated_config.content, content) + + def test_load_noexist(self): + no_exist_path = os.path.join( +@@ -137,17 +137,17 @@ class TestKubeconfigLoader(unittest.TestCase): + ) + loaded_config = self._loader.load_kubeconfig(no_exist_path) + self.assertEqual(loaded_config.content, _get_new_kubeconfig_content()) +- self._validator.validate_config.assert_called_with( +- Kubeconfig(no_exist_path, _get_new_kubeconfig_content()) +- ) ++ validated_config = self._validator.validate_config.call_args.args[0] ++ self.assertEqual(validated_config.path, no_exist_path) ++ self.assertEqual(validated_config.content, _get_new_kubeconfig_content()) + + def test_load_empty(self): + empty_path = self._clone_config("valid_empty_existing") + loaded_config = self._loader.load_kubeconfig(empty_path) + self.assertEqual(loaded_config.content, _get_new_kubeconfig_content()) +- self._validator.validate_config.assert_called_with( +- Kubeconfig(empty_path, _get_new_kubeconfig_content()) +- ) ++ validated_config = self._validator.validate_config.call_args.args[0] ++ self.assertEqual(validated_config.path, empty_path) ++ self.assertEqual(validated_config.content, _get_new_kubeconfig_content()) + + def test_load_directory(self): + current_directory = self._temp_directory diff --git a/awscli2.spec b/awscli2.spec index 73a655f..4f6dad5 100644 --- a/awscli2.spec +++ b/awscli2.spec @@ -1,10 +1,12 @@ %bcond tests 1 %global pkgname aws-cli +%global bash_completions_dir %{_datadir}/bash-completion/completions +%global zsh_completions_dir %{_datadir}/zsh/site-functions Name: awscli2 -Version: 2.15.31 -Release: 3%{?dist} +Version: 2.33.0 +Release: 1%{?dist} Summary: Universal Command Line Environment for AWS, version 2 # all files are licensed under Apache-2.0, except: @@ -12,47 +14,65 @@ Summary: Universal Command Line Environment for AWS, version 2 # - awscli/botocore/vendored/six.py is MIT License: Apache-2.0 AND MIT URL: https://github.com/aws/aws-cli/tree/v2 + Source0: https://github.com/aws/aws-cli/archive/%{version}/%{pkgname}-%{version}.tar.gz + +# adapt to whitespace formatting changes and removal of OrderedDict in ruamel-yaml +Patch0: ruamel-yaml-0.17.32.patch +# fix Python 3.12 incompatibilities +Patch1: python312.patch +# fix incorrect assertions in TestKubeconfigLoader +Patch2: assertions.patch +# Bump ceiling for botocore memory leak tests +# https://github.com/aws/aws-cli/pull/8744 +# https://github.com/boto/botocore/issues/3205 +Patch3: 0001-Bump-the-ceiling-for-botocore-memory-leak-tests-to-1.patch +# compatibility fixes for urllib3 v2 +Patch4: urllib3-v2.patch +# fix Python 3.14 incompatibilities +Patch5: python314.patch +# python-prompt-toolkit compatibility fixes +# incl. https://github.com/aws/aws-cli/pull/9684#issuecomment-3804078566 +Patch6: prompt-toolkit.patch BuildArch: noarch - -BuildRequires: python3-devel + +BuildRequires: python%{python3_pkgversion}-devel BuildRequires: python-unversioned-command BuildRequires: procps-ng - -# Needed for paging output from awscli2. See RHEL-14523. -Requires: less - + Recommends: groff - + Provides: bundled(python3dist(botocore)) = 2.0.0 Provides: bundled(python3dist(s3transfer)) = 0.5.1 - + Provides: awscli = %{version}-%{release} Obsoletes: awscli < 2 - + # provide an upgrade path from awscli-2 (Amazon Linux) Provides: awscli-2 = %{version}-%{release} Obsoletes: awscli-2 < %{version}-%{release} - -# python-awscrt does not build on s390x -ExcludeArch: s390x - - + +ExcludeArch: %{ix86} + + %description This package provides version 2 of the unified command line interface to Amazon Web Services. - - + + %prep %autosetup -p1 -n %{pkgname}-%{version} - + # fix permissions find awscli/examples/ -type f -name '*.rst' -executable -exec chmod -x '{}' + - # remove version caps on dependencies sed -i 's/,<=\?[^"]*"/"/' pyproject.toml - +# loosen awscrt version requirement +sed -i 's/awscrt==/awscrt>=/' pyproject.toml +# remove zipp dependency +sed -i "/zipp<3.21.0/d" pyproject.toml + # use unittest.mock find -type f -name '*.py' -exec sed \ -e 's/^\( *\)import mock$/\1from unittest import mock/' \ @@ -72,57 +92,59 @@ sed \ -e '/pytest-cov/d' \ %{?rhel:-e '/pytest-xdist/d'} \ requirements-test.txt > _requirements-test.txt - - + + %generate_buildrequires %pyproject_buildrequires _requirements-test.txt - - + + %build %pyproject_wheel - - + + %install %pyproject_install %pyproject_save_files awscli - + # remove unnecessary scripts rm -vf %{buildroot}%{_bindir}/{aws_bash_completer,aws_zsh_completer.sh,aws.cmd} - + # install shell completion install -Dpm0644 bin/aws_bash_completer \ - %{buildroot}%{_datadir}/bash-completion/completions/aws + %{buildroot}%{bash_completions_dir}/aws install -Dpm0644 bin/aws_zsh_completer.sh \ - %{buildroot}%{_datadir}/zsh/site-functions/_awscli - - + %{buildroot}%{zsh_completions_dir}/_awscli + + %check # it appears that some tests modify the environment and remove PYTHONPATH # so it's not passed to botocore cmd-runner, inject it here sed -i '/self.driver.start(env=env)/i \ \ \ \ \ \ \ \ env["PYTHONPATH"] = "%{buildroot}%{python3_sitelib}"' \ tests/utils/botocore/__init__.py - + export TESTS_REMOVE_REPO_ROOT_FROM_PATH=1 TZ=UTC -%if 0%{?rhel} export OPENSSL_ENABLE_SHA1_SIGNATURES=yes -%endif -%pytest --verbose %{!?rhel:--numprocesses=auto --dist=loadfile --maxprocesses=4} \ - --disable-pytest-warnings -Wd \ - tests/unit tests/functional \ - --ignore tests/functional/autocomplete/test_completion_files.py \ - --ignore tests/functional/botocore/test_waiter_config.py - +# the 'which' tests in tests/unit/customizations/emr/test_emr_utils.py are failing if they run after tests that change the environment and remove PATH +%pytest --verbose %{!?rhel:--numprocesses=auto --dist=loadfile --maxprocesses=4 -k 'not test_which'} -k \ + 'not test_error_with_case_conflicts_in_s3 and not test_return_focus_on_input_buffer and not test_multiple_connection_mode and not test_input_buffer_initialization and not TestHelpPanel and not TestOutputPanel' \ + --ignore tests/unit/customizations/wizard/test_app.py \ + tests/unit tests/functional + %files -f %{pyproject_files} %license LICENSE.txt %doc README.rst %{_bindir}/aws %{_bindir}/aws_completer -%{_datadir}/bash-completion/completions/aws -%{_datadir}/zsh/site-functions/_awscli - - +%{bash_completions_dir}/aws +%{zsh_completions_dir}/_awscli + + %changelog +* Mon Apr 13 2026 Kseniia Nivnia - 2.33.0-1 +- Update to v2.33.0 + Resolves RHEL-157870 + * Fri May 31 2024 Major Hayden - 2.15.31-3 - Add less to install requirements for paging output. Resolves RHEL-14523. diff --git a/prompt-toolkit.patch b/prompt-toolkit.patch new file mode 100644 index 0000000..cd0a212 --- /dev/null +++ b/prompt-toolkit.patch @@ -0,0 +1,76 @@ +--- a/awscli/customizations/wizard/ui/layout.py ++++ b/awscli/customizations/wizard/ui/layout.py +@@ -284,6 +284,7 @@ class ToolbarView(BaseToolbarView): + + def __init__(self): + self.content = to_container(self.create_window(self.help_text)) ++ self.alternative_content = None + self.filter = to_filter(self.CONDITION) + + def create_window(self, help_text): + + +--- a/awscli/autoprompt/factory.py ++++ b/awscli/autoprompt/factory.py +@@ -271,7 +271,7 @@ + buffer.switch_history_mode() + buffer.insert_text(' ') + +- @self._kb.add(Keys.F3) ++ @self._kb.add(Keys.F3, is_global=True) + def _(event): + current_buffer = event.app.current_buffer + if current_buffer.name == 'doc_buffer': +@@ -280,11 +280,11 @@ + layout.focus(input_buffer) + event.app.show_doc = not getattr(event.app, 'show_doc') + +- @self._kb.add(Keys.F4) ++ @self._kb.add(Keys.F4, is_global=True) + def _(event): + event.app.multi_column = not event.app.multi_column + +- @self._kb.add(Keys.F5) ++ @self._kb.add(Keys.F5, is_global=True) + def _(event): + event.app.show_output = not event.app.show_output + if event.app.current_buffer.name == 'output_buffer': +@@ -292,7 +292,7 @@ + input_buffer = layout.get_buffer_by_name('input_buffer') + layout.focus(input_buffer) + +- @self._kb.add(Keys.F1) ++ @self._kb.add(Keys.F1, is_global=True) + def _(event): + event.app.show_help = not event.app.show_help + +@@ -310,7 +310,7 @@ + text = f'> aws {input_buffer.document.text}' + event.app.exit(exception=PrompterKeyboardInterrupt(text)) + +- @self._kb.add(Keys.F2) ++ @self._kb.add(Keys.F2, is_global=True) + def _(event): + focus_next(event) + +--- a/awscli/autoprompt/widgets.py ++++ b/awscli/autoprompt/widgets.py +@@ -96,6 +96,7 @@ + def __init__(self): + window = self.create_window(self.create_buffer()) + super().__init__(window, self.CONDITION) ++ self.alternative_content = None + + @property + def help_text(self): + +--- a/tests/__init__.py ++++ b/tests/__init__.py +@@ -472,6 +472,7 @@ + self._done_rendering_event.clear() + self.app.input.send_text(self._convert_key_to_vt100_data(key)) + self._wait_until_app_is_done_updating() ++ time.sleep(0.01) + + def wait_for_completions_on_current_buffer(self): + if self._current_buffer_has_completions(): diff --git a/python312.patch b/python312.patch new file mode 100644 index 0000000..eabbfa5 --- /dev/null +++ b/python312.patch @@ -0,0 +1,882 @@ +diff --git a/awscli/botocore/auth.py b/awscli/botocore/auth.py +index 8ceb4ff..d7570b4 100644 +--- a/awscli/botocore/auth.py ++++ b/awscli/botocore/auth.py +@@ -419,7 +419,7 @@ class SigV4Auth(BaseSigner): + def add_auth(self, request): + if self.credentials is None: + raise NoCredentialsError() +- datetime_now = datetime.datetime.utcnow() ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP) + # This could be a retry. Make sure the previous + # authorization header is removed first. +@@ -465,7 +465,7 @@ class SigV4Auth(BaseSigner): + if 'Date' in request.headers: + del request.headers['Date'] + datetime_timestamp = datetime.datetime.strptime( +- request.context['timestamp'], SIGV4_TIMESTAMP ++ request.context['timestamp'], SIGV4_TIMESTAMP[:-1] + '%z' + ) + request.headers['Date'] = formatdate( + int(calendar.timegm(datetime_timestamp.timetuple())) +@@ -557,7 +557,7 @@ class S3ExpressPostAuth(S3ExpressAuth): + REQUIRES_IDENTITY_CACHE = True + + def add_auth(self, request): +- datetime_now = datetime.datetime.utcnow() ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP) + + fields = {} +@@ -818,7 +818,7 @@ class S3SigV4PostAuth(SigV4Auth): + """ + + def add_auth(self, request): +- datetime_now = datetime.datetime.utcnow() ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP) + + fields = {} +diff --git a/awscli/botocore/crt/auth.py b/awscli/botocore/crt/auth.py +index 2b96c7e..86104f3 100644 +--- a/awscli/botocore/crt/auth.py ++++ b/awscli/botocore/crt/auth.py +@@ -56,11 +56,7 @@ class CrtSigV4Auth(BaseSigner): + if self.credentials is None: + raise NoCredentialsError() + +- # Use utcnow() because that's what gets mocked by tests, but set +- # timezone because CRT assumes naive datetime is local time. +- datetime_now = datetime.datetime.utcnow().replace( +- tzinfo=datetime.timezone.utc +- ) ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + + # Use existing 'X-Amz-Content-SHA256' header if able + existing_sha256 = self._get_existing_sha256(request) +@@ -254,11 +250,7 @@ class CrtSigV4AsymAuth(BaseSigner): + if self.credentials is None: + raise NoCredentialsError() + +- # Use utcnow() because that's what gets mocked by tests, but set +- # timezone because CRT assumes naive datetime is local time. +- datetime_now = datetime.datetime.utcnow().replace( +- tzinfo=datetime.timezone.utc +- ) ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + + # Use existing 'X-Amz-Content-SHA256' header if able + existing_sha256 = self._get_existing_sha256(request) +diff --git a/awscli/botocore/signers.py b/awscli/botocore/signers.py +index 8cd7b37..3a1e3a3 100644 +--- a/awscli/botocore/signers.py ++++ b/awscli/botocore/signers.py +@@ -717,7 +717,7 @@ class S3PostPresigner: + policy = {} + + # Create an expiration date for the policy +- datetime_now = datetime.datetime.utcnow() ++ datetime_now = datetime.datetime.now(datetime.timezone.utc) + expire_date = datetime_now + datetime.timedelta(seconds=expires_in) + policy['expiration'] = expire_date.strftime(botocore.auth.ISO8601) + +diff --git a/awscli/botocore/utils.py b/awscli/botocore/utils.py +index 04d516c..4a82618 100644 +--- a/awscli/botocore/utils.py ++++ b/awscli/botocore/utils.py +@@ -625,7 +625,7 @@ class InstanceMetadataFetcher(IMDSFetcher): + return + try: + expiration = datetime.datetime.strptime( +- expiration, "%Y-%m-%dT%H:%M:%SZ" ++ expiration, "%Y-%m-%dT%H:%M:%S%z" + ) + refresh_interval = self._config.get( + "ec2_credential_refresh_window", 60 * 10 +@@ -633,7 +633,7 @@ class InstanceMetadataFetcher(IMDSFetcher): + refresh_interval_with_jitter = refresh_interval + random.randint( + 120, 600 + ) +- current_time = datetime.datetime.utcnow() ++ current_time = datetime.datetime.now(datetime.timezone.utc) + refresh_offset = datetime.timedelta( + seconds=refresh_interval_with_jitter + ) +diff --git a/awscli/compat.py b/awscli/compat.py +index fd5fa19..3dce1f5 100644 +--- a/awscli/compat.py ++++ b/awscli/compat.py +@@ -24,6 +24,7 @@ import shlex + import signal + import sys + import urllib.parse as urlparse ++import urllib.request + import zipfile + from configparser import RawConfigParser + from functools import partial +diff --git a/awscli/customizations/cloudformation/deployer.py b/awscli/customizations/cloudformation/deployer.py +index a5c512f..8783ebb 100644 +--- a/awscli/customizations/cloudformation/deployer.py ++++ b/awscli/customizations/cloudformation/deployer.py +@@ -15,7 +15,7 @@ import collections + import logging + import sys + import time +-from datetime import datetime ++from datetime import datetime, timezone + + import botocore + +@@ -98,7 +98,7 @@ class Deployer: + :return: + """ + +- now = datetime.utcnow().isoformat() ++ now = datetime.now(timezone.utc).isoformat() + description = f"Created by AWS CLI at {now} UTC" + + # Each changeset will get a unique name based on time +diff --git a/awscli/customizations/cloudtrail/validation.py b/awscli/customizations/cloudtrail/validation.py +index d5255f4..aca31f4 100644 +--- a/awscli/customizations/cloudtrail/validation.py ++++ b/awscli/customizations/cloudtrail/validation.py +@@ -18,7 +18,7 @@ import logging + import re + import sys + import zlib +-from datetime import datetime, timedelta ++from datetime import datetime, timedelta, timezone + from zlib import error as ZLibError + + from awscrt.crypto import RSA, RSASignatureAlgorithm +@@ -464,7 +464,7 @@ class DigestTraverser: + :param end_date: Date to stop validating at (inclusive). + """ + if end_date is None: +- end_date = datetime.utcnow() ++ end_date = datetime.now(timezone.utc) + end_date = normalize_date(end_date) + start_date = normalize_date(start_date) + bucket = self.starting_bucket +@@ -850,7 +850,7 @@ class CloudTrailValidateLogs(BasicCommand): + if args.end_time: + self.end_time = normalize_date(parse_date(args.end_time)) + else: +- self.end_time = normalize_date(datetime.utcnow()) ++ self.end_time = normalize_date(datetime.now(timezone.utc)) + if self.start_time > self.end_time: + raise ParamValidationError( + 'Invalid time range specified: start-time must ' +diff --git a/awscli/customizations/codecommit.py b/awscli/customizations/codecommit.py +index b001562..cb9e757 100644 +--- a/awscli/customizations/codecommit.py ++++ b/awscli/customizations/codecommit.py +@@ -159,7 +159,7 @@ class CodeCommitGetCommand(BasicCommand): + request = AWSRequest() + request.url = url_to_sign + request.method = 'GIT' +- now = datetime.datetime.utcnow() ++ now = datetime.datetime.now(datetime.timezone.utc) + request.context['timestamp'] = now.strftime('%Y%m%dT%H%M%S') + split = urlsplit(request.url) + # we don't want to include the port number in the signature +diff --git a/awscli/customizations/codedeploy/push.py b/awscli/customizations/codedeploy/push.py +index bdde82e..3281ce4 100644 +--- a/awscli/customizations/codedeploy/push.py ++++ b/awscli/customizations/codedeploy/push.py +@@ -16,7 +16,7 @@ import os + import sys + import tempfile + import zipfile +-from datetime import datetime ++from datetime import datetime, timezone + + from botocore.exceptions import ClientError + +@@ -131,7 +131,7 @@ class Push(BasicCommand): + ) + if not parsed_args.description: + parsed_args.description = ( +- f'Uploaded by AWS CLI {datetime.utcnow().isoformat()} UTC' ++ f'Uploaded by AWS CLI {datetime.now(timezone.utc).isoformat()} UTC' + ) + + def _push(self, params): +diff --git a/awscli/customizations/datapipeline/__init__.py b/awscli/customizations/datapipeline/__init__.py +index 5665769..d197339 100644 +--- a/awscli/customizations/datapipeline/__init__.py ++++ b/awscli/customizations/datapipeline/__init__.py +@@ -12,7 +12,7 @@ + # language governing permissions and limitations under the License. + + import json +-from datetime import datetime, timedelta ++from datetime import datetime, timedelta, timezone + + from awscli.arguments import CustomArgument + from awscli.customizations.commands import BasicCommand +@@ -197,7 +197,7 @@ class QueryArgBuilder: + + def __init__(self, current_time=None): + if current_time is None: +- current_time = datetime.utcnow() ++ current_time = datetime.now(timezone.utc) + self.current_time = current_time + + def build_query(self, parsed_args): +diff --git a/awscli/customizations/ec2/bundleinstance.py b/awscli/customizations/ec2/bundleinstance.py +index 240540f..8dc2e22 100644 +--- a/awscli/customizations/ec2/bundleinstance.py ++++ b/awscli/customizations/ec2/bundleinstance.py +@@ -129,7 +129,7 @@ def _generate_policy(params): + # Called if there is no policy supplied by the user. + # Creates a policy that provides access for 24 hours. + delta = datetime.timedelta(hours=24) +- expires = datetime.datetime.utcnow() + delta ++ expires = datetime.datetime.now(datetime.timezone.utc) + delta + expires_iso = expires.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + policy = POLICY.format( + expires=expires_iso, bucket=params['Bucket'], prefix=params['Prefix'] +diff --git a/awscli/customizations/eks/get_token.py b/awscli/customizations/eks/get_token.py +index 00fc4d3..ee8c74c 100644 +--- a/awscli/customizations/eks/get_token.py ++++ b/awscli/customizations/eks/get_token.py +@@ -14,7 +14,7 @@ import base64 + import json + import os + import sys +-from datetime import datetime, timedelta ++from datetime import datetime, timedelta, timezone + + import botocore + from botocore.model import ServiceId +@@ -105,7 +105,7 @@ class GetTokenCommand(BasicCommand): + ] + + def get_expiration_time(self): +- token_expiration = datetime.utcnow() + timedelta( ++ token_expiration = datetime.now(timezone.utc) + timedelta( + minutes=TOKEN_EXPIRATION_MINS + ) + return token_expiration.strftime('%Y-%m-%dT%H:%M:%SZ') +diff --git a/awscli/customizations/logs/tail.py b/awscli/customizations/logs/tail.py +index e22facd..9b464b4 100644 +--- a/awscli/customizations/logs/tail.py ++++ b/awscli/customizations/logs/tail.py +@@ -10,11 +10,12 @@ + # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + # ANY KIND, either express or implied. See the License for the specific + # language governing permissions and limitations under the License. ++import functools + import json + import re + import time + from collections import defaultdict +-from datetime import datetime, timedelta ++from datetime import datetime, timedelta, timezone + + import colorama + from botocore.utils import datetime2timestamp, parse_timestamp +@@ -266,7 +267,7 @@ class TimestampUtils: + def __init__(self, now=None): + self._now = now + if now is None: +- self._now = datetime.utcnow ++ self._now = functools.partial(datetime.now, timezone.utc) + + def to_epoch_millis(self, timestamp): + re_match = self._RELATIVE_TIMESTAMP_REGEX.match(timestamp) +diff --git a/tests/functional/botocore/test_ec2.py b/tests/functional/botocore/test_ec2.py +index 81f4b76..39423aa 100644 +--- a/tests/functional/botocore/test_ec2.py ++++ b/tests/functional/botocore/test_ec2.py +@@ -92,14 +92,14 @@ class TestCopySnapshotCustomization(BaseSessionTest): + '%s\n' + '\n' + ) +- self.now = datetime.datetime(2011, 9, 9, 23, 36) ++ self.now = datetime.datetime(2011, 9, 9, 23, 36, tzinfo=datetime.timezone.utc) + self.datetime_patch = mock.patch.object( + botocore.auth.datetime, + 'datetime', + mock.Mock(wraps=datetime.datetime), + ) + self.mocked_datetime = self.datetime_patch.start() +- self.mocked_datetime.utcnow.return_value = self.now ++ self.mocked_datetime.now.return_value = self.now + + def tearDown(self): + super().tearDown() +diff --git a/tests/functional/botocore/test_lex.py b/tests/functional/botocore/test_lex.py +index cded9c0..000f50d 100644 +--- a/tests/functional/botocore/test_lex.py ++++ b/tests/functional/botocore/test_lex.py +@@ -10,7 +10,7 @@ + # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + # ANY KIND, either express or implied. See the License for the specific + # language governing permissions and limitations under the License. +-from datetime import datetime ++from datetime import datetime, timezone + + from tests import BaseSessionTest, ClientHTTPStubber, mock + +@@ -31,10 +31,10 @@ class TestLex(BaseSessionTest): + 'inputStream': b'', + } + +- timestamp = datetime(2017, 3, 22, 0, 0) ++ timestamp = datetime(2017, 3, 22, 0, 0, tzinfo=timezone.utc) + + with mock.patch('botocore.auth.datetime') as _datetime: +- _datetime.datetime.utcnow.return_value = timestamp ++ _datetime.datetime.now.return_value = timestamp + self.http_stubber.add_response(body=b'{}') + with self.http_stubber: + self.client.post_content(**params) +diff --git a/tests/functional/botocore/test_s3express.py b/tests/functional/botocore/test_s3express.py +index 91ec532..1a516d2 100644 +--- a/tests/functional/botocore/test_s3express.py ++++ b/tests/functional/botocore/test_s3express.py +@@ -108,7 +108,6 @@ class TestS3ExpressAuth: + class TestS3ExpressIdentityCache: + def test_default_s3_express_cache(self, default_s3_client, mock_datetime): + mock_datetime.now.return_value = DATE +- mock_datetime.utcnow.return_value = DATE + + identity_cache = S3ExpressIdentityCache( + default_s3_client, +@@ -126,7 +125,6 @@ class TestS3ExpressIdentityCache: + self, default_s3_client, mock_datetime + ): + mock_datetime.now.return_value = DATE +- mock_datetime.utcnow.return_value = DATE + bucket = 'my_bucket' + + identity_cache = S3ExpressIdentityCache( +@@ -151,7 +149,6 @@ class TestS3ExpressIdentityCache: + self, default_s3_client, mock_datetime + ): + mock_datetime.now.return_value = DATE +- mock_datetime.utcnow.return_value = DATE + bucket = 'my_bucket' + other_bucket = 'other_bucket' + +@@ -204,7 +201,7 @@ class TestS3ExpressRequests: + ) + + def test_create_bucket(self, default_s3_client, mock_datetime): +- mock_datetime.utcnow.return_value = DATE ++ mock_datetime.now.return_value = DATE + + with ClientHTTPStubber(default_s3_client) as stubber: + stubber.add_response() +@@ -228,7 +225,6 @@ class TestS3ExpressRequests: + self._assert_standard_sigv4_signature(stubber.requests[0].headers) + + def test_get_object(self, default_s3_client, mock_datetime): +- mock_datetime.utcnow.return_value = DATE + mock_datetime.now.return_value = DATE + + with ClientHTTPStubber(default_s3_client) as stubber: +@@ -250,7 +246,6 @@ class TestS3ExpressRequests: + def test_cache_with_multiple_requests( + self, default_s3_client, mock_datetime + ): +- mock_datetime.utcnow.return_value = DATE + mock_datetime.now.return_value = DATE + + with ClientHTTPStubber(default_s3_client) as stubber: +@@ -275,7 +270,6 @@ class TestS3ExpressRequests: + def test_delete_objects_injects_correct_checksum( + self, default_s3_client, mock_datetime + ): +- mock_datetime.utcnow.return_value = DATE + mock_datetime.now.return_value = DATE + + with ClientHTTPStubber(default_s3_client) as stubber: +diff --git a/tests/functional/botocore/test_sts.py b/tests/functional/botocore/test_sts.py +index 6bf343e..407ebab 100644 +--- a/tests/functional/botocore/test_sts.py ++++ b/tests/functional/botocore/test_sts.py +@@ -11,7 +11,7 @@ + # ANY KIND, either express or implied. See the License for the specific + # language governing permissions and limitations under the License. + import re +-from datetime import datetime ++from datetime import datetime, timezone + + from botocore.config import Config + from botocore.stub import Stubber +@@ -32,9 +32,9 @@ class TestSTSPresignedUrl(BaseSessionTest): + self.stubber.activate() + + def test_presigned_url_contains_no_content_type(self): +- timestamp = datetime(2017, 3, 22, 0, 0) ++ timestamp = datetime(2017, 3, 22, 0, 0, tzinfo=timezone.utc) + with mock.patch('botocore.auth.datetime') as _datetime: +- _datetime.datetime.utcnow.return_value = timestamp ++ _datetime.datetime.now.return_value = timestamp + url = self.client.generate_presigned_url('get_caller_identity', {}) + + # There should be no 'content-type' in x-amz-signedheaders +diff --git a/tests/functional/dsql/test_generate_db_auth_token.py b/tests/functional/dsql/test_generate_db_auth_token.py +index 2b48453..39871ab 100644 +--- a/tests/functional/dsql/test_generate_db_auth_token.py ++++ b/tests/functional/dsql/test_generate_db_auth_token.py +@@ -50,7 +50,7 @@ class TestGenerateDBConnectAuthToken(BaseTestGenerateDBConnectAuthToken): + clock = datetime.datetime(2024, 11, 7, 17, 39, 33, tzinfo=tzutc()) + + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + stdout, _, _ = self.run_cmd(command, expected_rc=0) + + # Expected hashes are always the same as session variables come from the BaseAwsCommandParamsTest class +@@ -79,7 +79,7 @@ class TestGenerateDBConnectAuthToken(BaseTestGenerateDBConnectAuthToken): + clock = datetime.datetime(2024, 11, 7, 17, 39, 33, tzinfo=tzutc()) + + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + stdout, _, _ = self.run_cmd(command, expected_rc=252) + + +@@ -92,7 +92,7 @@ class TestGenerateDBConnectAdminAuthToken(BaseTestGenerateDBConnectAuthToken): + clock = datetime.datetime(2024, 11, 7, 17, 39, 33, tzinfo=tzutc()) + + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + stdout, _, _ = self.run_cmd(command, expected_rc=0) + + # Expected hashes are always the same as session variables come from the BaseAwsCommandParamsTest class +diff --git a/tests/functional/ec2/test_bundle_instance.py b/tests/functional/ec2/test_bundle_instance.py +index 5442929..06747f9 100644 +--- a/tests/functional/ec2/test_bundle_instance.py ++++ b/tests/functional/ec2/test_bundle_instance.py +@@ -31,7 +31,7 @@ class TestBundleInstance(BaseAWSCommandParamsTest): + + def setUp(self): + super(TestBundleInstance, self).setUp() +- # This mocks out datetime.datetime.utcnow() so that it always ++ # This mocks out datetime.datetime.now() so that it always + # returns the same datetime object. This is because this value + # is embedded into the policy file that is generated and we + # don't what the policy or its signature to change each time +@@ -42,7 +42,7 @@ class TestBundleInstance(BaseAWSCommandParamsTest): + mock.Mock(wraps=datetime.datetime), + ) + mocked_datetime = self.datetime_patcher.start() +- mocked_datetime.utcnow.return_value = datetime.datetime(2013, 8, 9) ++ mocked_datetime.now.return_value = datetime.datetime(2013, 8, 9, tzinfo=datetime.timezone.utc) + + def tearDown(self): + super(TestBundleInstance, self).tearDown() +diff --git a/tests/functional/ec2instanceconnect/test_opentunnel.py b/tests/functional/ec2instanceconnect/test_opentunnel.py +index 744aea6..ee8c47f 100644 +--- a/tests/functional/ec2instanceconnect/test_opentunnel.py ++++ b/tests/functional/ec2instanceconnect/test_opentunnel.py +@@ -367,10 +367,10 @@ def request_params_for_describe_eice(): + + + @pytest.fixture +-def datetime_utcnow_patch(): ++def datetime_now_patch(): + clock = datetime.datetime(2020, 1, 1, 1, 1, 1, tzinfo=tzutc()) + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + yield dt + + +@@ -462,7 +462,7 @@ class TestOpenTunnel: + fips_dns_name, + request_params_for_describe_instance, + request_params_for_describe_eice, +- datetime_utcnow_patch, ++ datetime_now_patch, + endpoint_state, + ): + cli_runner.env["AWS_USE_FIPS_ENDPOINT"] = "false" +diff --git a/tests/functional/eks/test_get_token.py b/tests/functional/eks/test_get_token.py +index 0e78f9a..5600bdf 100644 +--- a/tests/functional/eks/test_get_token.py ++++ b/tests/functional/eks/test_get_token.py +@@ -13,7 +13,7 @@ + import base64 + import json + import os +-from datetime import datetime ++from datetime import datetime, timezone + + from awscli.compat import urlparse + from awscli.testutils import BaseAWSCommandParamsTest, mock +@@ -78,7 +78,7 @@ class TestGetTokenCommand(BaseAWSCommandParamsTest): + + @mock.patch('awscli.customizations.eks.get_token.datetime') + def test_get_token(self, mock_datetime): +- mock_datetime.utcnow.return_value = datetime(2019, 10, 23, 23, 0, 0, 0) ++ mock_datetime.now.return_value = datetime(2019, 10, 23, 23, 0, 0, 0, tzinfo=timezone.utc) + cmd = 'eks get-token --cluster-name %s' % self.cluster_name + response = self.run_get_token(cmd) + self.assertEqual( +@@ -96,7 +96,7 @@ class TestGetTokenCommand(BaseAWSCommandParamsTest): + + @mock.patch('awscli.customizations.eks.get_token.datetime') + def test_query_nested_object(self, mock_datetime): +- mock_datetime.utcnow.return_value = datetime(2019, 10, 23, 23, 0, 0, 0) ++ mock_datetime.now.return_value = datetime(2019, 10, 23, 23, 0, 0, 0, tzinfo=timezone.utc) + cmd = 'eks get-token --cluster-name %s' % self.cluster_name + cmd += ' --query status' + response = self.run_get_token(cmd) +@@ -119,7 +119,7 @@ class TestGetTokenCommand(BaseAWSCommandParamsTest): + + @mock.patch('awscli.customizations.eks.get_token.datetime') + def test_output_text(self, mock_datetime): +- mock_datetime.utcnow.return_value = datetime(2019, 10, 23, 23, 0, 0, 0) ++ mock_datetime.now.return_value = datetime(2019, 10, 23, 23, 0, 0, 0, tzinfo=timezone.utc) + cmd = 'eks get-token --cluster-name %s' % self.cluster_name + cmd += ' --output text' + stdout, _, _ = self.run_cmd(cmd) +@@ -129,7 +129,7 @@ class TestGetTokenCommand(BaseAWSCommandParamsTest): + + @mock.patch('awscli.customizations.eks.get_token.datetime') + def test_output_table(self, mock_datetime): +- mock_datetime.utcnow.return_value = datetime(2019, 10, 23, 23, 0, 0, 0) ++ mock_datetime.now.return_value = datetime(2019, 10, 23, 23, 0, 0, 0, tzinfo=timezone.utc) + cmd = 'eks get-token --cluster-name %s' % self.cluster_name + cmd += ' --output table' + stdout, _, _ = self.run_cmd(cmd) +diff --git a/tests/functional/logs/test_tail.py b/tests/functional/logs/test_tail.py +index 0024bd4..8eaf61c 100644 +--- a/tests/functional/logs/test_tail.py ++++ b/tests/functional/logs/test_tail.py +@@ -155,7 +155,7 @@ class TestTailCommand(BaseAWSCommandParamsTest): + + def test_tail_defaults_to_10m(self): + datetime_mock = mock.Mock(wraps=datetime) +- datetime_mock.utcnow = mock.Mock( ++ datetime_mock.now = mock.Mock( + return_value=datetime(1970, 1, 1, 0, 10, 1, tzinfo=tz.tzutc()) + ) + with mock.patch( +@@ -182,7 +182,7 @@ class TestTailCommand(BaseAWSCommandParamsTest): + + def test_tail_with_relative_since(self): + datetime_mock = mock.Mock(wraps=datetime) +- datetime_mock.utcnow = mock.Mock( ++ datetime_mock.now = mock.Mock( + return_value=datetime(1970, 1, 1, 0, 0, 2, tzinfo=tz.tzutc()) + ) + with mock.patch( +diff --git a/tests/functional/rds/test_generate_db_auth_token.py b/tests/functional/rds/test_generate_db_auth_token.py +index 4cc562a..4ff83e1 100644 +--- a/tests/functional/rds/test_generate_db_auth_token.py ++++ b/tests/functional/rds/test_generate_db_auth_token.py +@@ -50,7 +50,7 @@ class TestGenerateDBAuthToken(BaseAWSCommandParamsTest): + clock = datetime.datetime(2016, 11, 7, 17, 39, 33, tzinfo=tzutc()) + + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + stdout, _, _ = self.run_cmd(command, expected_rc=0) + + expected = ( +diff --git a/tests/functional/s3/test_presign_command.py b/tests/functional/s3/test_presign_command.py +index 43ede9a..d315dd3 100644 +--- a/tests/functional/s3/test_presign_command.py ++++ b/tests/functional/s3/test_presign_command.py +@@ -22,13 +22,13 @@ from awscli.testutils import ( + temporary_file, + ) + +-# Values used to fix time.time() and datetime.datetime.utcnow() ++# Values used to fix time.time() and datetime.datetime.now() + # so we know the exact values of the signatures generated. + FROZEN_TIMESTAMP = 1471305652 + DEFAULT_EXPIRES = 3600 + FROZEN_TIME = mock.Mock(return_value=FROZEN_TIMESTAMP) + FROZEN_DATETIME = mock.Mock( +- return_value=datetime.datetime(2016, 8, 18, 14, 33, 3, 0) ++ return_value=datetime.datetime(2016, 8, 18, 14, 33, 3, 0, tzinfo=datetime.timezone.utc) + ) + + +@@ -76,7 +76,7 @@ class TestPresignCommand(BaseAWSCommandParamsTest): + def get_presigned_url_for_cmd(self, cmdline): + with mock.patch('time.time', FROZEN_TIME): + with mock.patch('datetime.datetime') as d: +- d.utcnow = FROZEN_DATETIME ++ d.now = FROZEN_DATETIME + stdout = self.assert_params_for_cmd(cmdline, None)[0].strip() + return stdout + +diff --git a/tests/integration/customizations/test_codecommit.py b/tests/integration/customizations/test_codecommit.py +index 7077c08..d904290 100644 +--- a/tests/integration/customizations/test_codecommit.py ++++ b/tests/integration/customizations/test_codecommit.py +@@ -12,7 +12,7 @@ + # language governing permissions and limitations under the License. + + import os +-from datetime import datetime ++from datetime import datetime, timezone + + from botocore.awsrequest import AWSRequest + from botocore.credentials import Credentials +@@ -64,7 +64,7 @@ class TestCodeCommitCredentialHelper(unittest.TestCase): + @mock.patch('sys.stdout', new_callable=StringIOWithFileNo) + @mock.patch.object(awscli.customizations.codecommit.datetime, 'datetime') + def test_integration_using_cli_driver(self, dt_mock, stdout_mock): +- dt_mock.utcnow.return_value = datetime(2010, 10, 8) ++ dt_mock.now.return_value = datetime(2010, 10, 8, tzinfo=timezone.utc) + driver = create_clidriver() + entry_point = AWSCLIEntryPoint(driver) + rc = entry_point.main('codecommit credential-helper get'.split()) +@@ -83,7 +83,7 @@ class TestCodeCommitCredentialHelper(unittest.TestCase): + @mock.patch('sys.stdout', new_callable=StringIOWithFileNo) + @mock.patch.object(awscli.customizations.codecommit.datetime, 'datetime') + def test_integration_fips_using_cli_driver(self, dt_mock, stdout_mock): +- dt_mock.utcnow.return_value = datetime(2010, 10, 8) ++ dt_mock.now.return_value = datetime(2010, 10, 8, tzinfo=timezone.utc) + driver = create_clidriver() + entry_point = AWSCLIEntryPoint(driver) + rc = entry_point.main('codecommit credential-helper get'.split()) +@@ -102,7 +102,7 @@ class TestCodeCommitCredentialHelper(unittest.TestCase): + @mock.patch('sys.stdout', new_callable=StringIOWithFileNo) + @mock.patch.object(awscli.customizations.codecommit.datetime, 'datetime') + def test_integration_vpc_using_cli_driver(self, dt_mock, stdout_mock): +- dt_mock.utcnow.return_value = datetime(2010, 10, 8) ++ dt_mock.now.return_value = datetime(2010, 10, 8, tzinfo=timezone.utc) + driver = create_clidriver() + entry_point = AWSCLIEntryPoint(driver) + rc = entry_point.main('codecommit credential-helper get'.split()) +diff --git a/tests/unit/botocore/auth/test_signers.py b/tests/unit/botocore/auth/test_signers.py +index 7effaf3..ff62c77 100644 +--- a/tests/unit/botocore/auth/test_signers.py ++++ b/tests/unit/botocore/auth/test_signers.py +@@ -28,10 +28,10 @@ from tests import mock, unittest + + class BaseTestWithFixedDate(unittest.TestCase): + def setUp(self): +- self.fixed_date = datetime.datetime(2014, 3, 10, 17, 2, 55, 0) ++ self.fixed_date = datetime.datetime(2014, 3, 10, 17, 2, 55, 0, tzinfo=datetime.timezone.utc) + self.datetime_patch = mock.patch('botocore.auth.datetime.datetime') + self.datetime_mock = self.datetime_patch.start() +- self.datetime_mock.utcnow.return_value = self.fixed_date ++ self.datetime_mock.now.return_value = self.fixed_date + self.datetime_mock.strptime.return_value = self.fixed_date + + def tearDown(self): +@@ -379,9 +379,9 @@ class TestSigV4(unittest.TestCase): + 'datetime', + mock.Mock(wraps=datetime.datetime), + ) as mock_datetime: +- original_utcnow = datetime.datetime(2014, 1, 1, 0, 0) ++ original_now = datetime.datetime(2014, 1, 1, 0, 0, tzinfo=datetime.timezone.utc) + +- mock_datetime.utcnow.return_value = original_utcnow ++ mock_datetime.now.return_value = original_now + # Go through the add_auth process once. This will attach + # a timestamp to the request at the beginning of auth. + auth.add_auth(request) +@@ -389,9 +389,8 @@ class TestSigV4(unittest.TestCase): + # Ensure the date is in the Authorization header + self.assertIn('20140101', request.headers['Authorization']) + # Now suppose the utc time becomes the next day all of a sudden +- mock_datetime.utcnow.return_value = datetime.datetime( +- 2014, 1, 2, 0, 0 +- ) ++ mock_datetime.now.return_value = datetime.datetime( ++ 2014, 1, 2, 0, 0, tzinfo=datetime.timezone.utc) + # Smaller methods like the canonical request and string_to_sign + # should have the timestamp attached to the request in their + # body and not what the time is now mocked as. This is to ensure +@@ -563,8 +562,8 @@ class TestSigV4Presign(BasePresignTest): + mock.Mock(wraps=datetime.datetime), + ) + mocked_datetime = self.datetime_patcher.start() +- mocked_datetime.utcnow.return_value = datetime.datetime( +- 2014, 1, 1, 0, 0 ++ mocked_datetime.now.return_value = datetime.datetime( ++ 2014, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ) + + def tearDown(self): +@@ -780,8 +779,8 @@ class TestS3SigV4Post(BaseS3PresignPostTest): + mock.Mock(wraps=datetime.datetime), + ) + mocked_datetime = self.datetime_patcher.start() +- mocked_datetime.utcnow.return_value = datetime.datetime( +- 2014, 1, 1, 0, 0 ++ mocked_datetime.now.return_value = datetime.datetime( ++ 2014, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ) + + def tearDown(self): +diff --git a/tests/unit/botocore/test_credentials.py b/tests/unit/botocore/test_credentials.py +index 29d36ac..96c18c3 100644 +--- a/tests/unit/botocore/test_credentials.py ++++ b/tests/unit/botocore/test_credentials.py +@@ -16,7 +16,7 @@ import os + import shutil + import subprocess + import tempfile +-from datetime import datetime, timedelta ++from datetime import datetime, timedelta, timezone + + import botocore.exceptions + import botocore.session +@@ -138,7 +138,7 @@ class TestRefreshableCredentials(TestCredentials): + + def test_refresh_needed(self): + # The expiry time was set for 30 minutes ago, so if we +- # say the current time is utcnow(), then we should need ++ # say the current time is now(), then we should need + # a refresh. + self.mock_time.return_value = datetime.now(tzlocal()) + self.assertTrue(self.creds.refresh_needed()) +@@ -326,8 +326,8 @@ class TestAssumeRoleCredentialFetcher(BaseEnvVar): + self.assertEqual(response, expected_response) + + def test_retrieves_from_cache(self): +- date_in_future = datetime.utcnow() + timedelta(seconds=1000) +- utc_timestamp = date_in_future.isoformat() + 'Z' ++ date_in_future = datetime.now(timezone.utc) + timedelta(seconds=1000) ++ utc_timestamp = date_in_future.isoformat() + cache_key = '793d6e2f27667ab2da104824407e486bfec24a47' + cache = { + cache_key: { +@@ -830,8 +830,8 @@ class TestAssumeRoleWithWebIdentityCredentialFetcher(BaseEnvVar): + self.assertEqual(response, expected_response) + + def test_retrieves_from_cache(self): +- date_in_future = datetime.utcnow() + timedelta(seconds=1000) +- utc_timestamp = date_in_future.isoformat() + 'Z' ++ date_in_future = datetime.now(timezone.utc) + timedelta(seconds=1000) ++ utc_timestamp = date_in_future.isoformat() + cache_key = '793d6e2f27667ab2da104824407e486bfec24a47' + cache = { + cache_key: { +@@ -1019,8 +1019,8 @@ class TestAssumeRoleWithWebIdentityCredentialProvider(unittest.TestCase): + mock_loader_cls.assert_called_with('/some/path/token.jwt') + + def test_assume_role_retrieves_from_cache(self): +- date_in_future = datetime.utcnow() + timedelta(seconds=1000) +- utc_timestamp = date_in_future.isoformat() + 'Z' ++ date_in_future = datetime.now(timezone.utc) + timedelta(seconds=1000) ++ utc_timestamp = date_in_future.isoformat() + + cache_key = 'c29461feeacfbed43017d20612606ff76abc073d' + cache = { +@@ -2260,8 +2260,8 @@ class TestAssumeRoleCredentialProvider(unittest.TestCase): + self.assertEqual(expiry_time, '2016-11-06T01:30:00UTC') + + def test_assume_role_retrieves_from_cache(self): +- date_in_future = datetime.utcnow() + timedelta(seconds=1000) +- utc_timestamp = date_in_future.isoformat() + 'Z' ++ date_in_future = datetime.now(timezone.utc) + timedelta(seconds=1000) ++ utc_timestamp = date_in_future.isoformat() + self.fake_config['profiles']['development']['role_arn'] = 'myrole' + + cache_key = '793d6e2f27667ab2da104824407e486bfec24a47' +@@ -2289,8 +2289,8 @@ class TestAssumeRoleCredentialProvider(unittest.TestCase): + self.assertEqual(creds.token, 'baz-cached') + + def test_chain_prefers_cache(self): +- date_in_future = datetime.utcnow() + timedelta(seconds=1000) +- utc_timestamp = date_in_future.isoformat() + 'Z' ++ date_in_future = datetime.now(timezone.utc) + timedelta(seconds=1000) ++ utc_timestamp = date_in_future.isoformat() + + # The profile we will be using has a cache entry, but the profile it + # is sourcing from does not. This should result in the cached +diff --git a/tests/unit/botocore/test_signers.py b/tests/unit/botocore/test_signers.py +index b97bf58..ed54e9a 100644 +--- a/tests/unit/botocore/test_signers.py ++++ b/tests/unit/botocore/test_signers.py +@@ -686,9 +686,9 @@ class TestS3PostPresigner(BaseSignerTest): + + self.datetime_patch = mock.patch('botocore.signers.datetime') + self.datetime_mock = self.datetime_patch.start() +- self.fixed_date = datetime.datetime(2014, 3, 10, 17, 2, 55, 0) ++ self.fixed_date = datetime.datetime(2014, 3, 10, 17, 2, 55, 0, tzinfo=datetime.timezone.utc) + self.fixed_delta = datetime.timedelta(seconds=3600) +- self.datetime_mock.datetime.utcnow.return_value = self.fixed_date ++ self.datetime_mock.datetime.now.return_value = self.fixed_date + self.datetime_mock.timedelta.return_value = self.fixed_delta + + def tearDown(self): +@@ -1141,7 +1141,7 @@ class TestGenerateDBAuthToken(BaseSignerTest): + clock = datetime.datetime(2016, 11, 7, 17, 39, 33, tzinfo=tzutc()) + + with mock.patch('datetime.datetime') as dt: +- dt.utcnow.return_value = clock ++ dt.now.return_value = clock + result = generate_db_auth_token( + self.client, hostname, port, username + ) +diff --git a/tests/unit/botocore/test_utils.py b/tests/unit/botocore/test_utils.py +index 0d02ade..90b73f0 100644 +--- a/tests/unit/botocore/test_utils.py ++++ b/tests/unit/botocore/test_utils.py +@@ -109,7 +109,7 @@ from dateutil.tz import tzoffset, tzutc + + from tests import FreezeTime, RawResponse, create_session, mock, unittest + +-DATE = datetime.datetime(2021, 12, 10, 00, 00, 00) ++DATE = datetime.datetime(2021, 12, 10, 00, 00, 00, tzinfo=datetime.timezone.utc) + DT_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + + +@@ -3155,7 +3155,7 @@ class TestInstanceMetadataFetcher(unittest.TestCase): + + def _get_datetime(self, dt=None, offset=None, offset_func=operator.add): + if dt is None: +- dt = datetime.datetime.utcnow() ++ dt = datetime.datetime.now(datetime.timezone.utc) + if offset is not None: + dt = offset_func(dt, offset) + +diff --git a/tests/unit/customizations/eks/test_get_token.py b/tests/unit/customizations/eks/test_get_token.py +index 7f830b3..0f019c3 100644 +--- a/tests/unit/customizations/eks/test_get_token.py ++++ b/tests/unit/customizations/eks/test_get_token.py +@@ -48,7 +48,7 @@ class TestGetTokenCommand(BaseTokenTest): + cmd = GetTokenCommand(self._session) + timestamp = cmd.get_expiration_time() + try: +- datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ') ++ datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S%z') + except ValueError: + raise ValueError( + "Incorrect data format, should be %Y-%m-%dT%H:%M:%SZ" +diff --git a/tests/utils/botocore/__init__.py b/tests/utils/botocore/__init__.py +index bbe6a59..dddaf26 100644 +--- a/tests/utils/botocore/__init__.py ++++ b/tests/utils/botocore/__init__.py +@@ -579,12 +579,12 @@ class FreezeTime(contextlib.ContextDecorator): + :param module: reference to imported module to patch (e.g. botocore.auth.datetime) + + :type date: datetime.datetime +- :param date: datetime object specifying the output for utcnow() ++ :param date: datetime object specifying the output for now() + """ + + def __init__(self, module, date=None): + if date is None: +- date = datetime.datetime.utcnow() ++ date = datetime.datetime.now(datetime.timezone.utc) + self.date = date + self.datetime_patcher = mock.patch.object( + module, 'datetime', mock.Mock(wraps=datetime.datetime) +@@ -592,7 +592,7 @@ class FreezeTime(contextlib.ContextDecorator): + + def __enter__(self, *args, **kwargs): + mock = self.datetime_patcher.start() +- mock.utcnow.return_value = self.date ++ mock.now.return_value = self.date + + def __exit__(self, *args, **kwargs): + self.datetime_patcher.stop() diff --git a/python314.patch b/python314.patch new file mode 100644 index 0000000..9eb9b85 --- /dev/null +++ b/python314.patch @@ -0,0 +1,140 @@ + +diff --git a/awscli/arguments.py b/awscli/arguments.py +index 77639cf..3ab060b 100644 +--- a/awscli/arguments.py ++++ b/awscli/arguments.py +@@ -451,7 +451,7 @@ class CLIArgument(BaseCLIArgument): + cli_name = self.cli_name + parser.add_argument( + cli_name, +- help=self.documentation, ++ help=self.documentation.replace("%", "%%"), + type=self.cli_type, + required=self.required, + ) +@@ -602,7 +602,7 @@ class BooleanArgument(CLIArgument): + def add_to_parser(self, parser): + parser.add_argument( + self.cli_name, +- help=self.documentation, ++ help=self.documentation.replace("%", "%%"), + action=self._action, + default=self._default, + dest=self._destination, +diff --git a/awscli/customizations/logs/startlivetail.py b/awscli/customizations/logs/startlivetail.py +index cc5849b..bc71a3d 100644 +--- a/awscli/customizations/logs/startlivetail.py ++++ b/awscli/customizations/logs/startlivetail.py +@@ -833,7 +833,7 @@ class InteractiveUI(BaseLiveTailUI): + await self._application.run_async() + + def run(self): +- asyncio.get_event_loop().run_until_complete(self._run_ui()) ++ asyncio.run(self._run_ui()) + + + class PrintOnlyPrinter(BaseLiveTailPrinter): +diff --git a/awscli/customizations/wizard/app.py b/awscli/customizations/wizard/app.py +index c813b97..028fcaa 100644 +--- a/awscli/customizations/wizard/app.py ++++ b/awscli/customizations/wizard/app.py +@@ -10,9 +10,9 @@ + # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + # ANY KIND, either express or implied. See the License for the specific + # language governing permissions and limitations under the License. ++import asyncio + import json + import os +-from asyncio import new_event_loop, set_event_loop + from collections.abc import MutableMapping + + from prompt_toolkit.application import Application +@@ -77,14 +77,12 @@ class WizardApp(Application): + ) + + def run(self, pre_run=None, **kwargs): +- loop = new_event_loop() +- try: +- set_event_loop(loop) ++ def create_event_loop(): ++ loop = asyncio.new_event_loop() + loop.set_exception_handler(self._handle_exception) +- f = self.run_async(pre_run=pre_run, set_exception_handler=False) +- return loop.run_until_complete(f) +- finally: +- loop.close() ++ return loop ++ f = self.run_async(pre_run=pre_run, set_exception_handler=False) ++ return asyncio.run(f, loop_factory=create_event_loop) + + def _handle_exception(self, loop, context): + self.exit(exception=UnexpectedWizardException(context['exception'])) +diff --git a/tests/__init__.py b/tests/__init__.py +index db70912..0ab2e4a 100644 +--- a/tests/__init__.py ++++ b/tests/__init__.py +@@ -471,16 +471,9 @@ class PromptToolkitAppRunner: + # This is the function that will be passed to our thread to + # actually run the application + try: +- # When we run the app in a separate thread, there is no +- # default event loop. This ensures we create one as it is +- # likely the application will try to grab the default loop +- loop = asyncio.new_event_loop() +- asyncio.set_event_loop(loop) + app_run_context.return_value = target(*target_args) + except BaseException as e: + app_run_context.raised_exception = e +- finally: +- loop.close() + + def _wait_until_app_is_done_updating(self): + self._wait_until_app_is_done_rendering() +diff --git a/tests/functional/test_utils.py b/tests/functional/test_utils.py +--- a/tests/functional/test_utils.py ++++ b/tests/functional/test_utils.py +@@ -18,7 +18,7 @@ class TestWriteException(unittest.TestCa + def test_write_exception(self): + error_message = "Some error message." + ex = Exception(error_message) +- with codecs.open(self.outfile, 'w+', encoding='utf-8') as outfile: ++ with open(self.outfile, 'w+', encoding='utf-8') as outfile: + write_exception(ex, outfile) + outfile.seek(0) + +diff --git a/tests/unit/botocore/test_utils.py b/tests/unit/botocore/test_utils.py +--- a/tests/unit/botocore/test_utils.py ++++ b/tests/unit/botocore/test_utils.py +@@ -3646,20 +3646,28 @@ def test_lru_cache_weakref(): + cls2 = ClassWithCachedMethod() + + assert cls1.cached_fn.cache_info().currsize == 0 +- assert getrefcount(cls1) == 2 +- assert getrefcount(cls2) == 2 ++ refcount1 = getrefcount(cls1) ++ refcount2 = getrefcount(cls2) ++ assert refcount1 in (1, 2) ++ assert refcount2 in (1, 2) + # "The count returned is generally one higher than you might expect, because + # it includes the (temporary) reference as an argument to getrefcount()." + # https://docs.python.org/3.8/library/sys.html#getrefcount + ++ # However, as of 3.14: "The interpreter internally avoids some reference ++ # count modifications when loading objects onto the operands stack by ++ # borrowing references when possible. This can lead to smaller reference ++ # count values compared to previous Python versions." ++ # https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-refcount ++ + cls1.cached_fn(1, 1) + cls2.cached_fn(1, 1) + + # The cache now has two entries, but the reference count remains the same as + # before. + assert cls1.cached_fn.cache_info().currsize == 2 +- assert getrefcount(cls1) == 2 +- assert getrefcount(cls2) == 2 ++ assert getrefcount(cls1) == refcount1 ++ assert getrefcount(cls2) == refcount2 + + # Deleting one of the objects does not interfere with the cache entries + # related to the other object. diff --git a/ruamel-yaml-0.17.32.patch b/ruamel-yaml-0.17.32.patch new file mode 100644 index 0000000..c1f54f0 --- /dev/null +++ b/ruamel-yaml-0.17.32.patch @@ -0,0 +1,113 @@ +diff --git a/awscli/customizations/cloudformation/yamlhelper.py b/awscli/customizations/cloudformation/yamlhelper.py +index 8e3b291..a926d53 100644 +--- a/awscli/customizations/cloudformation/yamlhelper.py ++++ b/awscli/customizations/cloudformation/yamlhelper.py +@@ -91,8 +91,14 @@ def yaml_dump(dict_to_dump): + yaml.Representer = FlattenAliasRepresenter + _add_yaml_1_1_boolean_resolvers(yaml.Resolver) + yaml.Representer.add_representer(OrderedDict, _dict_representer) ++ yaml.Representer.add_representer(dict, _dict_representer) + +- return dump_yaml_to_str(yaml, dict_to_dump) ++ result = dump_yaml_to_str(yaml, dict_to_dump) ++ ++ # let other YAML instances use the default dict representer ++ yaml.Representer.add_representer(dict, ruamel.yaml.representer.SafeRepresenter.represent_dict) ++ ++ return result + + + def _dict_constructor(loader, node): +diff --git a/awscli/customizations/eks/kubeconfig.py b/awscli/customizations/eks/kubeconfig.py +index 2d302d8..d00c4a8 100644 +--- a/awscli/customizations/eks/kubeconfig.py ++++ b/awscli/customizations/eks/kubeconfig.py +@@ -45,7 +45,7 @@ def _get_new_kubeconfig_content(): + ("contexts", []), + ("current-context", ""), + ("kind", "Config"), +- ("preferences", OrderedDict()), ++ ("preferences", {}), + ("users", []), + ] + ) +@@ -135,7 +135,7 @@ class KubeconfigValidator(object): + for key, value in self._validation_content.items(): + if key in config.content and type(config.content[key]) == list: + for element in config.content[key]: +- if not isinstance(element, OrderedDict): ++ if not isinstance(element, dict): + raise KubeconfigCorruptedError( + f"Entry in {key} not a {dict}. " + ) +diff --git a/awscli/customizations/eks/ordered_yaml.py b/awscli/customizations/eks/ordered_yaml.py +index f9b1a11..0cce12e 100644 +--- a/awscli/customizations/eks/ordered_yaml.py ++++ b/awscli/customizations/eks/ordered_yaml.py +@@ -50,10 +50,18 @@ def ordered_yaml_dump(to_dump, stream=None): + :type stream: file + """ + yaml = ruamel.yaml.YAML(typ="safe", pure=True) ++ yaml.width = 99999 + yaml.default_flow_style = False + yaml.Representer.add_representer(OrderedDict, _ordered_representer) ++ yaml.Representer.add_representer(dict, _ordered_representer) + + if stream is None: +- return dump_yaml_to_str(yaml, to_dump) ++ result = dump_yaml_to_str(yaml, to_dump) ++ else: ++ result = None ++ yaml.dump(to_dump, stream) + +- yaml.dump(to_dump, stream) ++ # let other YAML instances use the default dict representer ++ yaml.Representer.add_representer(dict, ruamel.yaml.representer.SafeRepresenter.represent_dict) ++ ++ return result +diff --git a/tests/unit/customizations/cloudformation/test_yamlhelper.py b/tests/unit/customizations/cloudformation/test_yamlhelper.py +index e7ac758..b9632e9 100644 +--- a/tests/unit/customizations/cloudformation/test_yamlhelper.py ++++ b/tests/unit/customizations/cloudformation/test_yamlhelper.py +@@ -130,28 +130,10 @@ class TestYaml(BaseYAMLTest): + ' Name: name1\n' + ) + output_dict = yaml_parse(input_template) +- expected_dict = OrderedDict( +- [ +- ( +- 'B_Resource', +- OrderedDict( +- [ +- ('Key2', {'Name': 'name2'}), +- ('Key1', {'Name': 'name1'}), +- ] +- ), +- ), +- ( +- 'A_Resource', +- OrderedDict( +- [ +- ('Key2', {'Name': 'name2'}), +- ('Key1', {'Name': 'name1'}), +- ] +- ), +- ), +- ] +- ) ++ expected_dict = { ++ 'B_Resource': {'Key2': {'Name': 'name2'}, 'Key1': {'Name': 'name1'}}, ++ 'A_Resource': {'Key2': {'Name': 'name2'}, 'Key1': {'Name': 'name1'}} ++ } + self.assertEqual(expected_dict, output_dict) + + output_template = yaml_dump(output_dict) +@@ -165,7 +147,7 @@ class TestYaml(BaseYAMLTest): + <<: *base + """ + output = yaml_parse(test_yaml) +- self.assertTrue(isinstance(output, OrderedDict)) ++ self.assertTrue(isinstance(output, dict)) + self.assertEqual(output.get('test').get('property'), 'value') + + def test_unroll_yaml_anchors(self): diff --git a/sources b/sources index d9cdd8b..f483ebb 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (aws-cli-2.15.31.tar.gz) = 3600409edf0218254f8dfc4346cd0e1df3af2d4047fca2e021f655a48344ec3f058a48258d830b150ed92ede98e723d164a200fcbd519fd8a1002bc6d0c0294d +SHA512 (aws-cli-2.33.0.tar.gz) = d8a26563e40c38f7811d3f4fb6fe09ce8ad49f5df6e3f3cb75e40fa78a010948453fc9cdc1eb6e2fed3fc2fd3e40dd59def92c51925dc6edbffb79360cb5f1a6 diff --git a/urllib3-v2.patch b/urllib3-v2.patch new file mode 100644 index 0000000..3f018f0 --- /dev/null +++ b/urllib3-v2.patch @@ -0,0 +1,37 @@ +diff --git a/awscli/botocore/awsrequest.py b/awscli/botocore/awsrequest.py +index f7a18a3..bf1ca13 100644 +--- a/awscli/botocore/awsrequest.py ++++ b/awscli/botocore/awsrequest.py +@@ -14,6 +14,7 @@ + import functools + import io + import logging ++from collections.abc import Mapping + import socket + import sys + +@@ -24,6 +25,7 @@ from botocore.compat import ( + HTTPResponse, + MutableMapping, + urlencode, ++ urlparse, + urlsplit, + urlunsplit, + ) +@@ -375,8 +377,14 @@ class AWSRequestPreparer: + def _prepare_url(self, original): + url = original.url + if original.params: +- params = urlencode(list(original.params.items()), doseq=True) +- url = f'{url}?{params}' ++ url_parts = urlparse(url) ++ delim = '&' if url_parts.query else '?' ++ if isinstance(original.params, Mapping): ++ params_to_encode = list(original.params.items()) ++ else: ++ params_to_encode = original.params ++ params = urlencode(params_to_encode, doseq=True) ++ url = delim.join((url, params)) + return url + + def _prepare_headers(self, original, prepared_body=None):