From f4acd6c63b1c8c67c50d3ac6a2eed901fc91bcca Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:25:19 +0000 Subject: [PATCH 01/28] Remove comment line --- modules/generic/testcontainers/generic/server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/generic/testcontainers/generic/server.py b/modules/generic/testcontainers/generic/server.py index 61e9c5eb9..fe990f179 100644 --- a/modules/generic/testcontainers/generic/server.py +++ b/modules/generic/testcontainers/generic/server.py @@ -9,8 +9,6 @@ from testcontainers.core.image import DockerImage from testcontainers.core.waiting_utils import wait_container_is_ready -# This comment can be removed (Used for testing) - class ServerContainer(DockerContainer): """ From af012648dc2b071a9f929317064ab3c2fe5f68d3 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:45:55 +0000 Subject: [PATCH 02/28] Added base generic db --- modules/generic/testcontainers/generic/db.py | 212 +++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 modules/generic/testcontainers/generic/db.py diff --git a/modules/generic/testcontainers/generic/db.py b/modules/generic/testcontainers/generic/db.py new file mode 100644 index 000000000..a025250ff --- /dev/null +++ b/modules/generic/testcontainers/generic/db.py @@ -0,0 +1,212 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is 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 logging +from typing import Any, Optional +from urllib.parse import quote, urlencode + +from testcontainers.core.container import DockerContainer +from testcontainers.core.exceptions import ContainerStartException +from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.waiting_utils import wait_container_is_ready + +logger = logging.getLogger(__name__) + +ADDITIONAL_TRANSIENT_ERRORS = [] +try: + from sqlalchemy.exc import DBAPIError + + ADDITIONAL_TRANSIENT_ERRORS.append(DBAPIError) +except ImportError: + logger.debug("SQLAlchemy not available, skipping DBAPIError handling") + + +class DbContainer(DockerContainer): + """ + Generic database container providing common database functionality. + + This class serves as a base for database-specific container implementations. + It provides connection management, URL construction, and basic lifecycle methods. + + Note: + This class is deprecated and will be removed in a future version. + Use database-specific container classes instead. + """ + + @wait_container_is_ready(*ADDITIONAL_TRANSIENT_ERRORS) + def _connect(self) -> None: + """ + Test database connectivity using SQLAlchemy. + + Raises: + ImportError: If SQLAlchemy is not installed + Exception: If connection fails + """ + try: + import sqlalchemy + except ImportError as e: + logger.error("SQLAlchemy is required for database connectivity testing") + raise ImportError("SQLAlchemy is required for database containers") from e + + connection_url = self.get_connection_url() + logger.debug(f"Testing database connection to {self._mask_password_in_url(connection_url)}") + + engine = sqlalchemy.create_engine(connection_url) + try: + with engine.connect(): + logger.info("Database connection test successful") + except Exception as e: + logger.error(f"Database connection test failed: {e}") + raise + finally: + engine.dispose() + + def get_connection_url(self) -> str: + """ + Get the database connection URL. + + Returns: + str: Database connection URL + + Raises: + NotImplementedError: Must be implemented by subclasses + """ + raise NotImplementedError("Subclasses must implement get_connection_url()") + + def _create_connection_url( + self, + dialect: str, + username: str, + password: str, + host: Optional[str] = None, + port: Optional[int] = None, + dbname: Optional[str] = None, + query_params: Optional[dict[str, str]] = None, + **kwargs: Any, + ) -> str: + """ + Create a database connection URL. + + Args: + dialect: Database dialect (e.g., 'postgresql', 'mysql') + username: Database username + password: Database password + host: Database host (defaults to container host) + port: Database port + dbname: Database name + query_params: Additional query parameters for the URL + **kwargs: Additional parameters (checked for deprecated usage) + + Returns: + str: Formatted database connection URL + + Raises: + ValueError: If unexpected arguments are provided or required parameters are missing + ContainerStartException: If container is not started + """ + if raise_for_deprecated_parameter(kwargs, "db_name", "dbname"): + raise ValueError(f"Unexpected arguments: {','.join(kwargs)}") + + if self._container is None: + raise ContainerStartException("Container has not been started") + + # Validate required parameters + if not dialect: + raise ValueError("Database dialect is required") + if not username: + raise ValueError("Database username is required") + if port is None: + raise ValueError("Database port is required") + + host = host or self.get_container_host_ip() + exposed_port = self.get_exposed_port(port) + + # Safely quote password to handle special characters + quoted_password = quote(password, safe="") + quoted_username = quote(username, safe="") + + # Build base URL + url = f"{dialect}://{quoted_username}:{quoted_password}@{host}:{exposed_port}" + + # Add database name if provided + if dbname: + quoted_dbname = quote(dbname, safe="") + url = f"{url}/{quoted_dbname}" + + # Add query parameters if provided + if query_params: + query_string = urlencode(query_params) + url = f"{url}?{query_string}" + + logger.debug(f"Created connection URL: {self._mask_password_in_url(url)}") + return url + + def _mask_password_in_url(self, url: str) -> str: + """ + Mask password in URL for safe logging. + + Args: + url: Database connection URL + + Returns: + str: URL with masked password + """ + try: + # Simple regex-based masking for logging + import re + + return re.sub(r"://([^:]+):([^@]+)@", r"://\1:***@", url) + except Exception: + return "[URL with masked credentials]" + + def start(self) -> "DbContainer": + """ + Start the database container and perform initialization. + + Returns: + DbContainer: Self for method chaining + + Raises: + ContainerStartException: If container fails to start + Exception: If configuration, seed transfer, or connection fails + """ + logger.info(f"Starting database container: {self.image}") + + try: + self._configure() + super().start() + self._transfer_seed() + self._connect() + logger.info("Database container started successfully") + except Exception as e: + logger.error(f"Failed to start database container: {e}") + raise + + return self + + def _configure(self) -> None: + """ + Configure the database container before starting. + + Raises: + NotImplementedError: Must be implemented by subclasses + """ + raise NotImplementedError("Subclasses must implement _configure()") + + def _transfer_seed(self) -> None: + """ + Transfer seed data to the database container. + + This method can be overridden by subclasses to provide + database-specific seeding functionality. + """ + logger.debug("No seed data to transfer") From 869a135ea3336c7f224ec6ab7d8ab16d24abbf1d Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:48:16 +0000 Subject: [PATCH 03/28] Rename test_generic --- modules/generic/tests/{test_generic.py => test_server.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/generic/tests/{test_generic.py => test_server.py} (100%) diff --git a/modules/generic/tests/test_generic.py b/modules/generic/tests/test_server.py similarity index 100% rename from modules/generic/tests/test_generic.py rename to modules/generic/tests/test_server.py From 5c3ffbc4e38440f71afcc0216ea0cc7d9ef38cd5 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:17:43 +0000 Subject: [PATCH 04/28] Renamed DB to SQL --- .../testcontainers/generic/{db.py => sql.py} | 46 ++----------------- 1 file changed, 5 insertions(+), 41 deletions(-) rename modules/generic/testcontainers/generic/{db.py => sql.py} (76%) diff --git a/modules/generic/testcontainers/generic/db.py b/modules/generic/testcontainers/generic/sql.py similarity index 76% rename from modules/generic/testcontainers/generic/db.py rename to modules/generic/testcontainers/generic/sql.py index a025250ff..06913258e 100644 --- a/modules/generic/testcontainers/generic/db.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -1,15 +1,3 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is 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 logging from typing import Any, Optional from urllib.parse import quote, urlencode @@ -30,16 +18,12 @@ logger.debug("SQLAlchemy not available, skipping DBAPIError handling") -class DbContainer(DockerContainer): +class SqlContainer(DockerContainer): """ - Generic database container providing common database functionality. + Generic SQL database container providing common functionality. - This class serves as a base for database-specific container implementations. + This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. - - Note: - This class is deprecated and will be removed in a future version. - Use database-specific container classes instead. """ @wait_container_is_ready(*ADDITIONAL_TRANSIENT_ERRORS) @@ -58,7 +42,6 @@ def _connect(self) -> None: raise ImportError("SQLAlchemy is required for database containers") from e connection_url = self.get_connection_url() - logger.debug(f"Testing database connection to {self._mask_password_in_url(connection_url)}") engine = sqlalchemy.create_engine(connection_url) try: @@ -147,33 +130,14 @@ def _create_connection_url( query_string = urlencode(query_params) url = f"{url}?{query_string}" - logger.debug(f"Created connection URL: {self._mask_password_in_url(url)}") return url - def _mask_password_in_url(self, url: str) -> str: - """ - Mask password in URL for safe logging. - - Args: - url: Database connection URL - - Returns: - str: URL with masked password - """ - try: - # Simple regex-based masking for logging - import re - - return re.sub(r"://([^:]+):([^@]+)@", r"://\1:***@", url) - except Exception: - return "[URL with masked credentials]" - - def start(self) -> "DbContainer": + def start(self) -> "SqlContainer": """ Start the database container and perform initialization. Returns: - DbContainer: Self for method chaining + SqlContainer: Self for method chaining Raises: ContainerStartException: If container fails to start From 39cb0a19b420fd56db073aeed650f864162a3ff3 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:18:05 +0000 Subject: [PATCH 05/28] Tests for SQL --- modules/generic/tests/test_sql.py | 236 ++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 modules/generic/tests/test_sql.py diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py new file mode 100644 index 000000000..209d77b2b --- /dev/null +++ b/modules/generic/tests/test_sql.py @@ -0,0 +1,236 @@ +import pytest +from testcontainers.core.exceptions import ContainerStartException +from testcontainers.generic.sql import SqlContainer + + +class SimpleSqlContainer(SqlContainer): + """Simple concrete implementation for testing.""" + + def __init__(self, image: str = "postgres:13"): + super().__init__(image) + self.username = "testuser" + self.password = "testpass" + self.dbname = "testdb" + self.port = 5432 + + def get_connection_url(self) -> str: + return self._create_connection_url( + dialect="postgresql", username=self.username, password=self.password, port=self.port, dbname=self.dbname + ) + + def _configure(self) -> None: + self.with_env("POSTGRES_USER", self.username) + self.with_env("POSTGRES_PASSWORD", self.password) + self.with_env("POSTGRES_DB", self.dbname) + self.with_exposed_ports(self.port) + + +class TestSqlContainer: + def test_abstract_methods_raise_not_implemented(self): + container = SqlContainer("test:latest") + + with pytest.raises(NotImplementedError): + container.get_connection_url() + + with pytest.raises(NotImplementedError): + container._configure() + + def test_transfer_seed_default_behavior(self): + container = SqlContainer("test:latest") + # Should not raise an exception + container._transfer_seed() + + def test_connection_url_creation_basic(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() # Simple mock + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url(dialect="postgresql", username="user", password="pass", port=5432) + + assert url == "postgresql://user:pass@localhost:5432" + + def test_connection_url_with_database_name(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url( + dialect="postgresql", username="user", password="pass", port=5432, dbname="mydb" + ) + + assert url == "postgresql://user:pass@localhost:5432/mydb" + + def test_connection_url_with_special_characters(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url( + dialect="postgresql", username="user@domain", password="p@ss/word", port=5432 + ) + + # Check that special characters are URL encoded + assert "user%40domain" in url + assert "p%40ss%2Fword" in url + + def test_connection_url_with_query_params(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url( + dialect="postgresql", + username="user", + password="pass", + port=5432, + query_params={"ssl": "require", "timeout": "30"}, + ) + + assert "?" in url + assert "ssl=require" in url + assert "timeout=30" in url + + def test_connection_url_validation_errors(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + + # Test missing dialect + with pytest.raises(ValueError, match="Database dialect is required"): + container._create_connection_url("", "user", "pass", port=5432) + + # Test missing username + with pytest.raises(ValueError, match="Database username is required"): + container._create_connection_url("postgresql", "", "pass", port=5432) + + # Test missing port + with pytest.raises(ValueError, match="Database port is required"): + container._create_connection_url("postgresql", "user", "pass", port=None) + + def test_connection_url_container_not_started(self): + container = SimpleSqlContainer() + container._container = None + + with pytest.raises(ContainerStartException, match="Container has not been started"): + container._create_connection_url("postgresql", "user", "pass", port=5432) + + def test_container_configuration(self): + container = SimpleSqlContainer("postgres:13") + + # Test that configuration sets up environment + container._configure() + + assert container.env["POSTGRES_USER"] == "testuser" + assert container.env["POSTGRES_PASSWORD"] == "testpass" + assert container.env["POSTGRES_DB"] == "testdb" + + def test_concrete_container_connection_url(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: 5432 + + url = container.get_connection_url() + + assert url.startswith("postgresql://") + assert "testuser" in url + assert "testpass" in url + assert "testdb" in url + assert "localhost:5432" in url + + def test_container_inheritance(self): + container = SimpleSqlContainer() + + assert isinstance(container, SqlContainer) + assert hasattr(container, "get_connection_url") + assert hasattr(container, "_configure") + assert hasattr(container, "_transfer_seed") + assert hasattr(container, "start") + + def test_additional_transient_errors_list(self): + from testcontainers.generic.sql import ADDITIONAL_TRANSIENT_ERRORS + + assert isinstance(ADDITIONAL_TRANSIENT_ERRORS, list) + # List may be empty if SQLAlchemy not available, or contain DBAPIError if it is + + def test_empty_password_handling(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url(dialect="postgresql", username="user", password="", port=5432) + + assert url == "postgresql://user:@localhost:5432" + + def test_unicode_characters_in_credentials(self): + container = SimpleSqlContainer() + container._container = type("MockContainer", (), {})() + container.get_container_host_ip = lambda: "localhost" + container.get_exposed_port = lambda port: port + + url = container._create_connection_url( + dialect="postgresql", username="usér", password="päss", port=5432, dbname="tëstdb" + ) + + assert "us%C3%A9r" in url + assert "p%C3%A4ss" in url + assert "t%C3%ABstdb" in url + + def test_start_postgres_container_integration(self): + """Integration test that actually starts a PostgreSQL container.""" + container = SimpleSqlContainer() + + # This will start the container and test the connection + container.start() + + # Verify the container is running + assert container._container is not None + + # Test that we can get a connection URL + url = container.get_connection_url() + assert url.startswith("postgresql://") + assert "testuser" in url + assert "testdb" in url + + # Verify environment variables are set + assert container.env["POSTGRES_USER"] == "testuser" + assert container.env["POSTGRES_PASSWORD"] == "testpass" + assert container.env["POSTGRES_DB"] == "testdb" + + # check logs + logs = container.get_logs() + assert "database system is ready to accept connections" in logs[0].decode("utf-8").lower() + + def test_sql_postgres_container_integration(self): + """Integration test for SqlContainer with PostgreSQL.""" + container = SimpleSqlContainer() + + # This will start the container and test the connection + container.start() + + # Verify the container is running + assert container._container is not None + + # Test that we can get a connection URL + url = container.get_connection_url() + + # check sql operations + import sqlalchemy + + engine = sqlalchemy.create_engine(url) + with engine.connect() as conn: + # Create a test table + conn.execute( + sqlalchemy.text("CREATE TABLE IF NOT EXISTS test_table (id SERIAL PRIMARY KEY, name VARCHAR(50));") + ) + # Insert a test record + conn.execute(sqlalchemy.text("INSERT INTO test_table (name) VALUES ('test_name');")) + # Query the test record + result = conn.execute(sqlalchemy.text("SELECT name FROM test_table WHERE name='test_name';")) + fetched = result.fetchone() + assert fetched is not None + assert fetched[0] == "test_name" From 5d15b6e106edaf950fda769afe8baab957aa41a5 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:20:14 +0000 Subject: [PATCH 06/28] Update warnning --- core/testcontainers/core/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/testcontainers/core/generic.py b/core/testcontainers/core/generic.py index e427c2ad5..162cc3520 100644 --- a/core/testcontainers/core/generic.py +++ b/core/testcontainers/core/generic.py @@ -30,6 +30,7 @@ class DbContainer(DockerContainer): """ **DEPRECATED (for removal)** + Please use database-specific container classes or `SqlContainer` instead. Generic database container. """ From 80a5355092a19d89bd7b3d5e734f5e6d2a75f62f Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:27:18 +0000 Subject: [PATCH 07/28] update docs --- modules/generic/README.rst | 17 +++++++++++++++++ .../generic/testcontainers/generic/__init__.py | 1 + 2 files changed, 18 insertions(+) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index 4497ec922..77a434dce 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -50,3 +50,20 @@ A more advance use-case, where we are using a FastAPI container that is using Re ... response = client.get(f"/get/{test_data['key']}") ... assert response.status_code == 200, "Failed to get data" ... assert response.json() == {"key": test_data["key"], "value": test_data["value"]} + +.. autoclass:: testcontainers.generic.SqlContainer +.. title:: testcontainers.generic.SqlContainer + +SQL container that is using :code:`SqlContainer` + +.. doctest:: + + >>> from testcontainers.generic import SqlContainer + >>> from sqlalchemy import text + >>> import sqlalchemy + + >>> with SqlContainer(image="postgres:15-alpine", port=5432, username="test", password="test", dbname="test") as postgres: + ... engine = sqlalchemy.create_engine(postgres.get_connection_url()) + ... with engine.connect() as conn: + ... result = conn.execute(text("SELECT 1")) + ... assert result.scalar() == 1 diff --git a/modules/generic/testcontainers/generic/__init__.py b/modules/generic/testcontainers/generic/__init__.py index f239a80c6..ce6610a3c 100644 --- a/modules/generic/testcontainers/generic/__init__.py +++ b/modules/generic/testcontainers/generic/__init__.py @@ -1 +1,2 @@ from .server import ServerContainer # noqa: F401 +from .sql import SqlContainer # noqa: F401 From 04e418c93379411475c68c61bfddeadc27c5b271 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:43:30 +0000 Subject: [PATCH 08/28] Fix doctests --- modules/generic/README.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index 77a434dce..90514bb09 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -54,7 +54,7 @@ A more advance use-case, where we are using a FastAPI container that is using Re .. autoclass:: testcontainers.generic.SqlContainer .. title:: testcontainers.generic.SqlContainer -SQL container that is using :code:`SqlContainer` +Postgres container that is using :code:`SqlContainer` .. doctest:: @@ -62,7 +62,25 @@ SQL container that is using :code:`SqlContainer` >>> from sqlalchemy import text >>> import sqlalchemy - >>> with SqlContainer(image="postgres:15-alpine", port=5432, username="test", password="test", dbname="test") as postgres: + >>> class CustomPostgresContainer(SqlContainer): + ... def __init__(self, image="postgres:15-alpine", + ... port=5432, username="test", password="test", dbname="test"): + ... super().__init__(image=image) + ... self.port_to_expose = port + ... self.username = username + ... self.password = password + ... self.dbname = dbname + ... def get_connection_url(self) -> str: + ... host = self.get_container_host_ip() + ... port = self.get_exposed_port(self.port_to_expose) + ... return f"postgresql://{self.username}:{self.password}@{host}:{port}/{self.dbname}" + ... def _configure(self) -> None: + ... self.with_exposed_ports(self.port_to_expose) + ... self.with_env("POSTGRES_USER", self.username) + ... self.with_env("POSTGRES_PASSWORD", self.password) + ... self.with_env("POSTGRES_DB", self.dbname) + + >>> with CustomPostgresContainer() as postgres: ... engine = sqlalchemy.create_engine(postgres.get_connection_url()) ... with engine.connect() as conn: ... result = conn.execute(text("SELECT 1")) From 09d9cad86e5d2d6670a72b234b7d5f2be8622fdd Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:58:16 +0000 Subject: [PATCH 09/28] Add generic dep --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index acdaa0d03..4e2c434c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,7 @@ elasticsearch = [] generic = [ "httpx", "redis", + "sqlalchemy", ] # The advance doctests for ServerContainer require redis test_module_import = ["httpx"] google = ["google-cloud-pubsub", "google-cloud-datastore"] From acec2c1370b14fc896bd883b8da465ec8e31a2ff Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 21:31:13 +0000 Subject: [PATCH 10/28] Update ref --- core/testcontainers/core/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/testcontainers/core/generic.py b/core/testcontainers/core/generic.py index 162cc3520..d193852e3 100644 --- a/core/testcontainers/core/generic.py +++ b/core/testcontainers/core/generic.py @@ -31,6 +31,7 @@ class DbContainer(DockerContainer): """ **DEPRECATED (for removal)** Please use database-specific container classes or `SqlContainer` instead. + # from testcontainers.generic.sql import SqlContainer Generic database container. """ From f0bf38138f1b01c80cd9f7c94e12b0e996b67fed Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 21:32:10 +0000 Subject: [PATCH 11/28] Update lock --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7411ad744..5d60ab8b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6797,7 +6797,7 @@ cockroachdb = [] cosmosdb = ["azure-cosmos"] db2 = ["ibm_db_sa", "sqlalchemy"] elasticsearch = [] -generic = ["httpx", "redis"] +generic = ["httpx", "redis", "sqlalchemy"] google = ["google-cloud-datastore", "google-cloud-pubsub"] influxdb = ["influxdb", "influxdb-client"] k3s = ["kubernetes", "pyyaml"] @@ -6836,4 +6836,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.1" python-versions = ">=3.9.2,<4.0" -content-hash = "241e8b6ba610907adea4496fdeaef4c3fdc3315d222ab87004692aa9371698fa" +content-hash = "c5a635d3fa182f964fe96d5685efdfb4bd5fcbab829388a5d5a5be90fb81eaee" From ec9d4e2cf4ee6594f783a66869003d835de17d33 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:18:57 +0000 Subject: [PATCH 12/28] Replaced wait --- modules/generic/testcontainers/generic/sql.py | 83 +++++++++++++++---- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 06913258e..039b51c53 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -5,7 +5,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.exceptions import ContainerStartException from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget logger = logging.getLogger(__name__) @@ -18,19 +18,67 @@ logger.debug("SQLAlchemy not available, skipping DBAPIError handling") +class DatabaseConnectionWaitStrategy(WaitStrategy): + """ + Wait strategy for database connection readiness using SqlContainer._connect(). + + This strategy implements retry logic and calls SqlContainer._connect() + repeatedly until it succeeds or times out. + """ + + def __init__(self, sql_container: "SqlContainer"): + super().__init__() + self.sql_container = sql_container + + def wait_until_ready(self, container: WaitStrategyTarget) -> None: + """ + Test database connectivity with retry logic by calling SqlContainer._connect(). + + Raises: + TimeoutError: If connection fails after timeout + Exception: Any non-transient errors from _connect() + """ + import time + + start_time = time.time() + + transient_exceptions = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) + + while True: + if time.time() - start_time > self._startup_timeout: + raise TimeoutError( + f"Database connection failed after {self._startup_timeout}s timeout. " + f"Hint: Check if the database container is ready and accessible." + ) + + try: + self.sql_container._connect() + return + except transient_exceptions as e: + logger.debug(f"Database connection attempt failed: {e}, retrying in {self._poll_interval}s...") + except Exception as e: + logger.error(f"Database connection test failed with non-transient error: {e}") + raise + + time.sleep(self._poll_interval) + + class SqlContainer(DockerContainer): """ Generic SQL database container providing common functionality. This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. + Database connection readiness is automatically handled by DatabaseConnectionWaitStrategy. """ - @wait_container_is_ready(*ADDITIONAL_TRANSIENT_ERRORS) def _connect(self) -> None: """ Test database connectivity using SQLAlchemy. + This method performs a single connection test without retry logic. + Retry logic is handled by the DatabaseConnectionWaitStrategy. + Raises: ImportError: If SQLAlchemy is not installed Exception: If connection fails @@ -42,29 +90,17 @@ def _connect(self) -> None: raise ImportError("SQLAlchemy is required for database containers") from e connection_url = self.get_connection_url() - engine = sqlalchemy.create_engine(connection_url) + try: with engine.connect(): logger.info("Database connection test successful") except Exception as e: - logger.error(f"Database connection test failed: {e}") + logger.debug(f"Database connection attempt failed: {e}") raise finally: engine.dispose() - def get_connection_url(self) -> str: - """ - Get the database connection URL. - - Returns: - str: Database connection URL - - Raises: - NotImplementedError: Must be implemented by subclasses - """ - raise NotImplementedError("Subclasses must implement get_connection_url()") - def _create_connection_url( self, dialect: str, @@ -147,9 +183,10 @@ def start(self) -> "SqlContainer": try: self._configure() + # Set up database connection wait strategy before starting + self.waiting_for(DatabaseConnectionWaitStrategy(self)) super().start() self._transfer_seed() - self._connect() logger.info("Database container started successfully") except Exception as e: logger.error(f"Failed to start database container: {e}") @@ -174,3 +211,15 @@ def _transfer_seed(self) -> None: database-specific seeding functionality. """ logger.debug("No seed data to transfer") + + def get_connection_url(self) -> str: + """ + Get the database connection URL. + + Returns: + str: Database connection URL + + Raises: + NotImplementedError: Must be implemented by subclasses + """ + raise NotImplementedError("Subclasses must implement get_connection_url()") From 0f53ccbe8b7872653f683c734096f23b3af4fec1 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:30:16 +0000 Subject: [PATCH 13/28] Improve Strategy --- modules/generic/testcontainers/generic/sql.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 039b51c53..e0c14102d 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -16,48 +16,49 @@ ADDITIONAL_TRANSIENT_ERRORS.append(DBAPIError) except ImportError: logger.debug("SQLAlchemy not available, skipping DBAPIError handling") +SQL_TRANSIENT_EXCEPTIONS = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) -class DatabaseConnectionWaitStrategy(WaitStrategy): +class ExceptionsWaitStrategy(WaitStrategy): """ - Wait strategy for database connection readiness using SqlContainer._connect(). + Generic wait strategy that retries a callable until it succeeds or times out. - This strategy implements retry logic and calls SqlContainer._connect() - repeatedly until it succeeds or times out. + This strategy can be used with any container method that needs retry logic + for handling transient errors. It calls the provided callable repeatedly + until it succeeds or the timeout is reached. """ - def __init__(self, sql_container: "SqlContainer"): + def __init__(self, callable_func: callable, transient_exceptions: Optional[tuple] = None): super().__init__() - self.sql_container = sql_container + self.callable_func = callable_func + self.transient_exceptions = transient_exceptions or (TimeoutError, ConnectionError) def wait_until_ready(self, container: WaitStrategyTarget) -> None: """ - Test database connectivity with retry logic by calling SqlContainer._connect(). + Execute the callable with retry logic until it succeeds or times out. Raises: - TimeoutError: If connection fails after timeout - Exception: Any non-transient errors from _connect() + TimeoutError: If callable fails after timeout + Exception: Any non-transient errors from the callable """ import time start_time = time.time() - transient_exceptions = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) - while True: if time.time() - start_time > self._startup_timeout: raise TimeoutError( - f"Database connection failed after {self._startup_timeout}s timeout. " - f"Hint: Check if the database container is ready and accessible." + f"Callable failed after {self._startup_timeout}s timeout. " + f"Hint: Check if the container is ready and the operation can succeed." ) try: - self.sql_container._connect() + self.callable_func() return - except transient_exceptions as e: - logger.debug(f"Database connection attempt failed: {e}, retrying in {self._poll_interval}s...") + except self.transient_exceptions as e: + logger.debug(f"Callable attempt failed: {e}, retrying in {self._poll_interval}s...") except Exception as e: - logger.error(f"Database connection test failed with non-transient error: {e}") + logger.error(f"Callable failed with non-transient error: {e}") raise time.sleep(self._poll_interval) @@ -69,7 +70,7 @@ class SqlContainer(DockerContainer): This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. - Database connection readiness is automatically handled by DatabaseConnectionWaitStrategy. + Database connection readiness is automatically handled by ExceptionsWaitStrategy. """ def _connect(self) -> None: @@ -183,8 +184,7 @@ def start(self) -> "SqlContainer": try: self._configure() - # Set up database connection wait strategy before starting - self.waiting_for(DatabaseConnectionWaitStrategy(self)) + self.waiting_for(ExceptionsWaitStrategy(self._connect, SQL_TRANSIENT_EXCEPTIONS)) super().start() self._transfer_seed() logger.info("Database container started successfully") From 29e4eceede074061af4e4c0430013d767e672f76 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:38:14 +0000 Subject: [PATCH 14/28] Better Strategy --- modules/generic/testcontainers/generic/sql.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index e0c14102d..416c7d22b 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -19,46 +19,52 @@ SQL_TRANSIENT_EXCEPTIONS = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) -class ExceptionsWaitStrategy(WaitStrategy): +class ConnectWaitStrategy(WaitStrategy): """ - Generic wait strategy that retries a callable until it succeeds or times out. + Wait strategy that retries a container's _connect method until it succeeds or times out. - This strategy can be used with any container method that needs retry logic - for handling transient errors. It calls the provided callable repeatedly - until it succeeds or the timeout is reached. + This strategy assumes the container has a _connect method and will call it repeatedly + until it succeeds or the timeout is reached. It handles transient connection errors + and provides appropriate retry logic for database connectivity testing. """ - def __init__(self, callable_func: callable, transient_exceptions: Optional[tuple] = None): + def __init__(self, transient_exceptions: Optional[tuple] = None): super().__init__() - self.callable_func = callable_func self.transient_exceptions = transient_exceptions or (TimeoutError, ConnectionError) def wait_until_ready(self, container: WaitStrategyTarget) -> None: """ - Execute the callable with retry logic until it succeeds or times out. + Execute the container's _connect method with retry logic until it succeeds or times out. + + Args: + container: The container that must have a _connect method Raises: - TimeoutError: If callable fails after timeout - Exception: Any non-transient errors from the callable + TimeoutError: If _connect fails after timeout + AttributeError: If container doesn't have _connect method + Exception: Any non-transient errors from _connect """ import time + if not hasattr(container, "_connect"): + raise AttributeError(f"Container {container} must have a _connect method") + start_time = time.time() while True: if time.time() - start_time > self._startup_timeout: raise TimeoutError( - f"Callable failed after {self._startup_timeout}s timeout. " - f"Hint: Check if the container is ready and the operation can succeed." + f"Container _connect failed after {self._startup_timeout}s timeout. " + f"Hint: Check if the container is ready and the database is accessible." ) try: - self.callable_func() + container._connect() return except self.transient_exceptions as e: - logger.debug(f"Callable attempt failed: {e}, retrying in {self._poll_interval}s...") + logger.debug(f"Connection attempt failed: {e}, retrying in {self._poll_interval}s...") except Exception as e: - logger.error(f"Callable failed with non-transient error: {e}") + logger.error(f"Connection failed with non-transient error: {e}") raise time.sleep(self._poll_interval) @@ -70,7 +76,7 @@ class SqlContainer(DockerContainer): This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. - Database connection readiness is automatically handled by ExceptionsWaitStrategy. + Database connection readiness is automatically handled by ConnectWaitStrategy. """ def _connect(self) -> None: @@ -78,7 +84,7 @@ def _connect(self) -> None: Test database connectivity using SQLAlchemy. This method performs a single connection test without retry logic. - Retry logic is handled by the DatabaseConnectionWaitStrategy. + Retry logic is handled by the ConnectWaitStrategy. Raises: ImportError: If SQLAlchemy is not installed @@ -184,7 +190,7 @@ def start(self) -> "SqlContainer": try: self._configure() - self.waiting_for(ExceptionsWaitStrategy(self._connect, SQL_TRANSIENT_EXCEPTIONS)) + self.waiting_for(ConnectWaitStrategy(SQL_TRANSIENT_EXCEPTIONS)) super().start() self._transfer_seed() logger.info("Database container started successfully") From 03dd5e0172b6bb5941bf71029cb8e694b7170d1b Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 14:36:26 +0000 Subject: [PATCH 15/28] Remove _connect --- modules/generic/testcontainers/generic/sql.py | 75 ++++++++----------- 1 file changed, 32 insertions(+), 43 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 416c7d22b..602919d5d 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -21,11 +21,11 @@ class ConnectWaitStrategy(WaitStrategy): """ - Wait strategy that retries a container's _connect method until it succeeds or times out. + Wait strategy that tests database connectivity until it succeeds or times out. - This strategy assumes the container has a _connect method and will call it repeatedly - until it succeeds or the timeout is reached. It handles transient connection errors - and provides appropriate retry logic for database connectivity testing. + This strategy performs database connection testing using SQLAlchemy directly, + handling transient connection errors and providing appropriate retry logic + for database connectivity testing. """ def __init__(self, transient_exceptions: Optional[tuple] = None): @@ -34,33 +34,51 @@ def __init__(self, transient_exceptions: Optional[tuple] = None): def wait_until_ready(self, container: WaitStrategyTarget) -> None: """ - Execute the container's _connect method with retry logic until it succeeds or times out. + Test database connectivity with retry logic until it succeeds or times out. Args: - container: The container that must have a _connect method + container: The SQL container that must have get_connection_url method Raises: - TimeoutError: If _connect fails after timeout - AttributeError: If container doesn't have _connect method - Exception: Any non-transient errors from _connect + TimeoutError: If connection fails after timeout + AttributeError: If container doesn't have get_connection_url method + ImportError: If SQLAlchemy is not installed + Exception: Any non-transient errors from connection attempts """ import time - if not hasattr(container, "_connect"): - raise AttributeError(f"Container {container} must have a _connect method") + if not hasattr(container, "get_connection_url"): + raise AttributeError(f"Container {container} must have a get_connection_url method") + + try: + import sqlalchemy + except ImportError as e: + logger.error("SQLAlchemy is required for database connectivity testing") + raise ImportError("SQLAlchemy is required for database containers") from e start_time = time.time() while True: if time.time() - start_time > self._startup_timeout: raise TimeoutError( - f"Container _connect failed after {self._startup_timeout}s timeout. " + f"Database connection failed after {self._startup_timeout}s timeout. " f"Hint: Check if the container is ready and the database is accessible." ) try: - container._connect() - return + connection_url = container.get_connection_url() + engine = sqlalchemy.create_engine(connection_url) + + try: + with engine.connect(): + logger.info("Database connection test successful") + return + except Exception as e: + logger.debug(f"Database connection attempt failed: {e}") + raise + finally: + engine.dispose() + except self.transient_exceptions as e: logger.debug(f"Connection attempt failed: {e}, retrying in {self._poll_interval}s...") except Exception as e: @@ -79,35 +97,6 @@ class SqlContainer(DockerContainer): Database connection readiness is automatically handled by ConnectWaitStrategy. """ - def _connect(self) -> None: - """ - Test database connectivity using SQLAlchemy. - - This method performs a single connection test without retry logic. - Retry logic is handled by the ConnectWaitStrategy. - - Raises: - ImportError: If SQLAlchemy is not installed - Exception: If connection fails - """ - try: - import sqlalchemy - except ImportError as e: - logger.error("SQLAlchemy is required for database connectivity testing") - raise ImportError("SQLAlchemy is required for database containers") from e - - connection_url = self.get_connection_url() - engine = sqlalchemy.create_engine(connection_url) - - try: - with engine.connect(): - logger.info("Database connection test successful") - except Exception as e: - logger.debug(f"Database connection attempt failed: {e}") - raise - finally: - engine.dispose() - def _create_connection_url( self, dialect: str, From 1da1724a98818913e269bd910143ed9ad44a3134 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 15:15:54 +0000 Subject: [PATCH 16/28] Refactor generic sql --- modules/generic/testcontainers/generic/sql.py | 86 +------------------ .../testcontainers/generic/sql_utils.py | 82 ++++++++++++++++++ modules/generic/tests/test_sql.py | 2 +- 3 files changed, 86 insertions(+), 84 deletions(-) create mode 100644 modules/generic/testcontainers/generic/sql_utils.py diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 602919d5d..450aa767f 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -4,88 +4,10 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.exceptions import ContainerStartException -from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget -logger = logging.getLogger(__name__) - -ADDITIONAL_TRANSIENT_ERRORS = [] -try: - from sqlalchemy.exc import DBAPIError - - ADDITIONAL_TRANSIENT_ERRORS.append(DBAPIError) -except ImportError: - logger.debug("SQLAlchemy not available, skipping DBAPIError handling") -SQL_TRANSIENT_EXCEPTIONS = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) - - -class ConnectWaitStrategy(WaitStrategy): - """ - Wait strategy that tests database connectivity until it succeeds or times out. - - This strategy performs database connection testing using SQLAlchemy directly, - handling transient connection errors and providing appropriate retry logic - for database connectivity testing. - """ +from .sql_utils import SqlConnectWaitStrategy - def __init__(self, transient_exceptions: Optional[tuple] = None): - super().__init__() - self.transient_exceptions = transient_exceptions or (TimeoutError, ConnectionError) - - def wait_until_ready(self, container: WaitStrategyTarget) -> None: - """ - Test database connectivity with retry logic until it succeeds or times out. - - Args: - container: The SQL container that must have get_connection_url method - - Raises: - TimeoutError: If connection fails after timeout - AttributeError: If container doesn't have get_connection_url method - ImportError: If SQLAlchemy is not installed - Exception: Any non-transient errors from connection attempts - """ - import time - - if not hasattr(container, "get_connection_url"): - raise AttributeError(f"Container {container} must have a get_connection_url method") - - try: - import sqlalchemy - except ImportError as e: - logger.error("SQLAlchemy is required for database connectivity testing") - raise ImportError("SQLAlchemy is required for database containers") from e - - start_time = time.time() - - while True: - if time.time() - start_time > self._startup_timeout: - raise TimeoutError( - f"Database connection failed after {self._startup_timeout}s timeout. " - f"Hint: Check if the container is ready and the database is accessible." - ) - - try: - connection_url = container.get_connection_url() - engine = sqlalchemy.create_engine(connection_url) - - try: - with engine.connect(): - logger.info("Database connection test successful") - return - except Exception as e: - logger.debug(f"Database connection attempt failed: {e}") - raise - finally: - engine.dispose() - - except self.transient_exceptions as e: - logger.debug(f"Connection attempt failed: {e}, retrying in {self._poll_interval}s...") - except Exception as e: - logger.error(f"Connection failed with non-transient error: {e}") - raise - - time.sleep(self._poll_interval) +logger = logging.getLogger(__name__) class SqlContainer(DockerContainer): @@ -128,8 +50,6 @@ def _create_connection_url( ValueError: If unexpected arguments are provided or required parameters are missing ContainerStartException: If container is not started """ - if raise_for_deprecated_parameter(kwargs, "db_name", "dbname"): - raise ValueError(f"Unexpected arguments: {','.join(kwargs)}") if self._container is None: raise ContainerStartException("Container has not been started") @@ -179,7 +99,7 @@ def start(self) -> "SqlContainer": try: self._configure() - self.waiting_for(ConnectWaitStrategy(SQL_TRANSIENT_EXCEPTIONS)) + self.waiting_for(SqlConnectWaitStrategy()) super().start() self._transfer_seed() logger.info("Database container started successfully") diff --git a/modules/generic/testcontainers/generic/sql_utils.py b/modules/generic/testcontainers/generic/sql_utils.py new file mode 100644 index 000000000..6ef98e2ab --- /dev/null +++ b/modules/generic/testcontainers/generic/sql_utils.py @@ -0,0 +1,82 @@ +import logging + +from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget + +logger = logging.getLogger(__name__) + +ADDITIONAL_TRANSIENT_ERRORS = [] +try: + from sqlalchemy.exc import DBAPIError + + ADDITIONAL_TRANSIENT_ERRORS.append(DBAPIError) +except ImportError: + logger.debug("SQLAlchemy not available, skipping DBAPIError handling") + + +class SqlConnectWaitStrategy(WaitStrategy): + """ + Wait strategy that tests database connectivity until it succeeds or times out. + + This strategy performs database connection testing using SQLAlchemy directly, + handling transient connection errors and providing appropriate retry logic + for database connectivity testing. + """ + + def __init__(self): + super().__init__() + self.transient_exceptions = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) + + def wait_until_ready(self, container: WaitStrategyTarget) -> None: + """ + Test database connectivity with retry logic until it succeeds or times out. + + Args: + container: The SQL container that must have get_connection_url method + + Raises: + TimeoutError: If connection fails after timeout + AttributeError: If container doesn't have get_connection_url method + ImportError: If SQLAlchemy is not installed + Exception: Any non-transient errors from connection attempts + """ + import time + + if not hasattr(container, "get_connection_url"): + raise AttributeError(f"Container {container} must have a get_connection_url method") + + try: + import sqlalchemy + except ImportError as e: + logger.error("SQLAlchemy is required for database connectivity testing") + raise ImportError("SQLAlchemy is required for database containers") from e + + start_time = time.time() + + while True: + if time.time() - start_time > self._startup_timeout: + raise TimeoutError( + f"Database connection failed after {self._startup_timeout}s timeout. " + f"Hint: Check if the container is ready and the database is accessible." + ) + + try: + connection_url = container.get_connection_url() + engine = sqlalchemy.create_engine(connection_url) + + try: + with engine.connect(): + logger.info("Database connection test successful") + return + except Exception as e: + logger.debug(f"Database connection attempt failed: {e}") + raise + finally: + engine.dispose() + + except self.transient_exceptions as e: + logger.debug(f"Connection attempt failed: {e}, retrying in {self._poll_interval}s...") + except Exception as e: + logger.error(f"Connection failed with non-transient error: {e}") + raise + + time.sleep(self._poll_interval) diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index 209d77b2b..8b36a5b30 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -151,7 +151,7 @@ def test_container_inheritance(self): assert hasattr(container, "start") def test_additional_transient_errors_list(self): - from testcontainers.generic.sql import ADDITIONAL_TRANSIENT_ERRORS + from testcontainers.generic.sql_utils import ADDITIONAL_TRANSIENT_ERRORS assert isinstance(ADDITIONAL_TRANSIENT_ERRORS, list) # List may be empty if SQLAlchemy not available, or contain DBAPIError if it is From fe4604b5dbf88c15161f2016dbd3597677dd08c8 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 09:48:11 +0000 Subject: [PATCH 17/28] SQL container with configurable wait strategy --- modules/generic/testcontainers/generic/sql.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 450aa767f..e24b8d617 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -4,6 +4,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.exceptions import ContainerStartException +from testcontainers.core.waiting_utils import WaitStrategy from .sql_utils import SqlConnectWaitStrategy @@ -16,9 +17,21 @@ class SqlContainer(DockerContainer): This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. - Database connection readiness is automatically handled by ConnectWaitStrategy. + Database connection readiness is automatically handled by the provided wait strategy. """ + def __init__(self, image: str, wait_strategy: Optional[WaitStrategy] = None, **kwargs): + """ + Initialize SqlContainer with optional wait strategy. + + Args: + image: Docker image name + wait_strategy: Wait strategy for SQL database connectivity (defaults to SqlConnectWaitStrategy) + **kwargs: Additional arguments passed to DockerContainer + """ + super().__init__(image, **kwargs) + self.wait_strategy = wait_strategy or SqlConnectWaitStrategy() + def _create_connection_url( self, dialect: str, @@ -99,7 +112,7 @@ def start(self) -> "SqlContainer": try: self._configure() - self.waiting_for(SqlConnectWaitStrategy()) + self.waiting_for(self.wait_strategy) super().start() self._transfer_seed() logger.info("Database container started successfully") From a99045aaa1f57d1b71b4c6499d3b5c01047f2bf5 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 10:18:17 +0000 Subject: [PATCH 18/28] use WaitStrategy._poll and with_transient_exceptions --- .../testcontainers/generic/sql_utils.py | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql_utils.py b/modules/generic/testcontainers/generic/sql_utils.py index 6ef98e2ab..31d271636 100644 --- a/modules/generic/testcontainers/generic/sql_utils.py +++ b/modules/generic/testcontainers/generic/sql_utils.py @@ -14,69 +14,32 @@ class SqlConnectWaitStrategy(WaitStrategy): - """ - Wait strategy that tests database connectivity until it succeeds or times out. - - This strategy performs database connection testing using SQLAlchemy directly, - handling transient connection errors and providing appropriate retry logic - for database connectivity testing. - """ + """Wait strategy for database connectivity testing using SQLAlchemy.""" def __init__(self): super().__init__() - self.transient_exceptions = (TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) + self.with_transient_exceptions(TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) def wait_until_ready(self, container: WaitStrategyTarget) -> None: - """ - Test database connectivity with retry logic until it succeeds or times out. - - Args: - container: The SQL container that must have get_connection_url method - - Raises: - TimeoutError: If connection fails after timeout - AttributeError: If container doesn't have get_connection_url method - ImportError: If SQLAlchemy is not installed - Exception: Any non-transient errors from connection attempts - """ - import time - + """Test database connectivity with retry logic until success or timeout.""" if not hasattr(container, "get_connection_url"): raise AttributeError(f"Container {container} must have a get_connection_url method") try: import sqlalchemy except ImportError as e: - logger.error("SQLAlchemy is required for database connectivity testing") raise ImportError("SQLAlchemy is required for database containers") from e - start_time = time.time() - - while True: - if time.time() - start_time > self._startup_timeout: - raise TimeoutError( - f"Database connection failed after {self._startup_timeout}s timeout. " - f"Hint: Check if the container is ready and the database is accessible." - ) - + def _test_connection() -> bool: + """Test database connection, returning True if successful.""" + engine = sqlalchemy.create_engine(container.get_connection_url()) try: - connection_url = container.get_connection_url() - engine = sqlalchemy.create_engine(connection_url) - - try: - with engine.connect(): - logger.info("Database connection test successful") - return - except Exception as e: - logger.debug(f"Database connection attempt failed: {e}") - raise - finally: - engine.dispose() - - except self.transient_exceptions as e: - logger.debug(f"Connection attempt failed: {e}, retrying in {self._poll_interval}s...") - except Exception as e: - logger.error(f"Connection failed with non-transient error: {e}") - raise - - time.sleep(self._poll_interval) + with engine.connect(): + logger.info("Database connection successful") + return True + finally: + engine.dispose() + + result = self._poll(_test_connection) + if not result: + raise TimeoutError(f"Database connection failed after {self._startup_timeout}s timeout") From ad1575aac2b4c16058f2ad2829106c7b1ab0bba3 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:18:01 +0000 Subject: [PATCH 19/28] Required wait_strategy --- modules/generic/testcontainers/generic/sql.py | 8 +++----- modules/generic/tests/test_sql.py | 8 +++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index e24b8d617..8ad927012 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -6,8 +6,6 @@ from testcontainers.core.exceptions import ContainerStartException from testcontainers.core.waiting_utils import WaitStrategy -from .sql_utils import SqlConnectWaitStrategy - logger = logging.getLogger(__name__) @@ -20,17 +18,17 @@ class SqlContainer(DockerContainer): Database connection readiness is automatically handled by the provided wait strategy. """ - def __init__(self, image: str, wait_strategy: Optional[WaitStrategy] = None, **kwargs): + def __init__(self, image: str, wait_strategy: WaitStrategy, **kwargs): """ Initialize SqlContainer with optional wait strategy. Args: image: Docker image name - wait_strategy: Wait strategy for SQL database connectivity (defaults to SqlConnectWaitStrategy) + wait_strategy: Wait strategy for SQL database connectivity **kwargs: Additional arguments passed to DockerContainer """ super().__init__(image, **kwargs) - self.wait_strategy = wait_strategy or SqlConnectWaitStrategy() + self.wait_strategy = wait_strategy def _create_connection_url( self, diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index 8b36a5b30..2a29982f2 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -1,5 +1,7 @@ import pytest + from testcontainers.core.exceptions import ContainerStartException +from testcontainers.generic.sql_utils import SqlConnectWaitStrategy from testcontainers.generic.sql import SqlContainer @@ -7,7 +9,7 @@ class SimpleSqlContainer(SqlContainer): """Simple concrete implementation for testing.""" def __init__(self, image: str = "postgres:13"): - super().__init__(image) + super().__init__(image, wait_strategy=SqlConnectWaitStrategy()) self.username = "testuser" self.password = "testpass" self.dbname = "testdb" @@ -27,7 +29,7 @@ def _configure(self) -> None: class TestSqlContainer: def test_abstract_methods_raise_not_implemented(self): - container = SqlContainer("test:latest") + container = SqlContainer("test:latest", SqlConnectWaitStrategy()) with pytest.raises(NotImplementedError): container.get_connection_url() @@ -36,7 +38,7 @@ def test_abstract_methods_raise_not_implemented(self): container._configure() def test_transfer_seed_default_behavior(self): - container = SqlContainer("test:latest") + container = SqlContainer("test:latest", SqlConnectWaitStrategy()) # Should not raise an exception container._transfer_seed() From 0cce48b0bffd267ced1ec015a2a532b6e2ae4c3b Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:27:06 +0000 Subject: [PATCH 20/28] Added note --- modules/generic/testcontainers/generic/sql.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 8ad927012..aeea3d276 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -16,6 +16,8 @@ class SqlContainer(DockerContainer): This class can serve as a base for database-specific container implementations. It provides connection management, URL construction, and basic lifecycle methods. Database connection readiness is automatically handled by the provided wait strategy. + + Note: `SqlConnectWaitStrategy` from `sql_utils` is a provided wait strategy for SQL databases. """ def __init__(self, image: str, wait_strategy: WaitStrategy, **kwargs): From 00e9eaf2798accce57c3397af38a9c1f998a39cb Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:44:48 +0000 Subject: [PATCH 21/28] Move connector and Rename --- modules/generic/testcontainers/generic/providers/__init__.py | 1 + .../generic/{sql_utils.py => providers/sql_connector.py} | 3 +++ modules/generic/tests/test_sql.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 modules/generic/testcontainers/generic/providers/__init__.py rename modules/generic/testcontainers/generic/{sql_utils.py => providers/sql_connector.py} (90%) diff --git a/modules/generic/testcontainers/generic/providers/__init__.py b/modules/generic/testcontainers/generic/providers/__init__.py new file mode 100644 index 000000000..cddc706bb --- /dev/null +++ b/modules/generic/testcontainers/generic/providers/__init__.py @@ -0,0 +1 @@ +from .sql_connector import SqlConnectWaitStrategy # noqa: F401 diff --git a/modules/generic/testcontainers/generic/sql_utils.py b/modules/generic/testcontainers/generic/providers/sql_connector.py similarity index 90% rename from modules/generic/testcontainers/generic/sql_utils.py rename to modules/generic/testcontainers/generic/providers/sql_connector.py index 31d271636..e548e1639 100644 --- a/modules/generic/testcontainers/generic/sql_utils.py +++ b/modules/generic/testcontainers/generic/providers/sql_connector.py @@ -1,3 +1,6 @@ +# This module provides a wait strategy for SQL database connectivity testing using SQLAlchemy. +# It includes handling for transient exceptions and connection retries. + import logging from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index 2a29982f2..afb87cf04 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -1,8 +1,8 @@ import pytest from testcontainers.core.exceptions import ContainerStartException -from testcontainers.generic.sql_utils import SqlConnectWaitStrategy from testcontainers.generic.sql import SqlContainer +from testcontainers.generic.providers.sql_connector import SqlConnectWaitStrategy class SimpleSqlContainer(SqlContainer): @@ -153,7 +153,7 @@ def test_container_inheritance(self): assert hasattr(container, "start") def test_additional_transient_errors_list(self): - from testcontainers.generic.sql_utils import ADDITIONAL_TRANSIENT_ERRORS + from testcontainers.generic.providers.sql_connector import ADDITIONAL_TRANSIENT_ERRORS assert isinstance(ADDITIONAL_TRANSIENT_ERRORS, list) # List may be empty if SQLAlchemy not available, or contain DBAPIError if it is From 336cb55662ff8dada9167193a74b918228875760 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:45:03 +0000 Subject: [PATCH 22/28] Update doctests --- modules/generic/README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index 90514bb09..79e1243ac 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -59,13 +59,14 @@ Postgres container that is using :code:`SqlContainer` .. doctest:: >>> from testcontainers.generic import SqlContainer + >>> from testcontainers.generic.providers.sql_connector import SqlConnectWaitStrategy >>> from sqlalchemy import text >>> import sqlalchemy >>> class CustomPostgresContainer(SqlContainer): ... def __init__(self, image="postgres:15-alpine", ... port=5432, username="test", password="test", dbname="test"): - ... super().__init__(image=image) + ... super().__init__(image=image, wait_strategy=SqlConnectWaitStrategy()) ... self.port_to_expose = port ... self.username = username ... self.password = password From ee5ab80d299100daf965e451be20b180bcf44078 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:15:31 +0000 Subject: [PATCH 23/28] Fix doctest --- modules/generic/README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index 79e1243ac..f62453102 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -9,6 +9,7 @@ FastAPI container that is using :code:`ServerContainer` >>> from testcontainers.generic import ServerContainer >>> from testcontainers.core.waiting_utils import wait_for_logs + >>> from testcontainers.core.image import DockerImage >>> with DockerImage(path="./modules/generic/tests/samples/fastapi", tag="fastapi-test:latest") as image: ... with ServerContainer(port=80, image=image) as fastapi_server: From 6dbb3307154d55508e54b2b7f2bc011015a81577 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:16:02 +0000 Subject: [PATCH 24/28] Remove extra validation + Improve testing --- modules/generic/testcontainers/generic/sql.py | 14 ----------- modules/generic/tests/test_sql.py | 24 +++++++++---------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index aeea3d276..3a69ce3be 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -67,30 +67,16 @@ def _create_connection_url( if self._container is None: raise ContainerStartException("Container has not been started") - # Validate required parameters - if not dialect: - raise ValueError("Database dialect is required") - if not username: - raise ValueError("Database username is required") - if port is None: - raise ValueError("Database port is required") - host = host or self.get_container_host_ip() exposed_port = self.get_exposed_port(port) - - # Safely quote password to handle special characters quoted_password = quote(password, safe="") quoted_username = quote(username, safe="") - - # Build base URL url = f"{dialect}://{quoted_username}:{quoted_password}@{host}:{exposed_port}" - # Add database name if provided if dbname: quoted_dbname = quote(dbname, safe="") url = f"{url}/{quoted_dbname}" - # Add query parameters if provided if query_params: query_string = urlencode(query_params) url = f"{url}?{query_string}" diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index afb87cf04..2248bf240 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import patch from testcontainers.core.exceptions import ContainerStartException from testcontainers.generic.sql import SqlContainer @@ -96,21 +97,20 @@ def test_connection_url_with_query_params(self): assert "ssl=require" in url assert "timeout=30" in url - def test_connection_url_validation_errors(self): + def test_connection_url_type_errors(self): + """Test that _create_connection_url raises TypeError with invalid types""" container = SimpleSqlContainer() - container._container = type("MockContainer", (), {})() - - # Test missing dialect - with pytest.raises(ValueError, match="Database dialect is required"): - container._create_connection_url("", "user", "pass", port=5432) + container._container = type("MockContainer", (), {"id": "test-id"})() - # Test missing username - with pytest.raises(ValueError, match="Database username is required"): - container._create_connection_url("postgresql", "", "pass", port=5432) + # Mock get_exposed_port to simulate what happens with None port + with patch.object(container, "get_exposed_port") as mock_get_port: + # Simulate the TypeError that would occur when int(None) is called + mock_get_port.side_effect = TypeError( + "int() argument must be a string, a bytes-like object or a real number, not 'NoneType'" + ) - # Test missing port - with pytest.raises(ValueError, match="Database port is required"): - container._create_connection_url("postgresql", "user", "pass", port=None) + with pytest.raises(TypeError, match="int\\(\\) argument must be a string"): + container._create_connection_url("postgresql", "user", "pass", port=None) def test_connection_url_container_not_started(self): container = SimpleSqlContainer() From bf6a553a6d7435a1f09d61772700f47b6ff47357 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:33:39 +0000 Subject: [PATCH 25/28] Omit generic.py from report --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4e2c434c6..67e2e0880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -231,6 +231,9 @@ exclude_lines = [ "pass", "raise NotImplementedError", # TODO: used in core/generic.py, not sure we need DbContainer ] +omit = [ + "core/testcontainers/core/generic.py", # Marked for deprecation +] [tool.ruff] target-version = "py39" From 6694e44572c4f3dd1f3a316d50a58f87af4746f3 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Wed, 8 Oct 2025 09:21:40 +0000 Subject: [PATCH 26/28] Renamed sql_connector --- modules/generic/README.rst | 2 +- modules/generic/testcontainers/generic/providers/__init__.py | 2 +- .../{sql_connector.py => sql_connection_wait_strategy.py} | 0 modules/generic/testcontainers/generic/sql.py | 2 +- modules/generic/tests/test_sql.py | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename modules/generic/testcontainers/generic/providers/{sql_connector.py => sql_connection_wait_strategy.py} (100%) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index f62453102..f2ae628d1 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -60,7 +60,7 @@ Postgres container that is using :code:`SqlContainer` .. doctest:: >>> from testcontainers.generic import SqlContainer - >>> from testcontainers.generic.providers.sql_connector import SqlConnectWaitStrategy + >>> from testcontainers.generic.providers.sql_connection_wait_strategy import SqlConnectWaitStrategy >>> from sqlalchemy import text >>> import sqlalchemy diff --git a/modules/generic/testcontainers/generic/providers/__init__.py b/modules/generic/testcontainers/generic/providers/__init__.py index cddc706bb..79e84487f 100644 --- a/modules/generic/testcontainers/generic/providers/__init__.py +++ b/modules/generic/testcontainers/generic/providers/__init__.py @@ -1 +1 @@ -from .sql_connector import SqlConnectWaitStrategy # noqa: F401 +from .sql_connection_wait_strategy import SqlConnectWaitStrategy # noqa: F401 diff --git a/modules/generic/testcontainers/generic/providers/sql_connector.py b/modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py similarity index 100% rename from modules/generic/testcontainers/generic/providers/sql_connector.py rename to modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 3a69ce3be..9c861c198 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -17,7 +17,7 @@ class SqlContainer(DockerContainer): It provides connection management, URL construction, and basic lifecycle methods. Database connection readiness is automatically handled by the provided wait strategy. - Note: `SqlConnectWaitStrategy` from `sql_utils` is a provided wait strategy for SQL databases. + Note: `SqlConnectWaitStrategy` from `sql_connection_wait_strategy` is a provided wait strategy for SQL databases. """ def __init__(self, image: str, wait_strategy: WaitStrategy, **kwargs): diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index 2248bf240..4abbab9f3 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -3,7 +3,7 @@ from testcontainers.core.exceptions import ContainerStartException from testcontainers.generic.sql import SqlContainer -from testcontainers.generic.providers.sql_connector import SqlConnectWaitStrategy +from testcontainers.generic.providers.sql_connection_wait_strategy import SqlConnectWaitStrategy class SimpleSqlContainer(SqlContainer): @@ -153,7 +153,7 @@ def test_container_inheritance(self): assert hasattr(container, "start") def test_additional_transient_errors_list(self): - from testcontainers.generic.providers.sql_connector import ADDITIONAL_TRANSIENT_ERRORS + from testcontainers.generic.providers.sql_connection_wait_strategy import ADDITIONAL_TRANSIENT_ERRORS assert isinstance(ADDITIONAL_TRANSIENT_ERRORS, list) # List may be empty if SQLAlchemy not available, or contain DBAPIError if it is From 4c7a67fa28d1676bc64ef29446af1631a3aeb711 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Wed, 8 Oct 2025 09:28:42 +0000 Subject: [PATCH 27/28] Renamed SqlConnectWaitStrategy --- modules/generic/README.rst | 4 ++-- .../generic/testcontainers/generic/providers/__init__.py | 2 +- .../generic/providers/sql_connection_wait_strategy.py | 2 +- modules/generic/testcontainers/generic/sql.py | 2 +- modules/generic/tests/test_sql.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/generic/README.rst b/modules/generic/README.rst index f2ae628d1..4b7281121 100644 --- a/modules/generic/README.rst +++ b/modules/generic/README.rst @@ -60,14 +60,14 @@ Postgres container that is using :code:`SqlContainer` .. doctest:: >>> from testcontainers.generic import SqlContainer - >>> from testcontainers.generic.providers.sql_connection_wait_strategy import SqlConnectWaitStrategy + >>> from testcontainers.generic.providers.sql_connection_wait_strategy import SqlAlchemyConnectWaitStrategy >>> from sqlalchemy import text >>> import sqlalchemy >>> class CustomPostgresContainer(SqlContainer): ... def __init__(self, image="postgres:15-alpine", ... port=5432, username="test", password="test", dbname="test"): - ... super().__init__(image=image, wait_strategy=SqlConnectWaitStrategy()) + ... super().__init__(image=image, wait_strategy=SqlAlchemyConnectWaitStrategy()) ... self.port_to_expose = port ... self.username = username ... self.password = password diff --git a/modules/generic/testcontainers/generic/providers/__init__.py b/modules/generic/testcontainers/generic/providers/__init__.py index 79e84487f..5b5eb95a2 100644 --- a/modules/generic/testcontainers/generic/providers/__init__.py +++ b/modules/generic/testcontainers/generic/providers/__init__.py @@ -1 +1 @@ -from .sql_connection_wait_strategy import SqlConnectWaitStrategy # noqa: F401 +from .sql_connection_wait_strategy import SqlAlchemyConnectWaitStrategy # noqa: F401 diff --git a/modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py b/modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py index e548e1639..bad46c743 100644 --- a/modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py +++ b/modules/generic/testcontainers/generic/providers/sql_connection_wait_strategy.py @@ -16,7 +16,7 @@ logger.debug("SQLAlchemy not available, skipping DBAPIError handling") -class SqlConnectWaitStrategy(WaitStrategy): +class SqlAlchemyConnectWaitStrategy(WaitStrategy): """Wait strategy for database connectivity testing using SQLAlchemy.""" def __init__(self): diff --git a/modules/generic/testcontainers/generic/sql.py b/modules/generic/testcontainers/generic/sql.py index 9c861c198..c7ed755ed 100644 --- a/modules/generic/testcontainers/generic/sql.py +++ b/modules/generic/testcontainers/generic/sql.py @@ -17,7 +17,7 @@ class SqlContainer(DockerContainer): It provides connection management, URL construction, and basic lifecycle methods. Database connection readiness is automatically handled by the provided wait strategy. - Note: `SqlConnectWaitStrategy` from `sql_connection_wait_strategy` is a provided wait strategy for SQL databases. + Note: `SqlAlchemyConnectWaitStrategy` from `sql_connection_wait_strategy` is a provided wait strategy for SQL databases. """ def __init__(self, image: str, wait_strategy: WaitStrategy, **kwargs): diff --git a/modules/generic/tests/test_sql.py b/modules/generic/tests/test_sql.py index 4abbab9f3..69fff2427 100644 --- a/modules/generic/tests/test_sql.py +++ b/modules/generic/tests/test_sql.py @@ -3,14 +3,14 @@ from testcontainers.core.exceptions import ContainerStartException from testcontainers.generic.sql import SqlContainer -from testcontainers.generic.providers.sql_connection_wait_strategy import SqlConnectWaitStrategy +from testcontainers.generic.providers.sql_connection_wait_strategy import SqlAlchemyConnectWaitStrategy class SimpleSqlContainer(SqlContainer): """Simple concrete implementation for testing.""" def __init__(self, image: str = "postgres:13"): - super().__init__(image, wait_strategy=SqlConnectWaitStrategy()) + super().__init__(image, wait_strategy=SqlAlchemyConnectWaitStrategy()) self.username = "testuser" self.password = "testpass" self.dbname = "testdb" @@ -30,7 +30,7 @@ def _configure(self) -> None: class TestSqlContainer: def test_abstract_methods_raise_not_implemented(self): - container = SqlContainer("test:latest", SqlConnectWaitStrategy()) + container = SqlContainer("test:latest", SqlAlchemyConnectWaitStrategy()) with pytest.raises(NotImplementedError): container.get_connection_url() @@ -39,7 +39,7 @@ def test_abstract_methods_raise_not_implemented(self): container._configure() def test_transfer_seed_default_behavior(self): - container = SqlContainer("test:latest", SqlConnectWaitStrategy()) + container = SqlContainer("test:latest", SqlAlchemyConnectWaitStrategy()) # Should not raise an exception container._transfer_seed() From aacc13a53df1f6a12f2bbdb4641284ba3fded170 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Tue, 28 Oct 2025 20:24:07 +0000 Subject: [PATCH 28/28] Update poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index adfc86fae..d152a3e5b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7070,4 +7070,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.1" python-versions = ">=3.9.2" -content-hash = "0ae5c80dd7afd87390cbf19eba012778f35bab72bac0e8377eb14b8a2a9b6c56" +content-hash = "c62edfc491a1ea69abf5fa69ee6946030a729c4dde986bfc346f10d15816fe18"