diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 937545b8..a65f805a 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.8", "3.11" ] + python-version: [ "3.8", "3.11", "3.12", "3.13" ] engine-version: [ "lts", "latest"] environment: ["mysql", "pg"] diff --git a/.github/workflows/integration_tests_codebuild.yml b/.github/workflows/integration_tests_codebuild.yml index c0deac51..837913c8 100644 --- a/.github/workflows/integration_tests_codebuild.yml +++ b/.github/workflows/integration_tests_codebuild.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.8", "3.11" ] + python-version: [ "3.8", "3.11", "3.12", "3.13" ] environment: [ "mysql", "pg" ] runs-on: ubuntu-latest diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89659835..89390031 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] poetry-version: ["1.8.2"] steps: diff --git a/README.md b/README.md index 89615730..d8e744be 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ For all other questions, please use [GitHub discussions](https://github.com/awsl 1. Set up your environment by following the directions in the [Development Guide](./docs/development-guide/DevelopmentGuide.md). 2. To contribute, first make a fork of this project. -3. Make any changes on your fork. Make sure you are aware of the requirements for the project (e.g. do not require Python 3.7 if we are supporting Python 3.8 - 3.11 (inclusive)). +3. Make any changes on your fork. Make sure you are aware of the requirements for the project (e.g. do not require Python 3.7 if we are supporting Python 3.8 - 3.13 (inclusive)). 4. Create a pull request from your fork. 5. Pull requests need to be approved and merged by maintainers into the main branch.
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 1e372f8d..4611f001 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -4,7 +4,7 @@ Before using the AWS Advanced Python Driver, you must install: -- Python 3.8 - 3.11 (inclusive). +- Python 3.8 - 3.13 (inclusive). - The AWS Advanced Python Driver. - Your choice of underlying Python driver. - To use the wrapper with Aurora with PostgreSQL compatibility, install [Psycopg](https://github.com/psycopg/psycopg). diff --git a/docs/development-guide/DevelopmentGuide.md b/docs/development-guide/DevelopmentGuide.md index 391b3237..27900c3b 100644 --- a/docs/development-guide/DevelopmentGuide.md +++ b/docs/development-guide/DevelopmentGuide.md @@ -1,7 +1,7 @@ # Development Guide ### Setup -Make sure you have Python 3.8 - 3.11 (inclusive) installed, along with your choice of underlying Python driver (see [minimum requirements](../GettingStarted.md#minimum-requirements)). +Make sure you have Python 3.8 - 3.13 (inclusive) installed, along with your choice of underlying Python driver (see [minimum requirements](../GettingStarted.md#minimum-requirements)). Clone the AWS Advanced Python Driver repository: diff --git a/pyproject.toml b/pyproject.toml index ae8796e2..f2a8289d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] [tool.poetry.dependencies] diff --git a/tests/integration/container/utils/target_python_version.py b/tests/integration/container/utils/target_python_version.py index db23eaf6..9969dd32 100644 --- a/tests/integration/container/utils/target_python_version.py +++ b/tests/integration/container/utils/target_python_version.py @@ -18,3 +18,5 @@ class TargetPythonVersion(Enum): PYTHON_3_11 = "PYTHON_3_11" PYTHON_3_8 = "PYTHON_3_8" + PYTHON_3_12 = "PYTHON_3_12" + PYTHON_3_13 = "PYTHON_3_13" diff --git a/tests/integration/host/build.gradle.kts b/tests/integration/host/build.gradle.kts index b746eb64..7775238c 100644 --- a/tests/integration/host/build.gradle.kts +++ b/tests/integration/host/build.gradle.kts @@ -69,6 +69,8 @@ tasks.register("test-python-3.11-mysql") { doFirst { systemProperty("exclude-performance", "true") systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-312", "true") + systemProperty("exclude-python-313", "true") systemProperty("exclude-multi-az-cluster", "true") systemProperty("exclude-multi-az-instance", "true") systemProperty("exclude-bg", "true") @@ -83,6 +85,8 @@ tasks.register("test-python-3.8-mysql") { doFirst { systemProperty("exclude-performance", "true") systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-312", "true") + systemProperty("exclude-python-313", "true") systemProperty("exclude-multi-az-cluster", "true") systemProperty("exclude-multi-az-instance", "true") systemProperty("exclude-bg", "true") @@ -97,6 +101,8 @@ tasks.register("test-python-3.11-pg") { doFirst { systemProperty("exclude-performance", "true") systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-312", "true") + systemProperty("exclude-python-313", "true") systemProperty("exclude-multi-az-cluster", "true") systemProperty("exclude-multi-az-instance", "true") systemProperty("exclude-bg", "true") @@ -113,6 +119,76 @@ tasks.register("test-python-3.8-pg") { doFirst { systemProperty("exclude-performance", "true") systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-312", "true") + systemProperty("exclude-python-313", "true") + systemProperty("exclude-multi-az-cluster", "true") + systemProperty("exclude-multi-az-instance", "true") + systemProperty("exclude-bg", "true") + systemProperty("exclude-mysql-driver", "true") + systemProperty("exclude-mysql-engine", "true") + systemProperty("exclude-mariadb-driver", "true") + systemProperty("exclude-mariadb-engine", "true") + } +} + +tasks.register("test-python-3.12-mysql") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("exclude-performance", "true") + systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-313", "true") + systemProperty("exclude-multi-az-cluster", "true") + systemProperty("exclude-multi-az-instance", "true") + systemProperty("exclude-bg", "true") + systemProperty("exclude-pg-driver", "true") + systemProperty("exclude-pg-engine", "true") + } +} + +tasks.register("test-python-3.12-pg") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("exclude-performance", "true") + systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-313", "true") + systemProperty("exclude-multi-az-cluster", "true") + systemProperty("exclude-multi-az-instance", "true") + systemProperty("exclude-bg", "true") + systemProperty("exclude-mysql-driver", "true") + systemProperty("exclude-mysql-engine", "true") + systemProperty("exclude-mariadb-driver", "true") + systemProperty("exclude-mariadb-engine", "true") + } +} + +tasks.register("test-python-3.13-mysql") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("exclude-performance", "true") + systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-312", "true") + systemProperty("exclude-multi-az-cluster", "true") + systemProperty("exclude-multi-az-instance", "true") + systemProperty("exclude-bg", "true") + systemProperty("exclude-pg-driver", "true") + systemProperty("exclude-pg-engine", "true") + } +} + +tasks.register("test-python-3.13-pg") { + group = "verification" + filter.includeTestsMatching("integration.host.TestRunner.runTests") + doFirst { + systemProperty("exclude-performance", "true") + systemProperty("exclude-python-38", "true") + systemProperty("exclude-python-311", "true") + systemProperty("exclude-python-312", "true") systemProperty("exclude-multi-az-cluster", "true") systemProperty("exclude-multi-az-instance", "true") systemProperty("exclude-bg", "true") diff --git a/tests/integration/host/src/test/java/integration/TargetPythonVersion.java b/tests/integration/host/src/test/java/integration/TargetPythonVersion.java index f6ac5bc0..ab6c1992 100644 --- a/tests/integration/host/src/test/java/integration/TargetPythonVersion.java +++ b/tests/integration/host/src/test/java/integration/TargetPythonVersion.java @@ -18,5 +18,7 @@ public enum TargetPythonVersion { PYTHON_3_11, - PYTHON_3_8 + PYTHON_3_8, + PYTHON_3_12, + PYTHON_3_13 } diff --git a/tests/integration/host/src/test/java/integration/host/TestEnvironment.java b/tests/integration/host/src/test/java/integration/host/TestEnvironment.java index 06a4fa30..f42ba308 100644 --- a/tests/integration/host/src/test/java/integration/host/TestEnvironment.java +++ b/tests/integration/host/src/test/java/integration/host/TestEnvironment.java @@ -1145,6 +1145,10 @@ private static String getContainerBaseImageName(TestEnvironmentRequest request) return "python:3.8.18"; case PYTHON_3_11: return "python:3.11.5"; + case PYTHON_3_12: + return "python:3.12"; + case PYTHON_3_13: + return "python:3.13"; default: throw new NotImplementedException(request.getTargetPythonVersion().toString()); } diff --git a/tests/integration/host/src/test/java/integration/host/TestEnvironmentConfiguration.java b/tests/integration/host/src/test/java/integration/host/TestEnvironmentConfiguration.java index f36f8af8..85733d14 100644 --- a/tests/integration/host/src/test/java/integration/host/TestEnvironmentConfiguration.java +++ b/tests/integration/host/src/test/java/integration/host/TestEnvironmentConfiguration.java @@ -69,6 +69,10 @@ public class TestEnvironmentConfiguration { Boolean.parseBoolean(System.getProperty("exclude-python-38", "false")); public boolean excludePython311 = Boolean.parseBoolean(System.getProperty("exclude-python-311", "false")); + public boolean excludePython312 = + Boolean.parseBoolean(System.getProperty("exclude-python-312", "false")); + public boolean excludePython313 = + Boolean.parseBoolean(System.getProperty("exclude-python-313", "false")); public String testFilter = System.getenv("FILTER"); diff --git a/tests/integration/host/src/test/java/integration/host/TestEnvironmentProvider.java b/tests/integration/host/src/test/java/integration/host/TestEnvironmentProvider.java index 15011003..9292e970 100644 --- a/tests/integration/host/src/test/java/integration/host/TestEnvironmentProvider.java +++ b/tests/integration/host/src/test/java/integration/host/TestEnvironmentProvider.java @@ -127,6 +127,12 @@ public Stream provideTestTemplateInvocationContex if (targetPythonVersion == TargetPythonVersion.PYTHON_3_11 && config.excludePython311) { continue; } + if (targetPythonVersion == TargetPythonVersion.PYTHON_3_12 && config.excludePython312) { + continue; + } + if (targetPythonVersion == TargetPythonVersion.PYTHON_3_13 && config.excludePython313) { + continue; + } for (boolean withBlueGreenFeature : Arrays.asList(true, false)) { if (!withBlueGreenFeature) { diff --git a/tests/unit/test_connection_provider.py b/tests/unit/test_connection_provider.py index 5d73c06e..6567ffb0 100644 --- a/tests/unit/test_connection_provider.py +++ b/tests/unit/test_connection_provider.py @@ -33,7 +33,31 @@ def default_provider_mock(mocker): @pytest.fixture def set_provider_mock(mocker): - return mocker.MagicMock() + # In Python 3.12+, isinstance checks with Protocols are stricter + # Create a real instance that implements the protocol, then wrap release_resources + # with a mock to allow method call tracking + class MockConnectionProviderWithRelease: + """Mock connection provider that implements CanReleaseResources protocol.""" + def accepts_host_info(self, host_info, props): + return True + + def accepts_strategy(self, role, strategy): + return True + + def get_host_info_by_strategy(self, hosts, role, strategy, props): + return hosts[0] if hosts else None + + def connect(self, target_func, driver_dialect, database_dialect, host_info, props): + return None + + def release_resources(self): + pass + + # Create a real instance + provider = MockConnectionProviderWithRelease() + # Replace release_resources with a MagicMock so we can assert it was called + provider.release_resources = mocker.MagicMock() + return provider @pytest.fixture @@ -177,4 +201,4 @@ def test_release_resources(connection_mock, default_provider_mock, set_provider_ ConnectionProviderManager.set_connection_provider(set_provider_mock) ConnectionProviderManager.release_resources() - connection_provider_manager._conn_provider.release_resources.assert_called_once() + set_provider_mock.release_resources.assert_called_once() diff --git a/tests/unit/test_limitless_plugin.py b/tests/unit/test_limitless_plugin.py index 8bcadf1c..c54038a6 100644 --- a/tests/unit/test_limitless_plugin.py +++ b/tests/unit/test_limitless_plugin.py @@ -14,7 +14,8 @@ import psycopg import pytest -from aws_advanced_python_wrapper.database_dialect import (DatabaseDialect, +from aws_advanced_python_wrapper.database_dialect import (AuroraPgDialect, + DatabaseDialect, MysqlDatabaseDialect) from aws_advanced_python_wrapper.errors import (AwsWrapperError, UnsupportedOperationError) @@ -36,6 +37,8 @@ def mock_plugin_service(mocker, mock_driver_dialect, mock_conn, host_info): service_mock = mocker.MagicMock() service_mock.current_connection = mock_conn service_mock.current_host_info = host_info + # Use a real AuroraPgDialect to pass isinstance checks in Python 3.12+ + service_mock.database_dialect = AuroraPgDialect() type(service_mock).driver_dialect = mocker.PropertyMock(return_value=mock_driver_dialect) return service_mock @@ -125,7 +128,8 @@ def test_connect_supported_dialect_after_refresh( mocker, plugin, host_info, props, mock_conn, mock_plugin_service, mock_limitless_router_service, mock_driver_dialect ): unsupported_dialect: DatabaseDialect = MysqlDatabaseDialect() - type(mock_plugin_service).database_dialect = PropertyMock(side_effect=[unsupported_dialect, mock_driver_dialect]) + supported_dialect: DatabaseDialect = AuroraPgDialect() + type(mock_plugin_service).database_dialect = PropertyMock(side_effect=[unsupported_dialect, supported_dialect]) def replace_context_connection(invocation): context = invocation._connection_plugin._context diff --git a/tests/unit/test_multi_az_rds_host_list_provider.py b/tests/unit/test_multi_az_rds_host_list_provider.py index 65161404..145926d0 100644 --- a/tests/unit/test_multi_az_rds_host_list_provider.py +++ b/tests/unit/test_multi_az_rds_host_list_provider.py @@ -17,6 +17,7 @@ import psycopg import pytest +from aws_advanced_python_wrapper.database_dialect import AuroraPgDialect from aws_advanced_python_wrapper.errors import (AwsWrapperError, QueryTimeoutError) from aws_advanced_python_wrapper.host_list_provider import ( @@ -59,7 +60,10 @@ def mock_cursor(mocker): @pytest.fixture def mock_provider_service(mocker): - return mocker.MagicMock() + service_mock = mocker.MagicMock() + # Use a real AuroraPgDialect to pass isinstance checks in Python 3.12+ + service_mock.database_dialect = AuroraPgDialect() + return service_mock @pytest.fixture diff --git a/tests/unit/test_mysql_driver_dialect.py b/tests/unit/test_mysql_driver_dialect.py index 938ead59..8926b612 100644 --- a/tests/unit/test_mysql_driver_dialect.py +++ b/tests/unit/test_mysql_driver_dialect.py @@ -14,8 +14,6 @@ import psycopg import pytest -from mysql.connector import CMySQLConnection -from mysql.connector.cursor_cext import CMySQLCursor from aws_advanced_python_wrapper.errors import AwsWrapperError from aws_advanced_python_wrapper.hostinfo import HostInfo @@ -23,6 +21,17 @@ from aws_advanced_python_wrapper.utils.properties import (Properties, WrapperProperties) +try: + from mysql.connector import CMySQLConnection + from mysql.connector.cursor_cext import CMySQLCursor +except ImportError: + # CMySQLConnection not available (e.g., Python 3.13 with mysql-connector-python 9.0.0) + # Skip all tests in this module if C extension is not available + pytest.skip( + "CMySQLConnection not available (C extension not supported on this Python version)", + allow_module_level=True + ) + @pytest.fixture def dialect(): diff --git a/tests/unit/test_pg_driver_dialect.py b/tests/unit/test_pg_driver_dialect.py index 8c01a7aa..358e6b99 100644 --- a/tests/unit/test_pg_driver_dialect.py +++ b/tests/unit/test_pg_driver_dialect.py @@ -14,7 +14,6 @@ import psycopg import pytest -from mysql.connector import CMySQLConnection from sqlalchemy import PoolProxiedConnection from aws_advanced_python_wrapper.errors import AwsWrapperError @@ -23,6 +22,14 @@ from aws_advanced_python_wrapper.utils.properties import (Properties, WrapperProperties) +try: + from mysql.connector import CMySQLConnection + HAS_C_MYSQL_CONNECTION = True +except ImportError: + # CMySQLConnection not available (e.g., Python 3.13 with mysql-connector-python 9.0.0) + HAS_C_MYSQL_CONNECTION = False + CMySQLConnection = None + @pytest.fixture def mock_conn(mocker): @@ -40,7 +47,12 @@ def mock_pool_conn(mocker, mock_conn): @pytest.fixture def mock_invalid_conn(mocker): - return mocker.MagicMock(spec=CMySQLConnection) + if HAS_C_MYSQL_CONNECTION: + return mocker.MagicMock(spec=CMySQLConnection) + else: + # Use a different invalid connection type when CMySQLConnection is not available + # This simulates an invalid connection type for error testing + return mocker.MagicMock(spec=object) @pytest.fixture diff --git a/tests/unit/test_rds_host_list_provider.py b/tests/unit/test_rds_host_list_provider.py index 3e375d4b..bd2fcc8a 100644 --- a/tests/unit/test_rds_host_list_provider.py +++ b/tests/unit/test_rds_host_list_provider.py @@ -18,6 +18,7 @@ import psycopg import pytest +from aws_advanced_python_wrapper.database_dialect import AuroraPgDialect from aws_advanced_python_wrapper.errors import (AwsWrapperError, QueryTimeoutError) from aws_advanced_python_wrapper.host_list_provider import RdsHostListProvider @@ -58,7 +59,10 @@ def mock_cursor(mocker): @pytest.fixture def mock_provider_service(mocker): - return mocker.MagicMock() + service_mock = mocker.MagicMock() + # Use a real AuroraPgDialect to pass isinstance checks in Python 3.12+ + service_mock.database_dialect = AuroraPgDialect() + return service_mock @pytest.fixture