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
29 changes: 27 additions & 2 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,26 @@ jobs:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
backend: ["local", "mongodb", "postgres"]
backend: ["local", "mongodb", "postgres", "redis"]
exclude:
# ToDo: take if back when the connection become stable
# or resolve using `InMemoryMongoClient`
- { os: "macOS-latest", backend: "mongodb" }
- { os: "macOS-latest", backend: "postgres" }
- { os: "macOS-latest", backend: "redis" }
- { os: "windows-latest", backend: "postgres" }
- { os: "windows-latest", backend: "redis" }
env:
CACHIER_TEST_HOST: "localhost"
CACHIER_TEST_PORT: "27017"
#CACHIER_TEST_DB: "dummy_db"
#CACHIER_TEST_USERNAME: "myuser"
#CACHIER_TEST_PASSWORD: "yourpassword"
CACHIER_TEST_VS_DOCKERIZED_MONGO: "true"
CACHIER_TEST_REDIS_HOST: "localhost"
CACHIER_TEST_REDIS_PORT: "6379"
CACHIER_TEST_REDIS_DB: "0"
CACHIER_TEST_VS_DOCKERIZED_REDIS: "true"

steps:
- uses: actions/checkout@v4
Expand All @@ -52,7 +58,7 @@ jobs:

- name: Unit tests (local)
if: matrix.backend == 'local'
run: pytest -m "not mongo and not sql" --cov=cachier --cov-report=term --cov-report=xml:cov.xml
run: pytest -m "not mongo and not sql and not redis" --cov=cachier --cov-report=term --cov-report=xml:cov.xml

- name: Setup docker (missing on MacOS)
if: runner.os == 'macOS' && matrix.backend == 'mongodb'
Expand Down Expand Up @@ -109,6 +115,25 @@ jobs:
SQLALCHEMY_DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
run: pytest -m sql --cov=cachier --cov-report=term --cov-report=xml:cov.xml

- name: Start Redis in docker
if: matrix.backend == 'redis'
run: |
docker run -d \
-p ${{ env.CACHIER_TEST_REDIS_PORT }}:6379 \
--name redis redis:7-alpine
# wait for Redis to start
sleep 5
docker ps -a

- name: Install Redis core test dependencies
if: matrix.backend == 'redis'
run: |
python -m pip install -e . -r tests/redis_requirements.txt

- name: Unit tests (Redis)
if: matrix.backend == 'redis'
run: pytest -m redis --cov=cachier --cov-report=term --cov-report=xml:cov.xml

- name: Upload coverage to Codecov
continue-on-error: true
uses: codecov/codecov-action@v5
Expand Down
51 changes: 51 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Features
* Defining "shelf life" for cached values.
* Local caching using pickle files.
* Cross-machine caching using MongoDB.
* Redis-based caching for high-performance scenarios.
* Thread-safety.
* **Per-call max age:** Specify a maximum age for cached values per call.

Expand Down Expand Up @@ -399,6 +400,56 @@ Cachier supports a generic SQL backend via SQLAlchemy, allowing you to use SQLit
def my_func(x):
return x * 2

Redis Core
----------

**Note:** The Redis core requires the redis package to be installed. It is not installed by default with cachier. To use the Redis backend, run::

pip install redis

Cachier supports Redis-based caching for high-performance scenarios. Redis provides fast in-memory storage with optional persistence.

**Usage Example (Local Redis):**

.. code-block:: python

import redis
from cachier import cachier

# Create Redis client
redis_client = redis.Redis(host='localhost', port=6379, db=0)

@cachier(backend="redis", redis_client=redis_client)
def my_func(x):
return x * 2

**Usage Example (Redis with custom key prefix):**

.. code-block:: python

import redis
from cachier import cachier

redis_client = redis.Redis(host='localhost', port=6379, db=0)

@cachier(backend="redis", redis_client=redis_client, key_prefix="myapp")
def my_func(x):
return x * 2

**Usage Example (Redis with callable client):**

.. code-block:: python

import redis
from cachier import cachier

def get_redis_client():
return redis.Redis(host='localhost', port=6379, db=0)

@cachier(backend="redis", redis_client=get_redis_client)
def my_func(x):
return x * 2

**Configuration Options:**

- ``sql_engine``: SQLAlchemy connection string, Engine, or callable returning an Engine.
Expand Down
187 changes: 187 additions & 0 deletions examples/redis_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Example demonstrating the Redis core for cachier.

This example shows how to use cachier with Redis as the backend for
high-performance caching.

Requirements:
pip install redis cachier

"""

import time
from datetime import timedelta

try:
import redis

from cachier import cachier
except ImportError as e:
print(f"Missing required package: {e}")
print("Install with: pip install redis cachier")
exit(1)


def setup_redis_client():
"""Set up a Redis client for caching."""
try:
# Connect to Redis (adjust host/port as needed)
client = redis.Redis(
host="localhost",
port=6379,
db=0,
decode_responses=False, # Important: keep as bytes for pickle
)
# Test connection
client.ping()
print("✓ Connected to Redis successfully")
return client
except redis.ConnectionError:
print("✗ Could not connect to Redis")
print("Make sure Redis is running on localhost:6379")
print("Or install and start Redis with: docker run -p 6379:6379 redis")
return None


def expensive_calculation(n):
"""Simulate an expensive calculation."""
print(f" Computing expensive_calculation({n})...")
time.sleep(2) # Simulate work
return n * n + 42


def demo_basic_caching():
"""Demonstrate basic Redis caching."""
print("\n=== Basic Redis Caching ===")

@cachier(backend="redis", redis_client=setup_redis_client())
def cached_calculation(n):
return expensive_calculation(n)

# First call - should be slow
start = time.time()
result1 = cached_calculation(5)
time1 = time.time() - start
print(f"First call: {result1} (took {time1:.2f}s)")

# Second call - should be fast (cached)
start = time.time()
result2 = cached_calculation(5)
time2 = time.time() - start
print(f"Second call: {result2} (took {time2:.2f}s)")

assert result1 == result2
assert time2 < time1
print("✓ Caching working correctly!")


def demo_stale_after():
"""Demonstrate stale_after functionality with Redis."""
print("\n=== Stale After Demo ===")

@cachier(
backend="redis",
redis_client=setup_redis_client(),
stale_after=timedelta(seconds=3),
)
def time_sensitive_calculation(n):
return expensive_calculation(n)

# First call
result1 = time_sensitive_calculation(10)
print(f"First call: {result1}")

# Second call within 3 seconds - should use cache
result2 = time_sensitive_calculation(10)
print(f"Second call (within 3s): {result2}")
assert result1 == result2

# Wait for cache to become stale
print("Waiting 4 seconds for cache to become stale...")
time.sleep(4)

# Third call after 4 seconds - should recalculate
result3 = time_sensitive_calculation(10)
print(f"Third call (after 4s): {result3}")
assert result3 != result1
print("✓ Stale after working correctly!")


def demo_callable_client():
"""Demonstrate using a callable Redis client."""
print("\n=== Callable Client Demo ===")

def get_redis_client():
"""Get a Redis client."""
return redis.Redis(
host="localhost", port=6379, db=0, decode_responses=False
)

@cachier(backend="redis", redis_client=get_redis_client)
def cached_with_callable(n):
return expensive_calculation(n)

result1 = cached_with_callable(15)
result2 = cached_with_callable(15)
assert result1 == result2
print(f"Callable client result: {result1}")
print("✓ Callable client working correctly!")


def demo_cache_management():
"""Demonstrate cache management functions."""
print("\n=== Cache Management Demo ===")

@cachier(backend="redis", redis_client=setup_redis_client())
def managed_calculation(n):
return expensive_calculation(n)

# Cache some values
managed_calculation(20)
managed_calculation(21)

# Clear the cache
managed_calculation.clear_cache()
print("✓ Cache cleared successfully!")

# Verify cache is empty
start = time.time()
result = managed_calculation(20) # Should be slow again
time_taken = time.time() - start
print(f"After clearing cache: {result} (took {time_taken:.2f}s)")


def main():
"""Run all Redis core demonstrations."""
print("Cachier Redis Core Demo")
print("=" * 50)

# Check if Redis is available
client = setup_redis_client()
if client is None:
return

try:
demo_basic_caching()
demo_stale_after()
demo_callable_client()
demo_cache_management()

print("\n" + "=" * 50)
print("✓ All Redis core demonstrations completed successfully!")
print("\nKey benefits of Redis core:")
print("- High-performance in-memory caching")
print("- Cross-process and cross-machine caching")
print("- Optional persistence with Redis configuration")
print("- Built-in expiration and eviction policies")

except Exception as e:
print(f"\n✗ Demo failed with error: {e}")
finally:
# Clean up
if client:
client.close()


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ lint.extend-select = [
lint.ignore = [
"C901",
"E203",
"S301",
]
lint.per-file-ignores."examples/**" = [
"S101",
]
lint.per-file-ignores."src/**/__init__.py" = [
"D104",
Expand Down Expand Up @@ -171,6 +175,7 @@ markers = [
"mongo: test the MongoDB core",
"memory: test the memory core",
"pickle: test the pickle core",
"redis: test the Redis core",
"sql: test the SQL core",
"maxage: test the max_age functionality",
]
Expand Down
6 changes: 4 additions & 2 deletions src/cachier/_types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import TYPE_CHECKING, Callable, Literal
from typing import TYPE_CHECKING, Callable, Literal, Union

if TYPE_CHECKING:
import pymongo.collection
import redis


HashFunc = Callable[..., str]
Mongetter = Callable[[], "pymongo.collection.Collection"]
Backend = Literal["pickle", "mongo", "memory"]
RedisClient = Union["redis.Redis", Callable[[], "redis.Redis"]]
Backend = Literal["pickle", "mongo", "memory", "redis"]
Loading
Loading