diff --git a/.github/workflows/ci-community.yml b/.github/workflows/ci-community.yml index 92e58e51..e750f02c 100644 --- a/.github/workflows/ci-community.yml +++ b/.github/workflows/ci-community.yml @@ -25,6 +25,7 @@ jobs: - arangodb - azurite - clickhouse + - compose - elasticsearch - google - kafka diff --git a/INDEX.rst b/INDEX.rst index be5e3d1c..2b61fb64 100644 --- a/INDEX.rst +++ b/INDEX.rst @@ -18,6 +18,7 @@ testcontainers-python facilitates the use of Docker containers for functional an modules/arangodb/README modules/azurite/README modules/clickhouse/README + modules/compose/README modules/elasticsearch/README modules/google/README modules/kafka/README diff --git a/modules/compose/README.rst b/modules/compose/README.rst new file mode 100644 index 00000000..beafee0d --- /dev/null +++ b/modules/compose/README.rst @@ -0,0 +1 @@ +.. autoclass:: testcontainers.compose.DockerCompose diff --git a/modules/compose/testcontainers/compose/__init__.py b/modules/compose/testcontainers/compose/__init__.py new file mode 100644 index 00000000..fa6ae3dc --- /dev/null +++ b/modules/compose/testcontainers/compose/__init__.py @@ -0,0 +1,227 @@ +# +# 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 subprocess +from typing import Iterable, Optional, Union # noqa: UP035 + +import requests + +from testcontainers.core.exceptions import NoSuchPortExposed +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class DockerCompose: + """ + Manage docker compose environments. + + Args: + filepath: Relative directory containing the docker compose configuration file. + compose_file_name: File name of the docker compose configuration file. + compose_command: The command to use for docker compose. If not specified, a call to + docker compose --help will be made to determine the correct command to use. + If docker compose is not installed, docker-compose will be used. + pull: Pull images before launching environment. + build: Build images referenced in the configuration file. + env_file: Path to an env file containing environment variables to pass to docker compose. + services: List of services to start. + + Example: + + This example spins up chrome and firefox containers using docker compose. + + .. doctest:: + + >>> from testcontainers.compose import DockerCompose + + >>> compose = DockerCompose("compose/tests", compose_file_name="docker-compose-4.yml", + ... pull=True) + >>> with compose: + ... stdout, stderr = compose.get_logs() + >>> b"Hello from Docker!" in stdout + True + + .. code-block:: yaml + + services: + hello-world: + image: "hello-world" + """ + + def __init__( + self, + filepath: str, + compose_file_name: Union[str, Iterable] = "docker-compose.yml", + compose_command: Optional[str] = None, + pull: bool = False, + build: bool = False, + env_file: Optional[str] = None, + services: Optional[list[str]] = None, + ) -> None: + self.filepath = filepath + if isinstance(compose_file_name, str): + self.compose_file_names = [compose_file_name] + else: + self.compose_file_names = list(compose_file_name) + self.pull = pull + self.build = build + self.env_file = env_file + self.services = services + self.compose_command = self._get_compose_command(compose_command) + + def __enter__(self) -> "DockerCompose": + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.stop() + + def _get_compose_command(self, command): + """ + Returns the basecommand parts used for the docker compose commands + depending on the docker compose api. + + Returns + ------- + list[str] + The docker compose command parts + """ + if command: + return command.split(" ") + + if ( + subprocess.run( + ["docker", "compose", "--help"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + ).returncode + == 0 + ): + return ["docker", "compose"] + + return ["docker-compose"] + + def docker_compose_command(self) -> list[str]: + """ + Returns command parts used for the docker compose commands + + Returns: + cmd: Docker compose command parts. + """ + docker_compose_cmd = self.compose_command.copy() + for file in self.compose_file_names: + docker_compose_cmd += ["-f", file] + if self.env_file: + docker_compose_cmd += ["--env-file", self.env_file] + return docker_compose_cmd + + def start(self) -> None: + """ + Starts the docker compose environment. + """ + if self.pull: + pull_cmd = [*self.docker_compose_command(), "pull"] + self._call_command(cmd=pull_cmd) + + up_cmd = [*self.docker_compose_command(), "up", "-d"] + if self.build: + up_cmd.append("--build") + if self.services: + up_cmd.extend(self.services) + self._call_command(cmd=up_cmd) + + def stop(self) -> None: + """ + Stops the docker compose environment. + """ + down_cmd = [*self.docker_compose_command(), "down", "-v"] + self._call_command(cmd=down_cmd) + + def get_logs(self) -> tuple[str, str]: + """ + Returns all log output from stdout and stderr + + Returns: + stdout: Standard output stream. + stderr: Standard error stream. + """ + logs_cmd = [*self.docker_compose_command(), "logs"] + result = subprocess.run(logs_cmd, cwd=self.filepath, capture_output=True) + return result.stdout, result.stderr + + def exec_in_container(self, service_name: str, command: list[str]) -> tuple[str, str]: + """ + Executes a command in the container of one of the services. + + Args: + service_name: Name of the docker compose service to run the command in. + command: Command to execute. + + Returns: + stdout: Standard output stream. + stderr: Standard error stream. + """ + exec_cmd = [*self.docker_compose_command(), "exec", "-T", service_name, *command] + result = subprocess.run(exec_cmd, cwd=self.filepath, capture_output=True) + return result.stdout.decode("utf-8"), result.stderr.decode("utf-8"), result.returncode + + def get_service_port(self, service_name: str, port: int) -> int: + """ + Returns the mapped port for one of the services. + + Args: + service_name: Name of the docker compose service. + port: Internal port to get the mapping for. + + Returns: + mapped_port: Mapped port on the host. + """ + return self._get_service_info(service_name, port)[1] + + def get_service_host(self, service_name: str, port: int) -> str: + """ + Returns the host for one of the services. + + Args: + service_name: Name of the docker compose service. + port: Internal port to get the mapping for. + + Returns: + host: Hostname for the service. + """ + return self._get_service_info(service_name, port)[0] + + def _get_service_info(self, service: str, port: int) -> list[str]: + port_cmd = [*self.docker_compose_command(), "port", service, str(port)] + try: + output = subprocess.check_output(port_cmd, cwd=self.filepath).decode("utf-8") + except subprocess.CalledProcessError as e: + raise NoSuchPortExposed(str(e.stderr)) from e + result = str(output).rstrip().split(":") + if len(result) != 2 or not all(result): + raise NoSuchPortExposed(f"port {port} is not exposed for service {service}") + return result + + def _call_command(self, cmd: Union[str, list[str]], filepath: Optional[str] = None) -> None: + if filepath is None: + filepath = self.filepath + subprocess.call(cmd, cwd=filepath) + + @wait_container_is_ready(requests.exceptions.ConnectionError) + def wait_for(self, url: str) -> "DockerCompose": + """ + Waits for a response from a given URL. This is typically used to block until a service in + the environment has started and is responding. Note that it does not assert any sort of + return code, only check that the connection was successful. + + Args: + url: URL from one of the services in the environment to use to wait on. + """ + requests.get(url) + return self diff --git a/modules/compose/tests/.env.test b/modules/compose/tests/.env.test new file mode 100644 index 00000000..84b2baaf --- /dev/null +++ b/modules/compose/tests/.env.test @@ -0,0 +1 @@ +TAG_TEST_ASSERT_KEY="test_has_passed" diff --git a/modules/compose/tests/docker-compose-2.yml b/modules/compose/tests/docker-compose-2.yml new file mode 100644 index 00000000..e360bb01 --- /dev/null +++ b/modules/compose/tests/docker-compose-2.yml @@ -0,0 +1,6 @@ +services: + alpine: + image: alpine + command: sleep 3600 + ports: + - "3306:3306" diff --git a/modules/compose/tests/docker-compose-3.yml b/modules/compose/tests/docker-compose-3.yml new file mode 100644 index 00000000..874126c7 --- /dev/null +++ b/modules/compose/tests/docker-compose-3.yml @@ -0,0 +1,8 @@ +services: + alpine: + image: alpine + command: sleep 3600 + ports: + - "3306:3306" + environment: + TEST_ASSERT_KEY: ${TAG_TEST_ASSERT_KEY} diff --git a/modules/compose/tests/docker-compose-4.yml b/modules/compose/tests/docker-compose-4.yml new file mode 100644 index 00000000..081e598d --- /dev/null +++ b/modules/compose/tests/docker-compose-4.yml @@ -0,0 +1,3 @@ +services: + hello-world: + image: "hello-world" diff --git a/modules/compose/tests/docker-compose.yml b/modules/compose/tests/docker-compose.yml new file mode 100644 index 00000000..6c12ae33 --- /dev/null +++ b/modules/compose/tests/docker-compose.yml @@ -0,0 +1,17 @@ +services: + hub: + image: selenium/hub + ports: + - "4444:4444" + firefox: + image: selenium/node-firefox + links: + - hub + expose: + - "5555" + chrome: + image: selenium/node-chrome + links: + - hub + expose: + - "5555" diff --git a/modules/compose/tests/test_docker_compose.py b/modules/compose/tests/test_docker_compose.py new file mode 100644 index 00000000..bcc1e49c --- /dev/null +++ b/modules/compose/tests/test_docker_compose.py @@ -0,0 +1,128 @@ +import os +from unittest.mock import patch + +import pytest + +from testcontainers.compose import DockerCompose +from testcontainers.core.docker_client import DockerClient +from testcontainers.core.exceptions import NoSuchPortExposed +from testcontainers.core.waiting_utils import wait_for_logs + +ROOT = os.path.dirname(__file__) + + +def test_can_spawn_service_via_compose(): + with DockerCompose(ROOT) as compose: + host = compose.get_service_host("hub", 4444) + port = compose.get_service_port("hub", 4444) + assert host == "0.0.0.0" + assert port == "4444" + + +def test_can_pull_images_before_spawning_service_via_compose(): + with DockerCompose(ROOT, pull=True) as compose: + host = compose.get_service_host("hub", 4444) + port = compose.get_service_port("hub", 4444) + assert host == "0.0.0.0" + assert port == "4444" + + +def test_can_build_images_before_spawning_service_via_compose(): + with patch.object(DockerCompose, "_call_command") as call_mock: + with DockerCompose(ROOT, build=True) as compose: + ... + + assert compose.build + docker_compose_cmd = call_mock.call_args_list[0][1]["cmd"] + assert "docker-compose" in docker_compose_cmd or ( + "docker" in docker_compose_cmd and "compose" in docker_compose_cmd + ) + assert "up" in docker_compose_cmd + assert "--build" in docker_compose_cmd + + +def test_can_specify_services(): + with patch.object(DockerCompose, "_call_command") as call_mock: + with DockerCompose(ROOT, services=["hub", "firefox"]) as compose: + ... + + assert compose.services + docker_compose_cmd = call_mock.call_args_list[0][1]["cmd"] + services_at_the_end = docker_compose_cmd[-2:] + assert "firefox" in services_at_the_end + assert "hub" in services_at_the_end + assert "chrome" not in docker_compose_cmd + + +@pytest.mark.parametrize( + "should_run_hub", + [ + [True], + [False], + ], +) +def test_can_run_specific_services(should_run_hub: bool): + # compose V2 will improve this test by being able to assert that "firefox" also started/exited + services = ["firefox"] + if should_run_hub: + services.append("hub") + + with DockerCompose(ROOT, services=services) as compose: + if should_run_hub: + assert compose.get_service_host("hub", 4444) + assert compose.get_service_port("hub", 4444) + else: + with pytest.raises(NoSuchPortExposed): + assert compose.get_service_host("hub", 4444) + + +def test_can_throw_exception_if_no_port_exposed(): + with DockerCompose(ROOT) as compose: + with pytest.raises(NoSuchPortExposed): + compose.get_service_host("hub", 5555) + + +def test_compose_wait_for_container_ready(): + with DockerCompose(ROOT) as compose: + docker = DockerClient() + compose.wait_for("http://%s:4444/wd/hub" % docker.host()) + + +def test_compose_can_wait_for_logs(): + with DockerCompose(filepath=ROOT, compose_file_name="docker-compose-4.yml") as compose: + wait_for_logs(compose, "Hello from Docker!") + + +def test_can_parse_multiple_compose_files(): + with DockerCompose(filepath=ROOT, compose_file_name=["docker-compose.yml", "docker-compose-2.yml"]) as compose: + host = compose.get_service_host("alpine", 3306) + port = compose.get_service_port("alpine", 3306) + assert host == "0.0.0.0" + assert port == "3306" + + host = compose.get_service_host("hub", 4444) + port = compose.get_service_port("hub", 4444) + assert host == "0.0.0.0" + assert port == "4444" + + +def test_can_get_logs(): + with DockerCompose(ROOT) as compose: + docker = DockerClient() + compose.wait_for("http://%s:4444/wd/hub" % docker.host()) + stdout, stderr = compose.get_logs() + assert stdout, "There should be something on stdout" + + +def test_can_pass_env_params_by_env_file(): + with DockerCompose(ROOT, compose_file_name="docker-compose-3.yml", env_file=".env.test") as compose: + stdout, *_ = compose.exec_in_container("alpine", ["printenv"]) + assert stdout.splitlines()[0], "test_has_passed" + + +def test_can_exec_commands(): + with DockerCompose(ROOT) as compose: + result = compose.exec_in_container("hub", ["echo", "my_test"]) + assert result[0] == "my_test\n", "The echo should be successful" + assert result[1] == "", "stderr should be empty" + assert result[2] == 0, "The exit code should be successful" diff --git a/poetry.lock b/poetry.lock index 41eda34b..3a072f62 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -1655,6 +1655,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -1663,6 +1664,8 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -2194,6 +2197,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2201,8 +2205,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2219,6 +2231,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2226,6 +2239,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2982,6 +2996,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p arangodb = ["python-arango"] azurite = ["azure-storage-blob"] clickhouse = ["clickhouse-driver"] +compose = [] elasticsearch = [] google = ["google-cloud-pubsub"] k3s = ["kubernetes", "pyyaml"] @@ -3004,4 +3019,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "89891a5aeea49686e42fd95780d6aa703a1a13d8483a1a965aa32603b39f3a3d" +content-hash = "4f9105ccc0ce459ccdd0f7217fca0fb6292a0e79cc5220aee0d08961d8043c4a" diff --git a/pyproject.toml b/pyproject.toml index b825d8eb..416082ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ packages = [ { include = "testcontainers", from = "modules/arangodb" }, { include = "testcontainers", from = "modules/azurite" }, { include = "testcontainers", from = "modules/clickhouse" }, + { include = "testcontainers", from = "modules/compose" }, { include = "testcontainers", from = "modules/elasticsearch" }, { include = "testcontainers", from = "modules/google" }, { include = "testcontainers", from = "modules/k3s" }, @@ -88,6 +89,7 @@ selenium = { version = "*", optional = true } arangodb = ["python-arango"] azurite = ["azure-storage-blob"] clickhouse = ["clickhouse-driver"] +compose = [] elasticsearch = [] google = ["google-cloud-pubsub"] k3s = ["kubernetes", "pyyaml"]