Skip to content

Commit e2b246a

Browse files
committed
more tests and updated readme
1 parent 7cba4b8 commit e2b246a

File tree

3 files changed

+74
-24
lines changed

3 files changed

+74
-24
lines changed

README.md

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ def update_user(user_id, name):
197197
# For manually resetting entity pass table name
198198
# for composite key pass tuple
199199
cache.invalidate_entity(User.__table__, user_id)
200+
201+
202+
203+
# Complex queries
204+
205+
# Here we explicitly *have* to say that we stick to order entity to be tracked for changes
206+
@memory_cache(Order, id_key=lambda item: item[0].id)
207+
def get_orders_with_user(session: Session, order_id: str) -> List[Row[Tuple[Order, User]]]:
208+
nonlocal call_count
209+
call_count += 1
210+
return session.query(Order, User).join(User).filter(Order.id == order_id).all()
211+
200212
```
201213

202214
#### Django Integration
@@ -355,6 +367,7 @@ Main class for creating cache decorators.
355367
```python
356368
cache = EntityCache(
357369
backend=None, # CacheBackend instance (optional, will use in-memory if None)
370+
global_supported_id_types=(int, str, UUID) # Tuple of primitive types that are considered valid entity IDs.
358371
locked_ttl=3600, # Default locked TTL in seconds, in case if set, decorator cannot override this value (optional)
359372
fail_on_missing_id=True, # Raise an error if an ID cannot be extracted from the result
360373
serializer=json.dumps, # Custom serializer function (optional)
@@ -375,7 +388,8 @@ The traditional decorator for caching function results:
375388
```python
376389
@cache(
377390
entity="user", # Type of entity returned (string) or ORM model class (optional)
378-
id_key="id", # Field name or callable resolved to entity ID, not relevant on flat lists (optional)
391+
id_key=None, # default = 'id'. Field name or callable resolved to entity ID, not relevant on flat lists (optional)
392+
supported_id_types = (int, str, UUID) # Types that are treated as valid entity IDs when extracted
379393
cache_key=None, # Custom cache key for function (optional)
380394
normalize_args=False, # Whether to normalize arguments (optional)
381395
ttl=None, # Override default TTL, raises error if locked_ttl is set (optional)
@@ -404,24 +418,7 @@ def get_article(article_id):
404418

405419
#### @cache.tracks()
406420

407-
A more expressive alias for `@cache()` that clearly communicates the function's results will be cached and entity references will be tracked:
408-
409-
```python
410-
@cache.tracks(
411-
entity="user", # Type of entity returned by this function (string or ORM model class)
412-
id_key="id", # How to extract entity IDs from results
413-
# All other parameters from @cache() are supported
414-
)
415-
def get_user(user_id):
416-
# ...
417-
418-
# With ORM model class
419-
from myapp.models import User # SQLAlchemy or Django model
420-
421-
@cache.tracks(User) # Automatically uses table name and extracts primary key
422-
def get_user(user_id):
423-
# ...
424-
```
421+
A more expressive alias for `@cache()`
425422

426423
#### @cache.invalidates()
427424

@@ -568,7 +565,9 @@ poetry install
568565

569566
## TODO
570567
- [ ] Automatic ORM model detection, @cache.orm()
571-
- [ ] Refactory & Cleanup
568+
- [ ] MyPy Refactory & Cleanup
569+
- [ ] Introduce proper reverse index interface to not lock into Redis OSS comp
570+
- [ ] * By default RedisCompReverser should be used with multi-exec capability
572571
- [ ] Stale refs sweep scheduler / CLI comand
573572
- [ ] Tests
574573

cacheref/cache.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ def update_user(user_id, data):
9090
def __init__(
9191
self,
9292
backend: Optional[CacheBackend] = None,
93+
global_supported_id_types: Optional[Tuple] = (int, str, UUID),
9394
locked_ttl: Optional[int] = None,
9495
fail_on_missing_id: bool = True,
9596
serializer: Optional[Callable] = None,
9697
deserializer: Optional[Callable] = None,
9798
debug: bool = False,
98-
global_supported_id_types: Optional[Tuple] = (int, str, UUID),
9999
enabled: bool = True,
100100
):
101101
"""
@@ -561,8 +561,8 @@ def _cache(self, fn_result: Any, func: Callable,
561561
logger.debug("[Reverse index] Recache ready to execute")
562562
else:
563563
logger.debug("[Reverse index] Skipping reverse index recache"\
564-
" for as either no entity or result is empty"\
565-
f"{func=} {entity=} {fn_result=}")
564+
f" for {func=} as either no entity or result is empty"\
565+
f" {entity=} {fn_result=}")
566566
pipeline.execute()
567567
except Exception as e:
568568
logger.warning("Cache backend operations failed: %s", e, exc_info=True)

tests/test_sqlalchemy_integration.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Tests for SQLAlchemy integration with cacheref."""
22

33
import uuid
4-
from typing import List, Optional
4+
from typing import List, Optional, Tuple
55

66
import pytest
7+
from sqlalchemy import Row
78
from sqlalchemy.orm import as_declarative, declared_attr
89

910
from cacheref import EntityCache
@@ -366,3 +367,53 @@ def get_order_by_product_id(session: Session, product_id: int) -> List[OrderItem
366367
oi = ois[0]
367368
assert oi.quantity == 3
368369
assert call_count == 2
370+
371+
372+
def test_cache_pair_of_objects_by_join(db_session):
373+
"""Test that caching with SQLAlchemy model works correctly."""
374+
# Create cache
375+
memory_cache = EntityCache(backend=MemoryBackend(key_prefix="sqlalchemy_test:"), debug=True)
376+
377+
# Counter to track function calls
378+
call_count = 0
379+
380+
# Create cached function that selects product as entity for reference from returned pairs
381+
@memory_cache(Order, id_key=lambda item: item[0].id)
382+
def get_orders_with_user(session: Session, order_id: str) -> List[Row[Tuple[Order, User]]]:
383+
nonlocal call_count
384+
call_count += 1
385+
return session.query(Order, User).join(User).filter(Order.id == order_id).all()
386+
order_id = "order-123"
387+
orders_with_user: List[Row[Tuple[Order, User]]] = get_orders_with_user(db_session, order_id)
388+
assert len(orders_with_user) == 1, db_session.query(Product).filter(Product.product_id == order_id).all()
389+
order, user = orders_with_user[0]
390+
assert order is not None
391+
assert order.id == order_id
392+
assert order.user_id == 1
393+
assert user is not None
394+
assert user.id == 1
395+
assert user.name == "Test User"
396+
assert call_count == 1
397+
# Second call should use the cache
398+
orders_with_user: List[Row[Tuple[Order, User]]] = get_orders_with_user(db_session, order_id)
399+
assert len(orders_with_user) == 1, db_session.query(Product).filter(Product.product_id == order_id).all()
400+
order, user = orders_with_user[0]
401+
assert order is not None
402+
assert order.id == order_id
403+
assert order.user_id == 1
404+
assert user is not None
405+
assert user.id == 1
406+
assert user.name == "Test User"
407+
assert call_count == 1
408+
# Third call after invalidation should call the function
409+
memory_cache.invalidate_entity(Order.__tablename__, order_id)
410+
orders_with_user: List[Row[Tuple[Order, User]]] = get_orders_with_user(db_session, order_id)
411+
assert len(orders_with_user) == 1, db_session.query(Product).filter(Product.product_id == order_id).all()
412+
order, user = orders_with_user[0]
413+
assert order is not None
414+
assert order.id == order_id
415+
assert order.user_id == 1
416+
assert user is not None
417+
assert user.id == 1
418+
assert user.name == "Test User"
419+
assert call_count == 2

0 commit comments

Comments
 (0)