From 470baf7477ae41be8d096ba41306b20f1cca70c9 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:25:19 +0000 Subject: [PATCH 01/27] 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 41f3758c605c9d03335ab36f375616cf825fb767 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:45:55 +0000 Subject: [PATCH 02/27] 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 8ee2f1a9eafce671b80bbc7331fb350b2dc327b4 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 12:48:16 +0000 Subject: [PATCH 03/27] 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 190dbfb7d5b433c03b03c582e345ad82bd330f0c Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:17:43 +0000 Subject: [PATCH 04/27] 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 6326c476d054ecb2a895c6defbc0300feedc7c29 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:18:05 +0000 Subject: [PATCH 05/27] 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 989f6770578f6bbf5dfb8859892472d394a2b811 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:20:14 +0000 Subject: [PATCH 06/27] 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 591a4a8a8..6801f328e 100644 --- a/core/testcontainers/core/generic.py +++ b/core/testcontainers/core/generic.py @@ -29,6 +29,7 @@ class DbContainer(DockerContainer): """ **DEPRECATED (for removal)** + Please use database-specific container classes or `SqlContainer` instead. Generic database container. """ From 42f4aeacb4d6020ec45b6aac4aa61548ab17e52f Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:27:18 +0000 Subject: [PATCH 07/27] 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 791d6afe2918e7368c205b08f7989979cb65c70a Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:43:30 +0000 Subject: [PATCH 08/27] 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 21461e377dd3f0a70c5cacce0fa3e996892509a3 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 20:58:16 +0000 Subject: [PATCH 09/27] Add generic dep --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f6a53ed96..1440bf0d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ elasticsearch = [] generic = [ "httpx", "redis>=7", + "sqlalchemy", ] test_module_import = ["httpx"] google = [ From 7321fc290f5f7a5a3209604127ea11a3c5748e8a Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Thu, 2 Oct 2025 21:31:13 +0000 Subject: [PATCH 10/27] 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 6801f328e..1410321ee 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. + # from testcontainers.generic.sql import SqlContainer Generic database container. """ From 619940d90d71acb28ed44a21a9d453b8a843234f Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:18:57 +0000 Subject: [PATCH 11/27] 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 60dcf01e016b1a222447e96cec66dd89f4c63ef2 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:30:16 +0000 Subject: [PATCH 12/27] 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 abd05d0a2ba859eab743ac24c3e26d33c285b322 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 13:38:14 +0000 Subject: [PATCH 13/27] 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 56628aa81b7c0d1959f0e99b517048c23fe4c6ef Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 14:36:26 +0000 Subject: [PATCH 14/27] 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 51591ae7b927df3199bf91b84faa47247fa64d3f Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Fri, 3 Oct 2025 15:15:54 +0000 Subject: [PATCH 15/27] 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 cdbdabbcbb6f14d1686b044e45c64d8ffead7d83 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 09:48:11 +0000 Subject: [PATCH 16/27] 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 ef4d5717a60cb1a4e16971f8d70574d71d2947f9 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 10:18:17 +0000 Subject: [PATCH 17/27] 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 44a0343e6af89e67debeb1fe307c69ce5069aa1e Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:18:01 +0000 Subject: [PATCH 18/27] 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 de608a262fc9788e6ea8815cb6160dd17f68e60e Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:27:06 +0000 Subject: [PATCH 19/27] 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 0f0eb9efe7e1016323ef5d829dabe1cbe313d91a Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:44:48 +0000 Subject: [PATCH 20/27] 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 f9bf8e20bdbab508071be6c960efa2dd915d137f Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 4 Oct 2025 12:45:03 +0000 Subject: [PATCH 21/27] 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 988a6cae9d1f45ffeeccc89c90ba9767ad139b81 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:15:31 +0000 Subject: [PATCH 22/27] 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 7fa110585f83012e848370ed775455fc6cda9d1b Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:16:02 +0000 Subject: [PATCH 23/27] 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 f613a611e254655549a163854c339222f4226508 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 5 Oct 2025 14:33:39 +0000 Subject: [PATCH 24/27] Omit generic.py from report --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1440bf0d5..00690f1d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -304,6 +304,9 @@ source = [ "*/site-packages/testcontainers", "*/dist-packages/testcontainers", ] +omit = [ + "core/testcontainers/core/generic.py", # Marked for deprecation +] [tool.ruff] target-version = "py39" From 07c8781c1887b1c3328abaca84d443bac6413202 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Wed, 8 Oct 2025 09:21:40 +0000 Subject: [PATCH 25/27] 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 d59870ab56ef6c8873a4bbe83bef64eb4d2cda0b Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Wed, 8 Oct 2025 09:28:42 +0000 Subject: [PATCH 26/27] 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 d502fb6d231859b8a3e10cb516d398a99761f1b9 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sat, 28 Mar 2026 20:36:02 +0000 Subject: [PATCH 27/27] Revert "Omit generic.py from report" This reverts commit f613a611e254655549a163854c339222f4226508. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00690f1d0..1440bf0d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -304,9 +304,6 @@ source = [ "*/site-packages/testcontainers", "*/dist-packages/testcontainers", ] -omit = [ - "core/testcontainers/core/generic.py", # Marked for deprecation -] [tool.ruff] target-version = "py39"