diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13f206d..78403e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.12'] + python-version: ['3.10', '3.11', '3.12'] os: [ubuntu-latest, macOS-latest, windows-latest] steps: @@ -27,4 +27,4 @@ jobs: python -m pip install -e .[test] - name: Run tests run: | - python -m pytest tests + python -m pytest -v tests diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a1984a5..c925139 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,12 +10,16 @@ permissions: jobs: lint: runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - name: Set up Python 3.12 - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d - with: - python-version: 3.12 - - name: Run pre-commit - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + with: + python-version: '${{ matrix.python-version }}' + - name: Run pre-commit + uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 diff --git a/pyproject.toml b/pyproject.toml index 3b3d6a1..42c42e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "aws-sdk-signers" -requires-python = ">=3.12" +requires-python = ">=3.10" authors = [ {name = "Amazon Web Services"}, ] @@ -14,10 +14,15 @@ license = {file = "LICENSE"} keywords = ["AWS", "Signing", "SigV4", "HTTP"] classifiers = [ "Development Status :: 2 - Pre-Alpha", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python" ] dynamic = ["version"] +dependencies = [ + 'typing-extensions >=4.12.2,<5 ; python_version == "3.10"', +] [project.optional-dependencies] test = [ @@ -38,8 +43,6 @@ path = "src/aws_sdk_signers/_version.py" line-length = 88 indent-width = 4 -target-version = "py312" - [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I", "UP"] ignore = [] diff --git a/src/aws_sdk_signers/_http.py b/src/aws_sdk_signers/_http.py index 8eef6d9..b9606b4 100644 --- a/src/aws_sdk_signers/_http.py +++ b/src/aws_sdk_signers/_http.py @@ -12,6 +12,7 @@ from __future__ import annotations +import sys from collections import Counter, OrderedDict from collections.abc import AsyncIterable, Iterable, Iterator from copy import deepcopy @@ -210,7 +211,12 @@ def __len__(self) -> int: return len(self.entries) def __repr__(self) -> str: - return f"Fields({self.entries})" + if sys.version_info < (3, 12): + # Dicts are ordered in 3.11 and 3.10, so it's safe to use dict. + # Used to mock a 3.12-like output. + return f"Fields(OrderedDict({dict(self.entries)}))" + else: + return f"Fields({self.entries})" def __contains__(self, key: str) -> bool: return self._normalize_field_name(key) in self.entries diff --git a/src/aws_sdk_signers/_identity.py b/src/aws_sdk_signers/_identity.py index 8b66ed9..7355060 100644 --- a/src/aws_sdk_signers/_identity.py +++ b/src/aws_sdk_signers/_identity.py @@ -3,11 +3,19 @@ SPDX-License-Identifier: Apache-2.0 """ +import sys from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import datetime, timezone from .interfaces.identity import Identity +if sys.version_info < (3, 12): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + @dataclass(kw_only=True) class AWSCredentialIdentity(Identity): diff --git a/src/aws_sdk_signers/_io.py b/src/aws_sdk_signers/_io.py index 477ab3b..a95503c 100644 --- a/src/aws_sdk_signers/_io.py +++ b/src/aws_sdk_signers/_io.py @@ -3,13 +3,16 @@ SPDX-License-Identifier: Apache-2.0 """ +import sys from asyncio import iscoroutinefunction from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable from io import BytesIO -from typing import ( - Self, - cast, -) +from typing import cast + +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self from aws_sdk_signers.interfaces.io import AsyncByteStream, ByteStream diff --git a/src/aws_sdk_signers/signers.py b/src/aws_sdk_signers/signers.py index 0b53e0c..f050e67 100644 --- a/src/aws_sdk_signers/signers.py +++ b/src/aws_sdk_signers/signers.py @@ -6,18 +6,31 @@ import datetime import hmac import io +import sys import warnings from collections.abc import AsyncIterable from copy import deepcopy from hashlib import sha256 -from typing import Required, TypedDict +from typing import TypedDict from urllib.parse import parse_qsl, quote +if sys.version_info < (3, 11): + from typing_extensions import Required +else: + from typing import Required + from ._http import URI, AWSRequest, Field from ._identity import AWSCredentialIdentity from ._io import AsyncBytesReader from .exceptions import AWSSDKWarning, MissingExpectedParameterException +if sys.version_info < (3, 12): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + HEADERS_EXCLUDED_FROM_SIGNING: tuple[str, ...] = ( "accept", "accept-encoding", @@ -185,7 +198,7 @@ def _generate_new_request(self, *, request: AWSRequest) -> AWSRequest: def _resolve_signing_date(self, *, date: str | None) -> str: if date is None: - date_obj = datetime.datetime.now(datetime.UTC) + date_obj = datetime.datetime.now(UTC) date = date_obj.strftime(SIGV4_TIMESTAMP_FORMAT) return date @@ -548,7 +561,7 @@ async def _generate_new_request(self, *, request: AWSRequest) -> AWSRequest: async def _resolve_signing_date(self, *, date: str | None) -> str: if date is None: - date_obj = datetime.datetime.now(datetime.UTC) + date_obj = datetime.datetime.now(UTC) date = date_obj.strftime(SIGV4_TIMESTAMP_FORMAT) return date diff --git a/tests/unit/auth/test_sigv4.py b/tests/unit/auth/test_sigv4.py index 4d0850a..b946c0e 100644 --- a/tests/unit/auth/test_sigv4.py +++ b/tests/unit/auth/test_sigv4.py @@ -1,8 +1,9 @@ import os import pathlib import re +import sys from collections.abc import Iterable -from datetime import UTC, datetime +from datetime import datetime from http.server import BaseHTTPRequestHandler from io import BytesIO @@ -24,6 +25,13 @@ ) from freezegun import freeze_time +if sys.version_info < (3, 12): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + SECRET_KEY: str = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ACCESS_KEY: str = "AKIDEXAMPLE" SERVICE: str = "service" diff --git a/tests/unit/test_identity.py b/tests/unit/test_identity.py index f09c99d..297024d 100644 --- a/tests/unit/test_identity.py +++ b/tests/unit/test_identity.py @@ -1,8 +1,16 @@ -from datetime import UTC, datetime, timedelta +import sys +from datetime import datetime, timedelta import pytest from aws_sdk_signers import AWSCredentialIdentity +if sys.version_info < (3, 12): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + @pytest.mark.parametrize( "access_key_id,secret_access_key,session_token,expiration", diff --git a/tests/unit/test_signers.py b/tests/unit/test_signers.py index 4592413..3616053 100644 --- a/tests/unit/test_signers.py +++ b/tests/unit/test_signers.py @@ -4,8 +4,9 @@ """ import re +import sys import typing -from datetime import UTC, datetime +from datetime import datetime from io import BytesIO import pytest @@ -19,6 +20,13 @@ SigV4SigningProperties, ) +if sys.version_info < (3, 12): + from datetime import timezone + + UTC = timezone.utc +else: + from datetime import UTC + SIGV4_RE = re.compile( r"AWS4-HMAC-SHA256 " r"Credential=(?P\w+)/\d+/"