Add Redis scan_keys iterator API.
Test Python Package / unit-tests (pull_request) Successful in 14s
Code Quality Pipeline / code-quality (pull_request) Successful in 32s
Test Python Package / coverage-report (pull_request) Successful in 16s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / integration-tests (pull_request) Successful in 1m3s

Provide a SCAN-based key iterator for Redis adapters so callers can enumerate large keyspaces without relying on blocking KEYS lookups.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Brian Bjarke Jensen
2026-07-08 21:06:13 +02:00
co-authored by Cursor
parent 2f19fcc972
commit 50444af982
6 changed files with 155 additions and 3 deletions
+32 -3
View File
@@ -1,6 +1,7 @@
"""Definition of RedisAdapter class."""
from __future__ import annotations
from collections.abc import Iterator
from typing import cast
from python_repositories.adapters.connection_aware_adapter import (
@@ -136,11 +137,13 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
self._client.json().delete(key)
self.logger.debug(f"Deleted {key}")
def list_keys(self, pattern: str) -> list[str]:
"""List keys in Redis matching a pattern."""
# Check input
def _validate_pattern(self, pattern: str) -> None:
if not isinstance(pattern, str) or len(pattern) == 0:
raise ValueError("Pattern must be a non-empty string")
def list_keys(self, pattern: str) -> list[str]:
"""List keys in Redis using KEYS; may block on large datasets."""
self._validate_pattern(pattern)
# Check connection
self._require_connected()
assert self._client is not None
@@ -152,3 +155,29 @@ class RedisAdapter(JsonRepositoryInterface, ConnectionAwareAdapter):
keys: list[str] = [key.decode(self.encoding) for key in keys_raw]
self.logger.debug(f"Got {keys} matching {pattern}")
return keys
def scan_keys(
self,
pattern: str,
*,
count: int | None = None,
) -> Iterator[str]:
"""Yield keys in Redis using SCAN to avoid blocking large datasets."""
self._validate_pattern(pattern)
self._require_connected()
assert self._client is not None
client = self._client
def _decode(key: bytes | str) -> str:
return key if isinstance(key, str) else key.decode(self.encoding)
def _iter() -> Iterator[str]:
scan_iter = (
client.scan_iter(match=pattern, count=count)
if count is not None
else client.scan_iter(match=pattern)
)
for key_raw in scan_iter:
yield _decode(key_raw)
return _iter()