Compare commits
95 Commits
f09b5b57b2
...
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 |
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
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.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
|
117
README.md
117
README.md
@@ -2,91 +2,90 @@
|
|||||||
Fastapi веб приложение реализующее api для общепита.
|
Fastapi веб приложение реализующее api для общепита.
|
||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
Данный проект, это результат выполнения практического домашнего задания интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||||
|
|
||||||
### Техническое задание
|
## Техническое задание
|
||||||
Написать проект на FastAPI с использованием PostgreSQL в качестве БД. В проекте следует реализовать REST API по работе с меню ресторана, все CRUD операции. Для проверки задания, к презентаций будет приложена Postman коллекция с тестами. Задание выполнено, если все тесты проходят успешно.
|
### Спринт 4 - Многопроцессорность, асинхронность
|
||||||
Даны 3 сущности: Меню, Подменю, Блюдо.
|
В этом домашнем задании необходимо:
|
||||||
|
1.Переписать текущее FastAPI приложение на асинхронное выполнение
|
||||||
|
2.Добавить в проект фоновую задачу с помощью Celery + RabbitMQ.
|
||||||
|
3.Добавить эндпоинт (GET) для вывода всех меню со всеми связанными подменю и со всеми связанными блюдами.
|
||||||
|
4.Реализовать инвалидация кэша в background task (встроено в FastAPI)
|
||||||
|
5.* Обновление меню из google sheets раз в 15 сек.
|
||||||
|
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
|
||||||
|
|
||||||
Зависимости:
|
Фоновая задача: синхронизация Excel документа и БД.
|
||||||
- У меню есть подменю, которые к ней привязаны.
|
В проекте создаем папку admin. В эту папку кладем файл Menu.xlsx (будет прикреплен к ДЗ). Не забываем запушить в гит.
|
||||||
- У подменю есть блюда.
|
При внесении изменений в файл все изменения должны отображаться в БД. Периодичность обновления 15 сек. Удалять БД при каждом обновлении – нельзя.
|
||||||
|
|
||||||
Условия:
|
|
||||||
- Блюдо не может быть привязано напрямую к меню, минуя подменю.
|
|
||||||
- Блюдо не может находиться в 2-х подменю одновременно.
|
|
||||||
- Подменю не может находиться в 2-х меню одновременно.
|
|
||||||
- Если удалить меню, должны удалиться все подменю и блюда этого меню.
|
|
||||||
- Если удалить подменю, должны удалиться все блюда этого подменю.
|
|
||||||
- Цены блюд выводить с округлением до 2 знаков после запятой.
|
|
||||||
- Во время выдачи списка меню, для каждого меню добавлять кол-во подменю и блюд в этом меню.
|
|
||||||
- Во время выдачи списка подменю, для каждого подменю добавлять кол-во блюд в этом подменю.
|
|
||||||
- Во время запуска тестового сценария БД должна быть пуста.
|
|
||||||
|
|
||||||
В папке ./postman_scripts находятся фалы тестов Postman, для тестирования функционала проекта.
|
Требования:
|
||||||
|
●Данные меню, подменю, блюд для нового эндпоинта должны доставаться одним 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 блюда}
|
||||||
|
|
||||||
## Возможности
|
|
||||||
В проекте реализованы 3 сущности: Menu, SubMenu и Dish. Для каждого них реализованы 4 метода http запросов: GET, POST, PATCH и DELETE c помощью которых можно управлять данными.
|
|
||||||
Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish.
|
|
||||||
|
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
- postgresql Для работы сервиса необходима установленная СУБД. Должна быть создана база данных и пользователь с правами на нее.
|
- docker
|
||||||
- poetry - Система управления зависимостями в Python.
|
- docker-compose
|
||||||
|
|
||||||
Остальное добавится автоматически на этапе установки.
|
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
### Linux
|
|
||||||
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
|
|
||||||
|
|
||||||
Установите систему управления зависимостями
|
|
||||||
> `$ pip[x] install poetry`
|
|
||||||
|
|
||||||
Клонируйте репозиторий
|
Клонируйте репозиторий
|
||||||
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
||||||
|
|
||||||
Перейдите в каталог
|
Перейдите в каталог
|
||||||
|
|
||||||
> `$ cd fastfood`
|
> `$ cd fastfood`
|
||||||
|
|
||||||
> `$ poetry install --no-root`
|
Запуск/остановка образов:
|
||||||
|
|
||||||
Создастся виртуальное окружение и установятся зависимости
|
- Запуск FAstAPI приложения c локальным файлом для фоновой задачи
|
||||||
|
> `$ docker-compose -f compose_app.yml up`
|
||||||
|
|
||||||
Файл example.env является образцом файла .env, который необходимо создать перед запуском проекта.
|
- Запуск FAstAPI приложения c Google Sheets для фоновой задачи
|
||||||
В нем указанны переменные необходимые для подключения к БД.
|
> `$ docker-compose -f compose_google.yml up`
|
||||||
Созданим файл .env
|
(ЧИТАЙТЕ СООБЩЕНИЕ В ЧАТЕ)
|
||||||
|
|
||||||
>`$ cp ./example.env ./.env`
|
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
||||||
|
|
||||||
Далее отредактируйте .env файл в соответствии с Вашими данными подключения к БД
|
По завершении работы остановите контейнеры
|
||||||
|
> `$ docker-compose -f compose_app.yml down`
|
||||||
|
|
||||||
## Запуск
|
- Запуск тестов
|
||||||
Запуск проекта возможен в 2х режимах:
|
> `$ docker-compose -f compose_test.yml up`
|
||||||
- Запуск в режиме "prod" с ключем --run-server
|
|
||||||
Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются.
|
|
||||||
|
|
||||||
- Запуск в режиме "dev" c ключем --run-test-server
|
По завершении работы остановите контейнеры
|
||||||
В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей.
|
> `$ docker-compose -f compose_test.yml down`
|
||||||
|
|
||||||
|
|
||||||
Для запуска проекта сначала активируем виртуальное окружение
|
|
||||||
|
|
||||||
> `$ poetry shell`
|
|
||||||
|
|
||||||
и запускаем проект в соответстующем режиме
|
|
||||||
|
|
||||||
>`$ python[x] manage.py --ключ`
|
|
||||||
|
|
||||||
вместо этого, так же допускается и другой вариант запуска одной командой без предварительной активации окружения
|
|
||||||
|
|
||||||
>`$ poetry run python[x] manage.py --ключ`
|
|
||||||
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- Добавить миграции
|
- Написать тесты для кривых данных
|
||||||
- Провести рефакторинг, много дублирующего кода
|
|
||||||
- Много чего другого :)
|
- Много чего другого :)
|
||||||
|
|
||||||
## Авторы
|
## Авторы
|
||||||
@@ -94,5 +93,3 @@ Fastapi веб приложение реализующее api для общеп
|
|||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
Распространяется под [MIT лицензией](https://mit-license.org/).
|
Распространяется под [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,5 +0,0 @@
|
|||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASS=postgres
|
|
||||||
DB_NAME=postgres
|
|
@@ -1,87 +1,27 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from fastfood.routers.dish import router as dish_router
|
from fastfood.routers.dish import router as dish_router
|
||||||
from fastfood.routers.menu import router as menu_router
|
from fastfood.routers.menu import router as menu_router
|
||||||
from fastfood.routers.submenu import router as submenu_router
|
from fastfood.routers.submenu import router as submenu_router
|
||||||
|
from fastfood.routers.summary import router as summary_router
|
||||||
description = """
|
|
||||||
# 🔥🔥🔥Fastfood-API поможет тебе подкрепиться 🔥🔥🔥
|
|
||||||
|
|
||||||
### У нас есть Menu. Ты можеш выбрать блюда из кухни, которая тебе нравится
|
|
||||||
|
|
||||||
## Menu
|
|
||||||
|
|
||||||
Ты можешь **add menu**.
|
|
||||||
|
|
||||||
Ты можешь **read menu**.
|
|
||||||
|
|
||||||
Ты можешь **patch menu**.
|
|
||||||
|
|
||||||
Ты можешь **delete menu**.
|
|
||||||
|
|
||||||
### У нас есть в SubMenu, где ты сможешь найти
|
|
||||||
десерты/напитки/супчики/прочие вкусности
|
|
||||||
|
|
||||||
# SubMenu
|
|
||||||
|
|
||||||
Ты можешь **add submenu into menu**.
|
|
||||||
|
|
||||||
Ты можешь **read submenu**.
|
|
||||||
|
|
||||||
Ты можешь **patch submenu**.
|
|
||||||
|
|
||||||
Ты можешь **delete menu**.
|
|
||||||
|
|
||||||
### У нас есть в Dish, где ты сможешь найти блюдо по вкусу
|
|
||||||
|
|
||||||
# Dish
|
|
||||||
|
|
||||||
Ты можешь **add dish into submenu**.
|
|
||||||
|
|
||||||
Ты можешь **read dish**.
|
|
||||||
|
|
||||||
Ты можешь **patch dish**.
|
|
||||||
|
|
||||||
Ты можешь **delete dish**.
|
|
||||||
|
|
||||||
## Приятного аппетита
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
tags_metadata = [
|
def create_app() -> FastAPI:
|
||||||
{
|
|
||||||
"name": "menu",
|
|
||||||
"description": "Операции с меню.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "submenu",
|
|
||||||
"description": "Подменю и работа с ним",
|
|
||||||
},
|
|
||||||
{"name": "dish", "description": "Блюда и работа с ними"},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
"""
|
"""
|
||||||
Фабрика FastAPI.
|
Фабрика FastAPI.
|
||||||
"""
|
"""
|
||||||
app = FastAPI(
|
app = FastAPI()
|
||||||
title="Fastfood-API",
|
|
||||||
description=description,
|
|
||||||
version="0.0.1",
|
|
||||||
contact={
|
|
||||||
"name": "Sergey Vanyushkin",
|
|
||||||
"url": "http://pi3c.ru",
|
|
||||||
"email": "pi3c@yandex.ru",
|
|
||||||
},
|
|
||||||
license_info={
|
|
||||||
"name": "MIT license",
|
|
||||||
"url": "https://mit-license.org/",
|
|
||||||
},
|
|
||||||
openapi_tags=tags_metadata,
|
|
||||||
)
|
|
||||||
app.include_router(menu_router)
|
app.include_router(menu_router)
|
||||||
app.include_router(submenu_router)
|
app.include_router(submenu_router)
|
||||||
app.include_router(dish_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
|
return app
|
||||||
|
@@ -1,21 +1,39 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
DB_HOST: str = "localhost"
|
# Конфиг PostgreSql
|
||||||
DB_PORT: int = 5432
|
POSTGRES_HOST: str = ''
|
||||||
DB_USER: str = "postrges"
|
POSTGRES_PORT: int = 5432
|
||||||
DB_PASS: str = "postgres"
|
POSTGRES_DB: str = ''
|
||||||
DB_NAME: str = "postgres"
|
POSTGRES_PASSWORD: str = ''
|
||||||
|
POSTGRES_USER: str = ''
|
||||||
|
POSTGRES_DB_TEST: str = ''
|
||||||
|
# Конфиг Redis
|
||||||
|
REDIS_HOST: str = ''
|
||||||
|
REDIS_PORT: int = 6379
|
||||||
|
REDIS_DB: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def DATABASE_URL_asyncpg(self):
|
def DATABASE_URL_asyncpg(self) -> str:
|
||||||
"""
|
"""
|
||||||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
Возвращает строку подключения к БД необходимую для 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 (
|
return (
|
||||||
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}"
|
'postgresql+asyncpg://'
|
||||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||||
|
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -23,13 +41,43 @@ class Settings(BaseSettings):
|
|||||||
"""
|
"""
|
||||||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
Возвращает строку подключения к БД необходимую для 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 (
|
return (
|
||||||
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}"
|
'postgresql+asyncpg://'
|
||||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}_test"
|
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'
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=".env")
|
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()
|
settings = Settings()
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sqlalchemy import delete, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import models, schemas
|
|
||||||
|
|
||||||
|
|
||||||
class DishCrud:
|
|
||||||
@staticmethod
|
|
||||||
async def get_dishes(submenu_id: UUID, session: AsyncSession):
|
|
||||||
async with session:
|
|
||||||
query = select(models.Dish).where(models.Dish.parent_submenu == submenu_id)
|
|
||||||
dishes = await session.execute(query)
|
|
||||||
return dishes.scalars().all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def create_dish_item(
|
|
||||||
submenu_id: UUID,
|
|
||||||
dish: schemas.DishBase,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
new_dish = models.Dish(**dish.model_dump())
|
|
||||||
new_dish.parent_submenu = submenu_id
|
|
||||||
session.add(new_dish)
|
|
||||||
await session.flush()
|
|
||||||
await session.commit()
|
|
||||||
return new_dish
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_dish_item(
|
|
||||||
dish_id: UUID,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
query = select(models.Dish).where(models.Dish.id == dish_id)
|
|
||||||
submenu = await session.execute(query)
|
|
||||||
return submenu.scalars().one_or_none()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_dish_item(
|
|
||||||
dish_id: UUID,
|
|
||||||
dish: schemas.DishBase,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
query = (
|
|
||||||
update(models.Dish)
|
|
||||||
.where(models.Dish.id == dish_id)
|
|
||||||
.values(**dish.model_dump())
|
|
||||||
)
|
|
||||||
await session.execute(query)
|
|
||||||
await session.commit()
|
|
||||||
qr = select(models.Dish).where(models.Dish.id == dish_id)
|
|
||||||
updated_submenu = await session.execute(qr)
|
|
||||||
return updated_submenu.scalars().one()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete_dish_item(dish_id: UUID, session: AsyncSession):
|
|
||||||
async with session:
|
|
||||||
query = delete(models.Dish).where(models.Dish.id == dish_id)
|
|
||||||
await session.execute(query)
|
|
||||||
await session.commit()
|
|
@@ -1,73 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sqlalchemy import delete, func, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import models, schemas
|
|
||||||
|
|
||||||
|
|
||||||
class MenuCrud:
|
|
||||||
@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)
|
|
||||||
menu = menu.scalars().one_or_none()
|
|
||||||
if menu is None:
|
|
||||||
return None
|
|
||||||
submenu_query = select(
|
|
||||||
func.count(models.SubMenu.id).label("counter")
|
|
||||||
).filter(models.SubMenu.parent_menu == menu_id)
|
|
||||||
counter = await session.execute(submenu_query)
|
|
||||||
|
|
||||||
dish_query = (
|
|
||||||
select(func.count(models.Dish.id))
|
|
||||||
.join(models.SubMenu)
|
|
||||||
.filter(models.Dish.parent_submenu == models.SubMenu.id)
|
|
||||||
.filter(models.SubMenu.parent_menu == menu_id)
|
|
||||||
)
|
|
||||||
dishes = await session.execute(dish_query)
|
|
||||||
menu.submenus_count = counter.scalars().one_or_none()
|
|
||||||
menu.dishes_count = dishes.scalars().one_or_none()
|
|
||||||
return menu
|
|
||||||
|
|
||||||
@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,77 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sqlalchemy import delete, func, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import models, schemas
|
|
||||||
|
|
||||||
|
|
||||||
class SubMenuCrud:
|
|
||||||
@staticmethod
|
|
||||||
async def get_submenus(menu_id: UUID, session: AsyncSession):
|
|
||||||
async with session:
|
|
||||||
query = select(models.SubMenu).where(models.SubMenu.parent_menu == menu_id)
|
|
||||||
submenus = await session.execute(query)
|
|
||||||
return submenus.scalars().all()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def create_submenu_item(
|
|
||||||
menu_id: UUID,
|
|
||||||
submenu: schemas.MenuBase,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
new_submenu = models.SubMenu(**submenu.model_dump())
|
|
||||||
new_submenu.parent_menu = menu_id
|
|
||||||
session.add(new_submenu)
|
|
||||||
await session.flush()
|
|
||||||
await session.commit()
|
|
||||||
return new_submenu
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_submenu_item(
|
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
query = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
|
|
||||||
submenu = await session.execute(query)
|
|
||||||
submenu = submenu.scalars().one_or_none()
|
|
||||||
if submenu is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
dish_query = (
|
|
||||||
select(func.count(models.Dish.id))
|
|
||||||
.join(models.SubMenu)
|
|
||||||
.filter(models.Dish.parent_submenu == models.SubMenu.id)
|
|
||||||
)
|
|
||||||
dishes = await session.execute(dish_query)
|
|
||||||
submenu.dishes_count = dishes.scalars().one_or_none()
|
|
||||||
|
|
||||||
return submenu
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_submenu_item(
|
|
||||||
submenu_id: UUID,
|
|
||||||
submenu: schemas.MenuBase,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
query = (
|
|
||||||
update(models.SubMenu)
|
|
||||||
.where(models.SubMenu.id == submenu_id)
|
|
||||||
.values(**submenu.model_dump())
|
|
||||||
)
|
|
||||||
await session.execute(query)
|
|
||||||
await session.commit()
|
|
||||||
qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
|
|
||||||
updated_submenu = await session.execute(qr)
|
|
||||||
return updated_submenu.scalars().one()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete_submenu_item(submenu_id: UUID, session: AsyncSession):
|
|
||||||
async with session:
|
|
||||||
query = delete(models.SubMenu).where(models.SubMenu.id == submenu_id)
|
|
||||||
await session.execute(query)
|
|
||||||
await session.commit()
|
|
@@ -1,7 +1,8 @@
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker,
|
import redis.asyncio as redis # type: ignore
|
||||||
create_async_engine)
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from fastfood.config import settings
|
from fastfood.config import settings
|
||||||
|
|
||||||
@@ -16,3 +17,13 @@ async_session_maker = async_sessionmaker(
|
|||||||
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
yield 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,9 +1,11 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Annotated, List, Optional
|
from copy import deepcopy
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.util import hybridproperty
|
||||||
|
|
||||||
uuidpk = Annotated[
|
uuidpk = Annotated[
|
||||||
uuid.UUID,
|
uuid.UUID,
|
||||||
@@ -19,38 +21,64 @@ str_25 = Annotated[str, 25]
|
|||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
id: Mapped[uuidpk]
|
id: Mapped[uuidpk]
|
||||||
title: Mapped[str_25]
|
title: Mapped[str_25]
|
||||||
description: Mapped[Optional[str]]
|
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):
|
class Menu(Base):
|
||||||
__tablename__ = "menu"
|
__tablename__ = 'menu'
|
||||||
|
|
||||||
submenus: Mapped[List["SubMenu"]] = relationship(
|
submenus: Mapped[list['SubMenu']] = relationship(
|
||||||
"SubMenu",
|
'SubMenu',
|
||||||
backref="menu",
|
backref='menu',
|
||||||
lazy="dynamic",
|
lazy='selectin',
|
||||||
cascade="all, delete",
|
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):
|
class SubMenu(Base):
|
||||||
__tablename__ = "submenu"
|
__tablename__ = 'submenu'
|
||||||
|
|
||||||
parent_menu: Mapped[uuid.UUID] = mapped_column(
|
parent_menu: Mapped[uuid.UUID] = mapped_column(
|
||||||
ForeignKey("menu.id", ondelete="CASCADE")
|
ForeignKey('menu.id', ondelete='CASCADE')
|
||||||
)
|
)
|
||||||
dishes: Mapped[List["Dish"]] = relationship(
|
dishes: Mapped[list['Dish']] = relationship(
|
||||||
"Dish",
|
'Dish',
|
||||||
backref="submenu",
|
backref='submenu',
|
||||||
lazy="dynamic",
|
lazy='selectin',
|
||||||
cascade="all, delete",
|
cascade='all, delete',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@hybridproperty
|
||||||
|
def dishes_count(self):
|
||||||
|
return len(self.dishes)
|
||||||
|
|
||||||
|
|
||||||
class Dish(Base):
|
class Dish(Base):
|
||||||
__tablename__ = "dish"
|
__tablename__ = 'dish'
|
||||||
|
|
||||||
price: Mapped[float]
|
price: Mapped[float]
|
||||||
parent_submenu: Mapped[uuid.UUID] = mapped_column(
|
parent_submenu: Mapped[uuid.UUID] = mapped_column(
|
||||||
ForeignKey("submenu.id", ondelete="CASCADE")
|
ForeignKey('submenu.id', ondelete='CASCADE')
|
||||||
)
|
)
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
from fastfood import models
|
from fastfood import models
|
||||||
from fastfood.dbase import async_engine
|
from fastfood.dbase import async_engine
|
||||||
|
|
||||||
from .dish import DishCrud
|
from .dish import DishRepository
|
||||||
from .menu import MenuCrud
|
from .menu import MenuRepository
|
||||||
from .submenu import SubMenuCrud
|
from .submenu import SubMenuRepository
|
||||||
|
|
||||||
|
|
||||||
async def create_db_and_tables():
|
async def create_db_and_tables():
|
||||||
@@ -12,8 +12,8 @@ async def create_db_and_tables():
|
|||||||
await conn.run_sync(models.Base.metadata.create_all)
|
await conn.run_sync(models.Base.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
class Crud(MenuCrud, SubMenuCrud, DishCrud):
|
class Repository(MenuRepository, SubMenuRepository, DishRepository):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
crud = Crud()
|
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()]
|
@@ -1,80 +1,102 @@
|
|||||||
from typing import List, Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import schemas
|
from fastfood.schemas import Dish, DishBase
|
||||||
from fastfood.cruds import crud
|
from fastfood.service.dish import DishService
|
||||||
from fastfood.dbase import get_async_session
|
|
||||||
from fastfood.utils import price_converter
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes",
|
prefix='/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes',
|
||||||
tags=["dish"],
|
tags=['dish'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get(
|
||||||
|
'/',
|
||||||
|
response_model=list[Dish],
|
||||||
|
)
|
||||||
async def get_dishes(
|
async def get_dishes(
|
||||||
menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session)
|
menu_id: UUID,
|
||||||
):
|
submenu_id: UUID,
|
||||||
result = await crud.get_dishes(submenu_id=submenu_id, session=session)
|
dish: DishService = Depends(),
|
||||||
|
) -> list[Dish]:
|
||||||
|
result = await dish.read_dishes(menu_id, submenu_id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", status_code=201)
|
@router.post(
|
||||||
|
'/',
|
||||||
|
status_code=201,
|
||||||
|
response_model=Dish,
|
||||||
|
)
|
||||||
async def create_dish(
|
async def create_dish(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish: schemas.DishBase,
|
dish_data: DishBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
dish: DishService = Depends(),
|
||||||
):
|
) -> Dish:
|
||||||
result = await crud.create_dish_item(
|
return await dish.create_dish(
|
||||||
submenu_id=submenu_id,
|
menu_id,
|
||||||
dish=dish,
|
submenu_id,
|
||||||
session=session,
|
dish_data,
|
||||||
)
|
)
|
||||||
return price_converter(result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{dish_id}")
|
@router.get(
|
||||||
|
'/{dish_id}',
|
||||||
|
response_model=Dish,
|
||||||
|
)
|
||||||
async def get_dish(
|
async def get_dish(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
dish: DishService = Depends(),
|
||||||
):
|
) -> Dish | None:
|
||||||
result = await crud.get_dish_item(
|
result = await dish.read_dish(
|
||||||
dish_id=dish_id,
|
menu_id,
|
||||||
session=session,
|
submenu_id,
|
||||||
|
dish_id,
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="dish not found")
|
raise HTTPException(
|
||||||
return price_converter(result)
|
status_code=404,
|
||||||
|
detail='dish not found',
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{dish_id}")
|
@router.patch(
|
||||||
|
'/{dish_id}',
|
||||||
|
response_model=Dish,
|
||||||
|
)
|
||||||
async def update_dish(
|
async def update_dish(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
dish: schemas.DishBase,
|
dish_data: DishBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
dish: DishService = Depends(),
|
||||||
):
|
) -> Dish:
|
||||||
result = await crud.update_dish_item(
|
result = await dish.update_dish(
|
||||||
dish_id=dish_id,
|
menu_id,
|
||||||
dish=dish,
|
submenu_id,
|
||||||
session=session,
|
dish_id,
|
||||||
|
dish_data,
|
||||||
)
|
)
|
||||||
return price_converter(result)
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='dish not found',
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{dish_id}")
|
@router.delete(
|
||||||
|
'/{dish_id}',
|
||||||
|
)
|
||||||
async def delete_dish(
|
async def delete_dish(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
dish_id: UUID,
|
dish_id: UUID,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
dish: DishService = Depends(),
|
||||||
):
|
) -> None:
|
||||||
await crud.delete_dish_item(dish_id=dish_id, session=session)
|
await dish.del_dish(menu_id, dish_id)
|
||||||
|
@@ -1,65 +1,85 @@
|
|||||||
from typing import List, Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import schemas
|
from fastfood.schemas import MenuBase, MenuRead
|
||||||
from fastfood.cruds import crud
|
from fastfood.service.menu import MenuService
|
||||||
from fastfood.dbase import get_async_session
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/api/v1/menus",
|
prefix='/api/v1/menus',
|
||||||
tags=["menu"],
|
tags=['menu'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=Optional[List[schemas.Menu]])
|
@router.get(
|
||||||
async def get_menus(session: AsyncSession = Depends(get_async_session)):
|
'/',
|
||||||
result = await crud.get_menus(session=session)
|
status_code=200,
|
||||||
return result
|
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=schemas.Menu)
|
@router.post(
|
||||||
|
'/',
|
||||||
|
status_code=201,
|
||||||
|
response_model=MenuRead,
|
||||||
|
)
|
||||||
async def add_menu(
|
async def add_menu(
|
||||||
menu: schemas.MenuBase,
|
menu: MenuBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
responce: MenuService = Depends(),
|
||||||
):
|
) -> MenuRead:
|
||||||
result = await crud.create_menu_item(
|
return await responce.create_menu(menu)
|
||||||
menu=menu,
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{menu_id}", response_model=schemas.MenuRead)
|
@router.get(
|
||||||
|
'/{menu_id}',
|
||||||
|
response_model=MenuRead,
|
||||||
|
)
|
||||||
async def get_menu(
|
async def get_menu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
responce: MenuService = Depends(),
|
||||||
):
|
) -> MenuRead:
|
||||||
result = await crud.get_menu_item(menu_id=menu_id, session=session)
|
result = await responce.read_menu(menu_id=menu_id)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="menu not found")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='menu not found',
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{menu_id}", response_model=schemas.MenuBase)
|
@router.patch(
|
||||||
|
'/{menu_id}',
|
||||||
|
response_model=MenuRead,
|
||||||
|
)
|
||||||
async def update_menu(
|
async def update_menu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
menu: schemas.MenuBase,
|
menu: MenuBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
responce: MenuService = Depends(),
|
||||||
):
|
) -> MenuRead:
|
||||||
result = await crud.update_menu_item(
|
result = await responce.update_menu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
menu=menu,
|
menu_data=menu,
|
||||||
session=session,
|
|
||||||
)
|
)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='menu not found',
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{menu_id}")
|
@router.delete(
|
||||||
|
'/{menu_id}',
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
async def delete_menu(
|
async def delete_menu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
menu: MenuService = Depends(),
|
||||||
):
|
) -> None:
|
||||||
await crud.delete_menu_item(menu_id=menu_id, session=session)
|
await menu.del_menu(menu_id)
|
||||||
|
@@ -1,76 +1,96 @@
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood import schemas
|
from fastfood.schemas import MenuBase, SubMenuRead
|
||||||
from fastfood.cruds import crud
|
from fastfood.service.submenu import SubmenuService
|
||||||
from fastfood.dbase import get_async_session
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/api/v1/menus/{menu_id}/submenus",
|
prefix='/api/v1/menus/{menu_id}/submenus',
|
||||||
tags=["submenu"],
|
tags=['submenu'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get(
|
||||||
|
'/',
|
||||||
|
response_model=list[SubMenuRead],
|
||||||
|
)
|
||||||
async def get_submenus(
|
async def get_submenus(
|
||||||
menu_id: UUID, session: AsyncSession = Depends(get_async_session)
|
menu_id: UUID,
|
||||||
):
|
submenu: SubmenuService = Depends(),
|
||||||
result = await crud.get_submenus(menu_id=menu_id, session=session)
|
) -> list[SubMenuRead]:
|
||||||
|
result = await submenu.read_submenus(menu_id=menu_id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", status_code=201)
|
@router.post(
|
||||||
|
'/',
|
||||||
|
status_code=201,
|
||||||
|
response_model=SubMenuRead,
|
||||||
|
)
|
||||||
async def create_submenu_item(
|
async def create_submenu_item(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu: schemas.MenuBase,
|
submenu_data: MenuBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
submenu: SubmenuService = Depends(),
|
||||||
):
|
) -> SubMenuRead:
|
||||||
result = await crud.create_submenu_item(
|
result = await submenu.create_submenu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
submenu=submenu,
|
submenu_data=submenu_data,
|
||||||
session=session,
|
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{submenu_id}", response_model=schemas.SubMenuRead)
|
@router.get(
|
||||||
|
'/{submenu_id}',
|
||||||
|
response_model=SubMenuRead,
|
||||||
|
)
|
||||||
async def get_submenu(
|
async def get_submenu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
submenu: SubmenuService = Depends(),
|
||||||
):
|
) -> SubMenuRead:
|
||||||
result = await crud.get_submenu_item(
|
result = await submenu.read_menu(
|
||||||
menu_id=menu_id,
|
menu_id=menu_id,
|
||||||
submenu_id=submenu_id,
|
submenu_id=submenu_id,
|
||||||
session=session,
|
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="submenu not found")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='submenu not found',
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
"/{submenu_id}",
|
'/{submenu_id}',
|
||||||
response_model=schemas.MenuBase,
|
response_model=SubMenuRead,
|
||||||
)
|
)
|
||||||
async def update_submenu(
|
async def update_submenu(
|
||||||
menu_id: UUID,
|
menu_id: UUID,
|
||||||
submenu_id: UUID,
|
submenu_id: UUID,
|
||||||
submenu: schemas.MenuBase,
|
submenu_data: MenuBase,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
submenu: SubmenuService = Depends(),
|
||||||
):
|
) -> SubMenuRead:
|
||||||
result = await crud.update_submenu_item(
|
result = await submenu.update_submenu(
|
||||||
|
menu_id=menu_id,
|
||||||
submenu_id=submenu_id,
|
submenu_id=submenu_id,
|
||||||
submenu=submenu,
|
submenu_data=submenu_data,
|
||||||
session=session,
|
|
||||||
)
|
)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='submenu not found',
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{submenu_id}")
|
@router.delete(
|
||||||
|
'/{submenu_id}',
|
||||||
|
)
|
||||||
async def delete_submenu(
|
async def delete_submenu(
|
||||||
menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session)
|
menu_id: UUID,
|
||||||
):
|
submenu_id: UUID,
|
||||||
await crud.delete_submenu_item(submenu_id=submenu_id, session=session)
|
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,4 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -6,7 +5,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
class MenuBase(BaseModel):
|
class MenuBase(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
description: Optional[str]
|
description: str | None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -26,8 +25,20 @@ class SubMenuRead(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class DishBase(MenuBase):
|
class DishBase(MenuBase):
|
||||||
price: float
|
price: str
|
||||||
|
|
||||||
|
|
||||||
class Dish(DishBase, Menu):
|
class Dish(DishBase, Menu):
|
||||||
pass
|
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
|
@@ -1,3 +0,0 @@
|
|||||||
def price_converter(dish: dict) -> dict:
|
|
||||||
dish.price = str(dish.price)
|
|
||||||
return dish
|
|
48
manage.py
48
manage.py
@@ -1,31 +1,59 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import multiprocessing
|
||||||
import sys
|
import sys
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from fastfood.cruds 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
|
Запуск FastAPI
|
||||||
"""
|
"""
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app="fastfood.app:create_app",
|
app='fastfood.app:create_app',
|
||||||
|
host='0.0.0.0',
|
||||||
|
port=8000,
|
||||||
reload=True,
|
reload=True,
|
||||||
factory=True,
|
factory=True,
|
||||||
|
workers=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def recreate():
|
async def recreate() -> None:
|
||||||
"""Удаление и создание таблиц в базе данных для тестирования"""
|
"""Удаление и создание таблиц в базе данных для тестирования"""
|
||||||
await create_db_and_tables()
|
await create_db_and_tables()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
if "--run-server" in sys.argv:
|
if '--run-docker-server' in sys.argv:
|
||||||
run_app()
|
"""Запуск FastAPI в докере. Celery запускается в отдельном контейнере"""
|
||||||
|
loop.run_until_complete(recreate())
|
||||||
|
loop.run_until_complete(run_app())
|
||||||
|
|
||||||
if "--run-test-server" in sys.argv:
|
if '--run-local-server' in sys.argv:
|
||||||
asyncio.run(recreate())
|
"""Локальный запуск FastAPI с запуском Celery в отдельных процессах"""
|
||||||
run_app()
|
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
1362
poetry.lock
generated
1362
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3887,4 +3887,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -90,4 +90,4 @@
|
|||||||
"_postman_variable_scope": "environment",
|
"_postman_variable_scope": "environment",
|
||||||
"_postman_exported_at": "2023-01-12T16:22:10.333Z",
|
"_postman_exported_at": "2023-01-12T16:22:10.333Z",
|
||||||
"_postman_exported_using": "Postman/10.6.7"
|
"_postman_exported_using": "Postman/10.6.7"
|
||||||
}
|
}
|
||||||
|
@@ -12,14 +12,21 @@ fastapi = "^0.109.0"
|
|||||||
uvicorn = "^0.26.0"
|
uvicorn = "^0.26.0"
|
||||||
asyncpg = "^0.29.0"
|
asyncpg = "^0.29.0"
|
||||||
pydantic-settings = "^2.1.0"
|
pydantic-settings = "^2.1.0"
|
||||||
psycopg2-binary = "^2.9.9"
|
|
||||||
email-validator = "^2.1.0.post1"
|
email-validator = "^2.1.0.post1"
|
||||||
pytest-asyncio = "^0.23.3"
|
pytest-asyncio = "^0.23.3"
|
||||||
httpx = "^0.26.0"
|
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]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^7.4.4"
|
pytest = "^7.4.4"
|
||||||
|
pytest-cov = "^4.1.0"
|
||||||
|
httpx = "^0.26.0"
|
||||||
|
pre-commit = "^3.6.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import AsyncClient
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from fastfood.app import create_app
|
|
||||||
|
|
||||||
|
from fastfood.app import create_app
|
||||||
from fastfood.config import settings
|
from fastfood.config import settings
|
||||||
from fastfood.dbase import get_async_session
|
from fastfood.dbase import get_async_session
|
||||||
from fastfood.models import Base
|
from fastfood.models import Base
|
||||||
@@ -17,21 +20,22 @@ async_session_maker = async_sessionmaker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
def event_loop():
|
def event_loop():
|
||||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
yield loop
|
yield loop
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function", autouse=True)
|
@pytest_asyncio.fixture(scope='session', autouse=True)
|
||||||
async def db_init():
|
async def db_init(event_loop):
|
||||||
async with async_engine.begin() as conn:
|
async with async_engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
async with async_engine.begin() as conn:
|
async with async_engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
|
||||||
@@ -41,8 +45,13 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
|
|||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest_asyncio.fixture(scope='session', autouse=True)
|
||||||
def app():
|
async def client(event_loop) -> AsyncGenerator[AsyncClient, None]:
|
||||||
app = create_app()
|
app: FastAPI = create_app()
|
||||||
app.dependency_overrides[get_async_session] = get_test_session
|
app.dependency_overrides[get_async_session] = get_test_session
|
||||||
yield app
|
|
||||||
|
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()
|
@@ -1,30 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from httpx import AsyncClient
|
|
||||||
|
|
||||||
url = "http://localhost:8000/api/v1/menus"
|
|
||||||
|
|
||||||
|
|
||||||
class TestCrud:
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_read_menus(self, app):
|
|
||||||
"""тест пустой бд"""
|
|
||||||
async with AsyncClient(app=app, base_url=url) as ac:
|
|
||||||
response = await ac.get("/")
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == []
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_write_menu(self, app):
|
|
||||||
""""""
|
|
||||||
async with AsyncClient(app=app, base_url=url) as ac:
|
|
||||||
response = await ac.post("/", json={"title": "menu 1", "description": None})
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert response.json()["title"] == "menu 1"
|
|
||||||
assert response.json()["description"] == None
|
|
||||||
|
|
||||||
|
|
||||||
class TestСontinuity:
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_postman_continuity(self, app):
|
|
||||||
async with AsyncClient(app=app, base_url=url) as ac:
|
|
||||||
pass
|
|
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