Skip to content

Commit 4b58b9e

Browse files
committed
Add first bare working implementation
1 parent ce12f22 commit 4b58b9e

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

modules/cratedb/README.rst

Whitespace-only changes.

modules/cratedb/example_basic.py

Whitespace-only changes.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import os
2+
import typing as t
3+
from urllib.parse import quote
4+
5+
from testcontainers.core.container import DockerContainer
6+
from testcontainers.core.exceptions import ContainerStartException
7+
from testcontainers.core.utils import raise_for_deprecated_parameter
8+
from testcontainers.core.wait_strategies import HttpWaitStrategy
9+
10+
11+
def asbool(obj) -> bool:
12+
# from sqlalchemy.util.langhelpers
13+
if isinstance(obj, str):
14+
obj = obj.strip().lower()
15+
if obj in ["true", "yes", "on", "y", "t", "1"]:
16+
return True
17+
elif obj in ["false", "no", "off", "n", "f", "0"]:
18+
return False
19+
else:
20+
raise ValueError(f"String is not true/false: {obj!r}")
21+
return bool(obj)
22+
23+
24+
# DockerSkippingContainer, KeepaliveContainer,
25+
class CrateDBContainer(DockerContainer):
26+
"""
27+
CrateDB database container.
28+
29+
Example:
30+
31+
The example spins up a CrateDB database and connects to it using
32+
SQLAlchemy and its Python driver.
33+
34+
.. doctest::
35+
36+
>>> from cratedb_toolkit.testing.testcontainers.cratedb import CrateDBContainer
37+
>>> import sqlalchemy
38+
39+
>>> cratedb_container = CrateDBContainer("crate:5.2.3")
40+
>>> cratedb_container.start()
41+
>>> with cratedb_container as cratedb:
42+
... engine = sqlalchemy.create_engine(cratedb.get_connection_url())
43+
... with engine.begin() as connection:
44+
... result = connection.execute(sqlalchemy.text("select version()"))
45+
... version, = result.fetchone()
46+
>>> version
47+
'CrateDB 5.2.3...'
48+
"""
49+
50+
CRATEDB_USER = os.environ.get("CRATEDB_USER", "crate")
51+
CRATEDB_PASSWORD = os.environ.get("CRATEDB_PASSWORD", "crate")
52+
CRATEDB_DB = os.environ.get("CRATEDB_DB", "doc")
53+
KEEPALIVE = asbool(os.environ.get("CRATEDB_KEEPALIVE", os.environ.get("TC_KEEPALIVE", False)))
54+
CMD_OPTS: t.ClassVar[dict[str, str]] = {
55+
"discovery.type": "single-node",
56+
"node.attr.storage": "hot",
57+
"path.repo": "/tmp/snapshots",
58+
}
59+
60+
def __init__(
61+
self,
62+
image: str = "crate/crate:nightly",
63+
ports: t.Optional[dict] = None,
64+
user: t.Optional[str] = None,
65+
password: t.Optional[str] = None,
66+
dbname: t.Optional[str] = None,
67+
cmd_opts: t.Optional[dict] = None,
68+
**kwargs,
69+
) -> None:
70+
"""
71+
:param image: docker hub image path with optional tag
72+
:param ports: optional dict that maps a port inside the container to a port on the host machine;
73+
`None` as a map value generates a random port;
74+
Dicts are ordered. By convention, the first key-val pair is designated to the HTTP interface.
75+
Example: {4200: None, 5432: 15432} - port 4200 inside the container will be mapped
76+
to a random port on the host, internal port 5432 for PSQL interface will be mapped
77+
to the 15432 port on the host.
78+
:param user: optional username to access the DB; if None, try `CRATEDB_USER` environment variable
79+
:param password: optional password to access the DB; if None, try `CRATEDB_PASSWORD` environment variable
80+
:param dbname: optional database name to access the DB; if None, try `CRATEDB_DB` environment variable
81+
:param cmd_opts: an optional dict with CLI arguments to be passed to the DB entrypoint inside the container
82+
:param kwargs: misc keyword arguments
83+
"""
84+
super().__init__(image=image, **kwargs)
85+
86+
self._name = "testcontainers-cratedb"
87+
88+
cmd_opts = cmd_opts or {}
89+
self._command = self._build_cmd({**self.CMD_OPTS, **cmd_opts})
90+
91+
self.CRATEDB_USER = user or self.CRATEDB_USER
92+
self.CRATEDB_PASSWORD = password or self.CRATEDB_PASSWORD
93+
self.CRATEDB_DB = dbname or self.CRATEDB_DB
94+
95+
self.port_mapping = ports if ports else {4200: None}
96+
self.port_to_expose, _ = next(iter(self.port_mapping.items()))
97+
98+
self.waiting_for(HttpWaitStrategy(4200).for_status_code(200).with_startup_timeout(5))
99+
100+
@staticmethod
101+
def _build_cmd(opts: dict) -> str:
102+
"""
103+
Return a string with command options concatenated and optimised for ES5 use
104+
"""
105+
cmd = []
106+
for key, val in opts.items():
107+
if isinstance(val, bool):
108+
val = str(val).lower()
109+
cmd.append(f"-C{key}={val}")
110+
return " ".join(cmd)
111+
112+
def _configure_ports(self) -> None:
113+
"""
114+
Bind all the ports exposed inside the container to the same port on the host
115+
"""
116+
# If host_port is `None`, a random port to be generated
117+
for container_port, host_port in self.port_mapping.items():
118+
self.with_bind_ports(container=container_port, host=host_port)
119+
120+
def _configure_credentials(self) -> None:
121+
self.with_env("CRATEDB_USER", self.CRATEDB_USER)
122+
self.with_env("CRATEDB_PASSWORD", self.CRATEDB_PASSWORD)
123+
self.with_env("CRATEDB_DB", self.CRATEDB_DB)
124+
125+
def _configure(self) -> None:
126+
self._configure_ports()
127+
self._configure_credentials()
128+
129+
def get_connection_url(self, dialect: str = "crate", host: t.Optional[str] = None) -> str:
130+
"""
131+
Return a connection URL to the DB
132+
133+
:param host: optional string
134+
:param dialect: a string with the dialect name to generate a DB URI
135+
:return: string containing a connection URL to te DB
136+
"""
137+
# TODO: When using `db_name=self.CRATEDB_DB`:
138+
# Connection.__init__() got an unexpected keyword argument 'database'
139+
return self._create_connection_url(
140+
dialect=dialect,
141+
username=self.CRATEDB_USER,
142+
password=self.CRATEDB_PASSWORD,
143+
host=host,
144+
port=self.port_to_expose,
145+
)
146+
147+
def _create_connection_url(
148+
self,
149+
dialect: str,
150+
username: str,
151+
password: str,
152+
host: t.Optional[str] = None,
153+
port: t.Optional[int] = None,
154+
dbname: t.Optional[str] = None,
155+
**kwargs: t.Any,
156+
) -> str:
157+
if raise_for_deprecated_parameter(kwargs, "db_name", "dbname"):
158+
raise ValueError(f"Unexpected arguments: {','.join(kwargs)}")
159+
if self._container is None:
160+
raise ContainerStartException("container has not been started")
161+
host = host or self.get_container_host_ip()
162+
assert port is not None
163+
port = self.get_exposed_port(port)
164+
quoted_password = quote(password, safe=" +")
165+
url = f"{dialect}://{username}:{quoted_password}@{host}:{port}"
166+
if dbname:
167+
url = f"{url}/{dbname}"
168+
return url

0 commit comments

Comments
 (0)