From 830e556a60501661e5e9718ead8f6b37b04e5cec Mon Sep 17 00:00:00 2001 From: bstrausser Date: Sat, 2 Mar 2024 00:23:27 +0000 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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"