Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
84 changes: 84 additions & 0 deletions modules/valkey/example_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import socket

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 raw socket and RESP protocol
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))

# PING command
s.sendall(b"*1\r\n$4\r\nPING\r\n")
response = s.recv(1024)
print(f"PING response: {response.decode()}")

# SET command
s.sendall(b"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n")
response = s.recv(1024)
print(f"SET response: {response.decode()}")

# GET command
s.sendall(b"*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n")
response = s.recv(1024)
print(f"GET response: {response.decode()}")


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}")

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))

# AUTH command
s.sendall(b"*2\r\n$4\r\nAUTH\r\n$10\r\nmypassword\r\n")
response = s.recv(1024)
print(f"AUTH response: {response.decode()}")

# PING after auth
s.sendall(b"*1\r\n$4\r\nPING\r\n")
response = s.recv(1024)
print(f"PING response: {response.decode()}")


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()

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)
print(f"PING response: {response.decode()}")


if __name__ == "__main__":
basic_example()
password_example()
version_example()
bundle_example()
138 changes: 138 additions & 0 deletions modules/valkey/testcontainers/valkey/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import socket
from typing import Optional

from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_container_is_ready


class ValkeyNotReady(Exception):
pass


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

Example:

.. doctest::

>>> from testcontainers.valkey import ValkeyContainer

>>> with ValkeyContainer() as valkey_container:
... connection_url = valkey_container.get_connection_url()
"""

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(f"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))

@wait_container_is_ready(ValkeyNotReady)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated Decorator Usage

Location: modules/valkey/testcontainers/valkey/__init__.py, line 113

Current Code:

@wait_container_is_ready(ValkeyNotReady)
def _connect(self) -> None:
    """Wait for Valkey to be ready by sending PING command."""
    # ... connection logic

Issue:

The @wait_container_is_ready decorator is deprecated in the codebase. Recent commits show active migration away from this pattern:

Evidence from Codebase:

# From pytest.ini_options filterwarnings:
"ignore:The @wait_container_is_ready decorator is deprecated.*:DeprecationWarning"

Recommended Fix:

Migrate to modern wait strategy pattern:

from testcontainers.core.wait_strategies import ExecWaitStrategy

class ValkeyContainer(DockerContainer):
    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 start(self) -> "ValkeyContainer":
        # Build wait strategy based on password
        if self.password:
            # Use custom wait strategy for authenticated connections
            self.waiting_for(self._create_auth_wait_strategy())
        else:
            # Use exec strategy for simple PING
            self.waiting_for(
                ExecWaitStrategy(["valkey-cli", "ping"])
            )
        super().start()
        return self

Benefits:

  • Aligns with project direction
  • More composable and testable
  • Consistent with other modules
  • Better error messages

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

def _connect(self) -> None:
"""Wait for Valkey to be ready by sending PING command."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((self.get_host(), self.get_exposed_port()))
if self.password:
s.sendall(f"*2\r\n$4\r\nAUTH\r\n${len(self.password)}\r\n{self.password}\r\n".encode())
auth_response = s.recv(1024)
if b"+OK" not in auth_response:
raise ValkeyNotReady("Authentication failed")
s.sendall(b"*1\r\n$4\r\nPING\r\n")
response = s.recv(1024)
if b"+PONG" not in response:
raise ValkeyNotReady("Valkey not ready yet")

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

Returns:
self: Started container instance.
"""
super().start()
self._connect()
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