Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f4acd6c
Remove comment line
Tranquility2 Oct 2, 2025
af01264
Added base generic db
Tranquility2 Oct 2, 2025
869a135
Rename test_generic
Tranquility2 Oct 2, 2025
5c3ffbc
Renamed DB to SQL
Tranquility2 Oct 2, 2025
39cb0a1
Tests for SQL
Tranquility2 Oct 2, 2025
5d15b6e
Update warnning
Tranquility2 Oct 2, 2025
80a5355
update docs
Tranquility2 Oct 2, 2025
04e418c
Fix doctests
Tranquility2 Oct 2, 2025
09d9cad
Add generic dep
Tranquility2 Oct 2, 2025
acec2c1
Update ref
Tranquility2 Oct 2, 2025
f0bf381
Update lock
Tranquility2 Oct 2, 2025
ec9d4e2
Replaced wait
Tranquility2 Oct 3, 2025
0f53ccb
Improve Strategy
Tranquility2 Oct 3, 2025
29e4ece
Better Strategy
Tranquility2 Oct 3, 2025
03dd5e0
Remove _connect
Tranquility2 Oct 3, 2025
1da1724
Refactor generic sql
Tranquility2 Oct 3, 2025
fe4604b
SQL container with configurable wait strategy
Tranquility2 Oct 4, 2025
ed1c991
Merge branch 'main' into generic_sql
Tranquility2 Oct 4, 2025
a99045a
use WaitStrategy._poll and with_transient_exceptions
Tranquility2 Oct 4, 2025
ad1575a
Required wait_strategy
Tranquility2 Oct 4, 2025
0cce48b
Added note
Tranquility2 Oct 4, 2025
00e9eaf
Move connector and Rename
Tranquility2 Oct 4, 2025
336cb55
Update doctests
Tranquility2 Oct 4, 2025
ee5ab80
Fix doctest
Tranquility2 Oct 5, 2025
6dbb330
Remove extra validation + Improve testing
Tranquility2 Oct 5, 2025
bf6a553
Omit generic.py from report
Tranquility2 Oct 5, 2025
6694e44
Renamed sql_connector
Tranquility2 Oct 8, 2025
4c7a67f
Renamed SqlConnectWaitStrategy
Tranquility2 Oct 8, 2025
af2efe3
Merge branch 'main' into generic_sql
Tranquility2 Oct 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/testcontainers/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
class DbContainer(DockerContainer):
"""
**DEPRECATED (for removal)**
Please use database-specific container classes or `SqlContainer` instead.
# from testcontainers.generic.sql import SqlContainer

Generic database container.
"""
Expand Down
35 changes: 35 additions & 0 deletions modules/generic/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,38 @@ 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

Postgres container that is using :code:`SqlContainer`

.. doctest::

>>> from testcontainers.generic import SqlContainer
>>> 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)
... 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"))
... assert result.scalar() == 1
1 change: 1 addition & 0 deletions modules/generic/testcontainers/generic/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .server import ServerContainer # noqa: F401
from .sql import SqlContainer # noqa: F401
2 changes: 0 additions & 2 deletions modules/generic/testcontainers/generic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
176 changes: 176 additions & 0 deletions modules/generic/testcontainers/generic/sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 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.
"""

@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()

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}"

return url

def start(self) -> "SqlContainer":
"""
Start the database container and perform initialization.
Returns:
SqlContainer: 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")
Loading
Loading