Compare commits
105 Commits
f8eca2e832
...
develop
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 | |||
fc9577c538 | |||
550a058b6f | |||
ffb5b855c4 | |||
d9633dcfbd | |||
e4656825cb | |||
3120910552 | |||
3b1a1614cf | |||
aa7db7cd35 | |||
27904e0c6a | |||
ee709a489e | |||
f8cca4b861 | |||
7d4c4d9be3 | |||
095ab07ebb | |||
f72c6fe4d7 | |||
a2ed5a6732 | |||
b3509d698d | |||
5c8c3f16ae | |||
c6e8e78c95 | |||
749e37354d | |||
a5eebd15ba | |||
43eca19d91 | |||
291c61f873 | |||
09d0627d70 | |||
5173fcd36c | |||
2754b82b5d | |||
35659529b4 | |||
181c6f10af | |||
015a0bcc87 | |||
628babc295 | |||
f807bdd275 | |||
2afba14e44 | |||
45dd8dc73e | |||
f667026d62 | |||
0ba422397a | |||
b223053cf6 | |||
58ecd82bb6 | |||
995be04dcb | |||
e2185cc904 | |||
7eefa8e5db | |||
75e3036e13 | |||
f86a783d1c | |||
64bc03b7fa | |||
ead24d9f28 | |||
f61cb3a2ee | |||
e378bf1da1 | |||
732bf9928c | |||
bde9581090 | |||
c27858e4fb | |||
479a997844 | |||
08f3297297 | |||
b5da5736e9 | |||
1b5182b41a | |||
bab8008ec8 | |||
cae407a5f4 | |||
dce3841d5a | |||
e2428d7cdc | |||
51b5b909c9 | |||
b282ceebe7 | |||
5ced7acef8 | |||
8f48352600 | |||
d6f1347fab | |||
b474e21f0f | |||
b20ff8bceb | |||
f09b5b57b2 | |||
0ae3293730 | |||
914814e267 | |||
706a8ec13c | |||
938d41d1ea | |||
c01affbcb9 | |||
ed11efa704 | |||
a4af3437ac | |||
f6c39cea05 | |||
ad497254cd |
14
.env
Normal file
14
.env
Normal file
@@ -0,0 +1,14 @@
|
||||
# PosgreSQL адрес сервера
|
||||
POSTGRES_HOST=127.0.0.1
|
||||
POSTGRES_PORT=5432
|
||||
# Пользователь БД Postgres
|
||||
POSTGRES_USER=testuser
|
||||
POSTGRES_PASSWORD=test
|
||||
# БД рабочая и тестовая
|
||||
POSTGRES_DB=fastfood_db
|
||||
POSTGRES_DB_TEST=fastfood_db_test
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -217,4 +217,3 @@ fabric.properties
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
|
50
.pre-commit-config.yaml
Normal file
50
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace # убирает лишние пробелы
|
||||
- id: check-added-large-files # проверяет тяжелые файлы на изменения
|
||||
- id: check-yaml # проверяет синтаксис .yaml файлов
|
||||
- id: check-json # проверяет синтаксис .json файлов
|
||||
exclude: launch.json
|
||||
- id: check-case-conflict # проверяет файлы, которые могут конфликтовать в файловых системах без учета регистра.
|
||||
- id: check-merge-conflict # проверяет файлы, содержащие конфликтные строки слияния.
|
||||
- id: double-quote-string-fixer # заменяет " на '
|
||||
- id: end-of-file-fixer # добавляет пустую строку в конце файла
|
||||
|
||||
# Отсортировывает импорты в проекте
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: __init__.py
|
||||
args: [ --profile, black, --filter-files ]
|
||||
|
||||
# Обновляет синтаксис Python кода в соответствии с последними версиями
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
|
||||
# Форматирует код под PEP8
|
||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||
rev: v2.0.1
|
||||
hooks:
|
||||
- id: autopep8
|
||||
args: [--max-line-length=120, --in-place]
|
||||
|
||||
# Сканер стилистических ошибок, нарушающие договоренности PEP8
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: "__init__.py"
|
||||
args: ["--ignore=E501,F821", "--max-line-length=120"]
|
||||
|
||||
# Проверка статических типов с помощью mypy
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.991
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: 'migrations'
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
RUN mkdir -p /usr/src/fastfood
|
||||
|
||||
WORKDIR /usr/src/fastfood
|
||||
|
||||
COPY ./example.env .
|
||||
|
||||
COPY ./poetry.lock .
|
||||
|
||||
COPY ./pyproject.toml .
|
||||
|
||||
RUN touch /usr/src/RUN_IN_DOCKER
|
||||
|
||||
RUN poetry install
|
95
README.md
95
README.md
@@ -0,0 +1,95 @@
|
||||
# fastfood
|
||||
Fastapi веб приложение реализующее api для общепита.
|
||||
|
||||
## Описание
|
||||
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||
|
||||
## Техническое задание
|
||||
### Спринт 4 - Многопроцессорность, асинхронность
|
||||
В этом домашнем задании необходимо:
|
||||
1.Переписать текущее FastAPI приложение на асинхронное выполнение
|
||||
2.Добавить в проект фоновую задачу с помощью Celery + RabbitMQ.
|
||||
3.Добавить эндпоинт (GET) для вывода всех меню со всеми связанными подменю и со всеми связанными блюдами.
|
||||
4.Реализовать инвалидация кэша в background task (встроено в FastAPI)
|
||||
5.* Обновление меню из google sheets раз в 15 сек.
|
||||
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
|
||||
|
||||
Фоновая задача: синхронизация Excel документа и БД.
|
||||
В проекте создаем папку admin. В эту папку кладем файл Menu.xlsx (будет прикреплен к ДЗ). Не забываем запушить в гит.
|
||||
При внесении изменений в файл все изменения должны отображаться в БД. Периодичность обновления 15 сек. Удалять БД при каждом обновлении – нельзя.
|
||||
|
||||
|
||||
Требования:
|
||||
●Данные меню, подменю, блюд для нового эндпоинта должны доставаться одним ORM-запросом в БД (использовать подзапросы и агрегирующие функций SQL).
|
||||
●Проект должен запускаться одной командой
|
||||
●Проект должен соответствовать требованиям всех предыдущих вебинаров. (Не забыть добавить тесты для нового API эндпоинта)
|
||||
|
||||
### Выполненные доп задания со *
|
||||
Спринт 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-compose
|
||||
|
||||
## Установка
|
||||
|
||||
Клонируйте репозиторий
|
||||
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
||||
|
||||
Перейдите в каталог
|
||||
> `$ cd fastfood`
|
||||
|
||||
Запуск/остановка образов:
|
||||
|
||||
- Запуск FAstAPI приложения c локальным файлом для фоновой задачи
|
||||
> `$ 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>
|
||||
|
||||
По завершении работы остановите контейнеры
|
||||
> `$ docker-compose -f compose_app.yml down`
|
||||
|
||||
- Запуск тестов
|
||||
> `$ docker-compose -f compose_test.yml up`
|
||||
|
||||
По завершении работы остановите контейнеры
|
||||
> `$ docker-compose -f compose_test.yml down`
|
||||
|
||||
|
||||
|
||||
## TODO
|
||||
- Написать тесты для кривых данных
|
||||
- Много чего другого :)
|
||||
|
||||
## Авторы
|
||||
- Сергей Ванюшкин <pi3c@yandex.ru>
|
||||
|
||||
## Лицензия
|
||||
Распространяется под [MIT лицензией](https://mit-license.org/).
|
||||
|
BIN
admin/Menu.xlsx
Normal file
BIN
admin/Menu.xlsx
Normal file
Binary file not shown.
0
bg_tasks/__init__.py
Normal file
0
bg_tasks/__init__.py
Normal file
50
bg_tasks/bg_task.py
Normal file
50
bg_tasks/bg_task.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
|
||||
from celery import Celery
|
||||
|
||||
from fastfood.config import settings
|
||||
|
||||
from .updater import main, main_gsheets
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
celery_app = Celery(
|
||||
'tasks',
|
||||
broker=settings.REBBITMQ_URL,
|
||||
backend='rpc://',
|
||||
include=['bg_tasks.bg_task'],
|
||||
)
|
||||
|
||||
celery_app.conf.beat_schedule = {
|
||||
'run-task-every-15-seconds': {
|
||||
'task': 'bg_tasks.bg_task.periodic_task',
|
||||
'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
|
||||
def periodic_task() -> None:
|
||||
result = loop.run_until_complete(main())
|
||||
return result
|
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
|
295
bg_tasks/updater.py
Normal file
295
bg_tasks/updater.py
Normal file
@@ -0,0 +1,295 @@
|
||||
import os
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as redis # type: ignore
|
||||
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
|
||||
|
||||
from .parser import file, gsheets_to_rows, local_xlsx_to_rows, rows_to_dict
|
||||
|
||||
redis = redis.Redis.from_url(url=settings.REDIS_URL)
|
||||
|
||||
async_engine = create_async_engine(settings.DATABASE_URL_asyncpg)
|
||||
async_session_maker = async_sessionmaker(
|
||||
async_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
"""Проверяет, изменен ли файл с последнего запуска таска."""
|
||||
if not os.path.exists(file):
|
||||
return False
|
||||
|
||||
mod_time = os.path.getmtime(file)
|
||||
cached_time = await redis.get('XLSX_MOD_TIME')
|
||||
|
||||
if cached_time is not None:
|
||||
cached_time = pickle.loads(cached_time)
|
||||
|
||||
if mod_time == cached_time:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def on_menu_change(
|
||||
new_menu: dict, old_menu: dict, session: AsyncSession
|
||||
) -> 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)
|
||||
|
||||
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']
|
||||
|
||||
else:
|
||||
# Удаляем меню
|
||||
await session.execute(delete(Menu).where(Menu.id == old_menu['id']))
|
||||
|
||||
await session.commit()
|
||||
return new_menu
|
||||
|
||||
|
||||
async def menus_updater(menus: dict, session: AsyncSession) -> None:
|
||||
"""Проверяет пункты меню на изменения
|
||||
При необходимости запускае обновление БД
|
||||
через фенкцию on_menu_change
|
||||
"""
|
||||
cached_menus = await redis.get('ALL_MENUS')
|
||||
|
||||
if cached_menus is not None:
|
||||
cached_menus = pickle.loads(cached_menus)
|
||||
else:
|
||||
cached_menus = {}
|
||||
|
||||
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 key in {k: cached_menus[k] for k in set(cached_menus) - set(menus)}:
|
||||
# Проверяем на удаленные меню
|
||||
await on_menu_change({}, cached_menus.pop(key), session)
|
||||
|
||||
await redis.set('ALL_MENUS', pickle.dumps(menus))
|
||||
|
||||
|
||||
async def on_submenu_change(
|
||||
new_sub: dict, old_sub: dict, session: AsyncSession
|
||||
) -> 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
|
||||
)
|
||||
submenus[key] = submenu
|
||||
else:
|
||||
submenus[key]['id'] = cached_sub[key]['id']
|
||||
submenus[key]['parent_menu'] = cached_sub[key]['parent_menu']
|
||||
|
||||
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 redis.set('ALL_SUBMENUS', pickle.dumps(submenus))
|
||||
|
||||
|
||||
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:
|
||||
"""Главная функция фоновой задачи"""
|
||||
changed = await is_changed_xls()
|
||||
if changed:
|
||||
rows = await local_xlsx_to_rows()
|
||||
await updater(rows)
|
||||
|
||||
|
||||
async def main_gsheets() -> None:
|
||||
"""Главная функция фоновой задачи для работы с Google"""
|
||||
rows = await gsheets_to_rows()
|
||||
await updater(rows)
|
112
compose_app.yml
Normal file
112
compose_app.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", "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:
|
||||
container_name: rabbit
|
||||
|
||||
image: "rabbitmq:management"
|
||||
|
||||
ports:
|
||||
- 5672:5672
|
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
|
61
compose_test.yml
Normal file
61
compose_test.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
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_test
|
||||
|
||||
image: postgres:15.1-alpine
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB_TEST}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
ports:
|
||||
- 6432:5432
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB_TEST}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
|
||||
app:
|
||||
container_name: fastfood_app_test
|
||||
|
||||
build:
|
||||
context: .
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
- .:/usr/src/fastfood
|
||||
|
||||
command: /bin/bash -c 'poetry run pytest -vv'
|
@@ -1,20 +1,27 @@
|
||||
import json
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from fastfood.routes import router
|
||||
from fastfood.routers.dish import router as dish_router
|
||||
from fastfood.routers.menu import router as menu_router
|
||||
from fastfood.routers.submenu import router as submenu_router
|
||||
from fastfood.routers.summary import router as summary_router
|
||||
|
||||
|
||||
async def generate_test_data():
|
||||
def create_app() -> FastAPI:
|
||||
"""
|
||||
Создание БД и наполнение ее данными
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
Создание экземпляра приложения FastAPI и врзврат его
|
||||
Фабрика FastAPI.
|
||||
"""
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
app.include_router(menu_router)
|
||||
app.include_router(submenu_router)
|
||||
app.include_router(dish_router)
|
||||
app.include_router(summary_router)
|
||||
|
||||
def custom_openapi():
|
||||
with open('openapi.json') as openapi:
|
||||
return json.load(openapi)
|
||||
|
||||
app.openapi = custom_openapi
|
||||
|
||||
return app
|
||||
|
@@ -1,24 +1,83 @@
|
||||
import os
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DB_HOST: str = "localhost"
|
||||
DB_PORT: int = 5432
|
||||
DB_USER: str = "postrges"
|
||||
DB_PASS: str = "postgres"
|
||||
DB_NAME: str = "demo_db"
|
||||
# Конфиг PostgreSql
|
||||
POSTGRES_HOST: str = ''
|
||||
POSTGRES_PORT: int = 5432
|
||||
POSTGRES_DB: str = ''
|
||||
POSTGRES_PASSWORD: str = ''
|
||||
POSTGRES_USER: str = ''
|
||||
POSTGRES_DB_TEST: str = ''
|
||||
# Конфиг Redis
|
||||
REDIS_HOST: str = ''
|
||||
REDIS_PORT: int = 6379
|
||||
REDIS_DB: int = 0
|
||||
|
||||
@property
|
||||
def DATABASE_URL_asyncpg(self):
|
||||
def DATABASE_URL_asyncpg(self) -> str:
|
||||
"""
|
||||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
||||
"""
|
||||
# Проверяем, в DOCKER или нет
|
||||
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||
if os.path.exists(file_path):
|
||||
return (
|
||||
'postgresql+asyncpg://'
|
||||
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||
f'@db:5432/{self.POSTGRES_DB}'
|
||||
)
|
||||
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}"
|
||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
'postgresql+asyncpg://'
|
||||
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}'
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
@property
|
||||
def TESTDATABASE_URL_asyncpg(self):
|
||||
"""
|
||||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
||||
"""
|
||||
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||
if os.path.exists(file_path):
|
||||
return (
|
||||
'postgresql+asyncpg://'
|
||||
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||
f'@db:5432/{self.POSTGRES_DB_TEST}'
|
||||
)
|
||||
|
||||
return (
|
||||
'postgresql+asyncpg://'
|
||||
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB_TEST}'
|
||||
)
|
||||
|
||||
@property
|
||||
def REDIS_URL(self):
|
||||
"""
|
||||
Возвращает строку подключения к REDIS
|
||||
"""
|
||||
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||
if os.path.exists(file_path):
|
||||
return 'redis://redis:6379/0'
|
||||
|
||||
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')
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
@@ -1,57 +0,0 @@
|
||||
from uuid import UUID
|
||||
from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastfood import models, schemas
|
||||
from fastfood.dbase import async_engine
|
||||
|
||||
|
||||
async def create_db_and_tables():
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(models.Base.metadata.drop_all)
|
||||
await conn.run_sync(models.Base.metadata.create_all)
|
||||
|
||||
|
||||
class Crud:
|
||||
@staticmethod
|
||||
async def get_menus(session: AsyncSession):
|
||||
async with session:
|
||||
query = select(models.Menu)
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def create_menu_item(menu: schemas.MenuBase, session: AsyncSession):
|
||||
async with session:
|
||||
new_menu = models.Menu(**menu.model_dump())
|
||||
session.add(new_menu)
|
||||
await session.commit()
|
||||
await session.refresh(new_menu)
|
||||
return new_menu
|
||||
|
||||
@staticmethod
|
||||
async def get_menu_item(menu_id: UUID, session: AsyncSession):
|
||||
async with session:
|
||||
query = select(models.Menu).where(models.Menu.id == menu_id)
|
||||
menu = await session.execute(query)
|
||||
return menu.scalars().one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def update_menu_item(menu_id: UUID,
|
||||
menu: schemas.MenuBase,
|
||||
session: AsyncSession,
|
||||
):
|
||||
async with session:
|
||||
query = update(models.Menu).where(models.Menu.id == menu_id).values(**menu.model_dump())
|
||||
await session.execute(query)
|
||||
await session.commit()
|
||||
qr = select(models.Menu).where(models.Menu.id == menu_id)
|
||||
updated_menu = await session.execute(qr)
|
||||
return updated_menu.scalars().one()
|
||||
|
||||
@staticmethod
|
||||
async def delete_menu_item(menu_id: UUID, session: AsyncSession):
|
||||
async with session:
|
||||
query = delete(models.Menu).where(models.Menu.id == menu_id)
|
||||
await session.execute(query)
|
||||
await session.commit()
|
@@ -1,14 +1,11 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from fastfood.config import settings
|
||||
|
||||
|
||||
async_engine = create_async_engine(settings.DATABASE_URL_asyncpg)
|
||||
async_session_maker = async_sessionmaker(
|
||||
async_engine,
|
||||
@@ -20,3 +17,13 @@ async_session_maker = async_sessionmaker(
|
||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
def get_redis_pool():
|
||||
return redis.from_url(settings.REDIS_URL, decode_responses=False)
|
||||
|
||||
|
||||
async def get_async_redis_client(
|
||||
redis_pool: redis.Redis = Depends(get_redis_pool),
|
||||
):
|
||||
return redis_pool
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, List, Optional
|
||||
from copy import deepcopy
|
||||
from typing import Annotated
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy.util import hybridproperty
|
||||
|
||||
uuidpk = Annotated[
|
||||
int,
|
||||
uuid.UUID,
|
||||
mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
@@ -18,33 +19,66 @@ str_25 = Annotated[str, 25]
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
id: Mapped[uuidpk]
|
||||
title: Mapped[str_25]
|
||||
description: Mapped[str | None]
|
||||
|
||||
def __eq__(self, other):
|
||||
classes_match = isinstance(other, self.__class__)
|
||||
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
|
||||
a.pop('_sa_instance_state', None)
|
||||
b.pop('_sa_instance_state', None)
|
||||
attrs_match = a == b
|
||||
return classes_match and attrs_match
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class Menu(Base):
|
||||
__tablename__ = "menu"
|
||||
__tablename__ = 'menu'
|
||||
|
||||
id: Mapped[uuidpk]
|
||||
title: Mapped[str_25]
|
||||
description: Mapped[Optional[str]]
|
||||
submenus: Mapped[List["SubMenu"]] = relationship()
|
||||
submenus: Mapped[list['SubMenu']] = relationship(
|
||||
'SubMenu',
|
||||
backref='menu',
|
||||
lazy='selectin',
|
||||
cascade='all, delete',
|
||||
)
|
||||
|
||||
@hybridproperty
|
||||
def submenus_count(self):
|
||||
return len(self.submenus)
|
||||
|
||||
@hybridproperty
|
||||
def dishes_count(self):
|
||||
counter = 0
|
||||
for sub in self.submenus:
|
||||
counter += len(sub.dishes)
|
||||
return counter
|
||||
|
||||
|
||||
class SubMenu(Base):
|
||||
__tablename__ = "submenu"
|
||||
__tablename__ = 'submenu'
|
||||
|
||||
id: Mapped[uuidpk]
|
||||
title: Mapped[str_25]
|
||||
description: Mapped[Optional[str]]
|
||||
parent_menu: Mapped[UUID] = mapped_column(ForeignKey("menu.id"))
|
||||
dishes: Mapped[List["Dish"]] = relationship()
|
||||
parent_menu: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey('menu.id', ondelete='CASCADE')
|
||||
)
|
||||
dishes: Mapped[list['Dish']] = relationship(
|
||||
'Dish',
|
||||
backref='submenu',
|
||||
lazy='selectin',
|
||||
cascade='all, delete',
|
||||
)
|
||||
|
||||
@hybridproperty
|
||||
def dishes_count(self):
|
||||
return len(self.dishes)
|
||||
|
||||
|
||||
class Dish(Base):
|
||||
__tablename__ = "dish"
|
||||
__tablename__ = 'dish'
|
||||
|
||||
id: Mapped[uuidpk]
|
||||
title: Mapped[str_25]
|
||||
description: Mapped[Optional[str]]
|
||||
price: Mapped[Decimal]
|
||||
parent_submenu: Mapped[UUID] = mapped_column(ForeignKey("submenu.id"))
|
||||
price: Mapped[float]
|
||||
parent_submenu: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey('submenu.id', ondelete='CASCADE')
|
||||
)
|
||||
|
19
fastfood/repository/__init__.py
Normal file
19
fastfood/repository/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastfood import models
|
||||
from fastfood.dbase import async_engine
|
||||
|
||||
from .dish import DishRepository
|
||||
from .menu import MenuRepository
|
||||
from .submenu import SubMenuRepository
|
||||
|
||||
|
||||
async def create_db_and_tables():
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(models.Base.metadata.drop_all)
|
||||
await conn.run_sync(models.Base.metadata.create_all)
|
||||
|
||||
|
||||
class Repository(MenuRepository, SubMenuRepository, DishRepository):
|
||||
pass
|
||||
|
||||
|
||||
ropo = Repository()
|
61
fastfood/repository/dish.py
Normal file
61
fastfood/repository/dish.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import delete, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Dish
|
||||
from fastfood.schemas import Dish_db
|
||||
|
||||
|
||||
class DishRepository:
|
||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||
self.db = session
|
||||
|
||||
async def get_dishes(self, submenu_id: UUID) -> list[Dish]:
|
||||
query = select(Dish).where(
|
||||
Dish.parent_submenu == submenu_id,
|
||||
)
|
||||
dishes = await self.db.execute(query)
|
||||
return [x for x in dishes.scalars().all()]
|
||||
|
||||
async def create_dish_item(
|
||||
self,
|
||||
submenu_id: UUID,
|
||||
dish_data: Dish_db,
|
||||
) -> Dish:
|
||||
new_dish = Dish(**dish_data.model_dump())
|
||||
new_dish.parent_submenu = submenu_id
|
||||
self.db.add(new_dish)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(new_dish)
|
||||
return new_dish
|
||||
|
||||
async def get_dish_item(
|
||||
self,
|
||||
dish_id: UUID,
|
||||
) -> Dish | None:
|
||||
query = select(Dish).where(Dish.id == dish_id)
|
||||
submenu = await self.db.execute(query)
|
||||
return submenu.scalars().one_or_none()
|
||||
|
||||
async def update_dish_item(
|
||||
self,
|
||||
dish_id: UUID,
|
||||
dish_data: Dish_db,
|
||||
) -> Dish | None:
|
||||
query = update(Dish).where(Dish.id == dish_id).values(**dish_data.model_dump())
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
||||
qr = select(Dish).where(Dish.id == dish_id)
|
||||
updated_submenu = await self.db.execute(qr)
|
||||
return updated_submenu.scalar_one_or_none()
|
||||
|
||||
async def delete_dish_item(
|
||||
self,
|
||||
dish_id: UUID,
|
||||
) -> None:
|
||||
query = delete(Dish).where(Dish.id == dish_id)
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
64
fastfood/repository/menu.py
Normal file
64
fastfood/repository/menu.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import delete, distinct, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from fastfood import schemas
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Dish, Menu, SubMenu
|
||||
|
||||
|
||||
class MenuRepository:
|
||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||
self.db = session
|
||||
|
||||
async def get_menus(self) -> list[Menu]:
|
||||
query = select(Menu)
|
||||
menus = await self.db.execute(query)
|
||||
return [x for x in menus.scalars().all()]
|
||||
|
||||
async def create_menu_item(self, menu: schemas.MenuBase) -> Menu:
|
||||
new_menu = Menu(**menu.model_dump())
|
||||
self.db.add(new_menu)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(new_menu)
|
||||
return new_menu
|
||||
|
||||
async def get_menu_item(self, menu_id: UUID) -> Menu | None:
|
||||
m = aliased(Menu)
|
||||
s = aliased(SubMenu)
|
||||
d = aliased(Dish)
|
||||
|
||||
query = (
|
||||
select(
|
||||
m,
|
||||
func.count(distinct(s.id)).label('submenus_count'),
|
||||
func.count(distinct(d.id)).label('dishes_count'),
|
||||
)
|
||||
.join(s, s.parent_menu == m.id, isouter=True)
|
||||
.join(d, d.parent_submenu == s.id, isouter=True)
|
||||
.group_by(m.id)
|
||||
.where(m.id == menu_id)
|
||||
)
|
||||
menu = await self.db.execute(query)
|
||||
menu = menu.scalars().one_or_none()
|
||||
return menu
|
||||
|
||||
async def update_menu_item(
|
||||
self,
|
||||
menu_id: UUID,
|
||||
menu: schemas.MenuBase,
|
||||
) -> Menu | None:
|
||||
query = update(Menu).where(Menu.id == menu_id).values(**menu.model_dump())
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
||||
qr = select(Menu).where(Menu.id == menu_id)
|
||||
updated_menu = await self.db.execute(qr)
|
||||
return updated_menu.scalar_one_or_none()
|
||||
|
||||
async def delete_menu_item(self, menu_id: UUID) -> None:
|
||||
query = delete(Menu).where(Menu.id == menu_id)
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
66
fastfood/repository/redis.py
Normal file
66
fastfood/repository/redis.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
|
||||
from fastfood.dbase import get_redis_pool
|
||||
|
||||
|
||||
def get_key(level: str, **kwargs) -> str:
|
||||
match level:
|
||||
case 'menus':
|
||||
return 'MENUS'
|
||||
case 'menu':
|
||||
return f"MENUS:{kwargs.get('menu_id')}"
|
||||
case 'submenus':
|
||||
return f"MENUS:{kwargs.get('menu_id')}:SUBMENUS"
|
||||
case 'submenu':
|
||||
return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}"
|
||||
case 'dishes':
|
||||
return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES"
|
||||
case 'dish':
|
||||
return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}"
|
||||
|
||||
return 'summary'
|
||||
|
||||
|
||||
class RedisRepository:
|
||||
def __init__(
|
||||
self,
|
||||
pool: redis.Redis = Depends(get_redis_pool),
|
||||
) -> None:
|
||||
self.pool = pool
|
||||
self.ttl = 1800
|
||||
|
||||
async def get(self, key: str) -> Any | None:
|
||||
data = await self.pool.get(key)
|
||||
if data is not None:
|
||||
return pickle.loads(data)
|
||||
return None
|
||||
|
||||
async def set(self, key: str, value: Any, bg_task: BackgroundTasks) -> None:
|
||||
data = pickle.dumps(value)
|
||||
bg_task.add_task(self._set_cache, key, data)
|
||||
|
||||
async def _set_cache(self, key: str, data: Any) -> None:
|
||||
await self.pool.setex(key, self.ttl, data)
|
||||
|
||||
async def delete(self, key: str, bg_task: BackgroundTasks) -> None:
|
||||
bg_task.add_task(self._delete_cache, key)
|
||||
|
||||
async def _delete_cache(self, key: str) -> None:
|
||||
await self.pool.delete(key)
|
||||
|
||||
async def clear_cache(self, pattern: str, bg_task: BackgroundTasks) -> None:
|
||||
keys = [key async for key in self.pool.scan_iter(pattern)]
|
||||
if keys:
|
||||
bg_task.add_task(self._clear_keys, keys)
|
||||
|
||||
async def _clear_keys(self, keys: list[str]) -> None:
|
||||
await self.pool.delete(*keys)
|
||||
|
||||
async def invalidate(self, key: str, bg_task: BackgroundTasks) -> None:
|
||||
await self.clear_cache(f'{key}*', bg_task)
|
||||
await self.clear_cache(f'{get_key("menus")}*', bg_task)
|
||||
await self.clear_cache('summary', bg_task)
|
77
fastfood/repository/submenu.py
Normal file
77
fastfood/repository/submenu.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import delete, distinct, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Dish, SubMenu
|
||||
from fastfood.schemas import MenuBase
|
||||
|
||||
|
||||
class SubMenuRepository:
|
||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||
self.db = session
|
||||
|
||||
async def get_submenus(self, menu_id: UUID) -> list[SubMenu]:
|
||||
query = select(SubMenu).where(
|
||||
SubMenu.parent_menu == menu_id,
|
||||
)
|
||||
submenus = await self.db.execute(query)
|
||||
return [x for x in submenus.scalars().all()]
|
||||
|
||||
async def create_submenu_item(
|
||||
self,
|
||||
menu_id: UUID,
|
||||
submenu: MenuBase,
|
||||
) -> SubMenu:
|
||||
new_submenu = SubMenu(**submenu.model_dump())
|
||||
new_submenu.parent_menu = menu_id
|
||||
self.db.add(new_submenu)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(new_submenu)
|
||||
|
||||
full_sub = await self.get_submenu_item(new_submenu.id)
|
||||
if full_sub is None:
|
||||
raise TypeError
|
||||
return full_sub
|
||||
|
||||
async def get_submenu_item(
|
||||
self,
|
||||
submenu_id: UUID,
|
||||
) -> SubMenu | None:
|
||||
s = aliased(SubMenu)
|
||||
d = aliased(Dish)
|
||||
query = (
|
||||
select(s, func.count(distinct(d.id)).label('dishes_count'))
|
||||
.join(d, s.id == d.parent_submenu, isouter=True)
|
||||
.group_by(s.id)
|
||||
.where(s.id == submenu_id)
|
||||
)
|
||||
submenu = await self.db.execute(query)
|
||||
submenu = submenu.scalars().one_or_none()
|
||||
return submenu
|
||||
|
||||
async def update_submenu_item(
|
||||
self,
|
||||
submenu_id: UUID,
|
||||
submenu_data: MenuBase,
|
||||
) -> SubMenu | None:
|
||||
query = (
|
||||
update(SubMenu)
|
||||
.where(SubMenu.id == submenu_id)
|
||||
.values(**submenu_data.model_dump())
|
||||
)
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
||||
qr = select(SubMenu).where(SubMenu.id == submenu_id)
|
||||
updated_submenu = await self.db.execute(qr)
|
||||
return updated_submenu.scalar_one_or_none()
|
||||
|
||||
async def delete_submenu_item(self, submenu_id: UUID) -> None:
|
||||
query = delete(SubMenu).where(
|
||||
SubMenu.id == submenu_id,
|
||||
)
|
||||
await self.db.execute(query)
|
||||
await self.db.commit()
|
21
fastfood/repository/summary.py
Normal file
21
fastfood/repository/summary.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Menu, SubMenu
|
||||
|
||||
|
||||
class SummaryRepository:
|
||||
def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None:
|
||||
self.db = session
|
||||
|
||||
async def get_data(self) -> list[Any]:
|
||||
query = select(Menu).options(
|
||||
selectinload(Menu.submenus).selectinload(SubMenu.dishes)
|
||||
)
|
||||
data = await self.db.execute(query)
|
||||
return [x for x in data.scalars().all()]
|
0
fastfood/routers/__init__.py
Normal file
0
fastfood/routers/__init__.py
Normal file
102
fastfood/routers/dish.py
Normal file
102
fastfood/routers/dish.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from fastfood.schemas import Dish, DishBase
|
||||
from fastfood.service.dish import DishService
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes',
|
||||
tags=['dish'],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/',
|
||||
response_model=list[Dish],
|
||||
)
|
||||
async def get_dishes(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish: DishService = Depends(),
|
||||
) -> list[Dish]:
|
||||
result = await dish.read_dishes(menu_id, submenu_id)
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
'/',
|
||||
status_code=201,
|
||||
response_model=Dish,
|
||||
)
|
||||
async def create_dish(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish_data: DishBase,
|
||||
dish: DishService = Depends(),
|
||||
) -> Dish:
|
||||
return await dish.create_dish(
|
||||
menu_id,
|
||||
submenu_id,
|
||||
dish_data,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{dish_id}',
|
||||
response_model=Dish,
|
||||
)
|
||||
async def get_dish(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish_id: UUID,
|
||||
dish: DishService = Depends(),
|
||||
) -> Dish | None:
|
||||
result = await dish.read_dish(
|
||||
menu_id,
|
||||
submenu_id,
|
||||
dish_id,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='dish not found',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.patch(
|
||||
'/{dish_id}',
|
||||
response_model=Dish,
|
||||
)
|
||||
async def update_dish(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish_id: UUID,
|
||||
dish_data: DishBase,
|
||||
dish: DishService = Depends(),
|
||||
) -> Dish:
|
||||
result = await dish.update_dish(
|
||||
menu_id,
|
||||
submenu_id,
|
||||
dish_id,
|
||||
dish_data,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='dish not found',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{dish_id}',
|
||||
)
|
||||
async def delete_dish(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish_id: UUID,
|
||||
dish: DishService = Depends(),
|
||||
) -> None:
|
||||
await dish.del_dish(menu_id, dish_id)
|
85
fastfood/routers/menu.py
Normal file
85
fastfood/routers/menu.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from fastfood.schemas import MenuBase, MenuRead
|
||||
from fastfood.service.menu import MenuService
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v1/menus',
|
||||
tags=['menu'],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/',
|
||||
status_code=200,
|
||||
response_model=list[MenuRead],
|
||||
)
|
||||
async def get_menus(
|
||||
menu: MenuService = Depends(),
|
||||
) -> list[MenuRead]:
|
||||
return await menu.read_menus()
|
||||
|
||||
|
||||
@router.post(
|
||||
'/',
|
||||
status_code=201,
|
||||
response_model=MenuRead,
|
||||
)
|
||||
async def add_menu(
|
||||
menu: MenuBase,
|
||||
responce: MenuService = Depends(),
|
||||
) -> MenuRead:
|
||||
return await responce.create_menu(menu)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{menu_id}',
|
||||
response_model=MenuRead,
|
||||
)
|
||||
async def get_menu(
|
||||
menu_id: UUID,
|
||||
responce: MenuService = Depends(),
|
||||
) -> MenuRead:
|
||||
result = await responce.read_menu(menu_id=menu_id)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='menu not found',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.patch(
|
||||
'/{menu_id}',
|
||||
response_model=MenuRead,
|
||||
)
|
||||
async def update_menu(
|
||||
menu_id: UUID,
|
||||
menu: MenuBase,
|
||||
responce: MenuService = Depends(),
|
||||
) -> MenuRead:
|
||||
result = await responce.update_menu(
|
||||
menu_id=menu_id,
|
||||
menu_data=menu,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='menu not found',
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{menu_id}',
|
||||
status_code=200,
|
||||
)
|
||||
async def delete_menu(
|
||||
menu_id: UUID,
|
||||
menu: MenuService = Depends(),
|
||||
) -> None:
|
||||
await menu.del_menu(menu_id)
|
96
fastfood/routers/submenu.py
Normal file
96
fastfood/routers/submenu.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from fastfood.schemas import MenuBase, SubMenuRead
|
||||
from fastfood.service.submenu import SubmenuService
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v1/menus/{menu_id}/submenus',
|
||||
tags=['submenu'],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/',
|
||||
response_model=list[SubMenuRead],
|
||||
)
|
||||
async def get_submenus(
|
||||
menu_id: UUID,
|
||||
submenu: SubmenuService = Depends(),
|
||||
) -> list[SubMenuRead]:
|
||||
result = await submenu.read_submenus(menu_id=menu_id)
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
'/',
|
||||
status_code=201,
|
||||
response_model=SubMenuRead,
|
||||
)
|
||||
async def create_submenu_item(
|
||||
menu_id: UUID,
|
||||
submenu_data: MenuBase,
|
||||
submenu: SubmenuService = Depends(),
|
||||
) -> SubMenuRead:
|
||||
result = await submenu.create_submenu(
|
||||
menu_id=menu_id,
|
||||
submenu_data=submenu_data,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{submenu_id}',
|
||||
response_model=SubMenuRead,
|
||||
)
|
||||
async def get_submenu(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
submenu: SubmenuService = Depends(),
|
||||
) -> SubMenuRead:
|
||||
result = await submenu.read_menu(
|
||||
menu_id=menu_id,
|
||||
submenu_id=submenu_id,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='submenu not found',
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.patch(
|
||||
'/{submenu_id}',
|
||||
response_model=SubMenuRead,
|
||||
)
|
||||
async def update_submenu(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
submenu_data: MenuBase,
|
||||
submenu: SubmenuService = Depends(),
|
||||
) -> SubMenuRead:
|
||||
result = await submenu.update_submenu(
|
||||
menu_id=menu_id,
|
||||
submenu_id=submenu_id,
|
||||
submenu_data=submenu_data,
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail='submenu not found',
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{submenu_id}',
|
||||
)
|
||||
async def delete_submenu(
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
submenu: SubmenuService = Depends(),
|
||||
) -> None:
|
||||
await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id)
|
16
fastfood/routers/summary.py
Normal file
16
fastfood/routers/summary.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from fastfood.schemas import MenuSummary
|
||||
from fastfood.service.summary import SummaryService
|
||||
|
||||
router = APIRouter(
|
||||
prefix='/api/v1/summary',
|
||||
tags=['summary'],
|
||||
)
|
||||
|
||||
|
||||
@router.get('/', response_model=list[MenuSummary])
|
||||
async def get_summary(
|
||||
sum: SummaryService = Depends(),
|
||||
) -> list[MenuSummary]:
|
||||
return await sum.read_data()
|
@@ -1,55 +0,0 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import insert, select
|
||||
from uuid import UUID
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from fastfood import models, schemas
|
||||
from fastfood.crud import Crud as crud
|
||||
from fastfood.dbase import get_async_session
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/api/v1/menus", response_model=Optional[List[schemas.Menu]])
|
||||
async def get_menus(session: AsyncSession = Depends(get_async_session)):
|
||||
result = await crud.get_menus(session=session)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/api/v1/menus", status_code=201, response_model=schemas.Menu)
|
||||
async def add_menu(
|
||||
menu: schemas.MenuBase,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
result = await crud.create_menu_item(
|
||||
menu=menu,
|
||||
session=session,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/api/v1/menus/{menu_id}", response_model=schemas.Menu)
|
||||
async def get_menu(
|
||||
menu_id: UUID,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
result = await crud.get_menu_item(menu_id=menu_id, session=session)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="menu not found")
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/api/v1/menus/{menu_id}", response_model=schemas.MenuBase)
|
||||
async def update_menu(
|
||||
menu_id: UUID,
|
||||
menu: schemas.MenuBase,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
result = await crud.update_menu_item(menu_id=menu_id, menu=menu, session=session)
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/api/v1/menus/{menu_id}")
|
||||
async def delete_menu(menu_id: UUID, session: AsyncSession = Depends(get_async_session)):
|
||||
await crud.delete_menu_item(menu_id=menu_id, session=session)
|
@@ -1,5 +1,3 @@
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -7,17 +5,40 @@ from pydantic import BaseModel
|
||||
|
||||
class MenuBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str]
|
||||
|
||||
description: str | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class Menu(MenuBase):
|
||||
id: UUID
|
||||
title: str
|
||||
description: Optional[str]
|
||||
# submenus: Optional[List[SubMenu]]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class MenuRead(Menu):
|
||||
submenus_count: int
|
||||
dishes_count: int
|
||||
|
||||
|
||||
class SubMenuRead(Menu):
|
||||
dishes_count: int
|
||||
|
||||
|
||||
class DishBase(MenuBase):
|
||||
price: str
|
||||
|
||||
|
||||
class Dish(DishBase, Menu):
|
||||
pass
|
||||
|
||||
|
||||
class Dish_db(MenuBase):
|
||||
price: float
|
||||
|
||||
|
||||
class SubMenuSummary(Menu):
|
||||
dishes: list[Dish_db]
|
||||
|
||||
|
||||
class MenuSummary(Menu):
|
||||
submenus: list[SubMenuSummary]
|
||||
|
144
fastfood/service/dish.py
Normal file
144
fastfood/service/dish.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from uuid import UUID
|
||||
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
|
||||
from fastfood import models
|
||||
from fastfood.dbase import get_async_redis_client
|
||||
from fastfood.repository.dish import DishRepository
|
||||
from fastfood.repository.redis import RedisRepository, get_key
|
||||
from fastfood.schemas import Dish, Dish_db, DishBase
|
||||
|
||||
|
||||
class DishService:
|
||||
def __init__(
|
||||
self,
|
||||
dish_repo: DishRepository = Depends(),
|
||||
redis_client: redis.Redis = Depends(get_async_redis_client),
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> None:
|
||||
self.dish_repo = dish_repo
|
||||
self.cache = RedisRepository(redis_client)
|
||||
self.bg_tasks = background_tasks
|
||||
self.key = get_key
|
||||
|
||||
async def _get_discont(self, dish) -> dict:
|
||||
discont = await self.cache.get(f"DISCONT:{str(dish.get('id'))}")
|
||||
if discont is not None:
|
||||
discont = float(discont)
|
||||
dish['price'] = round(dish['price'] - (dish['price'] * discont / 100), 2)
|
||||
return dish
|
||||
|
||||
async def _convert_dish_to_dict(self, row: models.Dish) -> Dish:
|
||||
dish = row.__dict__
|
||||
dish = await self._get_discont(dish)
|
||||
dish['price'] = str(dish['price'])
|
||||
return Dish(**dish)
|
||||
|
||||
async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]:
|
||||
cached_dishes = await self.cache.get(
|
||||
self.key('dishes', menu_id=str(menu_id), submenu_id=str(submenu_id))
|
||||
)
|
||||
if cached_dishes is not None:
|
||||
return cached_dishes
|
||||
|
||||
data = await self.dish_repo.get_dishes(submenu_id)
|
||||
response = []
|
||||
for row in data:
|
||||
dish = await self._convert_dish_to_dict(row)
|
||||
response.append(dish)
|
||||
|
||||
await self.cache.set(
|
||||
self.key(
|
||||
'dishes',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
),
|
||||
response,
|
||||
self.bg_tasks,
|
||||
)
|
||||
return response
|
||||
|
||||
async def create_dish(
|
||||
self,
|
||||
menu_id: UUID,
|
||||
submenu_id: UUID,
|
||||
dish_data: DishBase,
|
||||
) -> Dish:
|
||||
dish_db = Dish_db(**dish_data.model_dump())
|
||||
data = await self.dish_repo.create_dish_item(
|
||||
submenu_id,
|
||||
dish_db,
|
||||
)
|
||||
dish = await self._convert_dish_to_dict(data)
|
||||
await self.cache.set(
|
||||
self.key('dish', menu_id=str(menu_id), submenu_id=str(submenu_id)),
|
||||
dish,
|
||||
self.bg_tasks,
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
|
||||
return dish
|
||||
|
||||
async def read_dish(
|
||||
self, menu_id: UUID, submenu_id: UUID, dish_id: UUID
|
||||
) -> Dish | None:
|
||||
cached_dish = await self.cache.get(
|
||||
self.key(
|
||||
'dish',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
dish_id=str(dish_id),
|
||||
)
|
||||
)
|
||||
if cached_dish is not None:
|
||||
return cached_dish
|
||||
|
||||
data = await self.dish_repo.get_dish_item(dish_id)
|
||||
if data is None:
|
||||
return None
|
||||
dish = await self._convert_dish_to_dict(data)
|
||||
|
||||
await self.cache.set(
|
||||
self.key(
|
||||
'dish',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
dish_id=str(dish_id),
|
||||
),
|
||||
dish,
|
||||
self.bg_tasks,
|
||||
)
|
||||
return dish
|
||||
|
||||
async def update_dish(
|
||||
self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase
|
||||
) -> Dish | None:
|
||||
dish_db = Dish_db(**dish_data.model_dump())
|
||||
data = await self.dish_repo.update_dish_item(dish_id, dish_db)
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
dish = await self._convert_dish_to_dict(data)
|
||||
|
||||
await self.cache.set(
|
||||
self.key(
|
||||
'dish',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
dish_id=str(dish_id),
|
||||
),
|
||||
dish,
|
||||
self.bg_tasks,
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
|
||||
return dish
|
||||
|
||||
async def del_dish(self, menu_id: UUID, dish_id: UUID) -> None:
|
||||
await self.dish_repo.delete_dish_item(
|
||||
dish_id,
|
||||
)
|
||||
await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
112
fastfood/service/menu.py
Normal file
112
fastfood/service/menu.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from uuid import UUID
|
||||
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
|
||||
from fastfood.dbase import get_async_redis_client
|
||||
from fastfood.repository.menu import MenuRepository
|
||||
from fastfood.repository.redis import RedisRepository, get_key
|
||||
from fastfood.schemas import MenuBase, MenuRead
|
||||
|
||||
|
||||
class MenuService:
|
||||
def __init__(
|
||||
self,
|
||||
menu_repo: MenuRepository = Depends(),
|
||||
redis_client: redis.Redis = Depends(get_async_redis_client),
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> None:
|
||||
self.menu_repo = menu_repo
|
||||
self.cache = RedisRepository(redis_client)
|
||||
self.key = get_key
|
||||
self.bg_tasks = background_tasks
|
||||
|
||||
async def read_menus(self) -> list[MenuRead]:
|
||||
cached_menus = await self.cache.get(self.key('menus'))
|
||||
if cached_menus is not None:
|
||||
return cached_menus
|
||||
|
||||
data = await self.menu_repo.get_menus()
|
||||
menus = []
|
||||
for r in data:
|
||||
menu = r.__dict__
|
||||
menu = {k: v for k, v in menu.items() if not k.startswith('_')}
|
||||
dishes_conter = 0
|
||||
for sub in r.submenus:
|
||||
dishes_conter += len(sub.dishes)
|
||||
|
||||
menu['submenus_count'] = len(menu.pop('submenus'))
|
||||
menu['dishes_count'] = dishes_conter
|
||||
menu = MenuRead(**menu)
|
||||
menus.append(menu)
|
||||
|
||||
await self.cache.set(self.key('menus'), menus, self.bg_tasks)
|
||||
return menus
|
||||
|
||||
async def create_menu(self, menu_data: MenuBase) -> MenuRead:
|
||||
data = await self.menu_repo.create_menu_item(menu_data)
|
||||
menu = data.__dict__
|
||||
menu = {k: v for k, v in menu.items() if not k.startswith('_')}
|
||||
dishes_conter = 0
|
||||
|
||||
for sub in data.submenus:
|
||||
dishes_conter += len(sub.dishes)
|
||||
menu['submenus_count'] = len(menu.pop('submenus'))
|
||||
menu['dishes_count'] = dishes_conter
|
||||
await self.cache.set(
|
||||
key=get_key('menu', menu_id=str(menu.get('id'))),
|
||||
value=menu,
|
||||
bg_task=self.bg_tasks,
|
||||
)
|
||||
menu = MenuRead(**menu)
|
||||
await self.cache.set(
|
||||
self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu.id), bg_task=self.bg_tasks)
|
||||
return menu
|
||||
|
||||
async def read_menu(self, menu_id: UUID) -> MenuRead | None:
|
||||
cached_menu = await self.cache.get(self.key('menu', menu_id=str(menu_id)))
|
||||
if cached_menu is not None:
|
||||
return cached_menu
|
||||
|
||||
data = await self.menu_repo.get_menu_item(menu_id)
|
||||
if data is None:
|
||||
return None
|
||||
menu = data.__dict__
|
||||
menu = {k: v for k, v in menu.items() if not k.startswith('_')}
|
||||
dishes_conter = 0
|
||||
|
||||
for sub in data.submenus:
|
||||
dishes_conter += len(sub.dishes)
|
||||
menu['submenus_count'] = len(menu.pop('submenus'))
|
||||
menu['dishes_count'] = dishes_conter
|
||||
menu = MenuRead(**menu)
|
||||
await self.cache.set(
|
||||
self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks
|
||||
)
|
||||
return menu
|
||||
|
||||
async def update_menu(self, menu_id: UUID, menu_data) -> MenuRead | None:
|
||||
data = await self.menu_repo.update_menu_item(menu_id, menu_data)
|
||||
if data is None:
|
||||
return None
|
||||
menu = data.__dict__
|
||||
menu = {k: v for k, v in menu.items() if not k.startswith('_')}
|
||||
dishes_conter = 0
|
||||
|
||||
for sub in data.submenus:
|
||||
dishes_conter += len(sub.dishes)
|
||||
menu['submenus_count'] = len(menu.pop('submenus'))
|
||||
menu['dishes_count'] = dishes_conter
|
||||
menu = MenuRead(**menu)
|
||||
await self.cache.set(
|
||||
self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
return menu
|
||||
|
||||
async def del_menu(self, menu_id: UUID) -> None:
|
||||
await self.menu_repo.delete_menu_item(menu_id)
|
||||
await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
121
fastfood/service/submenu.py
Normal file
121
fastfood/service/submenu.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from uuid import UUID
|
||||
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
|
||||
from fastfood.dbase import get_async_redis_client
|
||||
from fastfood.repository.redis import RedisRepository, get_key
|
||||
from fastfood.repository.submenu import SubMenuRepository
|
||||
from fastfood.schemas import MenuBase, SubMenuRead
|
||||
|
||||
|
||||
class SubmenuService:
|
||||
def __init__(
|
||||
self,
|
||||
submenu_repo: SubMenuRepository = Depends(),
|
||||
redis_client: redis.Redis = Depends(get_async_redis_client),
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> None:
|
||||
|
||||
self.submenu_repo = submenu_repo
|
||||
self.cache = RedisRepository(redis_client)
|
||||
self.bg_tasks = background_tasks
|
||||
self.key = get_key
|
||||
|
||||
async def read_submenus(self, menu_id: UUID) -> list[SubMenuRead]:
|
||||
cached_submenus = await self.cache.get(
|
||||
self.key('submenus', menu_id=str(menu_id))
|
||||
)
|
||||
if cached_submenus is not None:
|
||||
return cached_submenus
|
||||
|
||||
data = await self.submenu_repo.get_submenus(menu_id=menu_id)
|
||||
submenus = []
|
||||
for r in data:
|
||||
submenu = r.__dict__
|
||||
subq = await self.submenu_repo.get_submenu_item(r.id)
|
||||
if subq is not None:
|
||||
submenu['dishes_count'] = len(subq.dishes)
|
||||
submenu = SubMenuRead(**submenu)
|
||||
submenus.append(submenu)
|
||||
|
||||
await self.cache.set(
|
||||
self.key('submenus', menu_id=str(menu_id)), submenus, self.bg_tasks
|
||||
)
|
||||
return submenus
|
||||
|
||||
async def create_submenu(
|
||||
self, menu_id: UUID, submenu_data: MenuBase
|
||||
) -> SubMenuRead:
|
||||
data = await self.submenu_repo.create_submenu_item(
|
||||
menu_id,
|
||||
submenu_data,
|
||||
)
|
||||
submenu = data.__dict__
|
||||
submenu = {k: v for k, v in submenu.items() if not k.startswith('_')}
|
||||
submenu['dishes_count'] = len(submenu.pop('dishes'))
|
||||
submenu = SubMenuRead(**submenu)
|
||||
await self.cache.set(
|
||||
self.key('submenu', menu_id=str(menu_id)), submenu, self.bg_tasks
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
|
||||
return submenu
|
||||
|
||||
async def read_menu(self, menu_id: UUID, submenu_id: UUID) -> SubMenuRead | None:
|
||||
cached_submenu = await self.cache.get(
|
||||
self.key(
|
||||
'submenu',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
)
|
||||
)
|
||||
if cached_submenu is not None:
|
||||
return cached_submenu
|
||||
|
||||
data = await self.submenu_repo.get_submenu_item(submenu_id)
|
||||
if data is None:
|
||||
return None
|
||||
submenu = data.__dict__
|
||||
submenu = {k: v for k, v in submenu.items() if not k.startswith('_')}
|
||||
submenu['dishes_count'] = len(submenu.pop('dishes'))
|
||||
menu = SubMenuRead(**submenu)
|
||||
await self.cache.set(
|
||||
self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)),
|
||||
submenu,
|
||||
self.bg_tasks,
|
||||
)
|
||||
return menu
|
||||
|
||||
async def update_submenu(
|
||||
self, menu_id: UUID, submenu_id: UUID, submenu_data: MenuBase
|
||||
) -> SubMenuRead | None:
|
||||
data = await self.submenu_repo.update_submenu_item(submenu_id, submenu_data)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
submenu = data.__dict__
|
||||
submenu = {k: v for k, v in submenu.items() if not k.startswith('_')}
|
||||
submenu['dishes_count'] = len(submenu.pop('dishes'))
|
||||
submenu = SubMenuRead(**submenu)
|
||||
|
||||
await self.cache.set(
|
||||
self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)),
|
||||
submenu,
|
||||
self.bg_tasks,
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
||||
|
||||
return submenu
|
||||
|
||||
async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> None:
|
||||
await self.submenu_repo.delete_submenu_item(submenu_id)
|
||||
await self.cache.delete(
|
||||
key=self.key(
|
||||
'submenu',
|
||||
menu_id=str(menu_id),
|
||||
submenu_id=str(submenu_id),
|
||||
),
|
||||
bg_task=self.bg_tasks,
|
||||
)
|
||||
await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks)
|
81
fastfood/service/summary.py
Normal file
81
fastfood/service/summary.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import redis.asyncio as redis # type: ignore
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
|
||||
from fastfood.dbase import get_async_redis_client
|
||||
from fastfood.repository.redis import RedisRepository, get_key
|
||||
from fastfood.repository.summary import SummaryRepository
|
||||
from fastfood.schemas import DishBase, MenuSummary, SubMenuSummary
|
||||
|
||||
|
||||
class SummaryService:
|
||||
def __init__(
|
||||
self,
|
||||
sum_repo: SummaryRepository = Depends(),
|
||||
redis_client: redis.Redis = Depends(get_async_redis_client),
|
||||
background_tasks: BackgroundTasks = None,
|
||||
) -> None:
|
||||
self.sum_repo = sum_repo
|
||||
self.cache = RedisRepository(redis_client)
|
||||
self.key = get_key
|
||||
self.bg_tasks = background_tasks
|
||||
|
||||
async def read_data(self) -> list[MenuSummary]:
|
||||
|
||||
result = []
|
||||
|
||||
async def dump_to_schema(
|
||||
schema, obj
|
||||
) -> MenuSummary | SubMenuSummary | DishBase:
|
||||
"""Функция преобразует объект SQLAlchemy к Pydantic модели
|
||||
|
||||
Входящие параметры
|
||||
schema: Pydantic модель
|
||||
obj: ORM объект
|
||||
|
||||
Возвращаемые данные
|
||||
schema: MenuSummary | SubMenuSummary | DishBase
|
||||
"""
|
||||
obj = obj.__dict__
|
||||
obj = {k: v for k, v in obj.items() if not k.startswith('_')}
|
||||
|
||||
if 'price' in obj.keys():
|
||||
discont = await self.cache.get(f"DISCONT:{str(obj.get('id'))}")
|
||||
|
||||
if discont is not None:
|
||||
try:
|
||||
discont = float(discont)
|
||||
except Exception:
|
||||
discont = 0.0
|
||||
obj['price'] = round(
|
||||
obj['price'] - (obj['price'] * discont / 100), 2
|
||||
)
|
||||
obj['price'] = str(obj['price'])
|
||||
|
||||
return schema(**obj)
|
||||
|
||||
cached_data = await self.cache.get(self.key('summary'))
|
||||
|
||||
if cached_data is not None:
|
||||
return cached_data
|
||||
|
||||
data = await self.sum_repo.get_data()
|
||||
|
||||
for menu in data:
|
||||
menus_res = await dump_to_schema(MenuSummary, menu)
|
||||
menus_res.submenus = []
|
||||
|
||||
for sub in menu.submenus:
|
||||
sub_res = await dump_to_schema(SubMenuSummary, sub)
|
||||
sub_res.dishes = []
|
||||
|
||||
for dish in sub.dishes:
|
||||
dish_res = await dump_to_schema(DishBase, dish)
|
||||
sub_res.dishes.append(dish_res)
|
||||
|
||||
menus_res.submenus.append(sub_res)
|
||||
|
||||
result.append(menus_res)
|
||||
|
||||
await self.cache.set(self.key('summary'), data, self.bg_tasks)
|
||||
|
||||
return result
|
56
manage.py
56
manage.py
@@ -1,29 +1,59 @@
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
|
||||
import uvicorn
|
||||
|
||||
from fastfood.crud import create_db_and_tables
|
||||
from fastfood.repository import create_db_and_tables
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
def run_app():
|
||||
def start_celery_worker() -> None:
|
||||
Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'worker', '--loglevel=info'])
|
||||
|
||||
|
||||
def start_celery_beat() -> None:
|
||||
Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'beat', '--loglevel=info'])
|
||||
|
||||
|
||||
celery_worker_process = multiprocessing.Process(target=start_celery_worker)
|
||||
celery_beat_process = multiprocessing.Process(target=start_celery_beat)
|
||||
|
||||
|
||||
async def run_app() -> None:
|
||||
"""
|
||||
Запуск
|
||||
Запуск FastAPI
|
||||
"""
|
||||
uvicorn.run(
|
||||
app="fastfood.app:create_app",
|
||||
app='fastfood.app:create_app',
|
||||
host='0.0.0.0',
|
||||
port=8000,
|
||||
reload=True,
|
||||
factory=True,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "filldb" in sys.argv:
|
||||
"""Наполнение БД демонстрационными данными"""
|
||||
pass
|
||||
async def recreate() -> None:
|
||||
"""Удаление и создание таблиц в базе данных для тестирования"""
|
||||
await create_db_and_tables()
|
||||
|
||||
if "--run-server" in sys.argv:
|
||||
async def create():
|
||||
await create_db_and_tables()
|
||||
asyncio.run(create())
|
||||
run_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if '--run-docker-server' in sys.argv:
|
||||
"""Запуск FastAPI в докере. Celery запускается в отдельном контейнере"""
|
||||
loop.run_until_complete(recreate())
|
||||
loop.run_until_complete(run_app())
|
||||
|
||||
if '--run-local-server' in sys.argv:
|
||||
"""Локальный запуск FastAPI с запуском Celery в отдельных процессах"""
|
||||
celery_worker_process.start()
|
||||
celery_beat_process.start()
|
||||
|
||||
loop.run_until_complete(recreate())
|
||||
loop.run_until_complete(run_app())
|
||||
|
||||
celery_beat_process.kill()
|
||||
celery_worker_process.kill()
|
||||
|
1203
openapi.json
Normal file
1203
openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
1386
poetry.lock
generated
1386
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
3890
postman_scripts/menu app.postman_collection.json
Normal file
3890
postman_scripts/menu app.postman_collection.json
Normal file
File diff suppressed because it is too large
Load Diff
93
postman_scripts/menu app.postman_environment.json
Normal file
93
postman_scripts/menu app.postman_environment.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"id": "7e7cd612-7f40-491b-8bd6-ba2322a3d0d7",
|
||||
"name": "menu app",
|
||||
"values": [
|
||||
{
|
||||
"key": "LOCAL_URL",
|
||||
"value": "127.0.0.1:8000",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "api_test_menu_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "api_test_submenu_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "api_test_dish_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_menu_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_menu_title",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_menu_description",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_submenu_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_submenu_title",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_submenu_description",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_dish_id",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_dish_title",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_dish_description",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "target_dish_price",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment",
|
||||
"_postman_exported_at": "2023-01-12T16:22:10.333Z",
|
||||
"_postman_exported_using": "Postman/10.6.7"
|
||||
}
|
@@ -12,10 +12,21 @@ fastapi = "^0.109.0"
|
||||
uvicorn = "^0.26.0"
|
||||
asyncpg = "^0.29.0"
|
||||
pydantic-settings = "^2.1.0"
|
||||
email-validator = "^2.1.0.post1"
|
||||
pytest-asyncio = "^0.23.3"
|
||||
redis = "^4.6.0"
|
||||
types-redis = "^4.6.0.3"
|
||||
mypy = "^1.4.1"
|
||||
celery = "^5.3.6"
|
||||
openpyxl = "^3.1.2"
|
||||
gspread = "^6.0.1"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.4"
|
||||
pytest-cov = "^4.1.0"
|
||||
httpx = "^0.26.0"
|
||||
pre-commit = "^3.6.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
@@ -23,3 +34,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ". fastfood"
|
||||
filterwarnings = [
|
||||
"ignore::UserWarning",
|
||||
"ignore::DeprecationWarning"
|
||||
]
|
||||
|
57
tests/conftest.py
Normal file
57
tests/conftest.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import FastAPI
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from fastfood.app import create_app
|
||||
from fastfood.config import settings
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Base
|
||||
|
||||
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
|
||||
async_session_maker = async_sessionmaker(
|
||||
async_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def event_loop():
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope='session', autouse=True)
|
||||
async def db_init(event_loop):
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope='session', autouse=True)
|
||||
async def client(event_loop) -> AsyncGenerator[AsyncClient, None]:
|
||||
app: FastAPI = create_app()
|
||||
app.dependency_overrides[get_async_session] = get_test_session
|
||||
|
||||
async with AsyncClient(
|
||||
app=app,
|
||||
base_url='http://localhost:8000',
|
||||
) as async_client:
|
||||
yield async_client
|
196
tests/repository.py
Normal file
196
tests/repository.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from httpx import AsyncClient, Response
|
||||
|
||||
from .urls import reverse
|
||||
|
||||
|
||||
class Repository:
|
||||
class Menu:
|
||||
@staticmethod
|
||||
async def read_all(ac: AsyncClient) -> tuple[int, dict]:
|
||||
"""чтение всех меню"""
|
||||
|
||||
response: Response = await ac.get(reverse('get_menus'))
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def get(ac: AsyncClient, data: dict) -> tuple[int, dict]:
|
||||
"""Получение меню по id"""
|
||||
response: Response = await ac.get(
|
||||
reverse('get_menu', menu_id=data.get('id'))
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def write(ac: AsyncClient, data: dict) -> tuple[int, dict]:
|
||||
"""создания меню"""
|
||||
response: Response = await ac.post(reverse('add_menu'), json=data)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def update(ac: AsyncClient, data: dict) -> tuple[int, dict]:
|
||||
"""Обновление меню по id"""
|
||||
response: Response = await ac.patch(
|
||||
reverse('update_menu', menu_id=data.get('id')),
|
||||
json=data,
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def delete(ac: AsyncClient, data: dict) -> int:
|
||||
"""Удаление меню по id"""
|
||||
response: Response = await ac.delete(
|
||||
reverse('delete_menu', menu_id=data.get('id')),
|
||||
)
|
||||
return response.status_code
|
||||
|
||||
class Submenu:
|
||||
@staticmethod
|
||||
async def read_all(ac: AsyncClient, menu: dict) -> tuple[int, dict]:
|
||||
"""чтение всех меню"""
|
||||
response: Response = await ac.get(
|
||||
reverse('get_submenus', menu_id=menu.get('id')),
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def get(
|
||||
ac: AsyncClient,
|
||||
menu: dict,
|
||||
submenu: dict,
|
||||
) -> tuple[int, dict]:
|
||||
"""Получение меню по id"""
|
||||
response: Response = await ac.get(
|
||||
reverse(
|
||||
'get_submenu',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
),
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def write(
|
||||
ac: AsyncClient,
|
||||
menu: dict,
|
||||
submenu: dict,
|
||||
) -> tuple[int, dict]:
|
||||
"""создания меню"""
|
||||
response: Response = await ac.post(
|
||||
reverse('create_submenu_item', menu_id=menu.get('id')),
|
||||
json=submenu,
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def update(
|
||||
ac: AsyncClient, menu: dict, submenu: dict
|
||||
) -> tuple[int, dict]:
|
||||
"""Обновление меню по id"""
|
||||
response: Response = await ac.patch(
|
||||
reverse(
|
||||
'update_submenu',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
),
|
||||
json=submenu,
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def delete(ac: AsyncClient, menu: dict, submenu: dict) -> int:
|
||||
"""Удаление меню по id"""
|
||||
response: Response = await ac.delete(
|
||||
reverse(
|
||||
'delete_submenu',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
),
|
||||
)
|
||||
return response.status_code
|
||||
|
||||
class Dish:
|
||||
@staticmethod
|
||||
async def read_all(
|
||||
ac: AsyncClient, menu: dict, submenu: dict
|
||||
) -> tuple[int, dict]:
|
||||
"""чтение всех блюд"""
|
||||
response: Response = await ac.get(
|
||||
reverse(
|
||||
'get_dishes',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
),
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def get(
|
||||
ac: AsyncClient, menu: dict, submenu: dict, dish: dict
|
||||
) -> tuple[int, dict]:
|
||||
"""Получение блюда по id"""
|
||||
response: Response = await ac.get(
|
||||
reverse(
|
||||
'get_dish',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
dish_id=dish.get('id'),
|
||||
),
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def write(
|
||||
ac: AsyncClient, menu: dict, submenu: dict, dish: dict
|
||||
) -> tuple[int, dict]:
|
||||
"""создания блюда"""
|
||||
response: Response = await ac.post(
|
||||
reverse(
|
||||
'create_dish',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
),
|
||||
json=dish,
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def update(
|
||||
ac: AsyncClient, menu: dict, submenu: dict, dish: dict
|
||||
) -> tuple[int, dict]:
|
||||
"""Обновление блюда по id"""
|
||||
response: Response = await ac.patch(
|
||||
reverse(
|
||||
'update_dish',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
dish_id=dish.get('id'),
|
||||
),
|
||||
json=dish,
|
||||
)
|
||||
return response.status_code, response.json()
|
||||
|
||||
@staticmethod
|
||||
async def delete(
|
||||
ac: AsyncClient,
|
||||
menu: dict,
|
||||
submenu: dict,
|
||||
dish: dict,
|
||||
) -> int:
|
||||
"""Удаление блюда по id"""
|
||||
response: Response = await ac.delete(
|
||||
reverse(
|
||||
'delete_dish',
|
||||
menu_id=menu.get('id'),
|
||||
submenu_id=submenu.get('id'),
|
||||
dish_id=dish.get('id'),
|
||||
),
|
||||
)
|
||||
return response.status_code
|
||||
|
||||
class Summary:
|
||||
@staticmethod
|
||||
async def read_summary(ac: AsyncClient) -> tuple[int, dict]:
|
||||
"""чтение summary"""
|
||||
|
||||
response: Response = await ac.get(reverse('get_summary'))
|
||||
return response.status_code, response.json()
|
174
tests/test_dish.py
Normal file
174
tests/test_dish.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .repository import Repository as Repo
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dishes_get_all(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Проверяем все блюда в подменю
|
||||
code, rspn = await Repo.Dish.read_all(client, menu, submenu)
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
# Добавляем блюдо
|
||||
dish = {
|
||||
'title': 'dish',
|
||||
'description': 'some dish',
|
||||
'price': '12.5',
|
||||
'parent_submenu': submenu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
assert code == 201
|
||||
dish.update(rspn)
|
||||
|
||||
code, upd_rspn = await Repo.Dish.read_all(client, menu, submenu)
|
||||
|
||||
assert code == 200
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Dish.delete(client, menu, submenu, dish)
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dishes_add(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Добавляем блюдо
|
||||
dish = {
|
||||
'title': 'dish',
|
||||
'description': 'some dish',
|
||||
'price': '12.5',
|
||||
'parent_submenu': submenu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
assert code == 201
|
||||
dish.update(rspn)
|
||||
|
||||
# Получаем блюдо
|
||||
code, rspn = await Repo.Dish.get(client, menu, submenu, dish)
|
||||
assert code == 200
|
||||
assert rspn['title'] == dish['title']
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Dish.delete(client, menu, submenu, dish)
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dishes_update(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Добавляем блюдо
|
||||
dish = {
|
||||
'title': 'dish',
|
||||
'description': 'some dish',
|
||||
'price': '12.5',
|
||||
'parent_submenu': submenu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
dish.update(rspn)
|
||||
|
||||
# Обновляем блюдо и проверяем
|
||||
dish['title'] = 'updated_dish'
|
||||
code, rspn = await Repo.Dish.update(client, menu, submenu, dish)
|
||||
assert code == 200
|
||||
assert dish['title'] == rspn['title']
|
||||
dish.update(rspn)
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Dish.delete(client, menu, submenu, dish)
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dishes_delete(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Добавляем блюдо
|
||||
dish = {
|
||||
'title': 'dish',
|
||||
'description': 'some dish',
|
||||
'price': '12.5',
|
||||
'parent_submenu': submenu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
dish.update(rspn)
|
||||
|
||||
# Удаляем подменю
|
||||
code = await Repo.Dish.delete(client, menu, submenu, dish)
|
||||
assert code == 200
|
||||
|
||||
# Проверяем удаленное блюдо
|
||||
code, rspn = await Repo.Dish.get(client, menu, submenu, dish)
|
||||
assert code == 404
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
80
tests/test_menu.py
Normal file
80
tests/test_menu.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .repository import Repository as Repo
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_empty(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
code, rspn = await Repo.Menu.read_all(client)
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_add(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
data = {'title': 'Menu', 'description': None}
|
||||
code, rspn = await Repo.Menu.write(client, data)
|
||||
assert code == 201
|
||||
assert rspn['title'] == 'Menu'
|
||||
assert rspn['description'] is None
|
||||
await Repo.Menu.delete(client, rspn)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_get(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
data = {'title': 'Menu', 'description': None}
|
||||
code, rspn = await Repo.Menu.write(client, data)
|
||||
code, menu = await Repo.Menu.get(client, {'id': rspn.get('id')})
|
||||
assert code == 200
|
||||
assert menu['title'] == rspn['title']
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_update(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
data = {'title': 'Menu', 'description': None}
|
||||
code, rspn = await Repo.Menu.write(client, data)
|
||||
|
||||
upd_data = {
|
||||
'id': rspn.get('id'),
|
||||
'title': 'upd Menu',
|
||||
'description': '',
|
||||
}
|
||||
code, upd_rspn = await Repo.Menu.update(client, upd_data)
|
||||
assert code == 200
|
||||
assert upd_rspn['title'] == 'upd Menu'
|
||||
await Repo.Menu.delete(client, upd_rspn)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_delete(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
data = {'title': 'Menu', 'description': None}
|
||||
code, rspn = await Repo.Menu.write(client, data)
|
||||
|
||||
code = await Repo.Menu.delete(client, rspn)
|
||||
assert code == 200
|
||||
|
||||
code, rspn = await Repo.Menu.get(client, {'id': rspn.get('id')})
|
||||
assert code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_menu_crud_get_all(client: AsyncClient) -> None:
|
||||
"""Тестирование функций меню"""
|
||||
code, rspn = await Repo.Menu.read_all(client)
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
data = {'title': 'Menu', 'description': None}
|
||||
code, rspn = await Repo.Menu.write(client, data)
|
||||
|
||||
code, upd_rspn = await Repo.Menu.read_all(client)
|
||||
assert code == 200
|
||||
assert upd_rspn == [rspn]
|
||||
await Repo.Menu.delete(client, rspn)
|
238
tests/test_postman.py
Normal file
238
tests/test_postman.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .repository import Repository as Repo
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def session_data() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_01(client: AsyncClient, session_data: dict):
|
||||
"""Проверяет создание меню"""
|
||||
menu = {'title': 'Menu', 'description': 'some_menu_desc'}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
|
||||
assert code == 201
|
||||
code, rspn = await Repo.Menu.get(client, rspn)
|
||||
session_data['target_menu_id'] = rspn.get('id')
|
||||
session_data['target_menu_title'] = rspn.get('title')
|
||||
session_data['target_menu_description'] = rspn.get('description')
|
||||
|
||||
assert code == 200
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert 'submenus_count' in rspn
|
||||
assert 'dishes_count' in rspn
|
||||
assert rspn['title'] == menu.get('title')
|
||||
assert rspn.get('description') == menu.get('description')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_02(client: AsyncClient, session_data: dict):
|
||||
submenu = {'title': 'Submenu', 'description': 'submenu_descr'}
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
|
||||
assert code == 201
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert 'dishes_count' in rspn
|
||||
assert rspn['title'] == submenu.get('title')
|
||||
assert rspn.get('description') == submenu.get('description')
|
||||
|
||||
session_data['target_submenu_id'] = rspn.get('id')
|
||||
session_data['target_submenu_title'] = rspn.get('title')
|
||||
session_data['target_submenu_description'] = rspn.get('description')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_03_dish1(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
submenu = {
|
||||
'id': session_data.get('target_submenu_id'),
|
||||
'title': session_data.get('target_submenu_title'),
|
||||
'description': session_data.get('target_submenu_description'),
|
||||
}
|
||||
dish = {'title': 'dish_1', 'description': 'dish 1 descr', 'price': '12.5'}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
|
||||
assert code == 201
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert 'price' in rspn
|
||||
assert rspn['title'] == dish.get('title')
|
||||
assert rspn.get('description') == dish.get('description')
|
||||
assert rspn.get('price') == dish.get('price')
|
||||
|
||||
session_data['target_dish_id'] = rspn.get('id')
|
||||
session_data['target_dish_title'] = rspn.get('title')
|
||||
session_data['target_dish_description'] = rspn.get('description')
|
||||
session_data['target_dish_price'] = rspn.get('price')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_04_dish2(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
submenu = {
|
||||
'id': session_data.get('target_submenu_id'),
|
||||
'title': session_data.get('target_submenu_title'),
|
||||
'description': session_data.get('target_submenu_description'),
|
||||
}
|
||||
dish = {'title': 'dish_2', 'description': 'dish 2 descr', 'price': '13.5'}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
|
||||
assert code == 201
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert 'price' in rspn
|
||||
assert rspn['title'] == dish.get('title')
|
||||
assert rspn.get('description') == dish.get('description')
|
||||
assert rspn.get('price') == dish.get('price')
|
||||
|
||||
session_data['target_dish1_id'] = rspn.get('id')
|
||||
session_data['target_dish1_title'] = rspn.get('title')
|
||||
session_data['target_dish1_description'] = rspn.get('description')
|
||||
session_data['target_dish1_price'] = rspn.get('price')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_05_check_menu(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
code, rspn = await Repo.Menu.get(client, menu)
|
||||
|
||||
assert code == 200
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert rspn.get('submenus_count') == 1
|
||||
assert rspn.get('dishes_count') == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_06_check_submenu(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
submenu = {
|
||||
'id': session_data.get('target_submenu_id'),
|
||||
'title': session_data.get('target_submenu_title'),
|
||||
'description': session_data.get('target_submenu_description'),
|
||||
}
|
||||
code, rspn = await Repo.Submenu.get(client, menu, submenu)
|
||||
|
||||
assert code == 200
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert rspn.get('dishes_count') == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_07_del_submenu(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
submenu = {
|
||||
'id': session_data.get('target_submenu_id'),
|
||||
'title': session_data.get('target_submenu_title'),
|
||||
'description': session_data.get('target_submenu_description'),
|
||||
}
|
||||
code = await Repo.Submenu.delete(client, menu, submenu)
|
||||
|
||||
assert code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_07_check_submenus(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
code, rspn = await Repo.Submenu.read_all(client, menu)
|
||||
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_08_check_dishes(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
submenu = {
|
||||
'id': session_data.get('target_submenu_id'),
|
||||
'title': session_data.get('target_submenu_title'),
|
||||
'description': session_data.get('target_submenu_description'),
|
||||
}
|
||||
code, rspn = await Repo.Dish.read_all(client, menu, submenu)
|
||||
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_09_check_menu(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
code, rspn = await Repo.Menu.get(client, menu)
|
||||
|
||||
assert code == 200
|
||||
assert 'id' in rspn
|
||||
assert 'title' in rspn
|
||||
assert 'description' in rspn
|
||||
assert rspn.get('submenus_count') == 0
|
||||
assert rspn.get('dishes_count') == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_10_del_menu(client: AsyncClient, session_data: dict):
|
||||
menu = {
|
||||
'id': session_data.get('target_menu_id'),
|
||||
'title': session_data.get('target_menu_title'),
|
||||
'description': session_data.get('target_menu_description'),
|
||||
}
|
||||
code = await Repo.Menu.delete(client, menu)
|
||||
|
||||
assert code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_11_check_menus(client: AsyncClient, session_data: dict):
|
||||
code, rspn = await Repo.Menu.read_all(client)
|
||||
|
||||
assert code == 200
|
||||
assert rspn == []
|
113
tests/test_submenu.py
Normal file
113
tests/test_submenu.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import pytest
|
||||
|
||||
from .repository import Repository as Repo
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submenus_get_all(client) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {'title': 'Menu', 'description': 'main menu'}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
assert code == 201
|
||||
menu.update(rspn)
|
||||
|
||||
# Проверяем наличие подменю
|
||||
code, rspn = await Repo.Submenu.read_all(client, menu)
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Проверяем наличие подменю
|
||||
code, upd_rspn = await Repo.Submenu.read_all(client, menu)
|
||||
assert code == 200
|
||||
assert upd_rspn == [rspn]
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submenus_add(client) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {'title': 'Menu', 'description': 'main menu'}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
assert code == 201
|
||||
submenu.update(rspn)
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submenus_update(client) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {'title': 'Menu', 'description': 'main menu'}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Обновляем подменю и проверяем
|
||||
submenu['title'] = 'updated_submenu'
|
||||
code, rspn = await Repo.Submenu.update(client, menu, submenu)
|
||||
assert code == 200
|
||||
assert submenu['title'] == rspn['title']
|
||||
submenu.update(rspn)
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Submenu.delete(client, menu, submenu)
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submenus_delete(client) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {'title': 'Menu', 'description': 'main menu'}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
|
||||
# Удаляем подменю
|
||||
code = await Repo.Submenu.delete(client, menu, submenu)
|
||||
assert code == 200
|
||||
|
||||
# Проверяем удаленное подменю
|
||||
code, rspn = await Repo.Submenu.get(client, menu, submenu)
|
||||
assert code == 404
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Menu.delete(client, menu)
|
113
tests/test_summary.py
Normal file
113
tests/test_summary.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from .repository import Repository as Repo
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summary_with_menu(client: AsyncClient) -> None:
|
||||
# Проверяем пустое summary
|
||||
code, rspn = await Repo.Summary.read_summary(client)
|
||||
assert code == 200
|
||||
assert rspn == []
|
||||
|
||||
# Создаем меню и проверяем ответ
|
||||
menu = {'title': 'Menu', 'description': 'main menu', 'submenus': []}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
# Удалим ненужные ключи, тк в модели они не используются
|
||||
del menu['submenus_count']
|
||||
del menu['dishes_count']
|
||||
|
||||
# Проверяем summary c меню
|
||||
code, rspn = await Repo.Summary.read_summary(client)
|
||||
assert code == 200
|
||||
assert rspn == [menu]
|
||||
|
||||
# удаляем сопутствующее
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summary_with_submenus(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu: dict[str, str | list | float] = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
'submenus': [],
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
del menu['submenus_count']
|
||||
del menu['dishes_count']
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu: dict[str, str | list | float] = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
'dishes': list(),
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
del submenu['dishes_count']
|
||||
del submenu['parent_menu']
|
||||
|
||||
menu['submenus'] = [submenu]
|
||||
|
||||
# Получаем блюдо
|
||||
code, rspn = await Repo.Summary.read_summary(client)
|
||||
assert code == 200
|
||||
assert rspn == [menu]
|
||||
|
||||
await Repo.Menu.delete(client, menu)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summary_with_dishes(client: AsyncClient) -> None:
|
||||
# Создаем меню и проверяем ответ
|
||||
menu: dict[str, str | list | float] = {
|
||||
'title': 'Menu',
|
||||
'description': 'main menu',
|
||||
'submenus': [],
|
||||
}
|
||||
code, rspn = await Repo.Menu.write(client, menu)
|
||||
menu.update(rspn)
|
||||
|
||||
del menu['submenus_count']
|
||||
del menu['dishes_count']
|
||||
|
||||
# Создаем и проверяем подменю
|
||||
submenu: dict[str, str | list | float] = {
|
||||
'title': 'Submenu',
|
||||
'description': 'submenu',
|
||||
'parent_menu': menu['id'],
|
||||
'dishes': [],
|
||||
}
|
||||
code, rspn = await Repo.Submenu.write(client, menu, submenu)
|
||||
submenu.update(rspn)
|
||||
del submenu['dishes_count']
|
||||
del submenu['parent_menu']
|
||||
|
||||
# Добавляем блюдо
|
||||
dish = {
|
||||
'title': 'dish',
|
||||
'description': 'some dish',
|
||||
'price': '12.5',
|
||||
'parent_submenu': submenu['id'],
|
||||
}
|
||||
code, rspn = await Repo.Dish.write(client, menu, submenu, dish)
|
||||
dish.update(rspn)
|
||||
del dish['parent_submenu']
|
||||
del dish['id']
|
||||
|
||||
submenu['dishes'] = [dish]
|
||||
menu['submenus'] = [submenu]
|
||||
|
||||
code, rspn = await Repo.Summary.read_summary(client)
|
||||
assert code == 200
|
||||
assert rspn == [menu]
|
||||
|
||||
await Repo.Menu.delete(client, menu)
|
9
tests/urls.py
Normal file
9
tests/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastfood.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
def reverse(loc: str, **kwargs) -> str:
|
||||
|
||||
url = app.url_path_for(loc, **kwargs)
|
||||
return url
|
Reference in New Issue
Block a user