Skip to content
Merged
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
16 changes: 12 additions & 4 deletions src/cachekit/backends/redis/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from __future__ import annotations

from pydantic import Field
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


Expand Down Expand Up @@ -56,7 +56,8 @@ class RedisBackendConfig(BaseSettings):

redis_url: str = Field(
default="redis://localhost:6379",
description="Redis connection URL",
validation_alias=AliasChoices("CACHEKIT_REDIS_URL", "REDIS_URL"),
description="Redis connection URL (env: CACHEKIT_REDIS_URL or REDIS_URL)",
)
connection_pool_size: int = Field(
default=10,
Expand All @@ -76,7 +77,11 @@ class RedisBackendConfig(BaseSettings):
def from_env(cls) -> RedisBackendConfig:
"""Create Redis configuration from environment variables.

Reads CACHEKIT_REDIS_URL, CACHEKIT_CONNECTION_POOL_SIZE, etc.
Priority (via AliasChoices): CACHEKIT_REDIS_URL > REDIS_URL > default

This allows standard 12-factor app conventions (REDIS_URL) while
supporting namespaced configuration (CACHEKIT_REDIS_URL) for
multi-service deployments.

Returns:
RedisBackendConfig instance loaded from environment
Expand All @@ -86,8 +91,11 @@ def from_env(cls) -> RedisBackendConfig:

.. code-block:: bash

# Option 1: Namespaced (takes priority)
export CACHEKIT_REDIS_URL="redis://localhost:6379"
export CACHEKIT_CONNECTION_POOL_SIZE=20

# Option 2: Standard 12-factor convention
export REDIS_URL="redis://localhost:6379"

.. code-block:: python

Expand Down
9 changes: 5 additions & 4 deletions src/cachekit/config/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ def _resolve_backend(explicit_backend: object = _UNSET) -> BaseBackend | None:
if _default_backend is not None:
return _default_backend

# Tier 3: Auto-create from REDIS_URL env var
redis_url = os.environ.get("REDIS_URL")
if redis_url:
# Tier 3: Auto-create from env var (CACHEKIT_REDIS_URL > REDIS_URL)
# Check if either env var is set as signal to create Redis backend
# Actual URL resolution handled by RedisBackendConfig via AliasChoices
if os.environ.get("CACHEKIT_REDIS_URL") or os.environ.get("REDIS_URL"):
# Lazy import to avoid circular dependency
from cachekit.backends.provider import CacheClientProvider
from cachekit.backends.redis import RedisBackend
Expand All @@ -118,7 +119,7 @@ def _resolve_backend(explicit_backend: object = _UNSET) -> BaseBackend | None:
# Inject client_provider explicitly (follows Dependency Injection Principle)
container = DIContainer()
client_provider = container.get(CacheClientProvider)
return RedisBackend(redis_url, client_provider=client_provider)
return RedisBackend(client_provider=client_provider)

# No backend configured - fail fast with helpful message
raise ConfigurationError(
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_config_env_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,52 @@ def test_backend_and_cache_configs_independent(self, monkeypatch):
# Verify no cross-contamination
assert not hasattr(cache_config, "redis_url")
assert hasattr(cache_config, "default_ttl")


class TestRedisUrlAliasChoicesPriority:
"""Test AliasChoices priority for redis_url field.

The redis_url field uses AliasChoices to support both:
- CACHEKIT_REDIS_URL (namespaced, takes priority)
- REDIS_URL (standard 12-factor convention, fallback)
"""

def test_only_redis_url_set(self, monkeypatch):
"""REDIS_URL alone should work (12-factor convention)."""
monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False)
monkeypatch.setenv("REDIS_URL", "redis://generic:6379")

config = RedisBackendConfig()
assert config.redis_url == "redis://generic:6379"

def test_only_redis_url_via_from_env(self, monkeypatch):
"""REDIS_URL works via from_env() factory method."""
monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False)
monkeypatch.setenv("REDIS_URL", "redis://factory:6379")

config = RedisBackendConfig.from_env()
assert config.redis_url == "redis://factory:6379"

def test_only_cachekit_redis_url_set(self, monkeypatch):
"""CACHEKIT_REDIS_URL alone should work (namespaced)."""
monkeypatch.delenv("REDIS_URL", raising=False)
monkeypatch.setenv("CACHEKIT_REDIS_URL", "redis://cachekit:6379")

config = RedisBackendConfig()
assert config.redis_url == "redis://cachekit:6379"

def test_both_set_cachekit_wins(self, monkeypatch):
"""CACHEKIT_REDIS_URL takes priority over REDIS_URL."""
monkeypatch.setenv("CACHEKIT_REDIS_URL", "redis://cachekit-priority:6379")
monkeypatch.setenv("REDIS_URL", "redis://generic-fallback:6379")

config = RedisBackendConfig()
assert config.redis_url == "redis://cachekit-priority:6379"

def test_neither_set_uses_default(self, monkeypatch):
"""Falls back to localhost:6379 when neither env var is set."""
monkeypatch.delenv("CACHEKIT_REDIS_URL", raising=False)
monkeypatch.delenv("REDIS_URL", raising=False)

config = RedisBackendConfig()
assert config.redis_url == "redis://localhost:6379"
Loading