diff --git a/app/api/api_v1/genres/list_views.py b/app/api/api_v1/genres/list_views.py index b0ccf50..4b386db 100644 --- a/app/api/api_v1/genres/list_views.py +++ b/app/api/api_v1/genres/list_views.py @@ -36,12 +36,12 @@ async def get_genres( ], ) async def search_genres_by_name( - genre_name: str, + search_query: str, genre_cache_service: GenreCacheServiceDep, size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> GenreResponseList: - return await genre_cache_service.search_genres_by_name(genre_name, size, page) + return await genre_cache_service.search_genres_by_name(search_query, size, page) @router.post( diff --git a/app/api/api_v1/movies/list_views.py b/app/api/api_v1/movies/list_views.py index c08dc42..828653a 100644 --- a/app/api/api_v1/movies/list_views.py +++ b/app/api/api_v1/movies/list_views.py @@ -11,7 +11,6 @@ from schemas.movie import ( MovieCreate, MovieFilter, - MovieResponseList, MovieWithGenreResponse, MovieWithGenreResponseList, ) @@ -49,8 +48,7 @@ async def watch_movie( movie_cache_service: MovieCacheServiceDep, ) -> RedirectResponse: movie = await movie_cache_service.watch_movie(user_id, create_watch_history_data) - url = BASE_MINIO_URL + movie.source_url - return RedirectResponse(url=url) + return RedirectResponse(url=movie.source_url) @router.post( @@ -74,23 +72,6 @@ async def search_movies_with_filters( ) -@router.get( - "/genre/{genre_id}", - response_model=MovieResponseList, - status_code=status.HTTP_200_OK, - dependencies=[ - Depends(check_rate_limit_not_auth), - ], -) -async def get_movies_by_genre_id( - genre_id: int, - movie_cache_service: MovieCacheServiceDep, - size: PaginationSizeDep = 10, - page: PaginationPageDep = 1, -) -> MovieResponseList: - return await movie_cache_service.get_movies_by_genre_id(genre_id, size, page) - - @router.post( "/", response_model=MovieWithGenreResponse, diff --git a/app/cache_services/favorite_movie.py b/app/cache_services/favorite_movie.py index 357d600..9a6710d 100644 --- a/app/cache_services/favorite_movie.py +++ b/app/cache_services/favorite_movie.py @@ -1,8 +1,12 @@ +import asyncio from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.favorite_movie import ( FavoriteMovieCreate, + FavoriteMovieResponse, FavoriteMovieWithMovieResponse, FavoriteMovieWithMovieResponseList, ) @@ -13,10 +17,12 @@ class FavoriteMovieCacheService: def __init__( self, favorite_movie_service: FavoriteMovieService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.favorite_movie_service = favorite_movie_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_favorite_movies_by_user_id( self, @@ -24,13 +30,15 @@ async def get_favorite_movies_by_user_id( size: int = 10, page: int = 1, ) -> FavoriteMovieWithMovieResponseList: - key = self.cache_service.create_cache_key( - "favorite movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.favorite_movie, + action="get", + version_params={"user_id": user_id}, user_id=user_id, - size=size, page=page, + size=size, ) - cached_favorite_movies_response = await self.cache_service.get( + cached_favorite_movies_response = await self.redis_service.get( key, FavoriteMovieWithMovieResponseList, ) @@ -48,10 +56,10 @@ async def get_favorite_movies_by_user_id( ) ) - await self.cache_service.set( + await self.redis_service.set( key, favorite_movies_response, - ttl=1800, + ttl=24 * 60 * 60, ) return favorite_movies_response @@ -59,11 +67,12 @@ async def get_favorite_movie_by_id( self, favorite_movie_id: int, ) -> FavoriteMovieWithMovieResponse: - key = self.cache_service.create_cache_key( - "favorite movie", - favorite_movie_id=favorite_movie_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=favorite_movie_id, + action="get", ) - cached_favorite_movie_response = await self.cache_service.get( + cached_favorite_movie_response = await self.redis_service.get( key, FavoriteMovieWithMovieResponse, ) @@ -75,33 +84,46 @@ async def get_favorite_movie_by_id( favorite_movie_id, ) ) - await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.redis_service.set( + key, + favorite_movie_response, + ttl=5 * 60, + ) return favorite_movie_response async def count_favorites_by_movie(self, movie_id: int) -> int: - key = self.cache_service.create_cache_key( - "favorite movie:count", - movie_id=movie_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", ) - cached_favorite_movie_response = await self.cache_service.get(key) + cached_favorite_movie_response = await self.redis_service.get(key) if cached_favorite_movie_response is not None: return cast(int, cached_favorite_movie_response) favorite_movie_response = ( await self.favorite_movie_service.count_favorites_by_movie(movie_id) ) - await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.redis_service.set( + key, + favorite_movie_response, + ttl=24 * 60 * 60, + ) return favorite_movie_response async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool: - # key = self.cache_service.create_cache_key( - # "favorite movie:status", - # user_id=user_id, - # movie_id=movie_id, - # ) - # cached_favorite_movie_response = await self.cache_service.get(key) - # if cached_favorite_movie_response is not None: - # return cast(bool, cached_favorite_movie_response) + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + cached_favorite_movie_response = await self.redis_service.get( + key, + is_boolean=True, + ) + if cached_favorite_movie_response is not None: + return cast(bool, cached_favorite_movie_response) favorite_movie_response = ( await self.favorite_movie_service.check_favorite_movie_status( @@ -109,7 +131,11 @@ async def check_favorite_movie_status(self, user_id: int, movie_id: int) -> bool movie_id, ) ) - # await self.cache_service.set(key, favorite_movie_response, ttl=1800) + await self.redis_service.set( + key, + favorite_movie_response, + ttl=24 * 60 * 60, + ) return favorite_movie_response async def create_user_favorite_movie( @@ -123,32 +149,97 @@ async def create_user_favorite_movie( create_favorite_movie_data, ) ) - key = self.cache_service.create_cache_key( - "favorite movie", + + movie_id = favorite_movie_response.movie_id + entity_id = (user_id, movie_id) + favorite_movie_status_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + count_favorite_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=1, + ), ) - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) return favorite_movie_response async def delete_favorite_movie_by_id( self, user_id: int, favorite_movie_id: int, - ) -> None: - await self.favorite_movie_service.delete_favorite_movie_by_id( + ) -> FavoriteMovieResponse: + favorite_movie = await self.favorite_movie_service.delete_favorite_movie_by_id( user_id, favorite_movie_id, ) - key = self.cache_service.create_cache_key( - "favorite movie", + + movie_id = favorite_movie.movie_id + entity_id = (user_id, movie_id) + favorite_movie_status_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + count_favorite_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=-1, + ), + ) + return favorite_movie + + async def delete_user_favorite_movie( + self, + user_id: int, + movie_id: int, + ) -> FavoriteMovieResponse: + favorite_movie = await self.favorite_movie_service.delete_user_favorite_movie( + user_id, + movie_id, ) - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> None: - await self.favorite_movie_service.delete_user_favorite_movie(user_id, movie_id) - key = self.cache_service.create_cache_key( - "favorite movie", + entity_id = (user_id, movie_id) + favorite_movie_status_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=entity_id, + action="get-status", + ) + count_favorite_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.favorite_movie, + entity_id=movie_id, + action="get-count", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.favorite_movie, + user_id=user_id, + ), + self.redis_service.delete(favorite_movie_status_key), + self.redis_service.incr_by( + count_favorite_movie_key, + amount=-1, + ), ) - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + return favorite_movie diff --git a/app/cache_services/genre.py b/app/cache_services/genre.py index 9c2e63a..e91bb0f 100644 --- a/app/cache_services/genre.py +++ b/app/cache_services/genre.py @@ -1,5 +1,11 @@ +import asyncio from typing import cast +from packages.celery.constants import Queue, TaskType + +from core.celery.celery_app import app +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.genre import ( GenreCreate, @@ -15,19 +21,29 @@ class GenreCacheService: def __init__( self, genre_service: GenreService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.genre_service = genre_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_genre_by_id(self, genre_id: int) -> GenreResponse: - key = RedisService.create_cache_key("genre", genre_id=genre_id) - cached_genre_response = await self.cache_service.get(key, GenreResponse) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.genre, + entity_id=genre_id, + action="get", + ) + cached_genre_response = await self.redis_service.get(key, GenreResponse) if cached_genre_response is not None: return cast(GenreResponse, cached_genre_response) genre_response = await self.genre_service.get_genre_by_id(genre_id) - await self.cache_service.set(key, genre_response, ttl=1800) + await self.redis_service.set( + key, + genre_response, + ttl=24 * 60 * 60, + ) return genre_response async def get_all_genres( @@ -35,48 +51,56 @@ async def get_all_genres( size: int = 10, page: int = 1, ) -> GenreResponseList: - key = RedisService.create_cache_key( - "genres", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.genre, + action="get", size=size, page=page, ) - cached_genres_response = await self.cache_service.get(key, GenreResponseList) + cached_genres_response = await self.redis_service.get(key, GenreResponseList) if cached_genres_response is not None: return cast(GenreResponseList, cached_genres_response) genres_response = await self.genre_service.get_all_genres(size, page) - await self.cache_service.set(key, genres_response, ttl=180) + await self.redis_service.set( + key, + genres_response, + ttl=24 * 60 * 60, + ) return genres_response async def search_genres_by_name( self, - name: str, + search_query: str, size: int = 10, page: int = 1, ) -> GenreResponseList: - key = RedisService.create_cache_key( - "genres", - name=name, + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.genre, + action="get", + search_query=search_query, size=size, page=page, ) - cached_genres_response = await self.cache_service.get(key, GenreResponseList) + cached_genres_response = await self.redis_service.get(key, GenreResponseList) if cached_genres_response is not None: return cast(GenreResponseList, cached_genres_response) genres_response = await self.genre_service.search_genres_by_name( - name, + search_query, size, page, ) - await self.cache_service.set(key, genres_response, ttl=180) + await self.redis_service.set( + key, + genres_response, + ttl=24 * 60 * 60, + ) return genres_response async def create_genre(self, create_data: GenreCreate) -> GenreResponse: genre_response = await self.genre_service.create_genre(create_data) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre) return genre_response async def update_genre( @@ -85,9 +109,21 @@ async def update_genre( update_data: GenreUpdate, ) -> GenreResponse: genre_response = await self.genre_service.update_genre(genre_id, update_data) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.genre, + entity_id=genre_response.id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.redis_service.delete(key=key), + ) + app.send_task( + args=[genre_id], + name=TaskType.invalidate_movie_detail_cache_by_genre.value, + queue=Queue.app.value, + ) return genre_response async def partial_update_genre( @@ -99,13 +135,32 @@ async def partial_update_genre( genre_id, update_data, ) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.genre, + entity_id=genre_response.id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.redis_service.delete(key=key), + ) + app.send_task( + args=[genre_id], + name=TaskType.invalidate_movie_detail_cache_by_genre.value, + queue=Queue.app.value, + ) return genre_response async def delete_genre_by_id(self, genre_id: int) -> None: await self.genre_service.delete_genre_by_id(genre_id) - key = RedisService.create_cache_key("genre") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.genre, + entity_id=genre_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.genre), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.redis_service.delete(key=key), + ) diff --git a/app/cache_services/movie.py b/app/cache_services/movie.py index 09f7df1..d5dbb60 100644 --- a/app/cache_services/movie.py +++ b/app/cache_services/movie.py @@ -1,5 +1,8 @@ +import asyncio from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep from schemas.movie import ( @@ -19,16 +22,22 @@ class MovieCacheService: def __init__( self, movie_service: MovieService, - cache_service_for_movie: RedisService, - cache_service_for_watch_history: RedisService, + movie_redis_service: RedisService, + watch_history_redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.movie_service = movie_service - self.cache_service_for_movie = cache_service_for_movie - self.cache_service_for_watch_history = cache_service_for_watch_history + self.movie_redis_service = movie_redis_service + self.watch_history_redis_service = watch_history_redis_service + self.cache_key_service = cache_key_service async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: - key = RedisService.create_cache_key("movie", movie_id=movie_id) - cached_movie_response = await self.cache_service_for_movie.get( + key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + cached_movie_response = await self.movie_redis_service.get( key, MovieWithGenreResponse, ) @@ -36,7 +45,11 @@ async def get_movie_by_id(self, movie_id: int) -> MovieWithGenreResponse: return cast(MovieWithGenreResponse, cached_movie_response) movie_response = await self.movie_service.get_movie_by_id(movie_id) - await self.cache_service_for_movie.set(key, movie_response, ttl=60) + await self.movie_redis_service.set( + key, + movie_response, + ttl=24 * 60 * 60, + ) return movie_response async def get_movies( @@ -44,8 +57,13 @@ async def get_movies( size: int = 10, page: int = 1, ) -> MovieWithGenreResponseList: - key = RedisService.create_cache_key("movies", size=size, page=page) - cached_movies_response = await self.cache_service_for_movie.get( + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get", + size=size, + page=page, + ) + cached_movies_response = await self.movie_redis_service.get( key, MovieWithGenreResponseList, ) @@ -53,7 +71,11 @@ async def get_movies( return cast(MovieWithGenreResponseList, cached_movies_response) movies_response = await self.movie_service.get_movies(size, page) - await self.cache_service_for_movie.set(key, movies_response, ttl=60) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def search_movies_with_filters( @@ -62,13 +84,14 @@ async def search_movies_with_filters( size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> MovieWithGenreResponseList: - key = RedisService.create_cache_key( - "movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get-search", size=size, page=page, **movie_filter.model_dump(), ) - cached_movies_response = await self.cache_service_for_movie.get( + cached_movies_response = await self.movie_redis_service.get( key, MovieWithGenreResponseList, ) @@ -80,22 +103,23 @@ async def search_movies_with_filters( size, page, ) - await self.cache_service_for_movie.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> MovieResponseList: - key = RedisService.create_cache_key( - "movies", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.movie, + action="get-search", genre_id=genre_id, - size=size, - page=page, ) - cached_movies_response = await self.cache_service_for_movie.get( + cached_movies_response = await self.movie_redis_service.get( key, MovieResponseList, ) @@ -104,10 +128,12 @@ async def get_movies_by_genre_id( movies_response = await self.movie_service.get_movies_by_genre_id( genre_id, - size, - page, ) - await self.cache_service_for_movie.set(key, movies_response, ttl=1800) + await self.movie_redis_service.set( + key, + movies_response, + ttl=24 * 60 * 60, + ) return movies_response async def watch_movie( @@ -119,9 +145,21 @@ async def watch_movie( user_id, create_watch_history_data, ) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.cache_service_for_watch_history.delete_by_pattern(pattern) + count_watch_history_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + await asyncio.gather( + self.watch_history_redis_service.incr_by( + count_watch_history_key, + amount=1, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), + ) return movie_response async def create_movie( @@ -129,9 +167,7 @@ async def create_movie( create_movie_data: MovieCreate, ) -> MovieWithGenreResponse: movie_response = await self.movie_service.create_movie(create_movie_data) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie) return movie_response async def update_movie( @@ -143,9 +179,15 @@ async def update_movie( movie_id, update_movie_data, ) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) return movie_response async def partial_update_movie( @@ -157,13 +199,25 @@ async def partial_update_movie( movie_id, update_movie_data, ) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) return movie_response async def delete_movie_by_id(self, movie_id: int) -> None: await self.movie_service.delete_movie_by_id(movie_id) - key = RedisService.create_cache_key("movie") - pattern = key + "*" - await self.cache_service_for_movie.delete_by_pattern(pattern) + movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.movie), + self.movie_redis_service.delete(movie_key), + ) diff --git a/app/cache_services/review.py b/app/cache_services/review.py index cf3deca..f123db1 100644 --- a/app/cache_services/review.py +++ b/app/cache_services/review.py @@ -1,5 +1,8 @@ +import asyncio from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep from schemas.review import ( @@ -18,18 +21,25 @@ class ReviewCacheService: def __init__( self, review_service: ReviewService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.review_service = review_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_reviews( self, size: int = 10, page: int = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key("reviews", size=size, page=page) - cached_reviews_response = await self.cache_service.get( + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", + size=size, + page=page, + ) + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -37,16 +47,20 @@ async def get_reviews( return cast(ReviewWithUserResponseList, cached_reviews_response) reviews_response = await self.review_service.get_reviews(size, page) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: - key = RedisService.create_cache_key("review", review_id=review_id) - cached_review_response = await self.cache_service.get( + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + cached_review_response = await self.redis_service.get( key, ReviewWithUserResponse, ) @@ -54,10 +68,10 @@ async def get_review_by_id(self, review_id: int) -> ReviewWithUserResponse: return cast(ReviewWithUserResponse, cached_review_response) review_response = await self.review_service.get_review_by_id(review_id) - await self.cache_service.set( + await self.redis_service.set( key, review_response, - ttl=1800, + ttl=30 * 60, ) return review_response @@ -67,13 +81,15 @@ async def get_user_reviews( size: int = 10, page: int = 1, ) -> ReviewWithMovieResponseList: - key = RedisService.create_cache_key( - "reviews", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", + version_params={"user_id": user_id}, user_id=user_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithMovieResponseList, ) @@ -85,10 +101,10 @@ async def get_user_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -97,12 +113,13 @@ async def get_user_review_about_movie( user_id: int, movie_id: int, ) -> ReviewResponse: - key = RedisService.create_cache_key( - "review", - user_id=user_id, - movie_id=movie_id, + entity_id = (user_id, movie_id) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", ) - cached_review_response = await self.cache_service.get(key, ReviewResponse) + cached_review_response = await self.redis_service.get(key, ReviewResponse) if cached_review_response is not None: return cast(ReviewResponse, cached_review_response) @@ -110,10 +127,10 @@ async def get_user_review_about_movie( user_id, movie_id, ) - await self.cache_service.set( + await self.redis_service.set( key, review_response, - ttl=1800, + ttl=30 * 60, ) return review_response @@ -123,13 +140,15 @@ async def get_movie_reviews( size: int = 10, page: int = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -141,10 +160,10 @@ async def get_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -154,13 +173,15 @@ async def get_low_rated_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:low-rated", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-low-rated", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -172,10 +193,10 @@ async def get_low_rated_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -185,13 +206,15 @@ async def get_top_rated_movie_reviews( size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-rated", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-rated", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -203,10 +226,10 @@ async def get_top_rated_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -216,13 +239,15 @@ async def get_top_newest_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-newest", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-newest", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) - cached_reviews_response = await self.cache_service.get( + cached_reviews_response = await self.redis_service.get( key, ReviewWithUserResponseList, ) @@ -234,10 +259,10 @@ async def get_top_newest_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -247,15 +272,17 @@ async def get_top_oldest_movie_reviews( size: int, page: int, ) -> ReviewWithUserResponseList: - key = RedisService.create_cache_key( - "reviews:top-oldest", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.review, + action="get-top-oldest", + version_params={"movie_id": movie_id}, movie_id=movie_id, size=size, page=page, ) cached_reviews_response = cast( ReviewWithUserResponseList, - await self.cache_service.get(key, ReviewWithUserResponseList), + await self.redis_service.get(key, ReviewWithUserResponseList), ) if cached_reviews_response is not None: return cached_reviews_response @@ -265,10 +292,10 @@ async def get_top_oldest_movie_reviews( size, page, ) - await self.cache_service.set( + await self.redis_service.set( key, reviews_response, - ttl=1800, + ttl=30 * 60, ) return reviews_response @@ -281,9 +308,18 @@ async def create_review( user_id, create_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + movie_id = review_response.movie_id + + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=user_id, + ), + ) return review_response async def update_review( @@ -297,9 +333,30 @@ async def update_review( review_id, update_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + review_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + user_id, movie_id = review_response.user_id, review_response.movie_id + entity_id = (user_id, movie_id) + user_review_about_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), + ) return review_response async def partial_update_review( @@ -313,9 +370,30 @@ async def partial_update_review( review_id, update_review_data, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + review_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + user_id, movie_id = review_response.user_id, review_response.movie_id + entity_id = (user_id, movie_id) + user_review_about_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), + ) return review_response async def delete_review( @@ -323,10 +401,31 @@ async def delete_review( current_user_id: int, review_id: int, ) -> None: - await self.review_service.delete_review( + review = await self.review_service.delete_review( current_user_id, review_id, ) - key = RedisService.create_cache_key("review") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_id, movie_id = review.user_id, review.movie_id + entity_id = (user_id, movie_id) + review_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=review_id, + action="get", + ) + user_review_about_movie_key = self.cache_key_service.build_item_key( + entity=CacheEntity.review, + entity_id=entity_id, + action="get", + ) + await asyncio.gather( + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie_id, + ), + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + user_id=current_user_id, + ), + self.redis_service.delete(review_key), + self.redis_service.delete(user_review_about_movie_key), + ) diff --git a/app/cache_services/user.py b/app/cache_services/user.py index 548c2ff..80b921a 100644 --- a/app/cache_services/user.py +++ b/app/cache_services/user.py @@ -1,5 +1,11 @@ +import asyncio from typing import cast +from packages.celery.constants import Queue, TaskType + +from core.celery.celery_app import app +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.user import ( UserCreate, @@ -15,39 +21,66 @@ class UserCacheService: def __init__( self, user_service: UserService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.user_service = user_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_user_by_id(self, user_id: int) -> UserResponse: - key = RedisService.create_cache_key("user", user_id=user_id) - cached_user_response = await self.cache_service.get(key, UserResponse) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + cached_user_response = await self.redis_service.get(key, UserResponse) if cached_user_response is not None: return cast(UserResponse, cached_user_response) user_response = await self.user_service.get_user_by_id(user_id) - await self.cache_service.set(key, user_response) + await self.redis_service.set( + key, + user_response, + ttl=24 * 60 * 60, + ) return user_response async def get_user_by_login(self, login: str) -> UserResponse: - key = RedisService.create_cache_key("user", login=login) - cached_user_response = await self.cache_service.get(key, UserResponse) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=login, + action="get-by-login", + ) + cached_user_response = await self.redis_service.get(key, UserResponse) if cached_user_response is not None: return cast(UserResponse, cached_user_response) user_response = await self.user_service.get_user_by_login(login) - await self.cache_service.set(key, user_response) + await self.redis_service.set( + key, + user_response, + ttl=24 * 60 * 60, + ) return user_response async def get_all_users(self, size: int = 10, page: int = 1) -> UserResponseList: - key = RedisService.create_cache_key("users", size=size, page=page) - cached_users_response = await self.cache_service.get(key, UserResponseList) + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.user, + action="get", + size=size, + page=page, + ) + cached_users_response = await self.redis_service.get(key, UserResponseList) if cached_users_response is not None: return cast(UserResponseList, cached_users_response) users_response = await self.user_service.get_all_users(size, page) - await self.cache_service.set(key, users_response) + await self.redis_service.set( + key, + users_response, + ttl=12 * 60 * 60, + ) return users_response async def create_user( @@ -55,9 +88,7 @@ async def create_user( user_create_data: UserCreate, ) -> UserResponse: user_response = await self.user_service.create_user(user_create_data) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + await self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user) return user_response async def update_user( @@ -66,9 +97,20 @@ async def update_user( update_data: UserUpdate, ) -> UserResponse: user_response = await self.user_service.update_user(user_id, update_data) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) return user_response async def partial_update_user( @@ -80,19 +122,47 @@ async def partial_update_user( user_id, update_data, ) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) return user_response async def delete_user_by_id(self, user_id: int) -> None: await self.user_service.delete_user_by_id(user_id) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=user_id, + action="get", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) + app.send_task( + args=[user_id], + name=TaskType.invalidate_reviews_cache_on_update_user.value, + queue=Queue.app.value, + ) async def delete_user_by_login(self, login: str) -> None: await self.user_service.delete_user_by_login(login) - key = RedisService.create_cache_key("user") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + user_key = self.cache_key_service.build_item_key( + entity=CacheEntity.user, + entity_id=login, + action="get-by-login", + ) + await asyncio.gather( + self.redis_service.delete(user_key), + self.cache_key_service.invalidate_list_keys(entity=CacheEntity.user), + ) diff --git a/app/cache_services/watch_history.py b/app/cache_services/watch_history.py index de2b9fe..71f89db 100644 --- a/app/cache_services/watch_history.py +++ b/app/cache_services/watch_history.py @@ -1,6 +1,9 @@ +import asyncio from datetime import date from typing import cast +from core.constants import CacheEntity +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService from schemas.watch_history import ( WatchHistoryWithMovieResponse, @@ -13,20 +16,23 @@ class WatchHistoryCacheService: def __init__( self, watch_history_service: WatchHistoryService, - cache_service: RedisService, + redis_service: RedisService, + cache_key_service: CacheKeyService, ) -> None: self.watch_history_service = watch_history_service - self.cache_service = cache_service + self.redis_service = redis_service + self.cache_key_service = cache_key_service async def get_watch_history_by_id( self, watch_history_id: int, ) -> WatchHistoryWithMovieResponse: - key = RedisService.create_cache_key( - "watch_history", - watch_history_id=watch_history_id, + key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=watch_history_id, + action="get", ) - cached_watch_history_response = await self.cache_service.get( + cached_watch_history_response = await self.redis_service.get( key, schema=WatchHistoryWithMovieResponse, ) @@ -36,7 +42,11 @@ async def get_watch_history_by_id( watch_history_response = ( await self.watch_history_service.get_watch_history_by_id(watch_history_id) ) - await self.cache_service.set(key, watch_history_response) + await self.redis_service.set( + key, + watch_history_response, + ttl=5 * 60, + ) return watch_history_response async def get_watch_history_list( @@ -45,13 +55,15 @@ async def get_watch_history_list( size: int = 10, page: int = 1, ) -> WatchHistoryWithMovieResponseList: - key = RedisService.create_cache_key( - "watch_history_list", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", + version_params={"user_id": user_id}, user_id=user_id, size=size, page=page, ) - cached_watch_history_list_response = await self.cache_service.get( + cached_watch_history_list_response = await self.redis_service.get( key, WatchHistoryWithMovieResponseList, ) @@ -64,7 +76,11 @@ async def get_watch_history_list( watch_history_list_response = ( await self.watch_history_service.get_watch_history_list(user_id, size, page) ) - await self.cache_service.set(key, watch_history_list_response) + await self.redis_service.set( + key, + watch_history_list_response, + ttl=15 * 60, + ) return watch_history_list_response async def get_watch_history_by_date_range( @@ -75,15 +91,17 @@ async def get_watch_history_by_date_range( size: int = 10, page: int = 1, ) -> WatchHistoryWithMovieResponseList: - key = RedisService.create_cache_key( - "watch_history_list", + key = await self.cache_key_service.build_list_key( + entity=CacheEntity.watch_history, + action="get", + version_params={"user_id": user_id}, user_id=user_id, start_date=start_date, end_date=end_date, size=size, page=page, ) - cached_watch_history_list_response = await self.cache_service.get( + cached_watch_history_list_response = await self.redis_service.get( key, WatchHistoryWithMovieResponseList, ) @@ -102,18 +120,30 @@ async def get_watch_history_by_date_range( page, ) ) - await self.cache_service.set(key, watch_history_list_response) + await self.redis_service.set( + key, + watch_history_list_response, + ttl=15 * 60, + ) return watch_history_list_response async def count_user_watch_history(self, user_id: int) -> int: - key = RedisService.create_cache_key("watch_history", user_id=user_id) - cached_watch_history_response = await self.cache_service.get(key) + key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + cached_watch_history_response = await self.redis_service.get(key) if cached_watch_history_response is not None: return cast(int, cached_watch_history_response) watch_history_response = ( await self.watch_history_service.count_user_watch_history(user_id) ) - await self.cache_service.set(key, watch_history_response) + await self.redis_service.set( + key, + watch_history_response, + ttl=24 * 60 * 60, + ) return watch_history_response async def delete_watch_history_by_id( @@ -125,12 +155,44 @@ async def delete_watch_history_by_id( user_id, watch_history_id, ) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + watch_history_count_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + watch_history_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=watch_history_id, + action="get", + ) + change_watch_history_count_cache = self.redis_service.incr_by( + key=watch_history_count_key, + amount=-1, + ) + delete_watch_history_item_cache = self.redis_service.delete(watch_history_key) + await asyncio.gather( + change_watch_history_count_cache, + delete_watch_history_item_cache, + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), + ) async def delete_user_watch_history(self, user_id: int) -> None: await self.watch_history_service.delete_user_watch_history(user_id) - key = RedisService.create_cache_key("watch_history") - pattern = key + "*" - await self.cache_service.delete_by_pattern(pattern) + watch_history_count_key = self.cache_key_service.build_item_key( + entity=CacheEntity.watch_history, + entity_id=user_id, + action="get-count", + ) + delete_watch_history_count_cache = self.redis_service.delete( + key=watch_history_count_key, + ) + await asyncio.gather( + delete_watch_history_count_cache, + self.cache_key_service.invalidate_list_keys( + entity=CacheEntity.watch_history, + user_id=user_id, + ), + ) diff --git a/app/core/celery/tasks.py b/app/core/celery/tasks.py index a873228..b1e7830 100644 --- a/app/core/celery/tasks.py +++ b/app/core/celery/tasks.py @@ -3,7 +3,11 @@ from packages.celery.utils import sync_run_coroutine_function from .celery_app import app -from .utils import get_data_to_send_inactive_users_movie_selection +from .utils import ( + get_data_to_send_inactive_users_movie_selection, + invalidate_movie_detail_cache_by_genre_id, + invalidate_reviews_cache_on_update_user, +) @app.task( # type: ignore[untyped-decorator] @@ -31,3 +35,21 @@ def create_chain_to_notify_inactive_users() -> None: send_inactive_users_email, ) task_chain.delay() + + +@app.task( # type: ignore[untyped-decorator] + name=TaskType.invalidate_movie_detail_cache_by_genre.value, +) +def invalidate_movie_detail_cache_by_genre(genre_id: int) -> None: + sync_run_coroutine_function( + invalidate_movie_detail_cache_by_genre_id(genre_id), + ) + + +@app.task( # type: ignore[untyped-decorator] + name=TaskType.invalidate_reviews_cache_on_update_user.value, +) +def invalidate_movie_reviews_cache_on_update_user(user_id: int) -> None: + sync_run_coroutine_function( + invalidate_reviews_cache_on_update_user(user_id), + ) diff --git a/app/core/celery/utils.py b/app/core/celery/utils.py index 4166191..73ab3e7 100644 --- a/app/core/celery/utils.py +++ b/app/core/celery/utils.py @@ -1,3 +1,4 @@ +import asyncio from asyncio import gather from packages.rabbitmq.connection import rabbitmq_connection_startup @@ -9,8 +10,13 @@ SendInactiveUsersMovieSelectionData, ) -from core.constants import INACTIVE_DAYS, SortMonotony, SortType -from core.utils import get_movie_service, get_user_service +from core.constants import INACTIVE_DAYS, CacheEntity, SortMonotony, SortType +from core.utils import ( + get_cache_key_service, + get_movie_redis_service, + get_movie_service, + get_user_service, +) from schemas.movie import MovieFilter @@ -63,3 +69,41 @@ async def get_data_to_send_inactive_users_movie_selection() -> ( inactive_users=inactive_users, movie_selection=movie_selection, ) + + +async def invalidate_movie_detail_cache_by_genre_id(genre_id: int) -> None: + await rabbitmq_connection_startup() + async with ( + get_movie_service() as movie_service, + get_movie_redis_service() as movie_redis_service, + get_cache_key_service() as cache_key_service, + ): + movies = await movie_service.get_movies_by_genre_id(genre_id=genre_id) + invalidate_movie_keys = [] + for movie in movies.movie_list: + movie_key = cache_key_service.build_item_key( + entity=CacheEntity.movie, + entity_id=movie.id, + action="get", + ) + invalidate_movie_keys.append(movie_key) + + await movie_redis_service.delete_list_of_keys(invalidate_movie_keys) + + +async def invalidate_reviews_cache_on_update_user(user_id: int) -> None: + await rabbitmq_connection_startup() + async with ( + get_movie_service() as movie_service, + get_cache_key_service() as cache_key_service, + ): + movies = await movie_service.get_movies_reviewed_by_user(user_id=user_id) + invalidate_review_coroutines = [] + for movie in movies.movie_list: + coroutine = cache_key_service.invalidate_list_keys( + entity=CacheEntity.review, + movie_id=movie.id, + ) + invalidate_review_coroutines.append(coroutine) + + await asyncio.gather(*invalidate_review_coroutines) diff --git a/app/core/config.py b/app/core/config.py index 8e47191..56f08ac 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -34,6 +34,7 @@ class RedisDataBaseConfig(BaseModel): watch_history: int = 6 auth: int = 7 celery_backend: int = 8 + cache_versioning: int = 9 class RedisConfig(BaseModel): @@ -127,6 +128,7 @@ class NotificationServiceConfig(BaseModel): class Settings(BaseSettings): + service_name: str = "movie-catalog" base_dir: Path = Path(__file__).parent.parent database: DataBaseConfig = DataBaseConfig() redis: RedisConfig = RedisConfig() diff --git a/app/core/constants.py b/app/core/constants.py index 8aa16cb..e161111 100644 --- a/app/core/constants.py +++ b/app/core/constants.py @@ -68,6 +68,15 @@ class ConfirmationCodeType(StrEnum): recover_password = "recover" +class CacheEntity(StrEnum): + genre = "genre" + movie = "movie" + favorite_movie = "favorite_movie" + review = "review" + user = "user" + watch_history = "watch_history" + + INACTIVE_DAYS = 7 ATTEMPT_FIELD = "attempt" diff --git a/app/core/rabbitmq/consumers.py b/app/core/rabbitmq/consumers.py index c36563a..67352a1 100644 --- a/app/core/rabbitmq/consumers.py +++ b/app/core/rabbitmq/consumers.py @@ -7,7 +7,10 @@ from cache_services import GenreCacheService, MovieCacheService from core.constants import AnyPydanticType -from core.utils import get_genre_cache_service, get_movie_cache_service +from core.utils import ( + get_genre_cache_service, + get_movie_cache_service, +) from schemas.genre import GenrePartialUpdate from schemas.movie import MoviePartialUpdate diff --git a/app/core/redis/cache_key_service.py b/app/core/redis/cache_key_service.py new file mode 100644 index 0000000..b5b4800 --- /dev/null +++ b/app/core/redis/cache_key_service.py @@ -0,0 +1,135 @@ +from datetime import date +from typing import cast + +from core.config import settings +from core.constants import CacheEntity, PrimitiveType +from core.redis import RedisService + + +class CacheKeyService: + list_field = "list" + + def __init__(self, redis: RedisService) -> None: + self.redis = redis + + async def __init_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> int: + key = self.__create_version_entity_key(entity, **version_params) + start_value = 1 + await self.redis.set( + key=key, + value=start_value, + ) + return start_value + + async def __get_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> int: + key = self.__create_version_entity_key(entity, **version_params) + version = await self.redis.get(key, is_integer=True) + return cast(int, version) + + async def __update_version( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> None: + key = self.__create_version_entity_key(entity, **version_params) + await self.redis.incr_by(key, amount=1) + + @classmethod + def __create_version_entity_key( + cls, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> str: + version = "version" + version_entity_key_list = [ + settings.service_name, + version, + cls.list_field, + entity.value, + ] + for parameter_name, value in version_params.items(): + string_value = str(value) + version_entity_key_list.append(parameter_name) + version_entity_key_list.append(string_value) + + version_entity_key = ":".join(version_entity_key_list) + return version_entity_key + + async def invalidate_list_keys( + self, + entity: CacheEntity, + **version_params: PrimitiveType, + ) -> None: + await self.__update_version( + entity=entity, + **version_params, + ) + + @staticmethod + def build_item_key( + entity: CacheEntity, + entity_id: int | str | tuple[int, ...], + action: str = "get", + **params: PrimitiveType, + ) -> str: + key = f"{settings.service_name}:{entity.value}:{entity_id}:{action}" + for parameter_name, value in params.items(): + key = f"{key}:{parameter_name}:{value}" + + return key + + @classmethod + def build_item_regex_key( + cls, + entity: CacheEntity, + entity_id_regex: int | str | tuple[int, ...], + action_regex: str = "get", + **params: PrimitiveType, + ) -> str: + return cls.build_item_key( + entity=entity, + entity_id=entity_id_regex, + action=action_regex, + **params, + ) + + async def build_list_key( + self, + entity: CacheEntity, + action: str = "get", + version_params: dict[str, PrimitiveType] | None = None, + **params: PrimitiveType | date, + ) -> str: + if version_params is None: + version_params = {} + version = await self.__get_version(entity, **version_params) + if version is None: + version = await self.__init_version(entity, **version_params) + key = f"{settings.service_name}:{entity.value}:{self.list_field}:version:{version}:{action}" # noqa: E501 + + for parameter_name, value in params.items(): + key = f"{key}:{parameter_name}:{value}" + + return key + + async def build_list_regex_key( + self, + entity: CacheEntity, + action_regex: str = "get", + version_params: dict[str, PrimitiveType] | None = None, + **params: PrimitiveType, + ) -> str: + return await self.build_list_key( + entity=entity, + action=action_regex, + version_params=version_params, + **params, + ) diff --git a/app/core/redis/client.py b/app/core/redis/client.py index 6c83a66..e4fed4d 100644 --- a/app/core/redis/client.py +++ b/app/core/redis/client.py @@ -38,7 +38,12 @@ async def get(self, key: str) -> str | None: async def exists(self, key: str) -> bool: return cast(bool, await self._redis.exists(key)) - async def set(self, key: str, value: str | int, expire: int) -> None: + async def set( + self, + key: str, + value: str | int, + expire: int | None = None, + ) -> None: await self._redis.set( key, value, @@ -48,9 +53,13 @@ async def set(self, key: str, value: str | int, expire: int) -> None: async def delete(self, key: str) -> None: await self._redis.delete(key) + async def delete_list_of_keys(self, keys: list[str]) -> None: + if len(keys) > 0: + await self._redis.delete(*keys) + async def delete_by_pattern(self, pattern: str) -> None: - async for key in self._redis.scan_iter(match=pattern): - await self.delete(key) + keys_to_delete = [key async for key in self._redis.scan_iter(match=pattern)] + await self.delete_list_of_keys(keys_to_delete) async def expire(self, key: str, ttl: int = 300) -> None: await self._redis.expire(key, ttl) diff --git a/app/core/redis/service.py b/app/core/redis/service.py index 75372f1..da5964a 100644 --- a/app/core/redis/service.py +++ b/app/core/redis/service.py @@ -59,7 +59,12 @@ async def _get_boolean(self, key: str) -> bool | None: return boolean[value] return None - async def set(self, key: str, value: Any, ttl: int = 300) -> None: + async def set( + self, + key: str, + value: Any, + ttl: int | None = None, + ) -> None: encoded_value = self.convert_object_to_string(value) await self.redis.set(key, encoded_value, ttl) @@ -75,6 +80,9 @@ async def exists(self, key: str) -> bool: async def delete(self, key: str) -> None: await self.redis.delete(key) + async def delete_list_of_keys(self, keys: list[str]) -> None: + await self.redis.delete_list_of_keys(keys) + async def delete_by_pattern(self, pattern: str) -> None: await self.redis.delete_by_pattern(pattern) @@ -87,7 +95,8 @@ def create_cache_key(cls, prefix: str, **kwargs: Any) -> str: @staticmethod def convert_string_to_object( - value: str | int, schema: type[AnyPydanticType], + value: str | int, + schema: type[AnyPydanticType], ) -> AnyPydanticType | PrimitiveType: if schema is None: return value @@ -95,9 +104,14 @@ def convert_string_to_object( @staticmethod def convert_object_to_string(value: Any) -> Any: + if isinstance(value, bool): + boolean = {True: "True", False: "False"} + return boolean[value] + if isinstance( value, - (str, int, float, bool), + (str, int, float), ): return value + return value.model_dump_json() diff --git a/app/core/utils.py b/app/core/utils.py index eca4984..5bb1c1a 100644 --- a/app/core/utils.py +++ b/app/core/utils.py @@ -7,18 +7,42 @@ from cache_services import GenreCacheService, MovieCacheService from core.database import session_factory from core.redis import RedisClient, RedisService -from dependencies.redis_client import ( +from core.redis.cache_key_service import CacheKeyService +from dependencies.redis_clients import ( + get_cache_versioning_redis_client as get_cache_versioning_redis_client_dependency, +) +from dependencies.redis_clients import ( get_genre_redis_client as get_genre_redis_client_dependency, ) -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_movie_redis_client as get_movie_redis_client_dependency, ) -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_watch_history_redis_client as get_watch_history_redis_client_dependency, ) from services import GenreService, MovieService, UserService +@asynccontextmanager +async def get_cache_versioning_redis_client() -> AsyncGenerator[RedisClient]: + async for redis_client in get_cache_versioning_redis_client_dependency(): + yield redis_client + + +@asynccontextmanager +async def get_cache_versioning_redis_service() -> AsyncGenerator[RedisService]: + async with get_cache_versioning_redis_client() as redis_client: + redis_service = RedisService(redis_client) + yield redis_service + + +@asynccontextmanager +async def get_cache_key_service() -> AsyncGenerator[CacheKeyService]: + async with get_cache_versioning_redis_service() as redis_service: + cache_key_service = CacheKeyService(redis_service) + yield cache_key_service + + @asynccontextmanager async def get_session() -> AsyncGenerator[AsyncSession]: async with session_factory() as session: @@ -56,9 +80,14 @@ async def get_genre_redis_service() -> AsyncGenerator[RedisService]: async def get_genre_cache_service() -> AsyncGenerator[GenreCacheService]: async with ( get_genre_service() as genre_service, - get_genre_redis_service() as cache_service, + get_genre_redis_service() as redis_service, + get_cache_key_service() as cache_key_service, ): - genre_cache_service = GenreCacheService(genre_service, cache_service) + genre_cache_service = GenreCacheService( + genre_service, + redis_service, + cache_key_service, + ) yield genre_cache_service @@ -99,12 +128,14 @@ async def get_watch_history_redis_service() -> AsyncGenerator[RedisService]: async def get_movie_cache_service() -> AsyncGenerator[MovieCacheService]: async with ( get_movie_service() as movie_service, - get_movie_redis_service() as cache_service_for_movies, - get_watch_history_redis_service() as cache_service_for_watch_history, + get_movie_redis_service() as movie_redis_service, + get_watch_history_redis_service() as watch_history_redis_service, + get_cache_key_service() as cache_key_service, ): movie_cache_service = MovieCacheService( movie_service, - cache_service_for_movies, - cache_service_for_watch_history, + movie_redis_service, + watch_history_redis_service, + cache_key_service, ) yield movie_cache_service diff --git a/app/dependencies/annotations/cache_services.py b/app/dependencies/annotations/cache_services.py index a3fd375..2d3143e 100644 --- a/app/dependencies/annotations/cache_services.py +++ b/app/dependencies/annotations/cache_services.py @@ -10,7 +10,6 @@ UserCacheService, WatchHistoryCacheService, ) -from core.redis import RedisService from dependencies.cache_services import ( get_favorite_movie_cache_service, get_genre_cache_service, @@ -19,52 +18,6 @@ get_user_cache_service, get_watch_history_cache_service, ) -from dependencies.redis_services import ( - get_favorite_movie_redis_service, - get_genre_redis_service, - get_movie_redis_service, - get_review_redis_service, - get_user_redis_service, - get_watch_history_redis_service, -) - -GenreRedisServiceDep = Annotated[ - RedisService, - Depends(get_genre_redis_service), -] - -MovieRedisServiceDep = Annotated[ - RedisService, - Depends(get_movie_redis_service), -] - -FavoriteMovieRedisServiceDep = Annotated[ - RedisService, - Depends( - get_favorite_movie_redis_service, - ), -] - -ReviewRedisServiceDep = Annotated[ - RedisService, - Depends( - get_review_redis_service, - ), -] - -UserRedisServiceDep = Annotated[ - RedisService, - Depends( - get_user_redis_service, - ), -] - -WatchHistoryRedisServiceDep = Annotated[ - RedisService, - Depends( - get_watch_history_redis_service, - ), -] GenreCacheServiceDep = Annotated[ GenreCacheService, diff --git a/app/dependencies/annotations/redis_services.py b/app/dependencies/annotations/redis_services.py new file mode 100644 index 0000000..826fdb1 --- /dev/null +++ b/app/dependencies/annotations/redis_services.py @@ -0,0 +1,51 @@ +from typing import Annotated + +from fastapi import Depends + +from core.redis import RedisService +from dependencies.redis_services import ( + get_favorite_movie_redis_service, + get_genre_redis_service, + get_movie_redis_service, + get_review_redis_service, + get_user_redis_service, + get_watch_history_redis_service, +) + +GenreRedisServiceDep = Annotated[ + RedisService, + Depends(get_genre_redis_service), +] + +MovieRedisServiceDep = Annotated[ + RedisService, + Depends(get_movie_redis_service), +] + +FavoriteMovieRedisServiceDep = Annotated[ + RedisService, + Depends( + get_favorite_movie_redis_service, + ), +] + +ReviewRedisServiceDep = Annotated[ + RedisService, + Depends( + get_review_redis_service, + ), +] + +UserRedisServiceDep = Annotated[ + RedisService, + Depends( + get_user_redis_service, + ), +] + +WatchHistoryRedisServiceDep = Annotated[ + RedisService, + Depends( + get_watch_history_redis_service, + ), +] diff --git a/app/dependencies/cache_key_service.py b/app/dependencies/cache_key_service.py new file mode 100644 index 0000000..6827feb --- /dev/null +++ b/app/dependencies/cache_key_service.py @@ -0,0 +1,25 @@ +from collections.abc import AsyncGenerator +from typing import Annotated + +from fastapi import Depends + +from core.redis import RedisService +from core.redis.cache_key_service import CacheKeyService +from dependencies.redis_services import get_cache_versioning_redis_service + + +async def get_cache_key_service( + cache_key_redis_service: Annotated[ + RedisService, + Depends( + get_cache_versioning_redis_service, + ), + ], +) -> AsyncGenerator[CacheKeyService]: + try: + cache_key_service = CacheKeyService(cache_key_redis_service) + yield cache_key_service + finally: + """ + Действия после view. + """ diff --git a/app/dependencies/cache_services.py b/app/dependencies/cache_services.py index b71f269..251ed21 100644 --- a/app/dependencies/cache_services.py +++ b/app/dependencies/cache_services.py @@ -11,7 +11,9 @@ UserCacheService, ) from cache_services.watch_history import WatchHistoryCacheService +from core.redis.cache_key_service import CacheKeyService from core.redis.service import RedisService +from dependencies.cache_key_service import get_cache_key_service from dependencies.redis_services import ( get_favorite_movie_redis_service, get_genre_redis_service, @@ -47,9 +49,17 @@ async def get_genre_cache_service( RedisService, Depends(get_genre_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[GenreCacheService]: try: - genre_cache_service = GenreCacheService(genre_service, genre_redis_service) + genre_cache_service = GenreCacheService( + genre_service, + genre_redis_service, + cache_key_service, + ) yield genre_cache_service finally: """ @@ -70,12 +80,17 @@ async def get_movie_cache_service( RedisService, Depends(get_watch_history_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[MovieCacheService]: try: movie_cache_service = MovieCacheService( movie_service, movie_redis_service, watch_history_redis_service, + cache_key_service, ) yield movie_cache_service finally: @@ -93,11 +108,16 @@ async def get_favorite_movie_cache_service( RedisService, Depends(get_favorite_movie_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[FavoriteMovieCacheService]: try: favorite_movie_cache_service = FavoriteMovieCacheService( favorite_movie_service, favorite_movie_redis_service, + cache_key_service, ) yield favorite_movie_cache_service finally: @@ -115,9 +135,17 @@ async def get_review_cache_service( RedisService, Depends(get_review_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[ReviewCacheService]: try: - review_cache_service = ReviewCacheService(review_service, review_redis_service) + review_cache_service = ReviewCacheService( + review_service, + review_redis_service, + cache_key_service, + ) yield review_cache_service finally: """ @@ -134,11 +162,16 @@ async def get_watch_history_cache_service( RedisService, Depends(get_watch_history_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[WatchHistoryCacheService]: try: watch_history_cache_service = WatchHistoryCacheService( watch_history_service, watch_history_redis_service, + cache_key_service, ) yield watch_history_cache_service finally: @@ -156,9 +189,17 @@ async def get_user_cache_service( RedisService, Depends(get_user_redis_service), ], + cache_key_service: Annotated[ + CacheKeyService, + Depends(get_cache_key_service), + ], ) -> AsyncGenerator[UserCacheService]: try: - user_cache_service = UserCacheService(user_service, user_redis_service) + user_cache_service = UserCacheService( + user_service, + user_redis_service, + cache_key_service, + ) yield user_cache_service finally: """ diff --git a/app/dependencies/rate_limiter.py b/app/dependencies/rate_limiter.py index 3317afc..3b39b0b 100644 --- a/app/dependencies/rate_limiter.py +++ b/app/dependencies/rate_limiter.py @@ -7,7 +7,7 @@ from core.exceptions.base import TooManyRequestsError from core.redis.client import RedisClient from core.redis.rate_limiter import RateLimiter -from dependencies.redis_client import get_rate_limiter_redis_client +from dependencies.redis_clients import get_rate_limiter_redis_client def rate_limit_dependency_factory( diff --git a/app/dependencies/redis_client.py b/app/dependencies/redis_clients.py similarity index 93% rename from app/dependencies/redis_client.py rename to app/dependencies/redis_clients.py index 93466bd..5cdfc06 100644 --- a/app/dependencies/redis_client.py +++ b/app/dependencies/redis_clients.py @@ -53,3 +53,7 @@ async def get_redis_client() -> AsyncGenerator[RedisClient]: get_auth_redis_client = redis_client_factory( db=settings.redis.db.auth, ) + +get_cache_versioning_redis_client = redis_client_factory( + db=settings.redis.db.cache_versioning, +) diff --git a/app/dependencies/redis_services.py b/app/dependencies/redis_services.py index ffe33d3..48b5d92 100644 --- a/app/dependencies/redis_services.py +++ b/app/dependencies/redis_services.py @@ -4,8 +4,9 @@ from fastapi import Depends from core.redis import RedisClient, RedisService -from dependencies.redis_client import ( +from dependencies.redis_clients import ( get_auth_redis_client, + get_cache_versioning_redis_client, get_favorite_movie_redis_client, get_genre_redis_client, get_movie_redis_client, @@ -41,21 +42,31 @@ async def dependency( get_genre_redis_service = redis_service_factory( get_genre_redis_client, ) + get_movie_redis_service = redis_service_factory( get_movie_redis_client, ) + get_review_redis_service = redis_service_factory( get_review_redis_client, ) + get_favorite_movie_redis_service = redis_service_factory( get_favorite_movie_redis_client, ) + get_watch_history_redis_service = redis_service_factory( get_watch_history_redis_client, ) + get_user_redis_service = redis_service_factory( get_user_redis_client, ) + get_auth_redis_service = redis_service_factory( get_auth_redis_client, ) + +get_cache_versioning_redis_service = redis_service_factory( + get_cache_versioning_redis_client, +) diff --git a/app/repositories/favorite_movie.py b/app/repositories/favorite_movie.py index 71e447a..d21a4a6 100644 --- a/app/repositories/favorite_movie.py +++ b/app/repositories/favorite_movie.py @@ -97,20 +97,25 @@ async def create_user_favorite_movie( await self.session.refresh(favorite_movie) return await self.get_favorite_movie_by_id(favorite_movie.id) - async def delete_favorite_movie_by_id(self, favorite_movie_id: int) -> bool: - if await self.get_favorite_movie_by_id(favorite_movie_id) is None: - return False + async def delete_favorite_movie_by_id( + self, favorite_movie_id: int, + ) -> FavoriteMovie | None: + favorite_movie = await self.get_favorite_movie_by_id(favorite_movie_id) + if favorite_movie is None: + return None stmt = delete(FavoriteMovie).where(FavoriteMovie.id == favorite_movie_id) await self.session.execute(stmt) await self.session.commit() - return True + return favorite_movie - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> bool: - if ( - await self.get_user_favorite_movie(user_id=user_id, movie_id=movie_id) - is None - ): - return False + async def delete_user_favorite_movie( + self, user_id: int, movie_id: int, + ) -> FavoriteMovie | None: + favorite_movie = await self.get_user_favorite_movie( + user_id=user_id, movie_id=movie_id, + ) + if favorite_movie is None: + return None stmt = delete(FavoriteMovie).where( and_( @@ -120,4 +125,4 @@ async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> bool: ) await self.session.execute(stmt) await self.session.commit() - return True + return favorite_movie diff --git a/app/repositories/movie.py b/app/repositories/movie.py index 0437d8b..bbc8150 100644 --- a/app/repositories/movie.py +++ b/app/repositories/movie.py @@ -4,7 +4,7 @@ from core.constants import SortMonotony, SortType from dependencies.annotations.validators import PaginationPageDep, PaginationSizeDep -from models import Movie +from models import Movie, Review from schemas.movie import MovieCreate, MovieFilter, MoviePartialUpdate, MovieUpdate @@ -49,6 +49,11 @@ async def get_movies( result = await self.session.execute(stmt) return list(result.scalars().all()) + async def get_movies_reviewed_by_user(self, user_id: int) -> list[Movie]: + stmt = select(Movie).join(Review).where(Review.user_id == user_id) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + async def search_movies_with_filters( # noqa: C901 self, movie_filter: MovieFilter, @@ -97,15 +102,8 @@ async def search_movies_with_filters( # noqa: C901 async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> list[Movie]: - stmt = ( - select(Movie) - .where(Movie.genre_id == genre_id) - .limit(size) - .offset(size * (page - 1)) - ) + stmt = select(Movie).where(Movie.genre_id == genre_id) result = await self.session.execute(stmt) return list(result.scalars().all()) diff --git a/app/repositories/review.py b/app/repositories/review.py index 3804662..272a7ec 100644 --- a/app/repositories/review.py +++ b/app/repositories/review.py @@ -209,11 +209,12 @@ async def partial_update_review( await self.session.refresh(review) return await self.get_review_by_id(review.id) - async def delete_review(self, review_id: int) -> bool: - if await self.get_review_by_id(review_id) is None: - return False + async def delete_review(self, review_id: int) -> Review | None: + review = await self.get_review_by_id(review_id) + if review is None: + return None stmt = delete(Review).where(Review.id == review_id) await self.session.execute(stmt) await self.session.commit() - return True + return review diff --git a/app/schemas/favorite_movie.py b/app/schemas/favorite_movie.py index 55009a9..78c11e3 100644 --- a/app/schemas/favorite_movie.py +++ b/app/schemas/favorite_movie.py @@ -36,8 +36,8 @@ class FavoriteMovieResponseList(BaseModel): """ favorite_movie_list: list[FavoriteMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None class FavoriteMovieWithMovieResponse(FavoriteMovieResponse): @@ -55,5 +55,5 @@ class FavoriteMovieWithMovieResponseList(BaseModel): """ favorite_movie_list: list[FavoriteMovieWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/genre.py b/app/schemas/genre.py index 805c4d9..7bf4bb7 100644 --- a/app/schemas/genre.py +++ b/app/schemas/genre.py @@ -52,5 +52,5 @@ class GenreResponseList(BaseModel): """ genre_list: list[GenreResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/movie.py b/app/schemas/movie.py index 1ff20b3..b3d9754 100644 --- a/app/schemas/movie.py +++ b/app/schemas/movie.py @@ -70,8 +70,8 @@ class MovieResponseList(BaseModel): """ movie_list: list[MovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None class MovieWithGenreResponse(MovieResponse): @@ -88,8 +88,8 @@ class MovieWithGenreResponseList(BaseModel): """ movie_list: list[MovieWithGenreResponse] - size: int - page: int + size: int | None = None + page: int | None = None class MovieFilter(BaseModel): diff --git a/app/schemas/review.py b/app/schemas/review.py index 2d9e426..0248179 100644 --- a/app/schemas/review.py +++ b/app/schemas/review.py @@ -59,8 +59,8 @@ class ReviewResponseList(BaseModel): """ review_list: list[ReviewResponse] - size: int - page: int + size: int | None = None + page: int | None = None class ReviewWithUserResponse(ReviewResponse): @@ -77,8 +77,8 @@ class ReviewWithUserResponseList(BaseModel): """ review_list: list[ReviewWithUserResponse] - size: int - page: int + size: int | None = None + page: int | None = None class ReviewWithMovieResponse(ReviewResponse): @@ -95,5 +95,5 @@ class ReviewWithMovieResponseList(BaseModel): """ review_list: list[ReviewWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/user.py b/app/schemas/user.py index ce2320b..cb97cd4 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -77,5 +77,5 @@ class UserResponseList(BaseModel): """ user_list: list[UserResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/schemas/watch_history.py b/app/schemas/watch_history.py index 20b06f3..d5614ed 100644 --- a/app/schemas/watch_history.py +++ b/app/schemas/watch_history.py @@ -37,8 +37,8 @@ class WatchHistoryResponseList(BaseModel): """ watch_history_list: list[WatchHistoryResponse] - size: int - page: int + size: int | None = None + page: int | None = None class WatchHistoryWithMovieResponse(WatchHistoryResponse): @@ -55,5 +55,5 @@ class WatchHistoryWithMovieResponseList(BaseModel): """ watch_history_list: list[WatchHistoryWithMovieResponse] - size: int - page: int + size: int | None = None + page: int | None = None diff --git a/app/services/favorite_movie.py b/app/services/favorite_movie.py index be7155d..fcf8bf1 100644 --- a/app/services/favorite_movie.py +++ b/app/services/favorite_movie.py @@ -12,6 +12,7 @@ from repositories.favorite_movie import FavoriteMovieRepository from schemas.favorite_movie import ( FavoriteMovieCreate, + FavoriteMovieResponse, FavoriteMovieWithMovieResponse, FavoriteMovieWithMovieResponseList, ) @@ -123,7 +124,7 @@ async def delete_favorite_movie_by_id( self, user_id: int, favorite_movie_id: int, - ) -> None: + ) -> FavoriteMovieResponse: if not await self.user_repository.user_id_exists(user_id): raise UserIdNotFoundError(user_id) @@ -131,17 +132,29 @@ async def delete_favorite_movie_by_id( if owner.id != user_id: raise PermissionDeniedError - await self.favorite_movie_repository.delete_favorite_movie_by_id( - favorite_movie_id, + favorite_movie = ( + await self.favorite_movie_repository.delete_favorite_movie_by_id( + favorite_movie_id, + ) ) - async def delete_user_favorite_movie(self, user_id: int, movie_id: int) -> None: + if favorite_movie is None: + raise FavoriteMovieIdNotFoundError(favorite_movie_id) + return FavoriteMovieResponse.model_validate(favorite_movie) + + async def delete_user_favorite_movie( + self, user_id: int, movie_id: int, + ) -> FavoriteMovieResponse: if not await self.user_repository.user_id_exists(user_id): raise UserIdNotFoundError(user_id) if not await self.movie_repository.movie_id_exists(movie_id): raise MovieIdNotFoundError(movie_id) - if not await self.favorite_movie_repository.delete_user_favorite_movie( - user_id, - movie_id, - ): + favorite_movie = ( + await self.favorite_movie_repository.delete_user_favorite_movie( + user_id, + movie_id, + ) + ) + if favorite_movie is None: raise FavoriteMovieNotFoundByUserAndMovieError(user_id, movie_id) + return FavoriteMovieResponse.model_validate(favorite_movie) diff --git a/app/services/genre.py b/app/services/genre.py index c4b1a87..b6116de 100644 --- a/app/services/genre.py +++ b/app/services/genre.py @@ -42,7 +42,6 @@ async def get_all_genres( size: int = 10, page: int = 1, ) -> GenreResponseList: - genres = [ GenreResponse.model_validate(genre) for genre in await self.genre_repository.get_all_genres(size, page) @@ -55,14 +54,14 @@ async def get_all_genres( async def search_genres_by_name( self, - name: str, + search_query: str, size: int = 10, page: int = 1, ) -> GenreResponseList: genre_list = [ GenreResponse.model_validate(genre) for genre in await self.genre_repository.search_genres_by_name( - name, + search_query, size, page, ) diff --git a/app/services/movie.py b/app/services/movie.py index 8f44f63..c7045d0 100644 --- a/app/services/movie.py +++ b/app/services/movie.py @@ -83,6 +83,17 @@ async def get_movies( page=page, ) + async def get_movies_reviewed_by_user(self, user_id: int) -> MovieResponseList: + movies = [ + MovieResponse.model_validate(movie) + for movie in await self.movie_repository.get_movies_reviewed_by_user( + user_id, + ) + ] + return MovieResponseList( + movie_list=movies, + ) + async def search_movies_with_filters( self, movie_filter: MovieFilter, @@ -106,8 +117,6 @@ async def search_movies_with_filters( async def get_movies_by_genre_id( self, genre_id: int, - size: int = 10, - page: int = 1, ) -> MovieResponseList: if not await self.genre_repository.genre_id_exists(genre_id): raise MovieIdNotFoundError(genre_id) @@ -116,14 +125,10 @@ async def get_movies_by_genre_id( MovieResponse.model_validate(movie) for movie in await self.movie_repository.get_movies_by_genre_id( genre_id, - size, - page, ) ] return MovieResponseList( movie_list=movies, - size=size, - page=page, ) async def create_movie( diff --git a/app/services/review.py b/app/services/review.py index a09f6a8..c831ff2 100644 --- a/app/services/review.py +++ b/app/services/review.py @@ -9,6 +9,7 @@ ReviewNotFoundByUserAndMovieError, ) from core.exceptions.user import UserIdNotFoundError +from models import Review from repositories import MovieRepository, ReviewRepository, UserRepository from schemas.review import ( ReviewCreate, @@ -273,7 +274,7 @@ async def delete_review( self, current_user_id: int, review_id: int, - ) -> None: + ) -> Review: if not await self.user_repository.user_id_exists(current_user_id): raise UserIdNotFoundError(current_user_id) @@ -286,7 +287,10 @@ async def delete_review( ): raise PermissionDeniedError - await self.review_repository.delete_review(review_id) + review = await self.review_repository.delete_review(review_id) + if review is None: + raise ReviewIdNotFoundError(review_id) + return review async def get_review_owner(self, review_id: int) -> UserResponse: user = await self.review_repository.get_review_owner(review_id) diff --git a/docker-compose.yml b/docker-compose.yml index 4a78656..bcf7515 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: start_period: 3s interval: 2s timeout: 2s - retries: 3 + retries: 5 depends_on: minio: condition: service_healthy diff --git a/frontend/app/api/api_v1/genres.js b/frontend/app/api/api_v1/genres.js index f69dd10..8c9326d 100644 --- a/frontend/app/api/api_v1/genres.js +++ b/frontend/app/api/api_v1/genres.js @@ -14,7 +14,7 @@ var p = page != null ? page : 1; var s = size != null ? size : 10; var q = - "genre_name=" + + "search_query=" + encodeURIComponent(name) + "&page=" + encodeURIComponent(p) + diff --git a/packages/celery/constants.py b/packages/celery/constants.py index 091ce5d..e47898c 100644 --- a/packages/celery/constants.py +++ b/packages/celery/constants.py @@ -24,3 +24,9 @@ class TaskType(StrEnum): "movie-catalog.mailing-list.get-data-to-send-inactive-users-email" ) send_inactive_users_email = "notification-service.email.send-inactive-users-email" + invalidate_movie_detail_cache_by_genre = ( + "movie-catalog.cache.invalidate-movie-detail-cache-by-genre" + ) + invalidate_reviews_cache_on_update_user = ( + "movie-catalog.cache.invalidate-reviews-cache-on-update-user" + ) diff --git a/packages/rabbitmq/constants.py b/packages/rabbitmq/constants.py index c6d3af2..285e067 100644 --- a/packages/rabbitmq/constants.py +++ b/packages/rabbitmq/constants.py @@ -22,6 +22,10 @@ class ActionType(StrEnum): copy_file = "copy_file" delete_file = "delete_file" + update_watch_history_cache_on_watch_movie = ( + "update_watch_history_cache_on_watch_movie" + ) + class ExchangeType(StrEnum): direct = "direct" @@ -69,3 +73,9 @@ class Queue(StrEnum): entity="content", action=ActionType.delete_file, ) + + update_watch_history_cache_on_watch_movie = create_queue_name( + consumer=ConsumerType.app, + entity="content", + action=ActionType.update_watch_history_cache_on_watch_movie, + ) diff --git a/packages/rabbitmq/utils.py b/packages/rabbitmq/utils.py index 18055f0..5143039 100644 --- a/packages/rabbitmq/utils.py +++ b/packages/rabbitmq/utils.py @@ -52,6 +52,8 @@ def create_queue_name( @asynccontextmanager async def get_channel() -> AsyncGenerator[AbstractChannel]: + # if connection.RABBIT_MQ_CONNECTION is None: + # await rabbitmq_connection_startup() assert connection.RABBIT_MQ_CONNECTION is not None async with cast( AbstractChannel,