From b2a284d7915c49f3aa26ddb273d49d7c3454ce71 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 22:22:59 +0300 Subject: [PATCH] =?UTF-8?q?upd=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BD=D0=B5=20=D0=B4=D1=80=D0=BE?= =?UTF-8?q?=D0=BF=D0=B0=D0=B5=D1=82=20=D0=B1=D0=B0=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/.~lock.Menu.xlsx# | 1 + admin/Menu.xlsx | Bin 10342 -> 10099 bytes bg_tasks/bg_task.py | 2 +- bg_tasks/parser.py | 94 ++++++++++ bg_tasks/updater.py | 325 +++++++++++++++++++++++++---------- fastfood/repository/redis.py | 10 +- 6 files changed, 338 insertions(+), 94 deletions(-) create mode 100644 admin/.~lock.Menu.xlsx# create mode 100644 bg_tasks/parser.py diff --git a/admin/.~lock.Menu.xlsx# b/admin/.~lock.Menu.xlsx# new file mode 100644 index 0000000..4cab078 --- /dev/null +++ b/admin/.~lock.Menu.xlsx# @@ -0,0 +1 @@ +,pi3c,pi3code,12.02.2024 22:20,file:///home/pi3c/.config/libreoffice/4; diff --git a/admin/Menu.xlsx b/admin/Menu.xlsx index ac0262075865bd6f07388245aa3a22805f905de4..216bc3d21bb3bfc5776d63b55e18cbb62ce240e1 100644 GIT binary patch delta 6515 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 diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index 3703ea6..2b86b54 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -19,7 +19,7 @@ celery_app = Celery( celery_app.conf.beat_schedule = { 'run-task-every-15-seconds': { 'task': 'bg_tasks.bg_task.periodic_task', - 'schedule': 15.0, + 'schedule': 30.0, }, } diff --git a/bg_tasks/parser.py b/bg_tasks/parser.py new file mode 100644 index 0000000..29d616f --- /dev/null +++ b/bg_tasks/parser.py @@ -0,0 +1,94 @@ +import os + +import gspread +import openpyxl + +file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') + + +async def gsheets_to_rows() -> list[list[str | int | float]]: + """Получение всех строк из Google Sheets""" + + def to_int(val: str) -> int | str: + try: + res = int(val) + except ValueError: + return val + return res + + def to_float(val: str) -> float | str: + val = val.replace(',', '.') + try: + res = float(val) + except ValueError: + return val + return res + + gc = gspread.service_account(filename='creds.json') + sh = gc.open('Menu') + data = sh.sheet1.get_all_values() + for row in data: + row[:3] = list(map(to_int, row[:3])) + row[-2:] = list(map(to_float, row[-2:])) + + return data + + +async def local_xlsx_to_rows() -> list[list[str | int | float]]: + """Получение всех строк из локального файла Menu""" + data = [] + wb = openpyxl.load_workbook(file).worksheets[0] + for row in wb.iter_rows(values_only=True): + data.append(list(row)) + return data + + +async def rows_to_dict(rows: list[list]) -> tuple: + """Парсит строки полученные и источников в словарь""" + + menus = {} + submenus = {} + dishes = {} + + menu_num = None + submenu_num = None + + for row in rows: + if all(row[:3]): + menu = { + row[0]: { + 'data': {'title': row[1], 'description': row[2]}, + 'id': None, + } + } + menu_num = row[0] + menus.update(menu) + + elif all(row[1:4]): + submenu = { + (menu_num, row[1]): { + 'data': {'title': row[2], 'description': row[3]}, + 'parent_num': menu_num, + 'id': None, + 'parent_menu': None, + } + } + submenu_num = row[1] + submenus.update(submenu) + + elif all(row[3:6]): + dish = { + (menu_num, submenu_num, row[2]): { + 'data': { + 'title': row[3], + 'description': row[4], + 'price': row[5], + }, + 'parent_num': (menu_num, submenu_num), + 'id': None, + 'parent_submenu': None, + 'discont': row[6], + }, + } + dishes.update(dish) + return menus, submenus, dishes diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py index 68c2f76..01af762 100644 --- a/bg_tasks/updater.py +++ b/bg_tasks/updater.py @@ -1,16 +1,14 @@ import os import pickle -from uuid import UUID -import openpyxl import redis.asyncio as redis # type: ignore -from sqlalchemy import delete +from sqlalchemy import delete, update from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from fastfood.config import settings from fastfood.models import Dish, Menu, SubMenu -file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') +from .parser import file, gsheets_to_rows, local_xlsx_to_rows, rows_to_dict redis = redis.Redis.from_url(url=settings.REDIS_URL) @@ -22,16 +20,10 @@ async_session_maker = async_sessionmaker( ) -async def refresh_cache(disconts: dict) -> None: - """Очищает кэш при обновлении БД и ставит отметку времени обновления - и сохраняет данные скидок на товар - """ - await redis.flushall() - - for key in disconts.keys(): - await redis.set(f'DISCONT:{str(key)}', pickle.dumps(disconts[key])) - - await redis.set('XLSX_MOD_TIME', pickle.dumps(os.path.getmtime(file))) +async def clear_cache(pattern: str) -> None: + keys = [key async for key in redis.scan_iter(pattern)] + if keys: + await redis.delete(*keys) async def is_changed_xls() -> bool: @@ -51,96 +43,253 @@ async def is_changed_xls() -> bool: return True -async def xlsx_to_dict() -> dict: - """Парсит Menu.xlsx в словарь""" - wb = openpyxl.load_workbook(file).worksheets[0] +async def on_menu_change( + new_menu: dict, old_menu: dict, session: AsyncSession +) -> dict | None: + if new_menu and not old_menu: + # Создаем меню + menu = Menu( + title=new_menu['data']['title'], + description=new_menu['data']['description'], + ) + session.add(menu) + await session.flush() + new_menu['id'] = str(menu.id) + elif new_menu and old_menu: + # Обновляем меню + await session.execute( + update(Menu).where(Menu.id == old_menu['id']).values(**(new_menu['data'])) + ) + new_menu['id'] = old_menu['id'] - data = {} + else: + # Удаляем меню + await session.execute(delete(Menu).where(Menu.id == old_menu['id'])) - menu = None - submenu = None - dish = None + await session.commit() + # Чистим кэш + await clear_cache('MENUS*') + await clear_cache('summary') - for row in wb.iter_rows(values_only=True): - if row[0] is not None: - menu = row[0] - data[menu] = { - 'id': None, - 'title': row[1], - 'description': row[2], - 'submenus': dict(), - } - elif row[1] is not None: - submenu = row[1] - data[menu]['submenus'][submenu] = { - 'id': None, - 'title': row[2], - 'description': row[3], - 'dishes': dict(), - } - elif row[2] is not None: - dish = row[2] - data[menu]['submenus'][submenu]['dishes'][dish] = { - 'id': None, - 'title': row[3], - 'description': row[4], - 'price': row[5], - 'discont': row[6], - } - return data + return new_menu -async def refresh_all_data(data: dict) -> dict[UUID, int | float]: - """Удаляет старые данные и сохраняет новые. - Создает и возвращает список со скидками с привязкой по UUID товара +async def menus_updater(menus: dict, session: AsyncSession) -> None: + """Проверяет пункты меню на изменения + При необходимости запускае обновление БД + через фенкцию on_menu_change """ + cached_menus = await redis.get('ALL_MENUS') - disconts = {} + if cached_menus is not None: + cached_menus = pickle.loads(cached_menus) + else: + cached_menus = {} - async with async_session_maker() as session: - await session.execute(delete(Menu)) - await session.commit() + for key in menus.keys(): + if key not in cached_menus.keys(): + # Создание меню + menu = await on_menu_change(menus[key], {}, session) + menus[key] = menu + elif key in cached_menus.keys(): + # Обновление меню + if menus[key].get('data') != cached_menus[key].get('data'): + menu = await on_menu_change(menus[key], cached_menus[key], session) + menus[key] = menu + else: + menus[key]['id'] = cached_menus[key]['id'] - for menu_key in data.keys(): - menu = Menu( - title=data[menu_key].get('title'), - description=data[menu_key].get('description'), - ) - session.add(menu) - await session.flush() + for key in {k: cached_menus[k] for k in set(cached_menus) - set(menus)}: + # Проверяем на удаленные меню + await on_menu_change({}, cached_menus.pop(key), session) - submenus = data[menu_key]['submenus'] - for sub_key in submenus.keys(): - submenu = SubMenu( - title=submenus[sub_key]['title'], - description=submenus[sub_key]['description'], - parent_menu=menu.id, + await redis.set('ALL_MENUS', pickle.dumps(menus)) + + +async def on_submenu_change( + new_sub: dict, old_sub: dict, session: AsyncSession +) -> dict: + if new_sub and not old_sub: + # Создаем меню + submenu = SubMenu( + title=new_sub['data']['title'], + description=new_sub['data']['description'], + ) + submenu.parent_menu = new_sub['parent_menu'] + + session.add(submenu) + await session.flush() + new_sub['id'] = str(submenu.id) + new_sub['parent_menu'] = str(submenu.parent_menu) + elif new_sub and old_sub: + # Обновляем меню + await session.execute( + update(SubMenu) + .where(SubMenu.id == old_sub['id']) + .values(**(new_sub['data'])) + ) + new_sub['id'] = old_sub['id'] + new_sub['parent_menu'] = old_sub['parent_menu'] + + else: + # Удаляем меню + await session.execute(delete(SubMenu).where(SubMenu.id == old_sub['id'])) + + await clear_cache('MENUS*') + await clear_cache('summary') + + await session.commit() + return new_sub + + +async def submenus_updater(submenus: dict, session: AsyncSession) -> None: + """Проверяет пункты подменю на изменения + При необходимости запускае обновление БД + """ + # Получаем Меню из кэша для получения их ID по померу в таблице + cached_menus = await redis.get('ALL_MENUS') + if cached_menus is not None: + cached_menus = pickle.loads(cached_menus) + else: + cached_menus = {} + + # Получаем подмен из кэша + cached_sub = await redis.get('ALL_SUBMENUS') + + if cached_sub is not None: + cached_sub = pickle.loads(cached_sub) + else: + cached_sub = {} + + for key in submenus.keys(): + parent = cached_menus[submenus[key]['parent_num']]['id'] + submenus[key]['parent_menu'] = parent + + if key not in cached_sub.keys(): + # Получаем и ставим UUID parent_menu + submenus[key]['parent_menu'] = parent + + submenu = await on_submenu_change(submenus[key], {}, session) + submenus[key] = submenu + elif key in cached_sub.keys(): + # Обновление меню + if submenus[key].get('data') != cached_sub[key].get('data'): + submenu = await on_submenu_change( + submenus[key], cached_sub[key], session ) - session.add(submenu) - await session.flush() + submenus[key] = submenu + else: + submenus[key]['id'] = cached_sub[key]['id'] + submenus[key]['parent_menu'] = cached_sub[key]['parent_menu'] - dishes = data[menu_key]['submenus'][sub_key]['dishes'] - print(dishes) - for dish_key in dishes.keys(): - dish = Dish( - title=dishes[dish_key]['title'], - description=dishes[dish_key]['description'], - price=dishes[dish_key]['price'], - parent_submenu=submenu.id, - ) - session.add(dish) - await session.flush() - if dishes[dish_key]['discont'] is not None: - disconts[dish.id] = dishes[dish_key]['discont'] + for key in {k: cached_sub[k] for k in set(cached_sub) - set(submenus)}: + # Проверяем на удаленные меню + await on_submenu_change({}, cached_sub.pop(key), session) - await session.commit() - return disconts + await redis.set('ALL_SUBMENUS', pickle.dumps(submenus)) + + +async def on_dish_change(new_dish: dict, old_dish, session: AsyncSession) -> dict: + if new_dish and not old_dish: + dish = Dish( + title=new_dish['data']['title'], + description=new_dish['data']['description'], + price=new_dish['data']['price'], + ) + dish.parent_submenu = new_dish['parent_submenu'] + + session.add(dish) + await session.flush() + new_dish['id'] = str(dish.id) + new_dish['parent_submenu'] = str(dish.parent_submenu) + new_dish['data']['price'] = str(dish.price) + elif new_dish and old_dish: + # Обновляем меню + await session.execute( + update(Dish).where(Dish.id == old_dish['id']).values(**(new_dish['data'])) + ) + new_dish['id'] = old_dish['id'] + new_dish['parent_submenu'] = old_dish['parent_submenu'] + new_dish['data']['price'] = old_dish['data']['price'] + + else: + # Удаляем меню + await session.execute(delete(Dish).where(Dish.id == old_dish['id'])) + + await clear_cache('MENUS*') + await clear_cache('summary') + + await session.commit() + return new_dish + + +async def dishes_updater(dishes: dict, session: AsyncSession) -> None: + """Проверяет блюда на изменения + При необходимости запускае обновление БД + """ + cached_submenus = await redis.get('ALL_SUBMENUS') + if cached_submenus is not None: + cached_submenus = pickle.loads(cached_submenus) + else: + cached_submenus = {} + + # Получаем подмен из кэша + cached_dishes = await redis.get('ALL_DISHES') + + if cached_dishes is not None: + cached_dishes = pickle.loads(cached_dishes) + else: + cached_dishes = {} + + await clear_cache('DISCONT*') + + for key in {k: cached_dishes[k] for k in set(cached_dishes) - set(dishes)}: + # Проверяем на удаленные меню + await on_submenu_change({}, cached_dishes.pop(key), session) + + for key in dishes.keys(): + parent = cached_submenus[dishes[key]['parent_num']]['id'] + dishes[key]['parent_submenu'] = parent + + if key not in cached_dishes.keys(): + # Получаем и ставим UUID parent_menu + dishes[key]['parent_submenu'] = parent + + dish = await on_dish_change(dishes[key], {}, session) + dishes[key] = dish + elif key in cached_dishes.keys(): + # Обновление меню + if dishes[key].get('data') != cached_dishes[key].get('data'): + dish = await on_dish_change(dishes[key], cached_dishes[key], session) + dishes[key] = dish + else: + dishes[key]['id'] = cached_dishes[key]['id'] + dishes[key]['parent_submenu'] = cached_dishes[key]['parent_submenu'] + + if dishes[key]['discont'] is not None: + await redis.set( + f"DISCONT:{dishes[key]['id']}", pickle.dumps(dishes[key]['discont']) + ) + + await redis.set('ALL_DISHES', pickle.dumps(dishes)) + + +async def updater(rows): + menus, submenus, dishes = await rows_to_dict(rows) + async with async_session_maker() as session: + await menus_updater(menus, session) + await submenus_updater(submenus, session) + await dishes_updater(dishes, session) async def main() -> None: """Главная функция фоновой задачи""" changed = await is_changed_xls() if changed: - menu_data = await xlsx_to_dict() - discont_data = await refresh_all_data(menu_data) - await refresh_cache(discont_data) + rows = await local_xlsx_to_rows() + await updater(rows) + + +async def main_gsheets() -> None: + rows = await gsheets_to_rows() + await updater(rows) diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index 80f0e35..81ff28d 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -12,15 +12,15 @@ def get_key(level: str, **kwargs) -> str: case 'menus': return 'MENUS' case 'menu': - return f"{kwargs.get('menu_id')}" + return f"MENUS:{kwargs.get('menu_id')}" case 'submenus': - return f"{kwargs.get('menu_id')}:SUBMENUS" + return f"MENUS:{kwargs.get('menu_id')}:SUBMENUS" case 'submenu': - return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" + return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" case 'dishes': - return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES" + return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES" case 'dish': - return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" + return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" return 'summary'