Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/community/cratedb.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. autoclass:: testcontainers.community.cratedb.CrateDBContainer
.. title:: testcontainers.community.cratedb.CrateDBContainer
34 changes: 34 additions & 0 deletions docs/modules/cratedb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CrateDB

## Introduction

The Testcontainers module for [CrateDB](https://cratedb.com), a distributed SQL
database for real-time analytics. CrateDB is PostgreSQL wire-compatible and
provides a SQLAlchemy dialect (`crate://`) that talks to its HTTP interface.

## Adding this module to your project dependencies

Please run the following command to add the CrateDB module to your python dependencies:

```bash
pip install testcontainers[cratedb] sqlalchemy sqlalchemy-cratedb
```

## Usage example

<!--codeinclude-->

[Creating a CrateDB container](cratedb_example.py)

<!--/codeinclude-->

## Configuration

The CrateDB container can be configured with the following parameters:

- `image`: Docker image to use (default: `"crate/crate:latest"`)
- `port`: container port used to build the connection URL (default: `4200`, the HTTP interface)
- `username`: Database username (default: `"crate"`, or the `CRATEDB_USER` env var)
- `password`: Database password (default: `"crate"`, or the `CRATEDB_PASSWORD` env var)
- `dialect`: SQLAlchemy dialect used in the connection URL (default: `"crate"`)
- `cmd_opts`: extra `-C<key>=<value>` options passed to CrateDB (merged over the single-node defaults)
43 changes: 43 additions & 0 deletions docs/modules/cratedb_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sqlalchemy
from sqlalchemy import text

from testcontainers.community.cratedb import CrateDBContainer


def basic_example():
with CrateDBContainer("crate:latest") as cratedb:
# CrateDB speaks the SQLAlchemy `crate://` dialect over its HTTP interface.
engine = sqlalchemy.create_engine(cratedb.get_connection_url())

with engine.begin() as conn:
conn.execute(
text("""
CREATE TABLE IF NOT EXISTS summits (
name TEXT PRIMARY KEY,
height INT
)
""")
)
print("Created table")

conn.execute(
text("INSERT INTO summits (name, height) VALUES (:name, :height)"),
[
{"name": "Mont Blanc", "height": 4808},
{"name": "Monte Rosa", "height": 4634},
{"name": "Dom", "height": 4545},
],
)
# CrateDB is eventually consistent for reads; refresh to read-your-writes.
conn.execute(text("REFRESH TABLE summits"))
print("Inserted data")

with engine.connect() as conn:
result = conn.execute(text("SELECT name, height FROM summits ORDER BY height DESC"))
print("\nQuery results:")
for row in result:
print(f"Name: {row.name}, Height: {row.height}")


if __name__ == "__main__":
basic_example()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ nav:
- modules/clickhouse.md
- modules/cockroachdb.md
- modules/cosmosdb.md
- modules/cratedb.md
- modules/db2.md
- modules/elasticsearch.md
- modules/influxdb.md
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ cassandra = []
clickhouse = ["clickhouse-driver"]
cosmosdb = ["azure-cosmos>=4"]
cockroachdb = []
cratedb = ["sqlalchemy-cratedb"]
db2 = [
"sqlalchemy>=2",
"ibm_db_sa; platform_machine != 'aarch64' and platform_machine != 'arm64'",
Expand Down Expand Up @@ -128,6 +129,7 @@ test = [
"pymilvus>=2",
"paho-mqtt>=2",
"sqlalchemy-cockroachdb>=2",
"sqlalchemy-cratedb",
"paramiko>=4",
"twine>=6.2.0",
"anyio>=4",
Expand Down
111 changes: 111 additions & 0 deletions src/testcontainers/community/cratedb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from typing import ClassVar, Optional

from testcontainers.community.generic.sql import SqlContainer
from testcontainers.core.utils import raise_for_deprecated_parameter
from testcontainers.core.wait_strategies import HttpWaitStrategy

# CrateDB's HTTP interface. The SQLAlchemy `crate://` dialect talks to CrateDB
# over HTTP, so this is the port used for connection URLs and readiness checks.
HTTP_PORT = 4200
# CrateDB's PostgreSQL wire-protocol interface, exposed for convenience.
PSQL_PORT = 5432


class CrateDBContainer(SqlContainer):
"""
CrateDB database container.

Example:

The example spins up a CrateDB database and connects to it using
SQLAlchemy and the ``sqlalchemy-cratedb`` dialect, which talks to
CrateDB over its HTTP interface (port 4200).

.. doctest::

>>> from testcontainers.community.cratedb import CrateDBContainer
>>> import sqlalchemy

>>> with CrateDBContainer("crate:5.10") as cratedb:
... engine = sqlalchemy.create_engine(cratedb.get_connection_url())
... with engine.begin() as connection:
... result = connection.execute(sqlalchemy.text("select name from sys.cluster"))
... cluster_name, = result.fetchone()
"""

# Default command-line options. CrateDB needs single-node discovery to run
# as a one-node cluster suitable for testing.
CMD_OPTS: ClassVar[dict[str, str]] = {"discovery.type": "single-node"}

def __init__(
self,
image: str = "crate/crate:latest",
port: int = HTTP_PORT,
username: Optional[str] = None,
password: Optional[str] = None,
dialect: str = "crate",
cmd_opts: Optional[dict[str, str]] = None,
**kwargs,
) -> None:
"""
:param image: Docker image name (with optional tag).
:param port: container port used to build the connection URL; defaults
to the HTTP port (4200) used by the ``crate://`` dialect.
:param username: username for the DB; falls back to the ``CRATEDB_USER``
environment variable, then ``crate``.
:param password: password for the DB; falls back to the
``CRATEDB_PASSWORD`` environment variable, then ``crate``.
:param dialect: SQLAlchemy dialect used in the connection URL.
:param cmd_opts: extra ``-C<key>=<value>`` options passed to CrateDB,
merged over (and able to override) the defaults.
"""
raise_for_deprecated_parameter(kwargs, "user", "username")
# Readiness is signalled by CrateDB's HTTP interface returning 200; this
# keeps startup free of any database client library.
super().__init__(image, wait_strategy=HttpWaitStrategy(HTTP_PORT).for_status_code(200), **kwargs)

cmd_opts = cmd_opts or {}
self._command = self._build_cmd({**self.CMD_OPTS, **cmd_opts})

self.username = username or os.environ.get("CRATEDB_USER", "crate")
self.password = password or os.environ.get("CRATEDB_PASSWORD", "crate")
self.port = port
self.dialect = dialect

self.with_exposed_ports(HTTP_PORT, PSQL_PORT)

@staticmethod
def _build_cmd(opts: dict[str, str]) -> str:
"""Render a CrateDB ``-C<key>=<value> ...`` command-line string."""
cmd = []
for key, val in opts.items():
if isinstance(val, bool):
val = str(val).lower()
cmd.append(f"-C{key}={val}")
return " ".join(cmd)

def _configure(self) -> None:
self.with_env("CRATEDB_USER", self.username)
self.with_env("CRATEDB_PASSWORD", self.password)

def get_connection_url(self, host: Optional[str] = None) -> str:
return self._create_connection_url(
dialect=self.dialect,
username=self.username,
password=self.password,
host=host,
port=self.port,
)
15 changes: 15 additions & 0 deletions src/testcontainers/cratedb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import warnings

from testcontainers.community.cratedb import (
CrateDBContainer,
)

warnings.warn(
"testcontainers.cratedb is deprecated, use testcontainers.community.cratedb instead",
DeprecationWarning,
stacklevel=2,
)

__all__ = [
"CrateDBContainer",
]
47 changes: 47 additions & 0 deletions tests/community/cratedb/test_cratedb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import urllib.parse

import pytest
import sqlalchemy

from testcontainers.community.cratedb import CrateDBContainer


@pytest.mark.parametrize("version", ["5.10", "latest"])
def test_docker_run_cratedb(version: str):
with CrateDBContainer(f"crate:{version}") as cratedb:
engine = sqlalchemy.create_engine(cratedb.get_connection_url())
with engine.begin() as connection:
result = connection.execute(sqlalchemy.text("select 1 + 2 + 3 + 4 + 5"))
assert result.fetchone()[0] == 15


def test_cratedb_connection_url():
with CrateDBContainer("crate:latest", username="crate", password="crate") as cratedb:
url = urllib.parse.urlparse(cratedb.get_connection_url())
assert url.scheme == "crate"
credentials, location = url.netloc.split("@")
assert credentials == "crate:crate"
host, port = location.split(":")
assert host == cratedb.get_container_host_ip()
assert int(port) == cratedb.get_exposed_port(cratedb.port)


@pytest.mark.parametrize(
"cmd_opts, expected",
[
pytest.param(
{"indices.breaker.total.limit": "90%"},
"-Cdiscovery.type=single-node -Cindices.breaker.total.limit=90%",
id="add_cmd_option",
),
pytest.param(
{"discovery.type": "zen", "indices.breaker.total.limit": "90%"},
"-Cdiscovery.type=zen -Cindices.breaker.total.limit=90%",
id="override_defaults",
),
],
)
def test_build_command(cmd_opts, expected):
# Pure unit test: the command line is assembled in __init__, no container is started.
cratedb = CrateDBContainer(cmd_opts=cmd_opts)
assert cratedb._command == expected
Loading