From be4d59f729d7fcd5735dae2bd8dcf47016a77353 Mon Sep 17 00:00:00 2001 From: Israel Fruchter Date: Thu, 30 Dec 2021 22:21:49 +0200 Subject: [PATCH] Adding support for Cassandra and Scylla This add the support for those cassandra based dbs and their drivers, cassandra-driver, scylla-driver Ref: https://cassandra.apache.org/ Ref: https://www.scylladb.com/ Ref: https://pypi.org/project/cassandra-driver/ Ref: https://pypi.org/project/scylla-driver/ --- .github/workflows/main.yml | 2 ++ docs/database.rst | 2 ++ requirements.in | 2 +- requirements/3.10.txt | 21 ++++++++++++++--- requirements/3.7.txt | 26 ++++++++++++++++---- requirements/3.8.txt | 21 ++++++++++++++--- requirements/3.9.txt | 21 ++++++++++++++--- setup.py | 2 ++ testcontainers/cassandra.py | 46 ++++++++++++++++++++++++++++++++++++ testcontainers/scylla.py | 47 +++++++++++++++++++++++++++++++++++++ tests/test_cassandra.py | 18 ++++++++++++++ tests/test_scylla.py | 18 ++++++++++++++ 12 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 testcontainers/cassandra.py create mode 100644 testcontainers/scylla.py create mode 100644 tests/test_cassandra.py create mode 100644 tests/test_scylla.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a23509d70..be5c6a1ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,6 +67,8 @@ jobs: - keycloak.py - arangodb.py - azurite.py + - scylla.py + - cassandra.py runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/docs/database.rst b/docs/database.rst index 869487433..16e73a12f 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -12,3 +12,5 @@ Allows to spin up database images such as MySQL, PostgreSQL, MariaDB, Oracle XE, .. autoclass:: testcontainers.clickhouse.ClickHouseContainer .. autoclass:: testcontainers.neo4j.Neo4jContainer .. autoclass:: testcontainers.arangodb.ArangoDbContainer +.. autoclass:: testcontainers.cassandra.CassandraContainer +.. autoclass:: testcontainers.scylla.ScyllaContainer diff --git a/requirements.in b/requirements.in index f41b1b145..44de11ef8 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ --e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,minio,mongo,redis,mssqlserver,neo4j,kafka,rabbitmq,clickhouse,keycloak,arangodb,azurite] +-e file:.[docker-compose,mysql,oracle,postgresql,selenium,google-cloud-pubsub,minio,mongo,redis,mssqlserver,neo4j,kafka,rabbitmq,clickhouse,keycloak,arangodb,azurite,cassandra,scylla] codecov>=2.1.0 cryptography<37 flake8<3.8.0 # 3.8.0 adds a dependency on importlib-metadata which conflicts with other packages. diff --git a/requirements/3.10.txt b/requirements/3.10.txt index 3c6ab5c12..f504a71dd 100644 --- a/requirements/3.10.txt +++ b/requirements/3.10.txt @@ -36,6 +36,8 @@ bleach==5.0.1 # via readme-renderer cachetools==5.2.0 # via google-auth +cassandra-driver==3.25.0 + # via testcontainers certifi==2022.12.7 # via # minio @@ -48,6 +50,8 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests +click==8.1.3 + # via geomet clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 @@ -96,6 +100,10 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in +geomet==0.2.1.post1 + # via + # cassandra-driver + # scylla-driver google-api-core[grpc]==2.11.0 # via google-cloud-pubsub google-auth==2.15.0 @@ -227,7 +235,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.3 +python-arango==7.5.4 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -245,7 +253,9 @@ pytz==2022.6 pytz-deprecation-shim==0.1.0.post0 # via tzlocal pyyaml==5.4.1 - # via docker-compose + # via + # docker-compose + # scylla-driver readme-renderer==37.3 # via twine redis==4.4.0 @@ -281,6 +291,8 @@ rsa==4.9 # python-jose scramp==1.4.4 # via pg8000 +scylla-driver==3.25.10 + # via testcontainers secretstorage==3.3.3 # via keyring selenium==4.7.2 @@ -289,13 +301,16 @@ six==1.16.0 # via # azure-core # bleach + # cassandra-driver # dockerpty # ecdsa + # geomet # google-auth # isodate # jsonschema # paramiko # python-dateutil + # scylla-driver # websocket-client sniffio==1.3.0 # via trio @@ -317,7 +332,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # via testcontainers texttable==1.6.7 # via docker-compose diff --git a/requirements/3.7.txt b/requirements/3.7.txt index d3d946913..14a79580d 100644 --- a/requirements/3.7.txt +++ b/requirements/3.7.txt @@ -42,6 +42,8 @@ cached-property==1.5.2 # via docker-compose cachetools==5.2.0 # via google-auth +cassandra-driver==3.25.0 + # via testcontainers certifi==2022.12.7 # via # minio @@ -54,6 +56,8 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests +click==8.1.3 + # via geomet clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 @@ -102,6 +106,10 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in +geomet==0.2.1.post1 + # via + # cassandra-driver + # scylla-driver google-api-core[grpc]==2.11.0 # via google-cloud-pubsub google-auth==2.15.0 @@ -135,6 +143,7 @@ imagesize==1.4.1 # via sphinx importlib-metadata==5.1.0 # via + # click # jsonschema # keyring # pg8000 @@ -187,7 +196,7 @@ packaging==22.0 # sphinx paramiko==2.12.0 # via docker -pg8000==1.29.3 +pg8000==1.29.4 # via -r requirements.in pika==1.3.1 # via testcontainers @@ -241,7 +250,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.3 +python-arango==7.5.4 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -249,7 +258,7 @@ python-dotenv==0.21.0 # via docker-compose python-jose==3.3.0 # via python-keycloak -python-keycloak==2.6.0 +python-keycloak==2.6.1 # via testcontainers pytz==2022.6 # via @@ -259,7 +268,9 @@ pytz==2022.6 pytz-deprecation-shim==0.1.0.post0 # via tzlocal pyyaml==5.4.1 - # via docker-compose + # via + # docker-compose + # scylla-driver readme-renderer==37.3 # via twine redis==4.4.0 @@ -295,6 +306,8 @@ rsa==4.9 # python-jose scramp==1.4.4 # via pg8000 +scylla-driver==3.25.10 + # via testcontainers secretstorage==3.3.3 # via keyring selenium==4.7.2 @@ -303,13 +316,16 @@ six==1.16.0 # via # azure-core # bleach + # cassandra-driver # dockerpty # ecdsa + # geomet # google-auth # isodate # jsonschema # paramiko # python-dateutil + # scylla-driver # websocket-client sniffio==1.3.0 # via trio @@ -331,7 +347,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # via testcontainers texttable==1.6.7 # via docker-compose diff --git a/requirements/3.8.txt b/requirements/3.8.txt index 15e458420..79f43c28a 100644 --- a/requirements/3.8.txt +++ b/requirements/3.8.txt @@ -40,6 +40,8 @@ bleach==5.0.1 # via readme-renderer cachetools==5.2.0 # via google-auth +cassandra-driver==3.25.0 + # via testcontainers certifi==2022.12.7 # via # minio @@ -52,6 +54,8 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests +click==8.1.3 + # via geomet clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 @@ -100,6 +104,10 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in +geomet==0.2.1.post1 + # via + # cassandra-driver + # scylla-driver google-api-core[grpc]==2.11.0 # via google-cloud-pubsub google-auth==2.15.0 @@ -232,7 +240,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.3 +python-arango==7.5.4 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -250,7 +258,9 @@ pytz==2022.6 pytz-deprecation-shim==0.1.0.post0 # via tzlocal pyyaml==5.4.1 - # via docker-compose + # via + # docker-compose + # scylla-driver readme-renderer==37.3 # via twine redis==4.4.0 @@ -286,6 +296,8 @@ rsa==4.9 # python-jose scramp==1.4.4 # via pg8000 +scylla-driver==3.25.10 + # via testcontainers secretstorage==3.3.3 # via keyring selenium==4.7.2 @@ -294,13 +306,16 @@ six==1.16.0 # via # azure-core # bleach + # cassandra-driver # dockerpty # ecdsa + # geomet # google-auth # isodate # jsonschema # paramiko # python-dateutil + # scylla-driver # websocket-client sniffio==1.3.0 # via trio @@ -322,7 +337,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # via testcontainers texttable==1.6.7 # via docker-compose diff --git a/requirements/3.9.txt b/requirements/3.9.txt index 668cb04d4..9c2ad531d 100644 --- a/requirements/3.9.txt +++ b/requirements/3.9.txt @@ -36,6 +36,8 @@ bleach==5.0.1 # via readme-renderer cachetools==5.2.0 # via google-auth +cassandra-driver==3.25.0 + # via testcontainers certifi==2022.12.7 # via # minio @@ -48,6 +50,8 @@ cffi==1.15.1 # pynacl charset-normalizer==2.1.1 # via requests +click==8.1.3 + # via geomet clickhouse-driver==0.2.5 # via testcontainers codecov==2.1.12 @@ -96,6 +100,10 @@ exceptiongroup==1.0.4 # trio flake8==3.7.9 # via -r requirements.in +geomet==0.2.1.post1 + # via + # cassandra-driver + # scylla-driver google-api-core[grpc]==2.11.0 # via google-cloud-pubsub google-auth==2.15.0 @@ -228,7 +236,7 @@ pytest==7.2.0 # pytest-cov pytest-cov==4.0.0 # via -r requirements.in -python-arango==7.5.3 +python-arango==7.5.4 # via testcontainers python-dateutil==2.8.2 # via pg8000 @@ -246,7 +254,9 @@ pytz==2022.6 pytz-deprecation-shim==0.1.0.post0 # via tzlocal pyyaml==5.4.1 - # via docker-compose + # via + # docker-compose + # scylla-driver readme-renderer==37.3 # via twine redis==4.4.0 @@ -282,6 +292,8 @@ rsa==4.9 # python-jose scramp==1.4.4 # via pg8000 +scylla-driver==3.25.10 + # via testcontainers secretstorage==3.3.3 # via keyring selenium==4.7.2 @@ -290,13 +302,16 @@ six==1.16.0 # via # azure-core # bleach + # cassandra-driver # dockerpty # ecdsa + # geomet # google-auth # isodate # jsonschema # paramiko # python-dateutil + # scylla-driver # websocket-client sniffio==1.3.0 # via trio @@ -318,7 +333,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # via testcontainers texttable==1.6.7 # via docker-compose diff --git a/setup.py b/setup.py index 82bfe42d9..2f8d973e9 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,8 @@ 'keycloak': ['python-keycloak'], 'arangodb': ['python-arango'], 'azurite': ['azure-storage-blob'], + 'cassandra': ['cassandra-driver'], + 'scylla': ['scylla-driver'], }, long_description_content_type="text/x-rst", long_description=long_description, diff --git a/testcontainers/cassandra.py b/testcontainers/cassandra.py new file mode 100644 index 000000000..b56a02054 --- /dev/null +++ b/testcontainers/cassandra.py @@ -0,0 +1,46 @@ +from testcontainers.core.config import MAX_TRIES +from testcontainers.core.generic import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs + + +class CassandraContainer(DockerContainer): + """ + Cassandra database container. + + Example + ------- + :: + + with CassandraContainer() as cassandra: + cluster = cassandra.get_cluster() + with cluster.connect() as session: + session.execute( + "CREATE KEYSPACE keyspace1 WITH replication = " + "{'class': 'SimpleStrategy', 'replication_factor': '1'};") + + """ + def __init__(self, image="rinscy/cassandra:latest", ports_to_expose=[9042]): + super(CassandraContainer, self).__init__(image) + self.ports_to_expose = ports_to_expose + self.with_exposed_ports(*self.ports_to_expose) + + @wait_container_is_ready() + def _connect(self): + wait_for_logs( + self, + predicate="Starting listening for CQL clients", + timeout=MAX_TRIES) + cluster = self.get_cluster() + cluster.connect() + + def start(self): + super(CassandraContainer, self).start() + self._connect() + return self + + def get_cluster(self, **kwargs): + from cassandra.cluster import Cluster + container = self.get_wrapped_container() + container.reload() + hostname = container.attrs['NetworkSettings']['IPAddress'] + return Cluster(contact_points=[hostname], **kwargs) diff --git a/testcontainers/scylla.py b/testcontainers/scylla.py new file mode 100644 index 000000000..9fe49a5e0 --- /dev/null +++ b/testcontainers/scylla.py @@ -0,0 +1,47 @@ +from testcontainers.core.config import MAX_TRIES +from testcontainers.core.generic import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs + + +class ScyllaContainer(DockerContainer): + """ + Scylla database container. + + Example + ------- + :: + + with ScyllaContainer() as scylla: + cluster = scylla.get_cluster() + with cluster.connect() as session: + session.execute( + "CREATE KEYSPACE keyspace1 WITH replication " + "= {'class': 'SimpleStrategy', 'replication_factor': '1'};") + + """ + def __init__(self, image="scylladb/scylla:latest", ports_to_expose=[9042]): + super(ScyllaContainer, self).__init__(image) + self.ports_to_expose = ports_to_expose + self.with_exposed_ports(*self.ports_to_expose) + self.with_command("--skip-wait-for-gossip-to-settle=0") + + @wait_container_is_ready() + def _connect(self): + wait_for_logs( + self, + predicate="Starting listening for CQL clients", + timeout=MAX_TRIES) + cluster = self.get_cluster() + cluster.connect() + + def start(self): + super(ScyllaContainer, self).start() + self._connect() + return self + + def get_cluster(self, **kwargs): + from cassandra.cluster import Cluster + container = self.get_wrapped_container() + container.reload() + hostname = container.attrs['NetworkSettings']['IPAddress'] + return Cluster(contact_points=[hostname], **kwargs) diff --git a/tests/test_cassandra.py b/tests/test_cassandra.py new file mode 100644 index 000000000..bf2aca017 --- /dev/null +++ b/tests/test_cassandra.py @@ -0,0 +1,18 @@ +from testcontainers.cassandra import CassandraContainer + + +def test_docker_run_cassandra(): + with CassandraContainer() as cassandra: + cluster = cassandra.get_cluster() + with cluster.connect() as session: + session.execute( + "CREATE KEYSPACE keyspace1 WITH replication = " + "{'class': 'SimpleStrategy', 'replication_factor': '1'};") + session.execute( + "CREATE TABLE keyspace1.table1 (key1 int, key2 int, PRIMARY KEY (key1));") + session.execute("INSERT INTO keyspace1.table1 (key1,key2) values (1,2);") + + response = session.execute("SELECT * FROM keyspace1.table1") + + assert response.one().key1 == 1 + assert response.one().key2 == 2 diff --git a/tests/test_scylla.py b/tests/test_scylla.py new file mode 100644 index 000000000..a8515e931 --- /dev/null +++ b/tests/test_scylla.py @@ -0,0 +1,18 @@ +from testcontainers.scylla import ScyllaContainer + + +def test_docker_run_scylla(): + with ScyllaContainer() as scylla: + cluster = scylla.get_cluster() + with cluster.connect() as session: + session.execute( + "CREATE KEYSPACE keyspace1 WITH replication = " + "{'class': 'SimpleStrategy', 'replication_factor': '1'};") + session.execute( + "CREATE TABLE keyspace1.table1 (key1 int, key2 int, PRIMARY KEY (key1));") + session.execute("INSERT INTO keyspace1.table1 (key1,key2) values (1,2);") + + response = session.execute("SELECT * FROM keyspace1.table1") + + assert response.one().key1 == 1 + assert response.one().key2 == 2