mirror of
https://github.com/flibusta-apps/users_settings_server.git
synced 2025-12-06 14:45:38 +01:00
Add redis cache
This commit is contained in:
@@ -3,7 +3,7 @@ from typing import cast
|
||||
from app.models import User, Language
|
||||
|
||||
|
||||
async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]):
|
||||
async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]) -> bool:
|
||||
user_allowed_langs = cast(list[Language], user.allowed_langs)
|
||||
|
||||
exists_langs = set(lang.code for lang in user_allowed_langs)
|
||||
@@ -16,9 +16,15 @@ async def update_user_allowed_langs(user: User, new_allowed_langs: list[str]):
|
||||
|
||||
langs = await Language.objects.filter(code__in=all_process_langs).all()
|
||||
|
||||
updated = False
|
||||
|
||||
for lang in langs:
|
||||
if lang.code in to_delete:
|
||||
await user.allowed_langs.remove(lang)
|
||||
updated = True
|
||||
|
||||
if lang.code in to_add:
|
||||
await user.allowed_langs.add(lang)
|
||||
updated = True
|
||||
|
||||
return updated
|
||||
157
src/app/services/users_data_manager.py
Normal file
157
src/app/services/users_data_manager.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
import aioredis
|
||||
import orjson
|
||||
|
||||
from app.models import User
|
||||
from app.serializers import UserCreateOrUpdate, UserDetail, UserUpdate
|
||||
from app.services.allowed_langs_updater import update_user_allowed_langs
|
||||
|
||||
|
||||
class UsersDataManager:
|
||||
@classmethod
|
||||
async def _get_user_from_db(cls, user_id: int) -> Optional[User]:
|
||||
return await User.objects.select_related("allowed_langs").get_or_none(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_cache_key(cls, user_id: int) -> str:
|
||||
return f"user_{user_id}"
|
||||
|
||||
@classmethod
|
||||
async def _get_user_from_cache(
|
||||
cls, user_id: int, redis: aioredis.Redis
|
||||
) -> Optional[UserDetail]:
|
||||
try:
|
||||
key = cls._get_cache_key(user_id)
|
||||
data = await redis.get(key)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
return UserDetail.parse_obj(orjson.loads(data))
|
||||
|
||||
except aioredis.RedisError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def _cache_user(cls, user: User, redis: aioredis.Redis) -> bool:
|
||||
try:
|
||||
key = cls._get_cache_key(user.id)
|
||||
data = orjson.dumps(user.dict())
|
||||
await redis.set(key, data)
|
||||
return True
|
||||
except aioredis.RedisError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_user(
|
||||
cls, user_id: int, redis: aioredis.Redis
|
||||
) -> Optional[UserDetail]:
|
||||
if cached_user := await cls._get_user_from_cache(user_id, redis):
|
||||
return cached_user
|
||||
|
||||
user = await cls._get_user_from_db(user_id)
|
||||
|
||||
if not user:
|
||||
return None
|
||||
|
||||
await cls._cache_user(user, redis)
|
||||
return user # type: ignore
|
||||
|
||||
@classmethod
|
||||
def _is_has_data_to_update(cls, new_user: UserUpdate) -> bool:
|
||||
data_dict = new_user.dict()
|
||||
|
||||
update_data = {}
|
||||
for key in data_dict:
|
||||
if data_dict[key] is not None:
|
||||
update_data[key] = data_dict[key]
|
||||
|
||||
return bool(update_data)
|
||||
|
||||
@classmethod
|
||||
async def _create(cls, data: UserCreateOrUpdate):
|
||||
data_dict = data.dict()
|
||||
allowed_langs = data_dict.pop("allowed_langs", None) or ["ru", "be", "uk"]
|
||||
|
||||
user_obj = await User.objects.select_related("allowed_langs").create(
|
||||
**data_dict
|
||||
)
|
||||
await update_user_allowed_langs(user_obj, allowed_langs)
|
||||
|
||||
return user_obj
|
||||
|
||||
@classmethod
|
||||
async def _update(
|
||||
cls, user_id: int, update_data: dict, redis: aioredis.Redis
|
||||
) -> User:
|
||||
user_obj = await cls._get_user_from_db(user_id)
|
||||
assert user_obj is not None
|
||||
|
||||
if allowed_langs := update_data.pop("allowed_langs", None):
|
||||
await update_user_allowed_langs(user_obj, allowed_langs)
|
||||
|
||||
if update_data:
|
||||
user_obj.update_from_dict(update_data)
|
||||
await user_obj.update()
|
||||
|
||||
await cls._cache_user(user_obj, redis)
|
||||
|
||||
return user_obj
|
||||
|
||||
@classmethod
|
||||
async def create_or_update_user(
|
||||
cls, data: UserCreateOrUpdate, redis: aioredis.Redis
|
||||
):
|
||||
user = await cls.get_user(data.user_id, redis)
|
||||
|
||||
if user is None:
|
||||
new_user = await cls._create(data)
|
||||
await cls._cache_user(new_user, redis)
|
||||
return new_user
|
||||
|
||||
if not cls._is_need_update(user, data):
|
||||
return user
|
||||
|
||||
return await cls._update(user.user_id, data.dict(), redis)
|
||||
|
||||
@classmethod
|
||||
def _is_need_update(
|
||||
cls, old_user: UserDetail, new_user: Union[UserUpdate, UserCreateOrUpdate]
|
||||
) -> bool:
|
||||
old_data = old_user.dict()
|
||||
new_data = new_user.dict()
|
||||
|
||||
allowed_langs = new_data.pop("allowed_lang", None)
|
||||
|
||||
for key in new_data:
|
||||
if new_data[key] != old_data[key]:
|
||||
return True
|
||||
|
||||
if allowed_langs and set(allowed_langs) != set(
|
||||
[lang.code for lang in old_user.allowed_langs]
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def update_user(
|
||||
cls, user_id: int, user_data: UserUpdate, redis: aioredis.Redis
|
||||
) -> Union[UserDetail, User]:
|
||||
user = await cls.get_user(user_id, redis)
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not cls._is_has_data_to_update(user_data):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not cls._is_need_update(user, user_data):
|
||||
return user
|
||||
|
||||
return await cls._update(user.user_id, user_data.dict(), redis)
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from fastapi import APIRouter, HTTPException, status, Depends, Request
|
||||
|
||||
import aioredis
|
||||
from fastapi_pagination import Page, Params
|
||||
from fastapi_pagination.ext.ormar import paginate
|
||||
|
||||
@@ -12,10 +13,7 @@ from app.serializers import (
|
||||
CreateLanguage,
|
||||
LanguageDetail,
|
||||
)
|
||||
from app.services import update_user_allowed_langs
|
||||
|
||||
|
||||
# TODO: add redis cache
|
||||
from app.services.users_data_manager import UsersDataManager
|
||||
|
||||
|
||||
users_router = APIRouter(
|
||||
@@ -29,10 +27,9 @@ async def get_users():
|
||||
|
||||
|
||||
@users_router.get("/{user_id}", response_model=UserDetail)
|
||||
async def get_user(user_id: int):
|
||||
user_data = await User.objects.select_related("allowed_langs").get_or_none(
|
||||
user_id=user_id
|
||||
)
|
||||
async def get_user(request: Request, user_id: int):
|
||||
redis: aioredis.Redis = request.app.state.redis
|
||||
user_data = await UsersDataManager.get_user(user_id, redis)
|
||||
|
||||
if user_data is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
@@ -41,62 +38,15 @@ async def get_user(user_id: int):
|
||||
|
||||
|
||||
@users_router.post("/", response_model=UserDetail)
|
||||
async def create_or_update_user(data: UserCreateOrUpdate):
|
||||
data_dict = data.dict()
|
||||
|
||||
user_data = await User.objects.select_related("allowed_langs").get_or_none(
|
||||
user_id=data_dict["user_id"]
|
||||
)
|
||||
|
||||
allowed_langs = data_dict.pop("allowed_langs")
|
||||
|
||||
if user_data is None:
|
||||
user_data = await User.objects.select_related("allowed_langs").create(
|
||||
**data_dict
|
||||
)
|
||||
if allowed_langs is None:
|
||||
allowed_langs = ["ru", "be", "uk"]
|
||||
else:
|
||||
data_dict.pop("user_id")
|
||||
user_data.update_from_dict(data_dict)
|
||||
|
||||
if allowed_langs:
|
||||
await update_user_allowed_langs(user_data, allowed_langs)
|
||||
|
||||
return user_data
|
||||
async def create_or_update_user(request: Request, data: UserCreateOrUpdate):
|
||||
redis: aioredis.Redis = request.app.state.redis
|
||||
return await UsersDataManager.create_or_update_user(data, redis)
|
||||
|
||||
|
||||
@users_router.patch("/{user_id}", response_model=UserDetail)
|
||||
async def update_user(user_id: int, data: UserUpdate):
|
||||
user_data = await User.objects.select_related("allowed_langs").get_or_none(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if user_data is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data_dict = data.dict()
|
||||
|
||||
update_data = {}
|
||||
for key in data_dict:
|
||||
if data_dict[key] is not None:
|
||||
update_data[key] = data_dict[key]
|
||||
|
||||
if not update_data:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
allowed_langs = update_data.pop("allowed_langs", None)
|
||||
|
||||
if update_data:
|
||||
user_data.update_from_dict(update_data)
|
||||
await user_data.update()
|
||||
|
||||
if not allowed_langs:
|
||||
return user_data
|
||||
|
||||
await update_user_allowed_langs(user_data, allowed_langs)
|
||||
|
||||
return user_data
|
||||
async def update_user(request: Request, user_id: int, data: UserUpdate):
|
||||
redis: aioredis.Redis = request.app.state.redis
|
||||
return await UsersDataManager.update_user(user_id, data, redis)
|
||||
|
||||
|
||||
languages_router = APIRouter(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
import aioredis
|
||||
from fastapi_pagination import add_pagination
|
||||
|
||||
from app.views import users_router, languages_router, healthcheck_router
|
||||
from core.config import env_config
|
||||
from core.db import database
|
||||
|
||||
|
||||
@@ -15,6 +17,13 @@ def start_app() -> FastAPI:
|
||||
|
||||
app.state.database = database
|
||||
|
||||
app.state.redis = aioredis.Redis(
|
||||
host=env_config.REDIS_HOST,
|
||||
port=env_config.REDIS_PORT,
|
||||
db=env_config.REDIS_DB,
|
||||
password=env_config.REDIS_PASSWORD,
|
||||
)
|
||||
|
||||
add_pagination(app)
|
||||
|
||||
@app.on_event("startup")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
@@ -10,5 +12,10 @@ class EnvConfig(BaseSettings):
|
||||
POSTGRES_PORT: int
|
||||
POSTGRES_DB: str
|
||||
|
||||
REDIS_HOST: str
|
||||
REDIS_PORT: int
|
||||
REDIS_DB: int
|
||||
REDIS_PASSWORD: Optional[str]
|
||||
|
||||
|
||||
env_config = EnvConfig()
|
||||
|
||||
Reference in New Issue
Block a user