Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions docs/modules/valkey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Valkey

Since testcontainers-python <a href="https://github.com/testcontainers/testcontainers-python/releases/tag/v4.14.0"><span class="tc-version">:material-tag: v4.14.0</span></a>

## Introduction

The Testcontainers module for Valkey.

## Adding this module to your project dependencies

Please run the following command to add the Valkey module to your python dependencies:

```bash
pip install testcontainers[valkey]
```

## Usage example

<!--codeinclude-->

[Creating a Valkey container](../../modules/valkey/example_basic.py)

<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nav:
- modules/redis.md
- modules/scylla.md
- modules/trino.md
- modules/valkey.md
- modules/weaviate.md
- modules/aws.md
- modules/azurite.md
Expand Down
2 changes: 2 additions & 0 deletions modules/valkey/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. autoclass:: testcontainers.valkey.ValkeyContainer
.. title:: testcontainers.valkey.ValkeyContainer
78 changes: 78 additions & 0 deletions modules/valkey/example_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from glide import GlideClient, NodeAddress

from testcontainers.valkey import ValkeyContainer


def basic_example():
with ValkeyContainer() as valkey_container:
# Get connection parameters
host = valkey_container.get_host()
port = valkey_container.get_exposed_port()
connection_url = valkey_container.get_connection_url()

print(f"Valkey connection URL: {connection_url}")
print(f"Host: {host}, Port: {port}")

# Connect using Glide client
client = GlideClient([NodeAddress(host, port)])

# PING command
pong = client.ping()
print(f"PING response: {pong}")

# SET command
client.set("key", "value")
print("SET response: OK")

# GET command
value = client.get("key")
print(f"GET response: {value}")

client.close()


def password_example():
with ValkeyContainer().with_password("mypassword") as valkey_container:
host = valkey_container.get_host()
port = valkey_container.get_exposed_port()
connection_url = valkey_container.get_connection_url()

print(f"\nValkey with password connection URL: {connection_url}")

# Connect using Glide client with password
client = GlideClient([NodeAddress(host, port)], password="mypassword")

# PING after auth
pong = client.ping()
print(f"PING response: {pong}")

client.close()


def version_example():
# Using specific version
with ValkeyContainer().with_image_tag("8.0") as valkey_container:
print(f"\nUsing image: {valkey_container.image}")
connection_url = valkey_container.get_connection_url()
print(f"Connection URL: {connection_url}")


def bundle_example():
# Using bundle with all modules (JSON, Bloom, Search, etc.)
with ValkeyContainer().with_bundle() as valkey_container:
print(f"\nUsing bundle image: {valkey_container.image}")
host = valkey_container.get_host()
port = valkey_container.get_exposed_port()

# Connect using Glide client
client = GlideClient([NodeAddress(host, port)])
pong = client.ping()
print(f"PING response: {pong}")
client.close()


if __name__ == "__main__":
basic_example()
password_example()
version_example()
bundle_example()
114 changes: 114 additions & 0 deletions modules/valkey/testcontainers/valkey/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#
# 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 typing import Optional

from testcontainers.core.container import DockerContainer
from testcontainers.core.wait_strategies import ExecWaitStrategy


class ValkeyContainer(DockerContainer):
"""
Valkey container.

"""

def __init__(self, image: str = "valkey/valkey:latest", port: int = 6379, **kwargs) -> None:
super().__init__(image, **kwargs)
self.port = port
self.password: Optional[str] = None
self.with_exposed_ports(self.port)

def with_password(self, password: str) -> "ValkeyContainer":
"""
Configure authentication for Valkey.

Args:
password: Password for Valkey authentication.

Returns:
self: Container instance for method chaining.
"""
self.password = password
self.with_command(["valkey-server", "--requirepass", password])
return self

def with_image_tag(self, tag: str) -> "ValkeyContainer":
"""
Specify Valkey version.

Args:
tag: Image tag (e.g., '8.0', 'latest', 'bundle:latest').

Returns:
self: Container instance for method chaining.
"""
base_image = self.image.split(":")[0]
self.image = f"{base_image}:{tag}"
return self

def with_bundle(self) -> "ValkeyContainer":
"""
Enable all modules by switching to valkey-bundle image.

Returns:
self: Container instance for method chaining.
"""
self.image = self.image.replace("valkey/valkey", "valkey/valkey-bundle")
return self

def get_connection_url(self) -> str:
"""
Get connection URL for Valkey.

Returns:
url: Connection URL in format valkey://[:password@]host:port
"""
host = self.get_host()
port = self.get_exposed_port()
if self.password:
return f"valkey://:{self.password}@{host}:{port}"
return f"valkey://{host}:{port}"

def get_host(self) -> str:
"""
Get container host.

Returns:
host: Container host IP.
"""
return self.get_container_host_ip()

def get_exposed_port(self) -> int:
"""
Get mapped port.

Returns:
port: Exposed port number.
"""
return int(super().get_exposed_port(self.port))

def start(self) -> "ValkeyContainer":
"""
Start the container and wait for it to be ready.

Returns:
self: Started container instance.
"""
if self.password:
self.waiting_for(ExecWaitStrategy(["valkey-cli", "-a", self.password, "ping"]))
else:
self.waiting_for(ExecWaitStrategy(["valkey-cli", "ping"]))

super().start()
return self
79 changes: 79 additions & 0 deletions modules/valkey/tests/test_valkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import socket

from testcontainers.valkey import ValkeyContainer


def test_docker_run_valkey():
with ValkeyContainer() as valkey:
host = valkey.get_host()
port = valkey.get_exposed_port()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
s.sendall(b"*1\r\n$4\r\nPING\r\n")
response = s.recv(1024)
assert b"+PONG" in response


def test_docker_run_valkey_with_password():
with ValkeyContainer().with_password("mypass") as valkey:
host = valkey.get_host()
port = valkey.get_exposed_port()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
# Authenticate
s.sendall(b"*2\r\n$4\r\nAUTH\r\n$6\r\nmypass\r\n")
auth_response = s.recv(1024)
assert b"+OK" in auth_response

# Test SET command
s.sendall(b"*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n")
set_response = s.recv(1024)
assert b"+OK" in set_response

# Test GET command
s.sendall(b"*2\r\n$3\r\nGET\r\n$5\r\nhello\r\n")
get_response = s.recv(1024)
assert b"world" in get_response


def test_get_connection_url():
with ValkeyContainer() as valkey:
url = valkey.get_connection_url()
assert url.startswith("valkey://")
assert str(valkey.get_exposed_port()) in url


def test_get_connection_url_with_password():
with ValkeyContainer().with_password("secret") as valkey:
url = valkey.get_connection_url()
assert url.startswith("valkey://:secret@")
assert str(valkey.get_exposed_port()) in url


def test_with_image_tag():
container = ValkeyContainer().with_image_tag("8.0")
assert "valkey/valkey:8.0" in container.image


def test_with_bundle():
container = ValkeyContainer().with_bundle()
assert container.image == "valkey/valkey-bundle:latest"


def test_with_bundle_and_tag():
container = ValkeyContainer().with_bundle().with_image_tag("9.0")
assert container.image == "valkey/valkey-bundle:9.0"


def test_bundle_starts():
with ValkeyContainer().with_bundle() as valkey:
host = valkey.get_host()
port = valkey.get_exposed_port()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
s.sendall(b"*1\r\n$4\r\nPING\r\n")
response = s.recv(1024)
assert b"+PONG" in response
3 changes: 2 additions & 1 deletion poetry.lock

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

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ packages = [
{ include = "testcontainers", from = "modules/selenium" },
{ include = "testcontainers", from = "modules/scylla" },
{ include = "testcontainers", from = "modules/trino" },
{ include = "testcontainers", from = "modules/valkey" },
{ include = "testcontainers", from = "modules/vault" },
{ include = "testcontainers", from = "modules/weaviate" },
]
Expand Down Expand Up @@ -188,6 +189,7 @@ rabbitmq = ["pika"]
redis = ["redis"]
registry = ["bcrypt"]
selenium = ["selenium"]
valkey = []
scylla = ["cassandra-driver"]
sftp = ["cryptography"]
vault = []
Expand Down