From 830e556a60501661e5e9718ead8f6b37b04e5cec Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 2 Mar 2024 00:23:27 +0000 Subject: [PATCH 01/16] Add untested code --- modules/nats/README.rst | 1 + modules/nats/testcontainers/nats/__init__.py | 69 ++++++++++++++++++++ modules/nats/tests/test_nats.py | 24 +++++++ 3 files changed, 94 insertions(+) create mode 100644 modules/nats/README.rst create mode 100644 modules/nats/testcontainers/nats/__init__.py create mode 100644 modules/nats/tests/test_nats.py diff --git a/modules/nats/README.rst b/modules/nats/README.rst new file mode 100644 index 000000000..a38a21466 --- /dev/null +++ b/modules/nats/README.rst @@ -0,0 +1 @@ +.. autoclass:: testcontainers.nats.NatsContainer diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py new file mode 100644 index 000000000..dabecfc44 --- /dev/null +++ b/modules/nats/testcontainers/nats/__init__.py @@ -0,0 +1,69 @@ +# +# 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. + +from testcontainers.core.container import DockerContainer +from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.waiting_utils import wait_container_is_ready +import typing + +import asyncio +import nats +from nats.errors import ConnectionClosedError, TimeoutError, NoServersError +from nats.aio.client import Client as NATSClient + + +class NatsContainer(DockerContainer): + """ + Nats container. + + Example: + + .. doctest:: + + >>> from testcontainers.nats import NatsContainer + + >>> with NatsContainer() as nats_container: + ... redis_client = redis_container.get_client() + """ + + def __init__(self, image: str = "nats:latest", client_port: int = 4222, manamgent_port:int = 8222, password: typing.Optional[str] = None, **kwargs) -> None: + raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") + super().__init__(image, **kwargs) + self.client_port = client_port + self.management_port = manamgent_port + self.password = password + self.with_exposed_ports(self.client_port,self.management_port) + + @wait_container_is_ready(TimeoutError,NoServersError) + def _connect(self) -> None: + pass + + async def get_client(self, **kwargs) -> NATSClient: + """ + Get a nats client. + + Args: + **kwargs: Keyword arguments passed to `redis.Redis`. + + Returns: + client: Nats client to connect to the container. + """ + conn_string = f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.client_port)}" + client = await nats.connect(conn_string) + return client + + + def start(self) -> "NatsContainer": + super().start() + self._connect() + return self diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py new file mode 100644 index 000000000..8256032d7 --- /dev/null +++ b/modules/nats/tests/test_nats.py @@ -0,0 +1,24 @@ +from testcontainers.nats import NatsContainer +import anyio +from nats.aio.client import Client as NATSClient +import uuid + +import pytest +pytest.mark.usefixtures("anyio_backend") + + +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) +async def test_basic_publishing(anyio_backend): + with NatsContainer() as container: + nc: NATSClient = container.get_client() + + topic= str(uuid.uuid4()) + + sub = await nc.subscribe(topic) + sent_message = b'Test-Containers' + await nc.publish(topic, b'Test-Containers') + received_msg = await sub.next_msg() + print("Received:", received_msg) + assert sent_message == received_msg + await nc.flush() + await nc.close() \ No newline at end of file From 51b89c6bde19cc6a7bd2121c4aa211254e8fa618 Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 2 Mar 2024 17:19:28 -0500 Subject: [PATCH 02/16] Add working test for nats --- modules/nats/tests/test_nats.py | 4 +-- poetry.lock | 58 +++++++++++++++++++++++++++++++-- pyproject.toml | 5 +++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index 8256032d7..25d03c0bc 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_basic_publishing(anyio_backend): with NatsContainer() as container: - nc: NATSClient = container.get_client() + nc: NATSClient = await container.get_client() topic= str(uuid.uuid4()) @@ -19,6 +19,6 @@ async def test_basic_publishing(anyio_backend): await nc.publish(topic, b'Test-Containers') received_msg = await sub.next_msg() print("Received:", received_msg) - assert sent_message == received_msg + assert sent_message == received_msg.data await nc.flush() await nc.close() \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 5cf56c8cb..a9cdc6a71 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.8.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -11,6 +11,28 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "argon2-cffi" version = "23.1.0" @@ -1417,6 +1439,21 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nats-py" +version = "2.7.2" +description = "NATS client for Python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "nats-py-2.7.2.tar.gz", hash = "sha256:0c97b4a57bed0ef1ff9ae6c19bc115ec7ca8ede5ab3e001fd00a377056a547cf"}, +] + +[package.extras] +aiohttp = ["aiohttp"] +fast-parse = ["fast-mail-parser"] +nkeys = ["nkeys"] + [[package]] name = "neo4j" version = "5.16.0" @@ -1655,6 +1692,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 +1701,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 +2234,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 +2242,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 +2268,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 +2276,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"}, @@ -2462,7 +2513,7 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, @@ -2992,6 +3043,7 @@ minio = ["minio"] mongodb = ["pymongo"] mssql = ["pymssql", "sqlalchemy"] mysql = ["pymysql", "sqlalchemy"] +nats = ["nats-py"] neo4j = ["neo4j"] nginx = [] opensearch = ["opensearch-py"] @@ -3004,4 +3056,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "0316bbdbb2824f6086c32cb9ed00a23507896bf48494a38d086e281faf1f8189" +content-hash = "5f2127e8ad17aadeef9be812f84c5f59e76347325485da6427ae6e6c0aade839" diff --git a/pyproject.toml b/pyproject.toml index 2059926c9..848d5f663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ packages = [ { include = "testcontainers", from = "modules/mongodb" }, { include = "testcontainers", from = "modules/mssql" }, { include = "testcontainers", from = "modules/mysql" }, + { include = "testcontainers", from = "modules/nats" }, { include = "testcontainers", from = "modules/neo4j" }, { include = "testcontainers", from = "modules/nginx" }, { include = "testcontainers", from = "modules/opensearch" }, @@ -83,6 +84,7 @@ psycopg2-binary = { version = "*", optional = true } pika = { version = "*", optional = true } redis = { version = "*", optional = true } selenium = { version = "*", optional = true } +nats-py = { version = "*", optional = true } [tool.poetry.extras] arangodb = ["python-arango"] @@ -98,6 +100,7 @@ minio = ["minio"] mongodb = ["pymongo"] mssql = ["sqlalchemy", "pymssql"] mysql = ["sqlalchemy", "pymysql"] +nats = ["nats-py"] neo4j = ["neo4j"] nginx = [] opensearch = ["opensearch-py"] @@ -114,6 +117,7 @@ sphinx = "^7.2.6" pg8000 = "*" twine = "^4.0.2" mypy = "1.7.1" +anyio = "^4.3.0" [[tool.poetry.source]] name = "PyPI" @@ -219,6 +223,7 @@ mypy_path = [ # "modules/mongodb", # "modules/mssql", # "modules/mysql", +# "modules/nats", # "modules/neo4j", # "modules/nginx", # "modules/opensearch", From a0ee763d923cec4a993e780b4f331945bd36fb49 Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 2 Mar 2024 21:03:13 -0500 Subject: [PATCH 03/16] Add additional test --- .pre-commit-config.yaml | 2 +- modules/nats/testcontainers/nats/__init__.py | 44 ++++++++++++------ modules/nats/tests/test_nats.py | 47 +++++++++++++++++--- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d2a53b63..a960cc85e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.9 + python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index dabecfc44..e803811da 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -11,15 +11,16 @@ # License for the specific language governing permissions and limitations # under the License. -from testcontainers.core.container import DockerContainer -from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready + +# import asyncio import typing +import uuid -import asyncio import nats -from nats.errors import ConnectionClosedError, TimeoutError, NoServersError from nats.aio.client import Client as NATSClient +from nats.errors import NoServersError, TimeoutError +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready class NatsContainer(DockerContainer): @@ -36,17 +37,33 @@ class NatsContainer(DockerContainer): ... redis_client = redis_container.get_client() """ - def __init__(self, image: str = "nats:latest", client_port: int = 4222, manamgent_port:int = 8222, password: typing.Optional[str] = None, **kwargs) -> None: - raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") + def __init__( + self, + image: str = "nats:latest", + client_port: int = 4222, + manamgent_port: int = 8222, + password: typing.Optional[str] = None, + **kwargs, + ) -> None: super().__init__(image, **kwargs) self.client_port = client_port self.management_port = manamgent_port self.password = password - self.with_exposed_ports(self.client_port,self.management_port) - - @wait_container_is_ready(TimeoutError,NoServersError) - def _connect(self) -> None: - pass + self.with_exposed_ports(self.client_port, self.management_port) + + @wait_container_is_ready(TimeoutError, NoServersError) + def _healthcheck(self) -> None: + + async def _ping(): + topic = str(uuid.uuid4()) + nc: NATSClient = await self.get_client() + await nc.publish(topic, b"Test-Containers") + await nc.flush() + await nc.close() + + # loop = asyncio.get_event_loop() + # coro = _ping() + # return loop.run_until_complete(coro) async def get_client(self, **kwargs) -> NATSClient: """ @@ -62,8 +79,7 @@ async def get_client(self, **kwargs) -> NATSClient: client = await nats.connect(conn_string) return client - def start(self) -> "NatsContainer": super().start() - self._connect() + self._healthcheck() return self diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index 25d03c0bc..ac7edf1c0 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -4,21 +4,54 @@ import uuid import pytest + pytest.mark.usefixtures("anyio_backend") @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_basic_publishing(anyio_backend): with NatsContainer() as container: - nc: NATSClient = await container.get_client() - - topic= str(uuid.uuid4()) - + nc: NATSClient = await container.get_client() + + topic = str(uuid.uuid4()) + sub = await nc.subscribe(topic) - sent_message = b'Test-Containers' - await nc.publish(topic, b'Test-Containers') + sent_message = b"Test-Containers" + await nc.publish(topic, b"Test-Containers") received_msg = await sub.next_msg() print("Received:", received_msg) assert sent_message == received_msg.data await nc.flush() - await nc.close() \ No newline at end of file + await nc.close() + + +pytest.mark.usefixtures("anyio_backend") + + +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) +async def test_more_complex_example(anyio_backend): + with NatsContainer() as container: + nc: NATSClient = await container.get_client() + + await nc.publish("greet.joe", b"hello") + + sub = await nc.subscribe("greet.*") + + try: + await sub.next_msg(timeout=0.1) + except TimeoutError: + pass + + await nc.publish("greet.joe", b"hello.joe") + await nc.publish("greet.pam", b"hello.pam") + + first = await sub.next_msg(timeout=0.1) + assert b"hello.joe" == first.data + + second = await sub.next_msg(timeout=0.1) + assert b"hello.pam" == second.data + + await nc.publish("greet.bob", b"hello") + + await sub.unsubscribe() + await nc.drain() From f4b33f5e2a223fa252dcb89210bc4ae2880e3f77 Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sun, 3 Mar 2024 10:15:24 -0500 Subject: [PATCH 04/16] Try fix doc test --- INDEX.rst | 1 + modules/nats/testcontainers/nats/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/INDEX.rst b/INDEX.rst index be5e3d1cd..dfaa34b8f 100644 --- a/INDEX.rst +++ b/INDEX.rst @@ -27,6 +27,7 @@ testcontainers-python facilitates the use of Docker containers for functional an modules/mongodb/README modules/mssql/README modules/mysql/README + modules/nats/README modules/neo4j/README modules/nginx/README modules/opensearch/README diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index e803811da..8f41b8d9b 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -34,7 +34,7 @@ class NatsContainer(DockerContainer): >>> from testcontainers.nats import NatsContainer >>> with NatsContainer() as nats_container: - ... redis_client = redis_container.get_client() + ... nc = nats_container.get_client() """ def __init__( From f8758b10c8c6f242913e43f0115f5536aca8a1c4 Mon Sep 17 00:00:00 2001 From: bstrausser Date: Wed, 6 Mar 2024 14:38:54 -0500 Subject: [PATCH 05/16] Use log based container ready --- modules/nats/testcontainers/nats/__init__.py | 22 +++++++------------- modules/nats/tests/test_nats.py | 2 ++ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index 8f41b8d9b..cce6abea1 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -14,13 +14,11 @@ # import asyncio import typing -import uuid import nats from nats.aio.client import Client as NATSClient -from nats.errors import NoServersError, TimeoutError from testcontainers.core.container import DockerContainer -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs class NatsContainer(DockerContainer): @@ -43,27 +41,21 @@ def __init__( client_port: int = 4222, manamgent_port: int = 8222, password: typing.Optional[str] = None, + expected_ready_log: str = "Server is ready", + ready_timeout_secs: int = 120, **kwargs, ) -> None: super().__init__(image, **kwargs) self.client_port = client_port self.management_port = manamgent_port self.password = password + self._expected_ready_log = expected_ready_log + self._ready_timeout_secs = max(ready_timeout_secs, 0) self.with_exposed_ports(self.client_port, self.management_port) - @wait_container_is_ready(TimeoutError, NoServersError) + @wait_container_is_ready() def _healthcheck(self) -> None: - - async def _ping(): - topic = str(uuid.uuid4()) - nc: NATSClient = await self.get_client() - await nc.publish(topic, b"Test-Containers") - await nc.flush() - await nc.close() - - # loop = asyncio.get_event_loop() - # coro = _ping() - # return loop.run_until_complete(coro) + wait_for_logs(self, self._expected_ready_log, timeout=self._ready_timeout_secs) async def get_client(self, **kwargs) -> NATSClient: """ diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index ac7edf1c0..f3893042f 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -1,8 +1,10 @@ from testcontainers.nats import NatsContainer +from testcontainers.core import config import anyio from nats.aio.client import Client as NATSClient import uuid +import os import pytest pytest.mark.usefixtures("anyio_backend") From c4d3a2db6bb915abaccb58c1f5463600ba93c6b6 Mon Sep 17 00:00:00 2001 From: David Ankin Date: Wed, 6 Mar 2024 23:34:13 -0500 Subject: [PATCH 06/16] update lockfile --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index afb17681d..1297807ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -3175,4 +3175,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "f4cb027301e265217ccb581b0ddd06fe6d91319fbcfbc3d20504a1fdbc45d7b1" +content-hash = "cc717c8c0bb7e05db643e5cb274b68647faffa3f3ee1357a0e20e1d6a9147008" From 411451579619e79b9a09a2b07c47ddff35eb90d9 Mon Sep 17 00:00:00 2001 From: David Ankin Date: Wed, 6 Mar 2024 23:57:58 -0500 Subject: [PATCH 07/16] some cleanup in tests, pytest-asyncio --- modules/nats/testcontainers/nats/__init__.py | 18 +++++++-------- modules/nats/tests/test_nats.py | 23 +++++++------------- poetry.lock | 20 ++++++++++++++++- pyproject.toml | 1 + 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index cce6abea1..5b6b26dd7 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -12,10 +12,7 @@ # under the License. -# import asyncio -import typing - -import nats +from nats import connect as nats_connect from nats.aio.client import Client as NATSClient from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -39,16 +36,14 @@ def __init__( self, image: str = "nats:latest", client_port: int = 4222, - manamgent_port: int = 8222, - password: typing.Optional[str] = None, + management_port: int = 8222, expected_ready_log: str = "Server is ready", ready_timeout_secs: int = 120, **kwargs, ) -> None: super().__init__(image, **kwargs) self.client_port = client_port - self.management_port = manamgent_port - self.password = password + self.management_port = management_port self._expected_ready_log = expected_ready_log self._ready_timeout_secs = max(ready_timeout_secs, 0) self.with_exposed_ports(self.client_port, self.management_port) @@ -57,6 +52,9 @@ def __init__( def _healthcheck(self) -> None: wait_for_logs(self, self._expected_ready_log, timeout=self._ready_timeout_secs) + def get_conn_string(self): + return f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.client_port)}" + async def get_client(self, **kwargs) -> NATSClient: """ Get a nats client. @@ -67,8 +65,8 @@ async def get_client(self, **kwargs) -> NATSClient: Returns: client: Nats client to connect to the container. """ - conn_string = f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.client_port)}" - client = await nats.connect(conn_string) + conn_string = self.get_conn_string() + client = await nats_connect(conn_string) return client def start(self) -> "NatsContainer": diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index f3893042f..9d464cb12 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -1,21 +1,17 @@ -from testcontainers.nats import NatsContainer -from testcontainers.core import config -import anyio -from nats.aio.client import Client as NATSClient -import uuid +from uuid import uuid4 -import os import pytest +from nats.aio.client import Client as NATSClient -pytest.mark.usefixtures("anyio_backend") +from testcontainers.nats import NatsContainer -@pytest.mark.parametrize("anyio_backend", ["asyncio"]) -async def test_basic_publishing(anyio_backend): +@pytest.mark.asyncio +async def test_basic_publishing(): with NatsContainer() as container: nc: NATSClient = await container.get_client() - topic = str(uuid.uuid4()) + topic = str(uuid4()) sub = await nc.subscribe(topic) sent_message = b"Test-Containers" @@ -27,11 +23,8 @@ async def test_basic_publishing(anyio_backend): await nc.close() -pytest.mark.usefixtures("anyio_backend") - - -@pytest.mark.parametrize("anyio_backend", ["asyncio"]) -async def test_more_complex_example(anyio_backend): +@pytest.mark.asyncio +async def test_more_complex_example(): with NatsContainer() as container: nc: NATSClient = await container.get_client() diff --git a/poetry.lock b/poetry.lock index 1297807ac..2ef48d2a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2180,6 +2180,24 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.5" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, + {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-cov" version = "4.1.0" @@ -3175,4 +3193,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "cc717c8c0bb7e05db643e5cb274b68647faffa3f3ee1357a0e20e1d6a9147008" +content-hash = "9a546ef8b1e1509a5d98576b207b05ec58fd29f5cc4c845cb4262116ee76a30e" diff --git a/pyproject.toml b/pyproject.toml index 63022ff9f..326170809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ pytest-cov = "4.1.0" sphinx = "^7.2.6" twine = "^4.0.2" anyio = "^4.3.0" +pytest-asyncio = "^0.23.5" [[tool.poetry.source]] name = "PyPI" From fcb2fe4e07813c0f1c97960ad34d4bb10e2ecc3c Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 9 Mar 2024 13:23:50 -0500 Subject: [PATCH 08/16] Remove client as hard-dependency --- .devcontainer/commands/post-create-command.sh | 1 + modules/nats/testcontainers/nats/__init__.py | 18 ++---- modules/nats/tests/test_nats.py | 58 +++++++++++++++++-- poetry.lock | 4 +- pyproject.toml | 2 +- 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/.devcontainer/commands/post-create-command.sh b/.devcontainer/commands/post-create-command.sh index c3229490f..aee5cecf2 100755 --- a/.devcontainer/commands/post-create-command.sh +++ b/.devcontainer/commands/post-create-command.sh @@ -2,4 +2,5 @@ echo "Running post-create-command.sh" curl -sSL https://install.python-poetry.org | python3 - +poetry lock --no-update poetry install --all-extras diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index 5b6b26dd7..4f6ed30d4 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -13,7 +13,6 @@ from nats import connect as nats_connect -from nats.aio.client import Client as NATSClient from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -52,22 +51,15 @@ def __init__( def _healthcheck(self) -> None: wait_for_logs(self, self._expected_ready_log, timeout=self._ready_timeout_secs) - def get_conn_string(self): + def nats_uri(self) -> str: return f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.client_port)}" - async def get_client(self, **kwargs) -> NATSClient: - """ - Get a nats client. - Args: - **kwargs: Keyword arguments passed to `redis.Redis`. + def nats_host_and_port(self) -> tuple[str, int]: + return self.get_container_host_ip(), self.get_exposed_port(self.client_port) - Returns: - client: Nats client to connect to the container. - """ - conn_string = self.get_conn_string() - client = await nats_connect(conn_string) - return client + def nats_management_uri(self) -> str: + return f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.management_port)}" def start(self) -> "NatsContainer": super().start() diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index 9d464cb12..b1345ace3 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -1,15 +1,62 @@ from uuid import uuid4 - import pytest from nats.aio.client import Client as NATSClient -from testcontainers.nats import NatsContainer + +""" +If you are developing this and you want to test more advanced scenarios using a client +Activate your poetry shell. +pip install nats-py +This will get nats-py into your environment but keep it out of the project + + +""" + + +NO_NATS_CLIENT = True +try: + from nats import connect as nats_connect + from nats.aio.client import Client as NATSClient + + NO_NATS_CLIENT = False +except ImportError: + pass + + +async def get_client(container: NatsContainer) -> NATSClient: + """ + Get a nats client. + + Returns: + client: Nats client to connect to the container. + """ + conn_string = container.nats_uri() + client = await nats_connect(conn_string) + return client + + @pytest.mark.asyncio -async def test_basic_publishing(): +async def test_basic_container_ops(anyio_backend): + with NatsContainer() as container: + # Not sure how to get type information without doing this + container: NatsContainer = container + h, p = container.nats_host_and_port() + assert h == "localhost" + uri = container.nats_uri() + management_uri = container.nats_management_uri() + + assert uri != management_uri + + +pytest.mark.usefixtures("anyio_backend") + + +@pytest.mark.skipif(NO_NATS_CLIENT, reason="No NATS Client Available") +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) with NatsContainer() as container: - nc: NATSClient = await container.get_client() + nc: NATSClient = await get_client(container) topic = str(uuid4()) @@ -25,8 +72,9 @@ async def test_basic_publishing(): @pytest.mark.asyncio async def test_more_complex_example(): +@pytest.mark.skipif(NO_NATS_CLIENT, reason="No NATS Client Available") with NatsContainer() as container: - nc: NATSClient = await container.get_client() + nc: NATSClient = await get_client(container) await nc.publish("greet.joe", b"hello") diff --git a/poetry.lock b/poetry.lock index 2ef48d2a1..ec7c58eb6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3180,7 +3180,7 @@ minio = ["minio"] mongodb = ["pymongo"] mssql = ["pymssql", "sqlalchemy"] mysql = ["pymysql", "sqlalchemy"] -nats = ["nats-py"] +nats = [] neo4j = ["neo4j"] nginx = [] opensearch = ["opensearch-py"] @@ -3193,4 +3193,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "9a546ef8b1e1509a5d98576b207b05ec58fd29f5cc4c845cb4262116ee76a30e" +content-hash = "0f720c1889b4c29fb263490f7ced364969717a89126bc7c6ecf5a082f32e5d26" diff --git a/pyproject.toml b/pyproject.toml index 326170809..e8dfec88d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ minio = ["minio"] mongodb = ["pymongo"] mssql = ["sqlalchemy", "pymssql"] mysql = ["sqlalchemy", "pymysql"] -nats = ["nats-py"] +nats = [] neo4j = ["neo4j"] nginx = [] opensearch = ["opensearch-py"] From 2f75b2153fa0720d19219b50b9471bd04c8b8798 Mon Sep 17 00:00:00 2001 From: Max Pfeiffer Date: Sat, 9 Mar 2024 12:13:32 +0100 Subject: [PATCH 09/16] fix: failing tests for elasticsearch on machines with ARM CPU (#454) - updated the image versions used for tests with the latest supported tags: https://hub.docker.com/_/elasticsearch - fixes https://github.com/testcontainers/testcontainers-python/issues/452 ![Screenshot 2024-03-09 at 09 49 53](https://github.com/testcontainers/testcontainers-python/assets/13573675/84384190-3cc2-47c3-b795-a86efc062e3c) --- modules/elasticsearch/tests/test_elasticsearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/elasticsearch/tests/test_elasticsearch.py b/modules/elasticsearch/tests/test_elasticsearch.py index e174ec47b..661a550c6 100644 --- a/modules/elasticsearch/tests/test_elasticsearch.py +++ b/modules/elasticsearch/tests/test_elasticsearch.py @@ -6,8 +6,8 @@ from testcontainers.elasticsearch import ElasticSearchContainer -# The versions below were the current supported versions at time of writing (2022-08-11) -@pytest.mark.parametrize("version", ["6.8.23", "7.17.5", "8.3.3"]) +# The versions below should reflect the latest stable releases +@pytest.mark.parametrize("version", ["7.17.18", "8.12.2"]) def test_docker_run_elasticsearch(version): with ElasticSearchContainer(f"elasticsearch:{version}", mem_limit="3G") as es: resp = urllib.request.urlopen(es.get_url()) From 8c9555451c20b3120c165150e7250f2a8c438397 Mon Sep 17 00:00:00 2001 From: David Ankin Date: Sat, 9 Mar 2024 06:14:19 -0500 Subject: [PATCH 10/16] fix(clickhouse): clickhouse waiting (#428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit im not actually sure if this fixes it but does: make clickhouse waiting look more like java-tc many other discussions about removing dependency on sqlalchemy + drivers --------- Co-authored-by: Bálint Bartha <39852431+totallyzen@users.noreply.github.com> --- .../testcontainers/clickhouse/__init__.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/clickhouse/testcontainers/clickhouse/__init__.py b/modules/clickhouse/testcontainers/clickhouse/__init__.py index 147940199..fbc8fab65 100644 --- a/modules/clickhouse/testcontainers/clickhouse/__init__.py +++ b/modules/clickhouse/testcontainers/clickhouse/__init__.py @@ -12,9 +12,8 @@ # under the License. import os from typing import Optional - -import clickhouse_driver -from clickhouse_driver.errors import Error +from urllib.error import HTTPError, URLError +from urllib.request import urlopen from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter @@ -48,7 +47,7 @@ def __init__( username: Optional[str] = None, password: Optional[str] = None, dbname: Optional[str] = None, - **kwargs + **kwargs, ) -> None: raise_for_deprecated_parameter(kwargs, "user", "username") super().__init__(image=image, **kwargs) @@ -57,11 +56,14 @@ def __init__( self.dbname = dbname or os.environ.get("CLICKHOUSE_DB", "test") self.port = port self.with_exposed_ports(self.port) + self.with_exposed_ports(8123) - @wait_container_is_ready(Error, EOFError) + @wait_container_is_ready(HTTPError, URLError) def _connect(self) -> None: - with clickhouse_driver.Client.from_url(self.get_connection_url()) as client: - client.execute("SELECT version()") + # noinspection HttpUrlsUsage + url = f"http://{self.get_container_host_ip()}:{self.get_exposed_port(8123)}" + with urlopen(url) as r: + assert b"Ok" in r.read() def _configure(self) -> None: self.with_env("CLICKHOUSE_USER", self.username) From 5b8ff31866fcb1f080812242118bd1f642f8a09e Mon Sep 17 00:00:00 2001 From: Shai Nagar Date: Sat, 9 Mar 2024 13:22:58 +0200 Subject: [PATCH 11/16] fix: unclosed socket warning in db containers (#378) This PR fixes #379 --- core/testcontainers/core/generic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/testcontainers/core/generic.py b/core/testcontainers/core/generic.py index 21bf9d7e4..a3bff96e2 100644 --- a/core/testcontainers/core/generic.py +++ b/core/testcontainers/core/generic.py @@ -36,7 +36,10 @@ def _connect(self) -> None: import sqlalchemy engine = sqlalchemy.create_engine(self.get_connection_url()) - engine.connect() + try: + engine.connect() + finally: + engine.dispose() def get_connection_url(self) -> str: raise NotImplementedError From 7bc73ce1cca55d6d2e9caaec3eda89ea4119765d Mon Sep 17 00:00:00 2001 From: Rodrigo Santa Cruz Ortega Date: Sat, 9 Mar 2024 09:07:48 -0500 Subject: [PATCH 12/16] fix: Close docker client when stopping the docker container (#380) Fixes the following warning ``` sys:1: ResourceWarning: unclosed ``` Related to #379 Co-authored-by: David Ankin --- core/testcontainers/core/container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 6ecc384bd..b21feabc2 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -76,6 +76,7 @@ def start(self) -> "DockerContainer": def stop(self, force=True, delete_volume=True) -> None: self._container.remove(force=force, v=delete_volume) + self.get_docker_client().client.close() def __enter__(self) -> "DockerContainer": return self.start() From ffbb2fc5876acbf208302c7be3d2058adbdf0f2d Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Sat, 9 Mar 2024 15:21:50 +0100 Subject: [PATCH 13/16] fix: Update the copyright header for readthedocs (#341) The current one references 2017, and it is confusing since the website may be perceived as outdated one --------- Co-authored-by: Till Hoffmann Co-authored-by: David Ankin --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 8ac304a25..5887d3a70 100644 --- a/conf.py +++ b/conf.py @@ -52,7 +52,7 @@ # General information about the project. project = "testcontainers" -copyright = "2017, Sergey Pirogov" # noqa: A001 +copyright = "2017-2024, Sergey Pirogov and Testcontainers Python contributors" # noqa: A001 author = "Sergey Pirogov" # The version info for the project you're documenting, acts as replacement for From 9058a25b4743d2c4b6ce92cee950e38ebe4cd3e9 Mon Sep 17 00:00:00 2001 From: David Ankin Date: Sat, 9 Mar 2024 10:52:58 -0500 Subject: [PATCH 14/16] fix(mongodb): waiting for container to start (it was not waiting at all before?) (#461) we were using this code to test if it was online or not:`MongoClient(self.get_connection_url())`, but that doesn't actually perform any connection, instead you have to do something like: ```python @wait_container_is_ready() def _connect(self): client = self.get_connection_client() # will raise pymongo.errors.ServerSelectionTimeoutError if no connection is established client.admin.command('ismaster') ``` thanks to @smparekh for pointing this out, in his PR: https://github.com/testcontainers/testcontainers-python/pull/80/files#diff-cf09f76f44db0af04c58ddb456ccae39f7e29ce1d9208acd5f514c0a7dccb646R78 this PR implements the workaround described in the PR: ```python @pytest.fixture(scope="session") def test_client(): # init mongo mongo_container = MongoDbContainer("mongo:4").start() wait_for_logs(mongo_container, 'waiting for connections on port 27017') ... Co-authored-by: Shaishav Parekh --- modules/mongodb/testcontainers/mongodb/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/mongodb/testcontainers/mongodb/__init__.py b/modules/mongodb/testcontainers/mongodb/__init__.py index 1ff029258..eee623c6d 100644 --- a/modules/mongodb/testcontainers/mongodb/__init__.py +++ b/modules/mongodb/testcontainers/mongodb/__init__.py @@ -17,7 +17,7 @@ from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.waiting_utils import wait_for_logs class MongoDbContainer(DbContainer): @@ -81,9 +81,8 @@ def get_connection_url(self) -> str: port=self.port, ) - @wait_container_is_ready() - def _connect(self) -> MongoClient: - return MongoClient(self.get_connection_url()) + def _connect(self) -> None: + wait_for_logs(self, "Waiting for connections") def get_connection_client(self) -> MongoClient: - return self._connect() + return MongoClient(self.get_connection_url()) From 87f8e342d00f2cd6a27891104cf9a6f131d0bdd8 Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 9 Mar 2024 13:30:20 -0500 Subject: [PATCH 15/16] update type import --- modules/nats/tests/test_nats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index b1345ace3..217c990af 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -23,7 +23,7 @@ pass -async def get_client(container: NatsContainer) -> NATSClient: +async def get_client(container: NatsContainer) -> "NATSClient": """ Get a nats client. From 8b84843a75aa781076173f5a3e1e628543bb65ef Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 9 Mar 2024 13:38:43 -0500 Subject: [PATCH 16/16] Fix merge --- modules/nats/testcontainers/nats/__init__.py | 2 -- modules/nats/tests/test_nats.py | 15 ++++++++------- poetry.lock | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/modules/nats/testcontainers/nats/__init__.py b/modules/nats/testcontainers/nats/__init__.py index 4f6ed30d4..a49c40d80 100644 --- a/modules/nats/testcontainers/nats/__init__.py +++ b/modules/nats/testcontainers/nats/__init__.py @@ -12,7 +12,6 @@ # under the License. -from nats import connect as nats_connect from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -54,7 +53,6 @@ def _healthcheck(self) -> None: def nats_uri(self) -> str: return f"nats://{self.get_container_host_ip()}:{self.get_exposed_port(self.client_port)}" - def nats_host_and_port(self) -> tuple[str, int]: return self.get_container_host_ip(), self.get_exposed_port(self.client_port) diff --git a/modules/nats/tests/test_nats.py b/modules/nats/tests/test_nats.py index 217c990af..4815d59e9 100644 --- a/modules/nats/tests/test_nats.py +++ b/modules/nats/tests/test_nats.py @@ -1,6 +1,6 @@ +from testcontainers.nats import NatsContainer from uuid import uuid4 import pytest -from nats.aio.client import Client as NATSClient """ @@ -35,10 +35,7 @@ async def get_client(container: NatsContainer) -> "NATSClient": return client - - -@pytest.mark.asyncio -async def test_basic_container_ops(anyio_backend): +def test_basic_container_ops(): with NatsContainer() as container: # Not sure how to get type information without doing this container: NatsContainer = container @@ -55,6 +52,7 @@ async def test_basic_container_ops(anyio_backend): @pytest.mark.skipif(NO_NATS_CLIENT, reason="No NATS Client Available") @pytest.mark.parametrize("anyio_backend", ["asyncio"]) +async def test_pubsub(anyio_backend): with NatsContainer() as container: nc: NATSClient = await get_client(container) @@ -70,9 +68,12 @@ async def test_basic_container_ops(anyio_backend): await nc.close() -@pytest.mark.asyncio -async def test_more_complex_example(): +pytest.mark.usefixtures("anyio_backend") + + +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) @pytest.mark.skipif(NO_NATS_CLIENT, reason="No NATS Client Available") +async def test_more_complex_example(anyio_backend): with NatsContainer() as container: nc: NATSClient = await get_client(container) diff --git a/poetry.lock b/poetry.lock index ec7c58eb6..49b11556c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -3193,4 +3193,4 @@ selenium = ["selenium"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "0f720c1889b4c29fb263490f7ced364969717a89126bc7c6ecf5a082f32e5d26" +content-hash = "9228589aa47564c2a463e34ed57463652eac3d45d47116ee2a749d64758bd85c"