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