Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions INDEX.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions modules/nats/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. autoclass:: testcontainers.nats.NatsContainer
75 changes: 75 additions & 0 deletions modules/nats/testcontainers/nats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#
# 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 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


class NatsContainer(DockerContainer):
"""
Nats container.

Example:

.. doctest::

>>> from testcontainers.nats import NatsContainer

>>> with NatsContainer() as nats_container:
... nc = nats_container.get_client()
"""

def __init__(
self,
image: str = "nats:latest",
client_port: int = 4222,
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 = 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)

@wait_container_is_ready()
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.

Args:
**kwargs: Keyword arguments passed to `redis.Redis`.

Returns:
client: Nats client to connect to the container.
"""
conn_string = self.get_conn_string()
client = await nats_connect(conn_string)
return client

def start(self) -> "NatsContainer":
super().start()
self._healthcheck()
return self
52 changes: 52 additions & 0 deletions modules/nats/tests/test_nats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from uuid import uuid4

import pytest
from nats.aio.client import Client as NATSClient

from testcontainers.nats import NatsContainer


@pytest.mark.asyncio
async def test_basic_publishing():
with NatsContainer() as container:
nc: NATSClient = await container.get_client()

topic = str(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.data
await nc.flush()
await nc.close()


@pytest.mark.asyncio
async def test_more_complex_example():
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()
38 changes: 36 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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"]
Expand All @@ -98,6 +100,7 @@ minio = ["minio"]
mongodb = ["pymongo"]
mssql = ["sqlalchemy", "pymssql"]
mysql = ["sqlalchemy", "pymysql"]
nats = ["nats-py"]
neo4j = ["neo4j"]
nginx = []
opensearch = ["opensearch-py"]
Expand All @@ -116,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"
Expand Down Expand Up @@ -222,6 +226,7 @@ mypy_path = [
# "modules/mongodb",
# "modules/mssql",
# "modules/mysql",
# "modules/nats",
# "modules/neo4j",
# "modules/nginx",
# "modules/opensearch",
Expand Down