Skip to content
Open
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
18 changes: 18 additions & 0 deletions backend/app/app/api/api_v1/endpoints/expenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from app import crud, models, schemas
from app.api import deps
from app.utilities.redis import invalidate_user_cache

router = APIRouter()

Expand Down Expand Up @@ -152,6 +153,10 @@ async def create_expense(
expense = await crud.expense.create_with_owner(
db=db, obj_in=expense_in, owner_id=current_user.id
)

# Invalidate user's cached data since a new expense was created
await invalidate_user_cache(current_user.id)

return expense


Expand All @@ -168,6 +173,10 @@ async def create_expenses_bulk(
expenses = await crud.expense.create_multi_with_owner(
db=db, obj_list=expenses_in, owner_id=current_user.id
)

# Invalidate user's cached data since expenses were created
await invalidate_user_cache(current_user.id)

return expenses


Expand Down Expand Up @@ -332,6 +341,9 @@ async def update_expense(
amount=amount_difference,
)

# Invalidate user's cached data since expense was updated
await invalidate_user_cache(current_user.id)

return updated_expense


Expand Down Expand Up @@ -391,6 +403,9 @@ async def delete_expense(
amount=-expense.amount,
)

# Invalidate user's cached data since expense was deleted
await invalidate_user_cache(current_user.id)

return schemas.DeletionResponse(message=f"Item {id} deleted")


Expand Down Expand Up @@ -476,6 +491,9 @@ async def delete_expenses_bulk(
amount=-expense.amount,
)

# Invalidate user's cached data since expenses were deleted
await invalidate_user_cache(current_user.id)

return schemas.BulkDeletionResponse(
message=f"Deleted {len(removed_expenses)} expenses",
deleted_ids=[e.id for e in removed_expenses],
Expand Down
17 changes: 17 additions & 0 deletions backend/app/app/api/api_v1/endpoints/incomes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app import crud, models, schemas
from app.api import deps
from app.api.deps import DateFilterType
from app.utilities.redis import invalidate_user_cache

router = APIRouter()

Expand Down Expand Up @@ -143,6 +144,10 @@ async def create_income(
income = await crud.income.create_with_owner(
db=db, obj_in=income_in, owner_id=current_user.id
)

# Invalidate user's cached data since a new income was created
await invalidate_user_cache(current_user.id)

return income


Expand All @@ -159,6 +164,10 @@ async def create_incomes_bulk(
incomes = await crud.income.create_multi_with_owner(
db=db, obj_list=incomes_in, owner_id=current_user.id
)

# Invalidate user's cached data since incomes were created
await invalidate_user_cache(current_user.id)

return incomes


Expand Down Expand Up @@ -311,6 +320,9 @@ async def update_income(
amount=amount_difference,
)

# Invalidate user's cached data since income was updated
await invalidate_user_cache(current_user.id)

return updated_income


Expand Down Expand Up @@ -369,6 +381,8 @@ async def delete_income(
)
await db.commit()

# Invalidate user's cached data since income was deleted
await invalidate_user_cache(current_user.id)

return schemas.DeletionResponse(message=f"Item {id} deleted")

Expand Down Expand Up @@ -452,6 +466,9 @@ async def delete_incomes_bulk(
amount=-income.amount,
)

# Invalidate user's cached data since incomes were deleted
await invalidate_user_cache(current_user.id)

return schemas.BulkDeletionResponse(
message=f"Deleted {len(removed_incomes)} incomes",
deleted_ids=[i.id for i in removed_incomes],
Expand Down
10 changes: 10 additions & 0 deletions backend/app/app/api/api_v1/endpoints/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from app import crud, models, schemas
from app.api import deps
from app.api.deps import DateFilterType
from app.utilities.redis import invalidate_user_cache

router = APIRouter()

Expand Down Expand Up @@ -146,6 +147,9 @@ async def create_transfer(
if transfer is None:
raise HTTPException(status_code=400, detail="Account not found")

# Invalidate user's cached data since a new transfer was created
await invalidate_user_cache(current_user.id)

return transfer


Expand Down Expand Up @@ -260,6 +264,9 @@ async def update_transfer(
amount=amount_difference,
)

# Invalidate user's cached data since transfer was updated
await invalidate_user_cache(current_user.id)

return updated_transfer

@router.delete("/{id}", response_model=schemas.DeletionResponse)
Expand Down Expand Up @@ -291,4 +298,7 @@ async def delete_transfer(
amount=-transfer.amount
)

# Invalidate user's cached data since transfer was deleted
await invalidate_user_cache(current_user.id)

return schemas.DeletionResponse(message=f"Item {id} deleted")
22 changes: 19 additions & 3 deletions backend/app/app/api/api_v2/endpoints/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
get_df,
transaction_charts,
)
from app.utilities.redis import get_user_data, store_user_data

router = APIRouter()

Expand Down Expand Up @@ -101,8 +102,13 @@ async def get_all_data(
current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
"""
Massive data retrieval for the dashboard.
Massive data retrieval for the dashboard with intelligent caching.
"""
# Try to get cached data first
cached_data = await get_user_data(current_user.id, date_filter_type.value, date)
if cached_data:
return cached_data

start_date: Date | None = None
end_date: Date | None = None
results = None
Expand Down Expand Up @@ -223,7 +229,7 @@ async def get_all_data(
) = results

if incomes_actual == [] and expenses_actual == []:
return {
empty_response = {
"currency": current_user.country,
"language": current_user.country,
"accounts": jsonable_encoder(accounts),
Expand All @@ -242,6 +248,11 @@ async def get_all_data(
"accounts": [],
},
}

# Cache the empty response as well
await store_user_data(current_user.id, date_filter_type.value, date, empty_response)

return empty_response

dfs = get_df(
expenses=jsonable_encoder(expenses_actual),
Expand Down Expand Up @@ -282,7 +293,7 @@ async def get_all_data(
incomes_df=dfs["incomes"], expenses_df=dfs["expenses"], transfers_df=dfs["transfers"]
)

return {
response_data = {
"currency": current_user.country,
"language": current_user.country,
"accounts": jsonable_encoder(accounts),
Expand All @@ -301,6 +312,11 @@ async def get_all_data(
"accounts": account_chart,
},
}

# Cache the response data for future requests
await store_user_data(current_user.id, date_filter_type.value, date, response_data)

return response_data


# @router.post("/", response_model=schemas.Expense)
Expand Down
47 changes: 47 additions & 0 deletions backend/app/app/utilities/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,50 @@ async def delete_transaction(transaction_id: str):
except Exception as e:
logging.error(f"Error deleting transaction {transaction_id}: {str(e)}")
return False

# User data caching functionality
async def store_user_data(user_id: int, date_filter_type: str, date: str, data, expire_time=1800):
"""Store get_all_data response in Redis with expiration time (30 min default)"""
try:
cache_key = f"user_data:{user_id}:{date_filter_type}:{date}"

await r.set(cache_key, json.dumps(data, cls=DateEncoder))
await r.expire(cache_key, expire_time)

logging.info(f"Cached user data for user {user_id} with key {cache_key}")
return True
except Exception as e:
logging.error(f"Redis error storing user data for user {user_id}: {str(e)}")
return False

async def get_user_data(user_id: int, date_filter_type: str, date: str):
"""Retrieve cached get_all_data response from Redis"""
try:
cache_key = f"user_data:{user_id}:{date_filter_type}:{date}"
cached_data = await r.get(cache_key)

if cached_data:
logging.info(f"Cache hit for user {user_id} with key {cache_key}")
return json.loads(cached_data)

logging.info(f"Cache miss for user {user_id} with key {cache_key}")
return None
except (Exception, json.JSONDecodeError) as e:
logging.error(f"Error retrieving user data for user {user_id}: {str(e)}")
return None

async def invalidate_user_cache(user_id: int):
"""Invalidate all cached data for a specific user when they add/update/delete data"""
try:
# Get all keys that match the user's cache pattern
pattern = f"user_data:{user_id}:*"
keys = await r.keys(pattern)

if keys:
await r.delete(*keys)
logging.info(f"Invalidated {len(keys)} cache entries for user {user_id}")

return True
except Exception as e:
logging.error(f"Error invalidating cache for user {user_id}: {str(e)}")
return False