Skip to content

Commit e3272c1

Browse files
logicbombjankatins
andcommitted
feat(postgres): Remove SqlAlchemy dependency from postgres container
Remove test that was testing sqlalchemy support for driver types Add tests for supported versions of Postgres Modify the `get_connection_url` convenience method to support a driverless url Co-authored-by: Jason Turim <[email protected]> Co-authored-by: Jan Katins <[email protected]>
1 parent ed3b9fa commit e3272c1

File tree

6 files changed

+75
-33
lines changed

6 files changed

+75
-33
lines changed

INDEX.rst

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,33 @@ Getting Started
4545
>>> from testcontainers.postgres import PostgresContainer
4646
>>> import sqlalchemy
4747

48-
>>> with PostgresContainer("postgres:9.5") as postgres:
49-
... engine = sqlalchemy.create_engine(postgres.get_connection_url())
48+
>>> with PostgresContainer("postgres:latest") as postgres:
49+
... psql_url = postgres.get_connection_url()
50+
... engine = sqlalchemy.create_engine(psql_url)
5051
... with engine.begin() as connection:
5152
... result = connection.execute(sqlalchemy.text("select version()"))
5253
... version, = result.fetchone()
5354
>>> version
54-
'PostgreSQL 9.5...'
55+
'PostgreSQL ...'
56+
57+
The snippet above will spin up the current latest version of a postgres database in a container. The :code:`get_connection_url()` convenience method returns a :code:`sqlalchemy` compatible url (using the :code:`psycopg2` driver per default) to connect to the database and retrieve the database version.
58+
59+
.. doctest::
60+
61+
>>> import asyncpg
62+
>>> from testcontainers.postgres import PostgresContainer
63+
64+
>>> with PostgresContainer("postgres:16", driver=None) as postgres:
65+
... psql_url = container.get_connection_url()
66+
... with asyncpg.create_pool(dsn=psql_url,server_settings={"jit": "off"}) as pool:
67+
... conn = await pool.acquire()
68+
... ret = await conn.fetchval("SELECT 1")
69+
... assert ret == 1
70+
71+
This snippet does the same, however using a specific version and the driver is set to None, to influence the :code:`get_connection_url()` convenience method to not include a driver in the URL (e.g. for compatibility with :code:`psycopg` v3).
72+
73+
Note, that the :code:`sqlalchemy` and :code:`psycopg2` packages are no longer a dependency of :code:`testcontainers[postgres]` and not needed to launch the Postgres container. Your project therefore needs to declare a dependency on the used driver and db access methods you use in your code.
5574

56-
The snippet above will spin up a postgres database in a container. The :code:`get_connection_url()` convenience method returns a :code:`sqlalchemy` compatible url we use to connect to the database and retrieve the database version.
5775

5876
Installation
5977
------------

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ For more information, see [the docs][readthedocs].
1212
>>> from testcontainers.postgres import PostgresContainer
1313
>>> import sqlalchemy
1414

15-
>>> with PostgresContainer("postgres:9.5") as postgres:
15+
>>> with PostgresContainer("postgres:16") as postgres:
1616
... engine = sqlalchemy.create_engine(postgres.get_connection_url())
1717
... with engine.begin() as connection:
1818
... result = connection.execute(sqlalchemy.text("select version()"))
1919
... version, = result.fetchone()
2020
>>> version
21-
'PostgreSQL 9.5...'
21+
'PostgreSQL 16...'
2222
```
2323

2424
The snippet above will spin up a postgres database in a container. The `get_connection_url()` convenience method returns a `sqlalchemy` compatible url we use to connect to the database and retrieve the database version.

modules/postgres/testcontainers/postgres/__init__.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
1313
import os
14+
from time import sleep
1415
from typing import Optional
1516

17+
from testcontainers.core.config import MAX_TRIES, SLEEP_TIME
1618
from testcontainers.core.generic import DbContainer
1719
from testcontainers.core.utils import raise_for_deprecated_parameter
20+
from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs
21+
22+
_UNSET = object()
1823

1924

2025
class PostgresContainer(DbContainer):
2126
"""
2227
Postgres database container.
2328
29+
To get a URL without a driver, pass in :code:`driver=None`.
30+
2431
Example:
2532
2633
The example spins up a Postgres database and connects to it using the :code:`psycopg`
@@ -31,7 +38,7 @@ class PostgresContainer(DbContainer):
3138
>>> from testcontainers.postgres import PostgresContainer
3239
>>> import sqlalchemy
3340
34-
>>> postgres_container = PostgresContainer("postgres:9.5")
41+
>>> postgres_container = PostgresContainer("postgres:16")
3542
>>> with postgres_container as postgres:
3643
... engine = sqlalchemy.create_engine(postgres.get_connection_url())
3744
... with engine.begin() as connection:
@@ -48,16 +55,16 @@ def __init__(
4855
username: Optional[str] = None,
4956
password: Optional[str] = None,
5057
dbname: Optional[str] = None,
51-
driver: str = "psycopg2",
58+
driver: Optional[str] = "psycopg2",
5259
**kwargs,
5360
) -> None:
5461
raise_for_deprecated_parameter(kwargs, "user", "username")
5562
super().__init__(image=image, **kwargs)
56-
self.username = username or os.environ.get("POSTGRES_USER", "test")
57-
self.password = password or os.environ.get("POSTGRES_PASSWORD", "test")
58-
self.dbname = dbname or os.environ.get("POSTGRES_DB", "test")
63+
self.username: str = username or os.environ.get("POSTGRES_USER", "test")
64+
self.password: str = password or os.environ.get("POSTGRES_PASSWORD", "test")
65+
self.dbname: str = dbname or os.environ.get("POSTGRES_DB", "test")
5966
self.port = port
60-
self.driver = driver
67+
self.driver = f"+{driver}" if driver else ""
6168

6269
self.with_exposed_ports(self.port)
6370

@@ -66,12 +73,37 @@ def _configure(self) -> None:
6673
self.with_env("POSTGRES_PASSWORD", self.password)
6774
self.with_env("POSTGRES_DB", self.dbname)
6875

69-
def get_connection_url(self, host=None) -> str:
76+
def get_connection_url(self, host: Optional[str] = None, driver: Optional[str] = _UNSET) -> str:
77+
"""Get a DB connection URL to connect to the PG DB.
78+
79+
If a driver is set in the constructor (defaults to psycopg2!), the URL will contain the
80+
driver. The optional driver argument to :code:`get_connection_url` overwrites the constructor
81+
set value. Pass :code:`driver=None` to get URLs without a driver.
82+
"""
83+
if driver is _UNSET:
84+
driver_str = self.driver
85+
else:
86+
driver_str = f"+{driver}"
7087
return super()._create_connection_url(
71-
dialect=f"postgresql+{self.driver}",
88+
dialect=f"postgresql{driver_str}",
7289
username=self.username,
7390
password=self.password,
7491
dbname=self.dbname,
7592
host=host,
7693
port=self.port,
7794
)
95+
96+
@wait_container_is_ready()
97+
def _connect(self) -> None:
98+
wait_for_logs(self, ".*database system is ready to accept connections.*", MAX_TRIES, SLEEP_TIME)
99+
100+
count = 0
101+
while count < MAX_TRIES:
102+
status, _ = self.exec(f"pg_isready -hlocalhost -p{self.port} -U{self.username}")
103+
if status == 0:
104+
return
105+
106+
sleep(SLEEP_TIME)
107+
count += 1
108+
109+
raise RuntimeError("Postgres could not get into a ready state")
Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import sqlalchemy
2-
31
from testcontainers.postgres import PostgresContainer
42

53

64
def test_docker_run_postgres():
7-
postgres_container = PostgresContainer("postgres:9.5")
8-
with postgres_container as postgres:
9-
engine = sqlalchemy.create_engine(postgres.get_connection_url())
10-
with engine.begin() as connection:
11-
result = connection.execute(sqlalchemy.text("select version()"))
12-
for row in result:
13-
assert row[0].lower().startswith("postgresql 9.5")
5+
# https://www.postgresql.org/support/versioning/
6+
supported_versions = ["12", "13", "14", "15", "16", "latest"]
147

8+
for version in supported_versions:
9+
postgres_container = PostgresContainer(f"postgres:{version}")
10+
with postgres_container as postgres:
11+
status, msg = postgres.exec(f"pg_isready -hlocalhost -p{postgres.port} -U{postgres.username}")
1512

16-
def test_docker_run_postgres_with_driver_pg8000():
17-
postgres_container = PostgresContainer("postgres:9.5", driver="pg8000")
18-
with postgres_container as postgres:
19-
engine = sqlalchemy.create_engine(postgres.get_connection_url())
20-
with engine.begin() as connection:
21-
connection.execute(sqlalchemy.text("select 1=1"))
13+
assert msg.decode("utf-8").endswith("accepting connections\n")
14+
assert status == 0

poetry.lock

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ neo4j = ["neo4j"]
102102
nginx = []
103103
opensearch = ["opensearch-py"]
104104
oracle = ["sqlalchemy", "cx_Oracle"]
105-
postgres = ["sqlalchemy", "psycopg2-binary"]
105+
postgres = []
106106
rabbitmq = ["pika"]
107107
redis = ["redis"]
108108
selenium = ["selenium"]

0 commit comments

Comments
 (0)