Skip to content

Commit 6e6d8e3

Browse files
gvenzlalexanderankin
andauthoredMar 31, 2024··
fix: Improved Oracle DB module (#363)
Hi, I took the liberty to improve the Oracle DB module for Testcontainers Python. The PR has several enhancements: * Leveraging `oracledb` thin Python driver * This makes Oracle DB tests on CI/CD now possible too * Usage of `gvenzl/oracle-free` image with the latest and greatest Oracle DB version * DB version independent readiness check * Support for various `gvenzl/oracle-free` image features (`ORACLE_DATABASE`, `APP_USER`, `APP_USER_PASSWORD`, etc) * Tests for Oracle DB for the various combinations Ideally, some more documentation on how the Container is supposed to be used would be handy but I couldn't really find a good example of how such a ReadMe should be structured. Any things are gladly appreciated! --------- Signed-off-by: gvenzl <gerald.venzl@gmail.com> Signed-off-by: Gerald Venzl <gerald.venzl@gmail.com> Co-authored-by: David Ankin <daveankin@gmail.com>
1 parent 0f554fb commit 6e6d8e3

File tree

8 files changed

+220
-85
lines changed

8 files changed

+220
-85
lines changed
 

‎index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ testcontainers-python facilitates the use of Docker containers for functional an
3535
modules/neo4j/README
3636
modules/nginx/README
3737
modules/opensearch/README
38-
modules/oracle/README
38+
modules/oracle-free/README
3939
modules/postgres/README
4040
modules/qdrant/README
4141
modules/rabbitmq/README
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from os import environ
2+
from secrets import randbits
3+
from typing import Optional
4+
5+
from testcontainers.core.generic import DbContainer
6+
7+
8+
class OracleDbContainer(DbContainer):
9+
"""
10+
Oracle database container.
11+
12+
Example:
13+
14+
.. doctest::
15+
16+
>>> import sys, pytest
17+
>>> if sys.platform.startswith('win') or sys.platform == 'darwin':
18+
... pytest.skip("linux only test")
19+
20+
>>> import sqlalchemy
21+
>>> from testcontainers.oracle import OracleDbContainer
22+
23+
>>> with OracleDbContainer() as oracle:
24+
... engine = sqlalchemy.create_engine(oracle.get_connection_url())
25+
... with engine.begin() as connection:
26+
... result = connection.execute(sqlalchemy.text("SELECT 1 FROM dual"))
27+
... result.fetchall()
28+
[(1,)]
29+
"""
30+
31+
def __init__(
32+
self,
33+
image: str = "gvenzl/oracle-free:slim",
34+
oracle_password: Optional[str] = None,
35+
username: Optional[str] = None,
36+
password: Optional[str] = None,
37+
port: int = 1521,
38+
dbname: Optional[str] = None,
39+
**kwargs
40+
) -> None:
41+
super().__init__(image=image, **kwargs)
42+
43+
self.port = port
44+
self.with_exposed_ports(self.port)
45+
46+
self.oracle_password = oracle_password or environ.get("ORACLE_PASSWORD") or hex(randbits(24))
47+
self.username = username or environ.get("APP_USER")
48+
self.password = password or environ.get("APP_USER_PASSWORD")
49+
self.dbname = dbname or environ.get("ORACLE_DATABASE")
50+
51+
def get_connection_url(self) -> str:
52+
return super()._create_connection_url(
53+
dialect="oracle+oracledb",
54+
username=self.username or "system",
55+
password=self.password or self.oracle_password,
56+
port=self.port,
57+
) + "/?service_name={}".format(self.dbname or "FREEPDB1")
58+
# Default DB is "FREEPDB1"
59+
60+
def _configure(self) -> None:
61+
# if self.oracle_password is not None:
62+
# self.with_env("ORACLE_PASSWORD", self.oracle_password)
63+
# # Either ORACLE_PASSWORD or ORACLE_RANDOM_PASSWORD need to be passed on
64+
# else:
65+
# self.with_env("ORACLE_RANDOM_PASSWORD", "y")
66+
# this module is unusable with a random password
67+
self.with_env("ORACLE_PASSWORD", self.oracle_password)
68+
69+
if self.username is not None:
70+
self.with_env("APP_USER", self.username)
71+
if self.password is not None:
72+
self.with_env("APP_USER_PASSWORD", self.password)
73+
74+
# FREE and FREEPDB1 are predefined databases, do not pass them on as ORACLE_DATABASE
75+
if self.dbname is not None and self.dbname.upper() not in ("FREE", "FREEPDB1"):
76+
self.with_env("ORACLE_DATABASE", self.dbname)
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
import sqlalchemy
3+
4+
from testcontainers.core.utils import is_arm
5+
from testcontainers.oracle import OracleDbContainer
6+
7+
8+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
9+
def test_docker_run_oracle_with_system_password():
10+
with OracleDbContainer(oracle_password="test") as oracledb:
11+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
12+
with engine.begin() as connection:
13+
test_val = 1
14+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
15+
for row in result:
16+
assert row[0] == test_val
17+
18+
19+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
20+
def test_docker_run_oracle_with_username_password():
21+
with OracleDbContainer(username="test", password="test") as oracledb:
22+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
23+
with engine.begin() as connection:
24+
test_val = 1
25+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
26+
for row in result:
27+
assert row[0] == test_val
28+
29+
30+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
31+
def test_docker_run_oracle_with_custom_db_and_system_username_password():
32+
with OracleDbContainer(oracle_password="coolpassword", dbname="myTestPDB") as oracledb:
33+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
34+
with engine.begin() as connection:
35+
test_val = 1
36+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
37+
for row in result:
38+
assert row[0] == test_val
39+
40+
41+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
42+
def test_docker_run_oracle_with_custom_db_and_app_username_password():
43+
with OracleDbContainer(username="mycooluser", password="123connect", dbname="anotherPDB") as oracledb:
44+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
45+
with engine.begin() as connection:
46+
test_val = 1
47+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
48+
for row in result:
49+
assert row[0] == test_val
50+
51+
52+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
53+
def test_docker_run_oracle_with_default_db_and_app_username_password():
54+
with OracleDbContainer(username="mycooluser", password="123connect") as oracledb:
55+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
56+
with engine.begin() as connection:
57+
test_val = 1
58+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
59+
for row in result:
60+
assert row[0] == test_val
61+
62+
63+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
64+
def test_docker_run_oracle_with_cdb_and_system_username():
65+
with OracleDbContainer(oracle_password="MyOraclePWD1", dbname="free") as oracledb:
66+
engine = sqlalchemy.create_engine(oracledb.get_connection_url())
67+
with engine.begin() as connection:
68+
test_val = 1
69+
result = connection.execute(sqlalchemy.text("SELECT {} FROM dual".format(test_val)))
70+
for row in result:
71+
assert row[0] == test_val
72+
73+
74+
@pytest.mark.skipif(is_arm(), reason="oracle-free container not available for ARM")
75+
def test_doctest():
76+
with OracleDbContainer() as oracle:
77+
print(oracle.get_connection_url())
78+
engine = sqlalchemy.create_engine(oracle.get_connection_url())
79+
with engine.begin() as connection:
80+
result = connection.execute(sqlalchemy.text("SELECT 1 FROM dual"))
81+
assert result.fetchall() == [(1,)]

‎modules/oracle/testcontainers/oracle/__init__.py

-33
This file was deleted.

‎modules/oracle/tests/test_oracle.py

-20
This file was deleted.

‎poetry.lock

+57-28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

+5-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ packages = [
4848
{ include = "testcontainers", from = "modules/neo4j" },
4949
{ include = "testcontainers", from = "modules/nginx" },
5050
{ include = "testcontainers", from = "modules/opensearch" },
51-
{ include = "testcontainers", from = "modules/oracle" },
51+
{ include = "testcontainers", from = "modules/oracle-free" },
5252
{ include = "testcontainers", from = "modules/postgres" },
5353
{ include = "testcontainers", from = "modules/qdrant" },
5454
{ include = "testcontainers", from = "modules/rabbitmq" },
@@ -88,7 +88,7 @@ pymssql = { version = "*", optional = true }
8888
pymysql = { version = "*", extras = ["rsa"], optional = true }
8989
neo4j = { version = "*", optional = true }
9090
opensearch-py = { version = "*", optional = true }
91-
cx_Oracle = { version = "*", optional = true }
91+
oracledb = { version = "*", optional = true }
9292
pika = { version = "*", optional = true }
9393
redis = { version = "*", optional = true }
9494
selenium = { version = "*", optional = true }
@@ -117,7 +117,8 @@ nats = ["nats-py"]
117117
neo4j = ["neo4j"]
118118
nginx = []
119119
opensearch = ["opensearch-py"]
120-
oracle = ["sqlalchemy", "cx_Oracle"]
120+
oracle = ["sqlalchemy", "oracledb"]
121+
oracle-free = ["sqlalchemy", "oracledb"]
121122
postgres = []
122123
qdrant = ["qdrant-client"]
123124
rabbitmq = ["pika"]
@@ -143,6 +144,7 @@ psycopg = "*"
143144
cassandra-driver = "*"
144145
pytest-asyncio = "0.23.5"
145146
kafka-python-ng = "^2.2.0"
147+
pip = "^24.0"
146148

147149
[[tool.poetry.source]]
148150
name = "PyPI"

0 commit comments

Comments
 (0)
Please sign in to comment.