-
Notifications
You must be signed in to change notification settings - Fork 339
feat(generic): Reintroducing the generic SQL module #892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f4acd6c
af01264
869a135
5c3ffbc
39cb0a1
5d15b6e
80a5355
04e418c
09d9cad
acec2c1
f0bf381
ec9d4e2
0f53ccb
29e4ece
03dd5e0
1da1724
fe4604b
ed1c991
a99045a
ad1575a
0cce48b
00e9eaf
336cb55
ee5ab80
6dbb330
bf6a553
6694e44
4c7a67f
af2efe3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .sql_connection_wait_strategy import SqlAlchemyConnectWaitStrategy # noqa: F401 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# This module provides a wait strategy for SQL database connectivity testing using SQLAlchemy. | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# It includes handling for transient exceptions and connection retries. | ||
|
||
import logging | ||
|
||
from testcontainers.core.waiting_utils import WaitStrategy, WaitStrategyTarget | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
ADDITIONAL_TRANSIENT_ERRORS = [] | ||
try: | ||
from sqlalchemy.exc import DBAPIError | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
ADDITIONAL_TRANSIENT_ERRORS.append(DBAPIError) | ||
except ImportError: | ||
logger.debug("SQLAlchemy not available, skipping DBAPIError handling") | ||
|
||
|
||
class SqlAlchemyConnectWaitStrategy(WaitStrategy): | ||
"""Wait strategy for database connectivity testing using SQLAlchemy.""" | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def __init__(self): | ||
super().__init__() | ||
self.with_transient_exceptions(TimeoutError, ConnectionError, *ADDITIONAL_TRANSIENT_ERRORS) | ||
|
||
def wait_until_ready(self, container: WaitStrategyTarget) -> None: | ||
"""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: | ||
raise ImportError("SQLAlchemy is required for database containers") from e | ||
|
||
def _test_connection() -> bool: | ||
"""Test database connection, returning True if successful.""" | ||
engine = sqlalchemy.create_engine(container.get_connection_url()) | ||
try: | ||
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") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
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.waiting_utils import WaitStrategy | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
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 the provided wait strategy. | ||
|
||
Note: `SqlAlchemyConnectWaitStrategy` from `sql_connection_wait_strategy` is a provided wait strategy for SQL databases. | ||
""" | ||
|
||
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 | ||
**kwargs: Additional arguments passed to DockerContainer | ||
""" | ||
super().__init__(image, **kwargs) | ||
self.wait_strategy = wait_strategy | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _create_connection_url( | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 self._container is None: | ||
raise ContainerStartException("Container has not been started") | ||
|
||
host = host or self.get_container_host_ip() | ||
exposed_port = self.get_exposed_port(port) | ||
quoted_password = quote(password, safe="") | ||
quoted_username = quote(username, safe="") | ||
url = f"{dialect}://{quoted_username}:{quoted_password}@{host}:{exposed_port}" | ||
|
||
if dbname: | ||
quoted_dbname = quote(dbname, safe="") | ||
url = f"{url}/{quoted_dbname}" | ||
|
||
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() | ||
self.waiting_for(self.wait_strategy) | ||
super().start() | ||
self._transfer_seed() | ||
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()") | ||
Tranquility2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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") | ||
|
||
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()") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this have a default implementation to just call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SimpleSqlContainer is just a test, are you sure this is the default for all SQL related implementations? |
Uh oh!
There was an error while loading. Please reload this page.