Compare commits
33 Commits
fc9577c538
...
main
Author | SHA1 | Date | |
---|---|---|---|
8189aaedd4 | |||
5ef6aaeb6f | |||
f75415d9d9 | |||
4c3779776d | |||
d54e704dfb | |||
68594eb7f0 | |||
8bfa166987 | |||
e0a81cf126 | |||
a4f8bce657 | |||
9ba42aae9f | |||
afdf1c5e2b | |||
74c0ccae2a | |||
2c48529a02 | |||
cedf27a04d | |||
e0798de713 | |||
5a133a05e1 | |||
3df3c67e7c | |||
a0ebe9bdb9 | |||
ed3d7d9352 | |||
3dbefda936 | |||
5a95b06300 | |||
ebe75b6dc3 | |||
22a876d3ce | |||
6a0776557d | |||
b2a284d791 | |||
5e213e759d | |||
f28637f5dd | |||
e6d1070d9a | |||
47cb0e08c7 | |||
e6576e9e58 | |||
02134d247a | |||
68db31a033 | |||
3120910552 |
65
README.md
65
README.md
@@ -5,29 +5,50 @@ Fastapi веб приложение реализующее api для общеп
|
|||||||
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||||
|
|
||||||
## Техническое задание
|
## Техническое задание
|
||||||
### Спринт 3 - Паттерны и принципы разработки
|
### Спринт 4 - Многопроцессорность, асинхронность
|
||||||
|
В этом домашнем задании необходимо:
|
||||||
|
1.Переписать текущее FastAPI приложение на асинхронное выполнение
|
||||||
|
2.Добавить в проект фоновую задачу с помощью Celery + RabbitMQ.
|
||||||
|
3.Добавить эндпоинт (GET) для вывода всех меню со всеми связанными подменю и со всеми связанными блюдами.
|
||||||
|
4.Реализовать инвалидация кэша в background task (встроено в FastAPI)
|
||||||
|
5.* Обновление меню из google sheets раз в 15 сек.
|
||||||
|
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
|
||||||
|
|
||||||
1.Вынести бизнес логику и запросы в БД в отдельные слои приложения.
|
Фоновая задача: синхронизация Excel документа и БД.
|
||||||
|
В проекте создаем папку admin. В эту папку кладем файл Menu.xlsx (будет прикреплен к ДЗ). Не забываем запушить в гит.
|
||||||
|
При внесении изменений в файл все изменения должны отображаться в БД. Периодичность обновления 15 сек. Удалять БД при каждом обновлении – нельзя.
|
||||||
|
|
||||||
2.Добавить кэширование запросов к API с использованием Redis. Не забыть про инвалидацию кэша.
|
|
||||||
|
|
||||||
3.Добавить pre-commit хуки в проект. Файл yaml будет прикреплен к ДЗ.
|
|
||||||
|
|
||||||
4.Покрыть проект type hints (тайпхинтами)
|
|
||||||
|
|
||||||
5.* Описать ручки API в соответствий c OpenAPI
|
|
||||||
|
|
||||||
6.** Реализовать в тестах аналог Django reverse() для FastAPI
|
|
||||||
|
|
||||||
Требования:
|
Требования:
|
||||||
●Код должен проходить все линтеры.
|
●Данные меню, подменю, блюд для нового эндпоинта должны доставаться одним ORM-запросом в БД (использовать подзапросы и агрегирующие функций SQL).
|
||||||
●Код должен соответствовать принципам SOLID, DRY, KISS.
|
●Проект должен запускаться одной командой
|
||||||
●Проект должен запускаться по одной команде (докер).
|
●Проект должен соответствовать требованиям всех предыдущих вебинаров. (Не забыть добавить тесты для нового API эндпоинта)
|
||||||
●Проект должен проходить все Postman тесты (коллекция с Вебинара №1).
|
|
||||||
●Тесты написанные вами после Вебинара №2, должны быть актуальны, запускать и успешно проходить
|
### Выполненные доп задания со *
|
||||||
|
Спринт 2
|
||||||
|
3.* Реализовать вывод количества подменю и блюд для Меню через один (сложный) ORM запрос.
|
||||||
|
`./fastfood/repository/menu.py` Метод `get_menu_item`
|
||||||
|
|
||||||
|
4.** Реализовать тестовый сценарий «Проверка кол-ва блюд и подменю в меню» из Postman с помощью pytest
|
||||||
|
`./tests/test_postman.py`
|
||||||
|
|
||||||
|
Спринт 3
|
||||||
|
5.* Описать ручки API в соответствий c OpenAPI
|
||||||
|
'./openapi.json'
|
||||||
|
|
||||||
|
6.** Реализовать в тестах аналог Django reverse() для FastAPI
|
||||||
|
'./tests/urls.py'
|
||||||
|
|
||||||
|
Спринт 4
|
||||||
|
5.* Обновление меню из google sheets раз в 15 сек.
|
||||||
|
`./bg_tasks/` Реализовано чтение как локальной, так и удаленной таблицы.
|
||||||
|
В зависимости какой compose поднять, тот и будет использоваться
|
||||||
|
|
||||||
|
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
|
||||||
|
`./fastfood/service/dish.py`, метод _get_discont, подменяет сумму в выдаче,
|
||||||
|
скидка хранится в REDIS под ключами вида DISCONT:{UUID блюда}
|
||||||
|
|
||||||
|
|
||||||
Дополнительно:
|
|
||||||
Контейнеры с проектом и с тестами запускаются разными командами.
|
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
- docker
|
- docker
|
||||||
@@ -43,8 +64,12 @@ Fastapi веб приложение реализующее api для общеп
|
|||||||
|
|
||||||
Запуск/остановка образов:
|
Запуск/остановка образов:
|
||||||
|
|
||||||
- Запуск FAstAPI приложения
|
- Запуск FAstAPI приложения c локальным файлом для фоновой задачи
|
||||||
> `$ docker-compose -f compose_app.yml up `
|
> `$ docker-compose -f compose_app.yml up`
|
||||||
|
|
||||||
|
- Запуск FAstAPI приложения c Google Sheets для фоновой задачи
|
||||||
|
> `$ docker-compose -f compose_google.yml up`
|
||||||
|
(ЧИТАЙТЕ СООБЩЕНИЕ В ЧАТЕ)
|
||||||
|
|
||||||
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
||||||
|
|
||||||
|
BIN
admin/Menu.xlsx
BIN
admin/Menu.xlsx
Binary file not shown.
@@ -2,14 +2,16 @@ import asyncio
|
|||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
|
||||||
from .updater import main
|
from fastfood.config import settings
|
||||||
|
|
||||||
|
from .updater import main, main_gsheets
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
|
||||||
celery_app = Celery(
|
celery_app = Celery(
|
||||||
'tasks',
|
'tasks',
|
||||||
broker='amqp://guest:guest@localhost',
|
broker=settings.REBBITMQ_URL,
|
||||||
backend='rpc://',
|
backend='rpc://',
|
||||||
include=['bg_tasks.bg_task'],
|
include=['bg_tasks.bg_task'],
|
||||||
)
|
)
|
||||||
@@ -17,10 +19,30 @@ celery_app = Celery(
|
|||||||
celery_app.conf.beat_schedule = {
|
celery_app.conf.beat_schedule = {
|
||||||
'run-task-every-15-seconds': {
|
'run-task-every-15-seconds': {
|
||||||
'task': 'bg_tasks.bg_task.periodic_task',
|
'task': 'bg_tasks.bg_task.periodic_task',
|
||||||
'schedule': 15.0,
|
'schedule': 30.0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
celery_app_google = Celery(
|
||||||
|
'tasks',
|
||||||
|
broker=settings.REBBITMQ_URL,
|
||||||
|
backend='rpc://',
|
||||||
|
include=['bg_tasks.bg_task'],
|
||||||
|
)
|
||||||
|
|
||||||
|
celery_app_google.conf.beat_schedule = {
|
||||||
|
'run-task-every-15-seconds': {
|
||||||
|
'task': 'bg_tasks.bg_task.periodic_task_google',
|
||||||
|
'schedule': 30.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app_google.task
|
||||||
|
def periodic_task_google() -> None:
|
||||||
|
result = loop.run_until_complete(main_gsheets())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task
|
@celery_app.task
|
||||||
def periodic_task() -> None:
|
def periodic_task() -> None:
|
||||||
|
97
bg_tasks/parser.py
Normal file
97
bg_tasks/parser.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
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[dict[int, Any], dict[Any, Any], dict[Any, Any]]:
|
||||||
|
"""Парсит строки полученные и источников в словарь"""
|
||||||
|
|
||||||
|
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
|
@@ -1,20 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
from uuid import UUID
|
from typing import Any
|
||||||
|
|
||||||
import openpyxl
|
|
||||||
import redis.asyncio as redis # type: ignore
|
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 sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from fastfood.config import settings
|
from fastfood.config import settings
|
||||||
from fastfood.models import Dish, Menu, SubMenu
|
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(
|
redis = redis.Redis.from_url(url=settings.REDIS_URL)
|
||||||
host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB
|
|
||||||
)
|
|
||||||
|
|
||||||
async_engine = create_async_engine(settings.DATABASE_URL_asyncpg)
|
async_engine = create_async_engine(settings.DATABASE_URL_asyncpg)
|
||||||
async_session_maker = async_sessionmaker(
|
async_session_maker = async_sessionmaker(
|
||||||
@@ -24,16 +21,10 @@ async_session_maker = async_sessionmaker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def refresh_cache(disconts: dict) -> None:
|
async def clear_cache(pattern: str) -> None:
|
||||||
"""Очищает кэш при обновлении БД и ставит отметку времени обновления
|
keys = [key async for key in redis.scan_iter(pattern)]
|
||||||
и сохраняет данные скидок на товар
|
if keys:
|
||||||
"""
|
await redis.delete(*keys)
|
||||||
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 is_changed_xls() -> bool:
|
async def is_changed_xls() -> bool:
|
||||||
@@ -53,96 +44,252 @@ async def is_changed_xls() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def xlsx_to_dict() -> dict:
|
async def on_menu_change(
|
||||||
"""Парсит Menu.xlsx в словарь"""
|
new_menu: dict, old_menu: dict, session: AsyncSession
|
||||||
wb = openpyxl.load_workbook(file).worksheets[0]
|
) -> dict[str, Any]:
|
||||||
|
"""Изменение, удаление или создание меню"""
|
||||||
|
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)
|
||||||
|
|
||||||
data = {}
|
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']
|
||||||
|
|
||||||
menu = None
|
else:
|
||||||
submenu = None
|
# Удаляем меню
|
||||||
dish = None
|
await session.execute(delete(Menu).where(Menu.id == old_menu['id']))
|
||||||
|
|
||||||
for row in wb.iter_rows(values_only=True):
|
await session.commit()
|
||||||
if row[0] is not None:
|
return new_menu
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def refresh_all_data(data: dict) -> dict[UUID, int | float]:
|
async def menus_updater(menus: dict, session: AsyncSession) -> None:
|
||||||
"""Удаляет старые данные и сохраняет новые.
|
"""Проверяет пункты меню на изменения
|
||||||
Создает и возвращает список со скидками с привязкой по UUID товара
|
При необходимости запускае обновление БД
|
||||||
|
через фенкцию 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:
|
for key in menus.keys():
|
||||||
await session.execute(delete(Menu))
|
if key not in cached_menus.keys():
|
||||||
await session.commit()
|
# Создание меню
|
||||||
|
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():
|
for key in {k: cached_menus[k] for k in set(cached_menus) - set(menus)}:
|
||||||
menu = Menu(
|
# Проверяем на удаленные меню
|
||||||
title=data[menu_key].get('title'),
|
await on_menu_change({}, cached_menus.pop(key), session)
|
||||||
description=data[menu_key].get('description'),
|
|
||||||
)
|
|
||||||
session.add(menu)
|
|
||||||
await session.flush()
|
|
||||||
|
|
||||||
submenus = data[menu_key]['submenus']
|
await redis.set('ALL_MENUS', pickle.dumps(menus))
|
||||||
for sub_key in submenus.keys():
|
|
||||||
submenu = SubMenu(
|
|
||||||
title=submenus[sub_key]['title'],
|
async def on_submenu_change(
|
||||||
description=submenus[sub_key]['description'],
|
new_sub: dict, old_sub: dict, session: AsyncSession
|
||||||
parent_menu=menu.id,
|
) -> dict[str, Any]:
|
||||||
|
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 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)
|
submenus[key] = submenu
|
||||||
await session.flush()
|
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']
|
for key in {k: cached_sub[k] for k in set(cached_sub) - set(submenus)}:
|
||||||
print(dishes)
|
# Проверяем на удаленные меню
|
||||||
for dish_key in dishes.keys():
|
await on_submenu_change({}, cached_sub.pop(key), session)
|
||||||
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']
|
|
||||||
|
|
||||||
await session.commit()
|
await redis.set('ALL_SUBMENUS', pickle.dumps(submenus))
|
||||||
return disconts
|
|
||||||
|
|
||||||
|
async def on_dish_change(
|
||||||
|
new_dish: dict, old_dish, session: AsyncSession
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
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 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_dish_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_submenu
|
||||||
|
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) -> None:
|
||||||
|
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)
|
||||||
|
# Чистим кэш
|
||||||
|
await clear_cache('MENUS*')
|
||||||
|
await clear_cache('summary')
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""Главная функция фоновой задачи"""
|
"""Главная функция фоновой задачи"""
|
||||||
changed = await is_changed_xls()
|
changed = await is_changed_xls()
|
||||||
if changed:
|
if changed:
|
||||||
menu_data = await xlsx_to_dict()
|
rows = await local_xlsx_to_rows()
|
||||||
discont_data = await refresh_all_data(menu_data)
|
await updater(rows)
|
||||||
await refresh_cache(discont_data)
|
|
||||||
|
|
||||||
|
async def main_gsheets() -> None:
|
||||||
|
"""Главная функция фоновой задачи для работы с Google"""
|
||||||
|
rows = await gsheets_to_rows()
|
||||||
|
await updater(rows)
|
||||||
|
@@ -60,10 +60,10 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/usr/src/fastfood
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server'
|
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-docker-server'
|
||||||
|
|
||||||
celery:
|
celery_worker:
|
||||||
container_name: celery
|
container_name: celeryworker
|
||||||
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -80,7 +80,28 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/usr/src/fastfood
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-selery'
|
command: ["celery", "-A", "bg_tasks.bg_task:celery_app", "worker", "--loglevel=info", "--concurrency", "1", "-P", "solo"]
|
||||||
|
|
||||||
|
celery_beat:
|
||||||
|
container_name: celerybeat
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- rabbitmq
|
||||||
|
- db
|
||||||
|
- app
|
||||||
|
- redis
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
|
command: ["celery", "-A", "bg_tasks.bg_task:celery_app", "beat", "--loglevel=info"]
|
||||||
|
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
container_name: rabbit
|
container_name: rabbit
|
||||||
|
112
compose_google.yml
Normal file
112
compose_google.yml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
container_name: redis_test
|
||||||
|
|
||||||
|
image: redis:7.2.4-alpine3.19
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- '6380:6379'
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli","ping" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: pgdb
|
||||||
|
|
||||||
|
image: postgres:15.1-alpine
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- 6432:5432
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
|
app:
|
||||||
|
container_name: fastfood_app
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
|
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-docker-server'
|
||||||
|
|
||||||
|
celery_worker:
|
||||||
|
container_name: celeryworker
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- rabbitmq
|
||||||
|
- db
|
||||||
|
- app
|
||||||
|
- redis
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
|
command: ["celery", "-A", "bg_tasks.bg_task:celery_app_google", "worker", "--loglevel=info", "--concurrency", "1", "-P", "solo"]
|
||||||
|
|
||||||
|
celery_beat:
|
||||||
|
container_name: celerybeat
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- rabbitmq
|
||||||
|
- db
|
||||||
|
- app
|
||||||
|
- redis
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
|
command: ["celery", "-A", "bg_tasks.bg_task:celery_app_google", "beat", "--loglevel=info"]
|
||||||
|
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
container_name: rabbit
|
||||||
|
|
||||||
|
image: "rabbitmq:management"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- 5672:5672
|
@@ -55,6 +55,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
restart: always
|
volumes:
|
||||||
|
- .:/usr/src/fastfood
|
||||||
|
|
||||||
command: /bin/bash -c 'poetry run pytest -vv'
|
command: /bin/bash -c 'poetry run pytest -vv'
|
||||||
|
13
creds.json
13
creds.json
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "psyched-ceiling-413920",
|
|
||||||
"private_key_id": "d19492eea6a030092cf8ad767b62d7909734ccb1",
|
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPP2UrfDE+UOlD\nA761Jemy1IKKdIanRKXSziGvDYJ7zbY5gPFxY8Vo+9fsh1oumvROXqEH4+1LiR0J\nnUiy33R1KDOHETeP4H/FJ3u8+gHoL8wsA7SN5pIX9AR8AyBZCBjgSSp+KJchfrp/\nWN2qnFAbgp248QPvmG7/wLzBNnVsAQhQULLKqSc46hbiZ8Jt7t0ajJgOFRJSp2wP\nT5VE5k737w6b4OH8mUnhw7VK04Wk6DBmQhN1jrnxmMxmdG2hSM2zR824RIMBBs/O\n1dF+5Vkav0tgja/tVqm41Aaa2vgPRACP6bpF13YS+8C1lzw6s+7M/VdE5TH5NXRU\nvChuRknBAgMBAAECggEABmuckna0krVsawaXhLaQ30DsLf5w9hdLTvDy6CCuO9Aw\nPKb//9UNNmjMKD4rlQNY1YFS6jbxZNZRrIC7aftwQOGE2mKuIMBl6+tinuy0tLr/\nl3baS+22VZyyG36ILNrqZJ8epGm08CEsNVYRKKwS0x3aXZKFnnlnqaeYn2CUzdqa\na9iNZqrdXdRt4O7KVP7IfdNi11WuOL4epmHwBBYmCxiN0Z2KAIYvS6AcflYWtYTZ\npsBFjCQexqS37PdUyyQX9E/gKwqNZmahYwIC3vsCMCLdQQ93iODYni7LKsG0vvls\nwz03TtlMmZpMJJQGkALeqlv7jeyj+oRuqg6gjs2moQKBgQDxuDt3u1rDWhTJ50bD\nAp5T1LaiV0/+lu29ElTmYpa0RF1tlHvrndFm/MrdUjpzP4/VISmRkP3bmAgwPP6p\nYeALqQXCCGJtl44LG6D9VIOCOZxntytjLHogY8S3BLpwzKC+VMFsd56ay6wCl03S\nJEnvG10FQX8sFd+6j5qMy73OoQKBgQDbfc7hV4/r7PMaUVWFRqWjLry3dtTErxnM\nTdX30BDtuqMrm+hx0zC85ePcsbx+Zhwneyaxw2ICN5F954mJurBqs9cVaxitNSv1\nX5XjAoZqf3TevufkmSBXog6t/p4FHqAHftHYzwQvQXIINFrmT15PJkbx0lMYEYzw\nPyB7doBHIQKBgGiJi7ZpYYRw1eLH0fOOk1if+uhUqHTrYx/M6MjGRHTryBgXCkzI\n8QIAO9/hqwOirpq2/9pDgXZR1uC90EkC2jlQvPvAUokg7T5ikYpd3Y4ZSkoUjoAS\ngTK20yFvuw4DgVUvJIO7a+14PgjU1MQYC52MEPuv6sbvItX1Oxq/FnRhAoGAHWYK\ncbBSvJzuKtY+CC3gPa0i5cfq07VIVU8Pm7OosM7Q0CR/y88ntgVsscC0qJFwr/EU\ny7aJyBY9TInYqDPzMTeJVXsUwQ5gJut4ngFWk6kitDsJwFqqNFKmeLOj4repY5ee\n79U6kEHJzkOE8VgsH5nW4sjzDEQ9hmhOJ3tFz0ECgYEA8N+7yq1tK/99S8ThYW1J\n9mvUXRhAcFamBYp+8bIBdnQlrM9bGd9j8gYzQj+RBcvfCpVHFM20z8CC8oN0bitk\nh5MEjLBkw1vaywFlA/hcnA8A3g+5/IgHl03Y1tPWnyAtB77vE2M2ThklZ5l4E8eT\nP1vYw9RUSAPjtd43XDTqPNQ=\n-----END PRIVATE KEY-----\n",
|
|
||||||
"client_email": "tester@psyched-ceiling-413920.iam.gserviceaccount.com",
|
|
||||||
"client_id": "100697987276606879445",
|
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/tester%40psyched-ceiling-413920.iam.gserviceaccount.com",
|
|
||||||
"universe_domain": "googleapis.com"
|
|
||||||
}
|
|
@@ -66,6 +66,17 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def REBBITMQ_URL(self):
|
||||||
|
"""
|
||||||
|
Возвращает строку подключения к REBBITMQ
|
||||||
|
"""
|
||||||
|
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return 'amqp://guest:guest@rabbitmq'
|
||||||
|
|
||||||
|
return 'amqp://guest:guest@127.0.0.1'
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file='.env')
|
model_config = SettingsConfigDict(env_file='.env')
|
||||||
|
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ class DishRepository:
|
|||||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||||
self.db = session
|
self.db = session
|
||||||
|
|
||||||
async def get_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]:
|
async def get_dishes(self, submenu_id: UUID) -> list[Dish]:
|
||||||
query = select(Dish).where(
|
query = select(Dish).where(
|
||||||
Dish.parent_submenu == submenu_id,
|
Dish.parent_submenu == submenu_id,
|
||||||
)
|
)
|
||||||
@@ -22,7 +22,6 @@ class DishRepository:
|
|||||||
|
|
||||||
async def create_dish_item(
|
async def create_dish_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_data: Dish_db,
|
dish_data: Dish_db,
|
||||||
) -> Dish:
|
) -> Dish:
|
||||||
@@ -35,8 +34,6 @@ class DishRepository:
|
|||||||
|
|
||||||
async def get_dish_item(
|
async def get_dish_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
) -> Dish | None:
|
) -> Dish | None:
|
||||||
query = select(Dish).where(Dish.id == dish_id)
|
query = select(Dish).where(Dish.id == dish_id)
|
||||||
@@ -45,8 +42,6 @@ class DishRepository:
|
|||||||
|
|
||||||
async def update_dish_item(
|
async def update_dish_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
dish_data: Dish_db,
|
dish_data: Dish_db,
|
||||||
) -> Dish | None:
|
) -> Dish | None:
|
||||||
@@ -59,8 +54,6 @@ class DishRepository:
|
|||||||
|
|
||||||
async def delete_dish_item(
|
async def delete_dish_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
) -> None:
|
) -> None:
|
||||||
query = delete(Dish).where(Dish.id == dish_id)
|
query = delete(Dish).where(Dish.id == dish_id)
|
||||||
|
@@ -12,15 +12,15 @@ def get_key(level: str, **kwargs) -> str:
|
|||||||
case 'menus':
|
case 'menus':
|
||||||
return 'MENUS'
|
return 'MENUS'
|
||||||
case 'menu':
|
case 'menu':
|
||||||
return f"{kwargs.get('menu_id')}"
|
return f"MENUS:{kwargs.get('menu_id')}"
|
||||||
case 'submenus':
|
case 'submenus':
|
||||||
return f"{kwargs.get('menu_id')}:SUBMENUS"
|
return f"MENUS:{kwargs.get('menu_id')}:SUBMENUS"
|
||||||
case 'submenu':
|
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':
|
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':
|
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'
|
return 'summary'
|
||||||
|
|
||||||
|
@@ -32,14 +32,13 @@ class SubMenuRepository:
|
|||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
await self.db.refresh(new_submenu)
|
await self.db.refresh(new_submenu)
|
||||||
|
|
||||||
full_sub = await self.get_submenu_item(menu_id, new_submenu.id)
|
full_sub = await self.get_submenu_item(new_submenu.id)
|
||||||
if full_sub is None:
|
if full_sub is None:
|
||||||
raise TypeError
|
raise TypeError
|
||||||
return full_sub
|
return full_sub
|
||||||
|
|
||||||
async def get_submenu_item(
|
async def get_submenu_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
) -> SubMenu | None:
|
) -> SubMenu | None:
|
||||||
s = aliased(SubMenu)
|
s = aliased(SubMenu)
|
||||||
@@ -56,7 +55,6 @@ class SubMenuRepository:
|
|||||||
|
|
||||||
async def update_submenu_item(
|
async def update_submenu_item(
|
||||||
self,
|
self,
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
submenu_data: MenuBase,
|
submenu_data: MenuBase,
|
||||||
) -> SubMenu | None:
|
) -> SubMenu | None:
|
||||||
@@ -71,7 +69,7 @@ class SubMenuRepository:
|
|||||||
updated_submenu = await self.db.execute(qr)
|
updated_submenu = await self.db.execute(qr)
|
||||||
return updated_submenu.scalar_one_or_none()
|
return updated_submenu.scalar_one_or_none()
|
||||||
|
|
||||||
async def delete_submenu_item(self, menu_id: UUID, submenu_id: UUID) -> None:
|
async def delete_submenu_item(self, submenu_id: UUID) -> None:
|
||||||
query = delete(SubMenu).where(
|
query = delete(SubMenu).where(
|
||||||
SubMenu.id == submenu_id,
|
SubMenu.id == submenu_id,
|
||||||
)
|
)
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -11,7 +13,7 @@ class SummaryRepository:
|
|||||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||||
self.db = session
|
self.db = session
|
||||||
|
|
||||||
async def get_data(self):
|
async def get_data(self) -> list[Any]:
|
||||||
query = select(Menu).options(
|
query = select(Menu).options(
|
||||||
selectinload(Menu.submenus).selectinload(SubMenu.dishes)
|
selectinload(Menu.submenus).selectinload(SubMenu.dishes)
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from fastfood.schemas import Dish, DishBase
|
from fastfood.schemas import Dish, DishBase
|
||||||
from fastfood.service.dish import DishService
|
from fastfood.service.dish import DishService
|
||||||
@@ -19,7 +19,6 @@ async def get_dishes(
|
|||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish: DishService = Depends(),
|
dish: DishService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> list[Dish]:
|
) -> list[Dish]:
|
||||||
result = await dish.read_dishes(menu_id, submenu_id)
|
result = await dish.read_dishes(menu_id, submenu_id)
|
||||||
return result
|
return result
|
||||||
@@ -35,7 +34,6 @@ async def create_dish(
|
|||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_data: DishBase,
|
dish_data: DishBase,
|
||||||
dish: DishService = Depends(),
|
dish: DishService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> Dish:
|
) -> Dish:
|
||||||
return await dish.create_dish(
|
return await dish.create_dish(
|
||||||
menu_id,
|
menu_id,
|
||||||
@@ -53,7 +51,6 @@ async def get_dish(
|
|||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
dish: DishService = Depends(),
|
dish: DishService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> Dish | None:
|
) -> Dish | None:
|
||||||
result = await dish.read_dish(
|
result = await dish.read_dish(
|
||||||
menu_id,
|
menu_id,
|
||||||
@@ -78,7 +75,6 @@ async def update_dish(
|
|||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
dish_data: DishBase,
|
dish_data: DishBase,
|
||||||
dish: DishService = Depends(),
|
dish: DishService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> Dish:
|
) -> Dish:
|
||||||
result = await dish.update_dish(
|
result = await dish.update_dish(
|
||||||
menu_id,
|
menu_id,
|
||||||
@@ -102,6 +98,5 @@ async def delete_dish(
|
|||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
dish: DishService = Depends(),
|
dish: DishService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> None:
|
) -> None:
|
||||||
await dish.del_dish(menu_id, submenu_id, dish_id)
|
await dish.del_dish(menu_id, dish_id)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from fastfood.schemas import MenuBase, MenuRead
|
from fastfood.schemas import MenuBase, MenuRead
|
||||||
from fastfood.service.menu import MenuService
|
from fastfood.service.menu import MenuService
|
||||||
@@ -18,7 +18,6 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
async def get_menus(
|
async def get_menus(
|
||||||
menu: MenuService = Depends(),
|
menu: MenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> list[MenuRead]:
|
) -> list[MenuRead]:
|
||||||
return await menu.read_menus()
|
return await menu.read_menus()
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ async def get_menus(
|
|||||||
async def add_menu(
|
async def add_menu(
|
||||||
menu: MenuBase,
|
menu: MenuBase,
|
||||||
responce: MenuService = Depends(),
|
responce: MenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> MenuRead:
|
) -> MenuRead:
|
||||||
return await responce.create_menu(menu)
|
return await responce.create_menu(menu)
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ async def add_menu(
|
|||||||
async def get_menu(
|
async def get_menu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
responce: MenuService = Depends(),
|
responce: MenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> MenuRead:
|
) -> MenuRead:
|
||||||
result = await responce.read_menu(menu_id=menu_id)
|
result = await responce.read_menu(menu_id=menu_id)
|
||||||
|
|
||||||
@@ -63,7 +60,6 @@ async def update_menu(
|
|||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
menu: MenuBase,
|
menu: MenuBase,
|
||||||
responce: MenuService = Depends(),
|
responce: MenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> MenuRead:
|
) -> MenuRead:
|
||||||
result = await responce.update_menu(
|
result = await responce.update_menu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
@@ -85,6 +81,5 @@ async def update_menu(
|
|||||||
async def delete_menu(
|
async def delete_menu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
menu: MenuService = Depends(),
|
menu: MenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> None:
|
) -> None:
|
||||||
await menu.del_menu(menu_id)
|
await menu.del_menu(menu_id)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from fastfood.schemas import MenuBase, SubMenuRead
|
from fastfood.schemas import MenuBase, SubMenuRead
|
||||||
from fastfood.service.submenu import SubmenuService
|
from fastfood.service.submenu import SubmenuService
|
||||||
@@ -18,7 +18,6 @@ router = APIRouter(
|
|||||||
async def get_submenus(
|
async def get_submenus(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu: SubmenuService = Depends(),
|
submenu: SubmenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> list[SubMenuRead]:
|
) -> list[SubMenuRead]:
|
||||||
result = await submenu.read_submenus(menu_id=menu_id)
|
result = await submenu.read_submenus(menu_id=menu_id)
|
||||||
return result
|
return result
|
||||||
@@ -33,7 +32,6 @@ async def create_submenu_item(
|
|||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_data: MenuBase,
|
submenu_data: MenuBase,
|
||||||
submenu: SubmenuService = Depends(),
|
submenu: SubmenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> SubMenuRead:
|
) -> SubMenuRead:
|
||||||
result = await submenu.create_submenu(
|
result = await submenu.create_submenu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
@@ -50,7 +48,6 @@ async def get_submenu(
|
|||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
submenu: SubmenuService = Depends(),
|
submenu: SubmenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> SubMenuRead:
|
) -> SubMenuRead:
|
||||||
result = await submenu.read_menu(
|
result = await submenu.read_menu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
@@ -73,7 +70,6 @@ async def update_submenu(
|
|||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
submenu_data: MenuBase,
|
submenu_data: MenuBase,
|
||||||
submenu: SubmenuService = Depends(),
|
submenu: SubmenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> SubMenuRead:
|
) -> SubMenuRead:
|
||||||
result = await submenu.update_submenu(
|
result = await submenu.update_submenu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
@@ -96,6 +92,5 @@ async def delete_submenu(
|
|||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
submenu: SubmenuService = Depends(),
|
submenu: SubmenuService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> None:
|
) -> None:
|
||||||
await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id)
|
await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from fastfood.schemas import MenuSummary
|
from fastfood.schemas import MenuSummary
|
||||||
from fastfood.service.summary import SummaryService
|
from fastfood.service.summary import SummaryService
|
||||||
@@ -12,6 +12,5 @@ router = APIRouter(
|
|||||||
@router.get('/', response_model=list[MenuSummary])
|
@router.get('/', response_model=list[MenuSummary])
|
||||||
async def get_summary(
|
async def get_summary(
|
||||||
sum: SummaryService = Depends(),
|
sum: SummaryService = Depends(),
|
||||||
background_tasks: BackgroundTasks = BackgroundTasks(),
|
|
||||||
) -> list[MenuSummary]:
|
) -> list[MenuSummary]:
|
||||||
return await sum.read_data()
|
return await sum.read_data()
|
||||||
|
@@ -42,7 +42,7 @@ class DishService:
|
|||||||
if cached_dishes is not None:
|
if cached_dishes is not None:
|
||||||
return cached_dishes
|
return cached_dishes
|
||||||
|
|
||||||
data = await self.dish_repo.get_dishes(menu_id, submenu_id)
|
data = await self.dish_repo.get_dishes(submenu_id)
|
||||||
response = []
|
response = []
|
||||||
for row in data:
|
for row in data:
|
||||||
dish = await self._convert_dish_to_dict(row)
|
dish = await self._convert_dish_to_dict(row)
|
||||||
@@ -67,7 +67,6 @@ class DishService:
|
|||||||
) -> Dish:
|
) -> Dish:
|
||||||
dish_db = Dish_db(**dish_data.model_dump())
|
dish_db = Dish_db(**dish_data.model_dump())
|
||||||
data = await self.dish_repo.create_dish_item(
|
data = await self.dish_repo.create_dish_item(
|
||||||
menu_id,
|
|
||||||
submenu_id,
|
submenu_id,
|
||||||
dish_db,
|
dish_db,
|
||||||
)
|
)
|
||||||
@@ -95,7 +94,7 @@ class DishService:
|
|||||||
if cached_dish is not None:
|
if cached_dish is not None:
|
||||||
return cached_dish
|
return cached_dish
|
||||||
|
|
||||||
data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id)
|
data = await self.dish_repo.get_dish_item(dish_id)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
dish = await self._convert_dish_to_dict(data)
|
dish = await self._convert_dish_to_dict(data)
|
||||||
@@ -116,9 +115,7 @@ class DishService:
|
|||||||
self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase
|
self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase
|
||||||
) -> Dish | None:
|
) -> Dish | None:
|
||||||
dish_db = Dish_db(**dish_data.model_dump())
|
dish_db = Dish_db(**dish_data.model_dump())
|
||||||
data = await self.dish_repo.update_dish_item(
|
data = await self.dish_repo.update_dish_item(dish_id, dish_db)
|
||||||
menu_id, submenu_id, dish_id, dish_db
|
|
||||||
)
|
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
@@ -139,10 +136,8 @@ class DishService:
|
|||||||
|
|
||||||
return dish
|
return dish
|
||||||
|
|
||||||
async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> None:
|
async def del_dish(self, menu_id: UUID, dish_id: UUID) -> None:
|
||||||
await self.dish_repo.delete_dish_item(
|
await self.dish_repo.delete_dish_item(
|
||||||
menu_id,
|
|
||||||
submenu_id,
|
|
||||||
dish_id,
|
dish_id,
|
||||||
)
|
)
|
||||||
await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks)
|
await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks)
|
||||||
|
@@ -33,7 +33,7 @@ class SubmenuService:
|
|||||||
submenus = []
|
submenus = []
|
||||||
for r in data:
|
for r in data:
|
||||||
submenu = r.__dict__
|
submenu = r.__dict__
|
||||||
subq = await self.submenu_repo.get_submenu_item(menu_id, r.id)
|
subq = await self.submenu_repo.get_submenu_item(r.id)
|
||||||
if subq is not None:
|
if subq is not None:
|
||||||
submenu['dishes_count'] = len(subq.dishes)
|
submenu['dishes_count'] = len(subq.dishes)
|
||||||
submenu = SubMenuRead(**submenu)
|
submenu = SubMenuRead(**submenu)
|
||||||
@@ -73,7 +73,7 @@ class SubmenuService:
|
|||||||
if cached_submenu is not None:
|
if cached_submenu is not None:
|
||||||
return cached_submenu
|
return cached_submenu
|
||||||
|
|
||||||
data = await self.submenu_repo.get_submenu_item(menu_id, submenu_id)
|
data = await self.submenu_repo.get_submenu_item(submenu_id)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
submenu = data.__dict__
|
submenu = data.__dict__
|
||||||
@@ -90,9 +90,7 @@ class SubmenuService:
|
|||||||
async def update_submenu(
|
async def update_submenu(
|
||||||
self, menu_id: UUID, submenu_id: UUID, submenu_data: MenuBase
|
self, menu_id: UUID, submenu_id: UUID, submenu_data: MenuBase
|
||||||
) -> SubMenuRead | None:
|
) -> SubMenuRead | None:
|
||||||
data = await self.submenu_repo.update_submenu_item(
|
data = await self.submenu_repo.update_submenu_item(submenu_id, submenu_data)
|
||||||
menu_id, submenu_id, submenu_data
|
|
||||||
)
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -111,7 +109,7 @@ class SubmenuService:
|
|||||||
return submenu
|
return submenu
|
||||||
|
|
||||||
async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> None:
|
async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> None:
|
||||||
await self.submenu_repo.delete_submenu_item(menu_id, submenu_id)
|
await self.submenu_repo.delete_submenu_item(submenu_id)
|
||||||
await self.cache.delete(
|
await self.cache.delete(
|
||||||
key=self.key(
|
key=self.key(
|
||||||
'submenu',
|
'submenu',
|
||||||
|
@@ -19,7 +19,7 @@ class SummaryService:
|
|||||||
self.key = get_key
|
self.key = get_key
|
||||||
self.bg_tasks = background_tasks
|
self.bg_tasks = background_tasks
|
||||||
|
|
||||||
async def read_data(self):
|
async def read_data(self) -> list[MenuSummary]:
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@@ -42,7 +42,10 @@ class SummaryService:
|
|||||||
discont = await self.cache.get(f"DISCONT:{str(obj.get('id'))}")
|
discont = await self.cache.get(f"DISCONT:{str(obj.get('id'))}")
|
||||||
|
|
||||||
if discont is not None:
|
if discont is not None:
|
||||||
discont = float(discont)
|
try:
|
||||||
|
discont = float(discont)
|
||||||
|
except Exception:
|
||||||
|
discont = 0.0
|
||||||
obj['price'] = round(
|
obj['price'] = round(
|
||||||
obj['price'] - (obj['price'] * discont / 100), 2
|
obj['price'] - (obj['price'] * discont / 100), 2
|
||||||
)
|
)
|
||||||
|
13
manage.py
13
manage.py
@@ -42,14 +42,13 @@ async def recreate() -> None:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if '--run-celery' in sys.argv:
|
if '--run-docker-server' in sys.argv:
|
||||||
celery_worker_process.start()
|
"""Запуск FastAPI в докере. Celery запускается в отдельном контейнере"""
|
||||||
celery_beat_process.start()
|
loop.run_until_complete(recreate())
|
||||||
|
loop.run_until_complete(run_app())
|
||||||
|
|
||||||
if '--run-server' in sys.argv:
|
if '--run-local-server' in sys.argv:
|
||||||
pass
|
"""Локальный запуск FastAPI с запуском Celery в отдельных процессах"""
|
||||||
|
|
||||||
if '--run-test-server' in sys.argv:
|
|
||||||
celery_worker_process.start()
|
celery_worker_process.start()
|
||||||
celery_beat_process.start()
|
celery_beat_process.start()
|
||||||
|
|
||||||
|
@@ -12,17 +12,13 @@ async def test_summary_with_menu(client: AsyncClient) -> None:
|
|||||||
assert rspn == []
|
assert rspn == []
|
||||||
|
|
||||||
# Создаем меню и проверяем ответ
|
# Создаем меню и проверяем ответ
|
||||||
menu = {
|
menu = {'title': 'Menu', 'description': 'main menu', 'submenus': []}
|
||||||
'title': 'Menu',
|
|
||||||
'description': 'main menu',
|
|
||||||
}
|
|
||||||
code, rspn = await Repo.Menu.write(client, menu)
|
code, rspn = await Repo.Menu.write(client, menu)
|
||||||
menu.update(rspn)
|
menu.update(rspn)
|
||||||
|
|
||||||
# Удалим ненужные ключи, тк в модели они не используются
|
# Удалим ненужные ключи, тк в модели они не используются
|
||||||
del menu['submenus_count']
|
del menu['submenus_count']
|
||||||
del menu['dishes_count']
|
del menu['dishes_count']
|
||||||
menu.__setattr__('submenus', list())
|
|
||||||
|
|
||||||
# Проверяем summary c меню
|
# Проверяем summary c меню
|
||||||
code, rspn = await Repo.Summary.read_summary(client)
|
code, rspn = await Repo.Summary.read_summary(client)
|
||||||
@@ -36,30 +32,30 @@ async def test_summary_with_menu(client: AsyncClient) -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_summary_with_submenus(client: AsyncClient) -> None:
|
async def test_summary_with_submenus(client: AsyncClient) -> None:
|
||||||
# Создаем меню и проверяем ответ
|
# Создаем меню и проверяем ответ
|
||||||
menu = {
|
menu: dict[str, str | list | float] = {
|
||||||
'title': 'Menu',
|
'title': 'Menu',
|
||||||
'description': 'main menu',
|
'description': 'main menu',
|
||||||
|
'submenus': [],
|
||||||
}
|
}
|
||||||
code, rspn = await Repo.Menu.write(client, menu)
|
code, rspn = await Repo.Menu.write(client, menu)
|
||||||
menu.update(rspn)
|
menu.update(rspn)
|
||||||
|
|
||||||
del menu['submenus_count']
|
del menu['submenus_count']
|
||||||
del menu['dishes_count']
|
del menu['dishes_count']
|
||||||
menu.__setattr__('submenus', list())
|
|
||||||
|
|
||||||
# Создаем и проверяем подменю
|
# Создаем и проверяем подменю
|
||||||
submenu = {
|
submenu: dict[str, str | list | float] = {
|
||||||
'title': 'Submenu',
|
'title': 'Submenu',
|
||||||
'description': 'submenu',
|
'description': 'submenu',
|
||||||
'parent_menu': menu['id'],
|
'parent_menu': menu['id'],
|
||||||
|
'dishes': list(),
|
||||||
}
|
}
|
||||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||||
submenu.update(rspn)
|
submenu.update(rspn)
|
||||||
submenu.__setattr__('dishes', list())
|
|
||||||
del submenu['dishes_count']
|
del submenu['dishes_count']
|
||||||
del submenu['parent_menu']
|
del submenu['parent_menu']
|
||||||
|
|
||||||
menu.__setattr__('submenus', [submenu])
|
menu['submenus'] = [submenu]
|
||||||
|
|
||||||
# Получаем блюдо
|
# Получаем блюдо
|
||||||
code, rspn = await Repo.Summary.read_summary(client)
|
code, rspn = await Repo.Summary.read_summary(client)
|
||||||
@@ -72,26 +68,26 @@ async def test_summary_with_submenus(client: AsyncClient) -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_summary_with_dishes(client: AsyncClient) -> None:
|
async def test_summary_with_dishes(client: AsyncClient) -> None:
|
||||||
# Создаем меню и проверяем ответ
|
# Создаем меню и проверяем ответ
|
||||||
menu = {
|
menu: dict[str, str | list | float] = {
|
||||||
'title': 'Menu',
|
'title': 'Menu',
|
||||||
'description': 'main menu',
|
'description': 'main menu',
|
||||||
|
'submenus': [],
|
||||||
}
|
}
|
||||||
code, rspn = await Repo.Menu.write(client, menu)
|
code, rspn = await Repo.Menu.write(client, menu)
|
||||||
menu.update(rspn)
|
menu.update(rspn)
|
||||||
|
|
||||||
del menu['submenus_count']
|
del menu['submenus_count']
|
||||||
del menu['dishes_count']
|
del menu['dishes_count']
|
||||||
menu.__setattr__('submenus', list())
|
|
||||||
|
|
||||||
# Создаем и проверяем подменю
|
# Создаем и проверяем подменю
|
||||||
submenu = {
|
submenu: dict[str, str | list | float] = {
|
||||||
'title': 'Submenu',
|
'title': 'Submenu',
|
||||||
'description': 'submenu',
|
'description': 'submenu',
|
||||||
'parent_menu': menu['id'],
|
'parent_menu': menu['id'],
|
||||||
|
'dishes': [],
|
||||||
}
|
}
|
||||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||||
submenu.update(rspn)
|
submenu.update(rspn)
|
||||||
submenu.__setattr__('dishes', list())
|
|
||||||
del submenu['dishes_count']
|
del submenu['dishes_count']
|
||||||
del submenu['parent_menu']
|
del submenu['parent_menu']
|
||||||
|
|
||||||
@@ -107,8 +103,8 @@ async def test_summary_with_dishes(client: AsyncClient) -> None:
|
|||||||
del dish['parent_submenu']
|
del dish['parent_submenu']
|
||||||
del dish['id']
|
del dish['id']
|
||||||
|
|
||||||
submenu.__setattr__('dishes', dish)
|
submenu['dishes'] = [dish]
|
||||||
menu.__setattr__('submenus', submenu)
|
menu['submenus'] = [submenu]
|
||||||
|
|
||||||
code, rspn = await Repo.Summary.read_summary(client)
|
code, rspn = await Repo.Summary.read_summary(client)
|
||||||
assert code == 200
|
assert code == 200
|
||||||
|
Reference in New Issue
Block a user