Skip to content

Commit cf97379

Browse files
committed
redis core
1 parent b9706c3 commit cf97379

11 files changed

Lines changed: 879 additions & 10 deletions

File tree

.github/workflows/ci-test.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@ jobs:
2222
matrix:
2323
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
2424
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
25-
backend: ["local", "mongodb", "postgres"]
25+
backend: ["local", "mongodb", "postgres", "redis"]
2626
exclude:
2727
# ToDo: take if back when the connection become stable
2828
# or resolve using `InMemoryMongoClient`
2929
- { os: "macOS-latest", backend: "mongodb" }
3030
- { os: "macOS-latest", backend: "postgres" }
31+
- { os: "macOS-latest", backend: "redis" }
3132
- { os: "windows-latest", backend: "postgres" }
33+
- { os: "windows-latest", backend: "redis" }
3234
env:
3335
CACHIER_TEST_HOST: "localhost"
3436
CACHIER_TEST_PORT: "27017"
3537
#CACHIER_TEST_DB: "dummy_db"
3638
#CACHIER_TEST_USERNAME: "myuser"
3739
#CACHIER_TEST_PASSWORD: "yourpassword"
3840
CACHIER_TEST_VS_DOCKERIZED_MONGO: "true"
41+
CACHIER_TEST_REDIS_HOST: "localhost"
42+
CACHIER_TEST_REDIS_PORT: "6379"
43+
CACHIER_TEST_REDIS_DB: "0"
44+
CACHIER_TEST_VS_DOCKERIZED_REDIS: "true"
3945

4046
steps:
4147
- uses: actions/checkout@v4
@@ -52,7 +58,7 @@ jobs:
5258
5359
- name: Unit tests (local)
5460
if: matrix.backend == 'local'
55-
run: pytest -m "not mongo and not sql" --cov=cachier --cov-report=term --cov-report=xml:cov.xml
61+
run: pytest -m "not mongo and not sql and not redis" --cov=cachier --cov-report=term --cov-report=xml:cov.xml
5662

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

118+
- name: Start Redis in docker
119+
if: matrix.backend == 'redis'
120+
run: |
121+
docker run -d \
122+
-p ${{ env.CACHIER_TEST_REDIS_PORT }}:6379 \
123+
--name redis redis:7-alpine
124+
# wait for Redis to start
125+
sleep 5
126+
docker ps -a
127+
128+
- name: Install Redis core test dependencies
129+
if: matrix.backend == 'redis'
130+
run: |
131+
python -m pip install -e . -r tests/redis_requirements.txt
132+
133+
- name: Unit tests (Redis)
134+
if: matrix.backend == 'redis'
135+
run: pytest -m redis --cov=cachier --cov-report=term --cov-report=xml:cov.xml
136+
112137
- name: Upload coverage to Codecov
113138
continue-on-error: true
114139
uses: codecov/codecov-action@v5

README.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Features
5050
* Defining "shelf life" for cached values.
5151
* Local caching using pickle files.
5252
* Cross-machine caching using MongoDB.
53+
* Redis-based caching for high-performance scenarios.
5354
* Thread-safety.
5455
* **Per-call max age:** Specify a maximum age for cached values per call.
5556

@@ -399,6 +400,56 @@ Cachier supports a generic SQL backend via SQLAlchemy, allowing you to use SQLit
399400
def my_func(x):
400401
return x * 2
401402
403+
Redis Core
404+
---------
405+
406+
**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::
407+
408+
pip install redis
409+
410+
Cachier supports Redis-based caching for high-performance scenarios. Redis provides fast in-memory storage with optional persistence.
411+
412+
**Usage Example (Local Redis):**
413+
414+
.. code-block:: python
415+
416+
import redis
417+
from cachier import cachier
418+
419+
# Create Redis client
420+
redis_client = redis.Redis(host='localhost', port=6379, db=0)
421+
422+
@cachier(backend="redis", redis_client=redis_client)
423+
def my_func(x):
424+
return x * 2
425+
426+
**Usage Example (Redis with custom key prefix):**
427+
428+
.. code-block:: python
429+
430+
import redis
431+
from cachier import cachier
432+
433+
redis_client = redis.Redis(host='localhost', port=6379, db=0)
434+
435+
@cachier(backend="redis", redis_client=redis_client, key_prefix="myapp")
436+
def my_func(x):
437+
return x * 2
438+
439+
**Usage Example (Redis with callable client):**
440+
441+
.. code-block:: python
442+
443+
import redis
444+
from cachier import cachier
445+
446+
def get_redis_client():
447+
return redis.Redis(host='localhost', port=6379, db=0)
448+
449+
@cachier(backend="redis", redis_client=get_redis_client)
450+
def my_func(x):
451+
return x * 2
452+
402453
**Configuration Options:**
403454

404455
- ``sql_engine``: SQLAlchemy connection string, Engine, or callable returning an Engine.

examples/redis_example.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example demonstrating the Redis core for cachier.
4+
5+
This example shows how to use cachier with Redis as the backend for
6+
high-performance caching.
7+
8+
Requirements:
9+
pip install redis cachier
10+
"""
11+
12+
import time
13+
from datetime import timedelta
14+
15+
try:
16+
import redis
17+
from cachier import cachier
18+
except ImportError as e:
19+
print(f"Missing required package: {e}")
20+
print("Install with: pip install redis cachier")
21+
exit(1)
22+
23+
24+
def setup_redis_client():
25+
"""Set up a Redis client for caching."""
26+
try:
27+
# Connect to Redis (adjust host/port as needed)
28+
client = redis.Redis(
29+
host="localhost",
30+
port=6379,
31+
db=0,
32+
decode_responses=False, # Important: keep as bytes for pickle
33+
)
34+
# Test connection
35+
client.ping()
36+
print("✓ Connected to Redis successfully")
37+
return client
38+
except redis.ConnectionError:
39+
print("✗ Could not connect to Redis")
40+
print("Make sure Redis is running on localhost:6379")
41+
print("Or install and start Redis with: docker run -p 6379:6379 redis")
42+
return None
43+
44+
45+
def expensive_calculation(n):
46+
"""Simulate an expensive calculation."""
47+
print(f" Computing expensive_calculation({n})...")
48+
time.sleep(2) # Simulate work
49+
return n * n + 42
50+
51+
52+
def demo_basic_caching():
53+
"""Demonstrate basic Redis caching."""
54+
print("\n=== Basic Redis Caching ===")
55+
56+
@cachier(backend="redis", redis_client=setup_redis_client())
57+
def cached_calculation(n):
58+
return expensive_calculation(n)
59+
60+
# First call - should be slow
61+
start = time.time()
62+
result1 = cached_calculation(5)
63+
time1 = time.time() - start
64+
print(f"First call: {result1} (took {time1:.2f}s)")
65+
66+
# Second call - should be fast (cached)
67+
start = time.time()
68+
result2 = cached_calculation(5)
69+
time2 = time.time() - start
70+
print(f"Second call: {result2} (took {time2:.2f}s)")
71+
72+
assert result1 == result2
73+
assert time2 < time1
74+
print("✓ Caching working correctly!")
75+
76+
77+
def demo_stale_after():
78+
"""Demonstrate stale_after functionality with Redis."""
79+
print("\n=== Stale After Demo ===")
80+
81+
@cachier(
82+
backend="redis",
83+
redis_client=setup_redis_client(),
84+
stale_after=timedelta(seconds=3),
85+
)
86+
def time_sensitive_calculation(n):
87+
return expensive_calculation(n)
88+
89+
# First call
90+
result1 = time_sensitive_calculation(10)
91+
print(f"First call: {result1}")
92+
93+
# Second call within 3 seconds - should use cache
94+
result2 = time_sensitive_calculation(10)
95+
print(f"Second call (within 3s): {result2}")
96+
assert result1 == result2
97+
98+
# Wait for cache to become stale
99+
print("Waiting 4 seconds for cache to become stale...")
100+
time.sleep(4)
101+
102+
# Third call after 4 seconds - should recalculate
103+
result3 = time_sensitive_calculation(10)
104+
print(f"Third call (after 4s): {result3}")
105+
assert result3 != result1
106+
print("✓ Stale after working correctly!")
107+
108+
109+
def demo_callable_client():
110+
"""Demonstrate using a callable Redis client."""
111+
print("\n=== Callable Client Demo ===")
112+
113+
def get_redis_client():
114+
"""Factory function for Redis client."""
115+
return redis.Redis(
116+
host="localhost", port=6379, db=0, decode_responses=False
117+
)
118+
119+
@cachier(backend="redis", redis_client=get_redis_client)
120+
def cached_with_callable(n):
121+
return expensive_calculation(n)
122+
123+
result1 = cached_with_callable(15)
124+
result2 = cached_with_callable(15)
125+
assert result1 == result2
126+
print(f"Callable client result: {result1}")
127+
print("✓ Callable client working correctly!")
128+
129+
130+
def demo_cache_management():
131+
"""Demonstrate cache management functions."""
132+
print("\n=== Cache Management Demo ===")
133+
134+
@cachier(backend="redis", redis_client=setup_redis_client())
135+
def managed_calculation(n):
136+
return expensive_calculation(n)
137+
138+
# Cache some values
139+
managed_calculation(20)
140+
managed_calculation(21)
141+
142+
# Clear the cache
143+
managed_calculation.clear_cache()
144+
print("✓ Cache cleared successfully!")
145+
146+
# Verify cache is empty
147+
start = time.time()
148+
result = managed_calculation(20) # Should be slow again
149+
time_taken = time.time() - start
150+
print(f"After clearing cache: {result} (took {time_taken:.2f}s)")
151+
152+
153+
def main():
154+
"""Run all Redis core demonstrations."""
155+
print("Cachier Redis Core Demo")
156+
print("=" * 50)
157+
158+
# Check if Redis is available
159+
client = setup_redis_client()
160+
if client is None:
161+
return
162+
163+
try:
164+
demo_basic_caching()
165+
demo_stale_after()
166+
demo_callable_client()
167+
demo_cache_management()
168+
169+
print("\n" + "=" * 50)
170+
print("✓ All Redis core demonstrations completed successfully!")
171+
print("\nKey benefits of Redis core:")
172+
print("- High-performance in-memory caching")
173+
print("- Cross-process and cross-machine caching")
174+
print("- Optional persistence with Redis configuration")
175+
print("- Built-in expiration and eviction policies")
176+
177+
except Exception as e:
178+
print(f"\n✗ Demo failed with error: {e}")
179+
finally:
180+
# Clean up
181+
if client:
182+
client.close()
183+
184+
185+
if __name__ == "__main__":
186+
main()

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ lint.extend-select = [
108108
lint.ignore = [
109109
"C901",
110110
"E203",
111+
"S301",
111112
]
112113
lint.per-file-ignores."src/**/__init__.py" = [
113114
"D104",
@@ -171,6 +172,7 @@ markers = [
171172
"mongo: test the MongoDB core",
172173
"memory: test the memory core",
173174
"pickle: test the pickle core",
175+
"redis: test the Redis core",
174176
"sql: test the SQL core",
175177
"maxage: test the max_age functionality",
176178
]

src/cachier/_types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
from typing import TYPE_CHECKING, Callable, Literal
1+
from typing import TYPE_CHECKING, Callable, Literal, Union
22

33
if TYPE_CHECKING:
44
import pymongo.collection
5+
import redis
56

67

78
HashFunc = Callable[..., str]
89
Mongetter = Callable[[], "pymongo.collection.Collection"]
9-
Backend = Literal["pickle", "mongo", "memory"]
10+
RedisClient = Union["redis.Redis", Callable[[], "redis.Redis"]]
11+
Backend = Literal["pickle", "mongo", "memory", "redis"]

0 commit comments

Comments
 (0)