Compare commits
78 Commits
bde9581090
...
main
Author | SHA1 | Date | |
---|---|---|---|
8189aaedd4 | |||
5ef6aaeb6f | |||
f75415d9d9 | |||
4c3779776d | |||
d54e704dfb | |||
68594eb7f0 | |||
8bfa166987 | |||
e0a81cf126 | |||
a4f8bce657 | |||
9ba42aae9f | |||
afdf1c5e2b | |||
74c0ccae2a | |||
2c48529a02 | |||
cedf27a04d | |||
e0798de713 | |||
5a133a05e1 | |||
3df3c67e7c | |||
a0ebe9bdb9 | |||
ed3d7d9352 | |||
3dbefda936 | |||
5a95b06300 | |||
ebe75b6dc3 | |||
22a876d3ce | |||
6a0776557d | |||
b2a284d791 | |||
5e213e759d | |||
f28637f5dd | |||
e6d1070d9a | |||
47cb0e08c7 | |||
e6576e9e58 | |||
02134d247a | |||
68db31a033 | |||
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 |
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'
|
20
Dockerfile
20
Dockerfile
@@ -1,15 +1,19 @@
|
|||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
RUN mkdir /fastfood
|
|
||||||
|
|
||||||
WORKDIR /fastfood
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
|
|
||||||
RUN poetry config virtualenvs.create false
|
RUN poetry config virtualenvs.create false
|
||||||
|
|
||||||
RUN poetry install
|
RUN mkdir -p /usr/src/fastfood
|
||||||
|
|
||||||
RUN chmod a+x scripts/*.sh
|
WORKDIR /usr/src/fastfood
|
||||||
|
|
||||||
|
COPY ./example.env .
|
||||||
|
|
||||||
|
COPY ./poetry.lock .
|
||||||
|
|
||||||
|
COPY ./pyproject.toml .
|
||||||
|
|
||||||
|
RUN touch /usr/src/RUN_IN_DOCKER
|
||||||
|
|
||||||
|
RUN poetry install
|
||||||
|
170
README.md
170
README.md
@@ -5,157 +5,87 @@ Fastapi веб приложение реализующее api для общеп
|
|||||||
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||||
|
|
||||||
## Техническое задание
|
## Техническое задание
|
||||||
### Спринт 1 - Создание API
|
### Спринт 4 - Многопроцессорность, асинхронность
|
||||||
Написать проект на FastAPI с использованием PostgreSQL в качестве БД. В проекте следует реализовать REST API по работе с меню ресторана, все CRUD операции. Для проверки задания, к презентаций будет приложена Postman коллекция с тестами. Задание выполнено, если все тесты проходят успешно.
|
В этом домашнем задании необходимо:
|
||||||
Даны 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, для тестирования функционала проекта.
|
|
||||||
|
|
||||||
### Спринт 2 - Docker && pytest
|
|
||||||
В этом домашнем задании надо написать тесты для ранее разработанных ендпоинтов вашего API после Вебинара №1.
|
|
||||||
|
|
||||||
Обернуть программные компоненты в контейнеры. Контейнеры должны запускаться по одной команде “docker-compose up -d” или той которая описана вами в readme.md.
|
|
||||||
|
|
||||||
Образы для Docker:
|
|
||||||
(API) python:3.10-slim
|
|
||||||
(DB) postgres:15.1-alpine
|
|
||||||
|
|
||||||
1.Написать CRUD тесты для ранее разработанного API с помощью библиотеки pytest
|
|
||||||
2.Подготовить отдельный контейнер для запуска тестов. Команду для запуска указать в README.md
|
|
||||||
3.* Реализовать вывод количества подменю и блюд для Меню через один (сложный) ORM запрос.
|
|
||||||
4.** Реализовать тестовый сценарий «Проверка кол-ва блюд и подменю в меню» из Postman с помощью pytest
|
|
||||||
Если FastAPI синхронное - тесты синхронные, Если асинхронное - тесты асинхронные
|
|
||||||
|
|
||||||
|
|
||||||
*Оборачиваем приложение в докер.
|
Требования:
|
||||||
**CRUD – create/update/retrieve/delete.
|
●Данные меню, подменю, блюд для нового эндпоинта должны доставаться одним ORM-запросом в БД (использовать подзапросы и агрегирующие функций SQL).
|
||||||
|
●Проект должен запускаться одной командой
|
||||||
|
●Проект должен соответствовать требованиям всех предыдущих вебинаров. (Не забыть добавить тесты для нового API эндпоинта)
|
||||||
|
|
||||||
<a href="https://drive.google.com/drive/folders/13t6fsMO0B6Ls0qYl-uVgAHWOyhFTFv4Z?usp=sharing">Дополнительные материалы</a>
|
### Выполненные доп задания со *
|
||||||
|
Спринт 2
|
||||||
|
3.* Реализовать вывод количества подменю и блюд для Меню через один (сложный) ORM запрос.
|
||||||
|
`./fastfood/repository/menu.py` Метод `get_menu_item`
|
||||||
|
|
||||||
## Возможности
|
4.** Реализовать тестовый сценарий «Проверка кол-ва блюд и подменю в меню» из Postman с помощью pytest
|
||||||
### Спринт 1
|
`./tests/test_postman.py`
|
||||||
В проекте реализованы 3 сущности: Menu, SubMenu и Dish. Для каждого них реализованы 4 метода http запросов: GET, POST, PATCH и DELETE c помощью которых можно управлять данными.
|
|
||||||
Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish.
|
|
||||||
|
|
||||||
### Спринт 2
|
Спринт 3
|
||||||
- 1й пункт ТЗ
|
5.* Описать ручки API в соответствий c OpenAPI
|
||||||
Тесты реализованы в виде 2х классов
|
'./openapi.json'
|
||||||
`TastBaseCrud` включает 3 подкласса `Menu`, `Submenu`, `Dish` которые реализуют интерфейсы взаимодействия с endpoint'ами реализованных на предыдущем спринте сущностей. Каждый подкласс реализует методы GET(получение всех сущностей), Get(получение конкректной сущности), Post(создание), Patch(обновление), Delete(удаления). Так же в классе реализованы 3 тестовых функции, которые осуществляют тестирование соответствующих endpoint'ов
|
|
||||||
`TestContinuity` реализует последовательность сценария «Проверка кол-ва блюд и подменю в меню» из Postman
|
|
||||||
|
|
||||||
- 2й пункт ТЗ
|
6.** Реализовать в тестах аналог Django reverse() для FastAPI
|
||||||
Реализованы 3 контейнера(db, app, tests). В db написан блок "проверки здоровья", от которого зависят контейнеры app и test, который гарантирует, что зависимые контейнеры не будут запущены о полной готовности db.
|
'./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й пункт ТЗ
|
|
||||||
см. функцию `get_menu_item` на 28 строке в файле
|
|
||||||
<base_dir>/fastfood/crud/menu.py
|
|
||||||
|
|
||||||
- 4й пункт ТЗ
|
|
||||||
см. класс `TestContinuity` в файле
|
|
||||||
<base_dir>/tests/test_api.py
|
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
Для локальной установки
|
|
||||||
- postgresql Для работы сервиса необходима установленная СУБД. Должна быть создана база данных и пользователь с правами на нее.
|
|
||||||
- poetry - Система управления зависимостями в Python.
|
|
||||||
|
|
||||||
Остальное добавится автоматически на этапе установки.
|
|
||||||
|
|
||||||
Для запуска в контейнере
|
|
||||||
- docker
|
- docker
|
||||||
- docker-compose
|
- docker-compose
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
### Docker
|
|
||||||
Для запуска необходимы установленные приложения docker и docker-compose
|
|
||||||
Клонируйте репозиторий
|
Клонируйте репозиторий
|
||||||
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
||||||
|
|
||||||
Перейдите в каталог
|
Перейдите в каталог
|
||||||
> `$ cd fastfood`
|
> `$ cd fastfood`
|
||||||
|
|
||||||
Создадим файл .env из шаблона
|
Запуск/остановка образов:
|
||||||
>`$ cp ./example.env ./.env`
|
|
||||||
Для теста изменять файл .env не требуется.
|
|
||||||
Однако Вы можете изменить имя пользователя, пароль и имя базы данных по своему усмотрению. При таких изменениях, нужно будет отредактировать
|
|
||||||
файл `db_prepare.sql` в папке `scripts/`, так чтобы sql команда приняла вид:
|
|
||||||
`CREATE DATABASE <db_name>_test WITH OWNER <db_user>;`
|
|
||||||
где <db_name> и <db_user> соответвтовали POSTGRES_DB и POSTGRES_USER в файле `.env`
|
|
||||||
|
|
||||||
Создайте и запустите образы
|
- Запуск FAstAPI приложения c локальным файлом для фоновой задачи
|
||||||
> `$ docker-compose up -d --build`
|
> `$ docker-compose -f compose_app.yml up`
|
||||||
|
|
||||||
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
- Запуск FAstAPI приложения c Google Sheets для фоновой задачи
|
||||||
|
> `$ docker-compose -f compose_google.yml up`
|
||||||
|
(ЧИТАЙТЕ СООБЩЕНИЕ В ЧАТЕ)
|
||||||
|
|
||||||
Для запуска тестов pytest поднимаем контейнер tests
|
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
||||||
> `$ docker-compose up tests`
|
|
||||||
|
|
||||||
### Linux
|
По завершении работы остановите контейнеры
|
||||||
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
|
> `$ docker-compose -f compose_app.yml down`
|
||||||
|
|
||||||
Установите систему управления зависимостями
|
- Запуск тестов
|
||||||
> `$ pip[x] install poetry`
|
> `$ docker-compose -f compose_test.yml up`
|
||||||
|
|
||||||
Клонируйте репозиторий
|
По завершении работы остановите контейнеры
|
||||||
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
|
> `$ docker-compose -f compose_test.yml down`
|
||||||
|
|
||||||
Перейдите в каталог
|
|
||||||
|
|
||||||
> `$ cd fastfood`
|
|
||||||
|
|
||||||
> `$ poetry install --no-root`
|
|
||||||
|
|
||||||
Создастся виртуальное окружение и установятся зависимости
|
|
||||||
|
|
||||||
Файл example.env является образцом файла .env, который необходимо создать перед запуском проекта.
|
|
||||||
В нем указанны переменные необходимые для подключения к БД.
|
|
||||||
Создадим файл .env
|
|
||||||
|
|
||||||
>`$ cp ./example.env ./.env`
|
|
||||||
|
|
||||||
Далее отредактируйте .env файл в соответствии с Вашими данными подключения к БД
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
Запуск проекта возможен в 2х режимах:
|
|
||||||
- Запуск в режиме "prod" с ключем --run-server
|
|
||||||
Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются.
|
|
||||||
|
|
||||||
- Запуск в режиме "dev" c ключем --run-test-server
|
|
||||||
В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей.
|
|
||||||
|
|
||||||
|
|
||||||
Для запуска проекта сначала активируем виртуальное окружение
|
|
||||||
|
|
||||||
> `$ poetry shell`
|
|
||||||
|
|
||||||
и запускаем проект в соответстующем режиме
|
|
||||||
|
|
||||||
>`$ python[x] manage.py --ключ`
|
|
||||||
|
|
||||||
вместо этого, так же допускается и другой вариант запуска одной командой без предварительной активации окружения
|
|
||||||
|
|
||||||
>`$ poetry run python[x] manage.py --ключ`
|
|
||||||
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- Написать тесты для кривых данных
|
- Написать тесты для кривых данных
|
||||||
- Добавить миграции
|
|
||||||
- Провести рефакторинг, много дублирующего кода
|
|
||||||
- Много чего другого :)
|
- Много чего другого :)
|
||||||
|
|
||||||
## Авторы
|
## Авторы
|
||||||
@@ -163,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,44 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
services:
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:15.1-alpine
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
container_name: pgdatabase
|
|
||||||
ports:
|
|
||||||
- 6432:5432
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
volumes:
|
|
||||||
- ./scripts/db_prepare.sql:/docker-entrypoint-initdb.d/db_prepare.sql
|
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
container_name: fastfood_app
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
command: ["/fastfood/scripts/migrate_and_run.sh"]
|
|
||||||
ports:
|
|
||||||
- 8000:8000
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
tests:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
container_name: tests
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
app:
|
|
||||||
condition: service_started
|
|
||||||
command: ["/fastfood/scripts/testing.sh"]
|
|
@@ -1,5 +0,0 @@
|
|||||||
DB_HOST=db
|
|
||||||
DB_PORT=5432
|
|
||||||
POSTGRES_USER=postges
|
|
||||||
POSTGRES_PASSWORD=postgres
|
|
||||||
POSTGRES_DB=fastfood_db
|
|
@@ -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,22 +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 = "db"
|
# Конфиг PostgreSql
|
||||||
DB_PORT: int = 5432
|
POSTGRES_HOST: str = ''
|
||||||
POSTGRES_DB: str = "fastfod_db"
|
POSTGRES_PORT: int = 5432
|
||||||
POSTGRES_PASSWORD: str = "postgres"
|
POSTGRES_DB: str = ''
|
||||||
POSTGRES_USER: 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 (
|
||||||
"postgresql+asyncpg://"
|
'postgresql+asyncpg://'
|
||||||
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}"
|
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -24,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 (
|
||||||
"postgresql+asyncpg://"
|
'postgresql+asyncpg://'
|
||||||
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
|
||||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}_test"
|
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB_TEST}'
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=".env")
|
@property
|
||||||
|
def REDIS_URL(self):
|
||||||
|
"""
|
||||||
|
Возвращает строку подключения к REDIS
|
||||||
|
"""
|
||||||
|
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return 'redis://redis:6379/0'
|
||||||
|
|
||||||
|
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def REBBITMQ_URL(self):
|
||||||
|
"""
|
||||||
|
Возвращает строку подключения к REBBITMQ
|
||||||
|
"""
|
||||||
|
file_path = '/usr/src/RUN_IN_DOCKER'
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return 'amqp://guest:guest@rabbitmq'
|
||||||
|
|
||||||
|
return 'amqp://guest:guest@127.0.0.1'
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(env_file='.env')
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
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,74 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sqlalchemy import delete, distinct, func, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import aliased
|
|
||||||
|
|
||||||
from fastfood import models, schemas
|
|
||||||
|
|
||||||
|
|
||||||
class MenuCrud:
|
|
||||||
@staticmethod
|
|
||||||
async def get_menus(session: AsyncSession):
|
|
||||||
async with session:
|
|
||||||
query = select(models.Menu)
|
|
||||||
menus = await session.execute(query)
|
|
||||||
return menus
|
|
||||||
|
|
||||||
@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:
|
|
||||||
m = aliased(models.Menu)
|
|
||||||
s = aliased(models.SubMenu)
|
|
||||||
d = aliased(models.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 session.execute(query)
|
|
||||||
menu = menu.scalars().one_or_none()
|
|
||||||
if menu is None:
|
|
||||||
return 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
|
|
||||||
|
|
||||||
@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,80 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from sqlalchemy import delete, distinct, func, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
from sqlalchemy.orm import aliased
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@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.commit()
|
|
||||||
await session.refresh(new_submenu)
|
|
||||||
return new_submenu
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_submenu_item(
|
|
||||||
menu_id: UUID,
|
|
||||||
submenu_id: UUID,
|
|
||||||
session: AsyncSession,
|
|
||||||
):
|
|
||||||
async with session:
|
|
||||||
s = aliased(models.SubMenu)
|
|
||||||
d = aliased(models.Dish)
|
|
||||||
query = (
|
|
||||||
select(s, func.count(distinct(d.id)))
|
|
||||||
.join(d, s.id == d.parent_submenu, isouter=True)
|
|
||||||
.group_by(s.id)
|
|
||||||
.where(s.id == submenu_id)
|
|
||||||
)
|
|
||||||
submenu = await session.execute(query)
|
|
||||||
submenu = submenu.scalars().one_or_none()
|
|
||||||
if submenu is None:
|
|
||||||
return 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
|
|
||||||
|
|
||||||
@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,6 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Annotated, List, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
@@ -21,13 +21,13 @@ 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):
|
def __eq__(self, other):
|
||||||
classes_match = isinstance(other, self.__class__)
|
classes_match = isinstance(other, self.__class__)
|
||||||
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
|
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
|
||||||
a.pop("_sa_instance_state", None)
|
a.pop('_sa_instance_state', None)
|
||||||
b.pop("_sa_instance_state", None)
|
b.pop('_sa_instance_state', None)
|
||||||
attrs_match = a == b
|
attrs_match = a == b
|
||||||
return classes_match and attrs_match
|
return classes_match and attrs_match
|
||||||
|
|
||||||
@@ -36,13 +36,13 @@ class Base(DeclarativeBase):
|
|||||||
|
|
||||||
|
|
||||||
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="selectin",
|
lazy='selectin',
|
||||||
cascade="all, delete",
|
cascade='all, delete',
|
||||||
)
|
)
|
||||||
|
|
||||||
@hybridproperty
|
@hybridproperty
|
||||||
@@ -58,16 +58,16 @@ class Menu(Base):
|
|||||||
|
|
||||||
|
|
||||||
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="selectin",
|
lazy='selectin',
|
||||||
cascade="all, delete",
|
cascade='all, delete',
|
||||||
)
|
)
|
||||||
|
|
||||||
@hybridproperty
|
@hybridproperty
|
||||||
@@ -76,9 +76,9 @@ class SubMenu(Base):
|
|||||||
|
|
||||||
|
|
||||||
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,66 +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.scalars().all()
|
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,
|
|
||||||
)
|
)
|
||||||
return result.scalars().one()
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='menu not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
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]:
|
||||||
return result.scalars().all()
|
result = await submenu.read_submenus(menu_id=menu_id)
|
||||||
|
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,
|
|
||||||
)
|
)
|
||||||
return result.scalars().one()
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail='submenu not found',
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
|
47
manage.py
47
manage.py
@@ -1,18 +1,34 @@
|
|||||||
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",
|
host='0.0.0.0',
|
||||||
port=8000,
|
port=8000,
|
||||||
reload=True,
|
reload=True,
|
||||||
factory=True,
|
factory=True,
|
||||||
@@ -20,15 +36,24 @@ def run_app():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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
1340
poetry.lock
generated
1340
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,16 +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"
|
||||||
pytest-cov = "^4.1.0"
|
types-redis = "^4.6.0.3"
|
||||||
alembic = "^1.13.1"
|
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 +0,0 @@
|
|||||||
CREATE DATABASE fastfood_db_test WITH OWNER postgres;
|
|
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Тут можно выполнить миграции или дополнительные перед запуском приложения
|
|
||||||
#
|
|
||||||
poetry run python manage.py --run-test-server
|
|
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
poetry run pytest -vv
|
|
@@ -1,20 +1,17 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from httpx import AsyncClient
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from sqlalchemy.ext.asyncio import (
|
from fastapi import FastAPI
|
||||||
AsyncSession,
|
from httpx import AsyncClient
|
||||||
async_sessionmaker,
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
|
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
|
||||||
async_session_maker = async_sessionmaker(
|
async_session_maker = async_sessionmaker(
|
||||||
async_engine,
|
async_engine,
|
||||||
@@ -23,7 +20,7 @@ async_session_maker = async_sessionmaker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
def event_loop():
|
def event_loop():
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@@ -33,8 +30,8 @@ def event_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)
|
||||||
@@ -48,22 +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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
|
||||||
async def client(app):
|
|
||||||
async with AsyncClient(
|
async with AsyncClient(
|
||||||
app=app, base_url="http://localhost:8000/api/v1/menus",
|
app=app,
|
||||||
|
base_url='http://localhost:8000',
|
||||||
) as async_client:
|
) as async_client:
|
||||||
yield async_client
|
yield async_client
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
|
||||||
async def asession() -> AsyncGenerator[AsyncSession, None]:
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
yield session
|
|
||||||
|
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,383 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
class TestBaseCrud:
|
|
||||||
class Menu:
|
|
||||||
@staticmethod
|
|
||||||
async def read_all(ac):
|
|
||||||
"""чтение всех меню"""
|
|
||||||
response = await ac.get("/")
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get(ac, data):
|
|
||||||
"""Получение меню по id"""
|
|
||||||
response = await ac.get(f"/{data.get('id')}")
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def write(ac, data):
|
|
||||||
"""создания меню"""
|
|
||||||
response = await ac.post("/", json=data)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(ac, data):
|
|
||||||
"""Обновление меню по id"""
|
|
||||||
response = await ac.patch(f"/{data.get('id')}", json=data)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(ac, data):
|
|
||||||
"""Удаление меню по id"""
|
|
||||||
response = await ac.delete(f"/{data.get('id')}")
|
|
||||||
return response.status_code
|
|
||||||
|
|
||||||
class Submenu:
|
|
||||||
@staticmethod
|
|
||||||
async def read_all(ac, menu):
|
|
||||||
"""чтение всех меню"""
|
|
||||||
response = await ac.get(f"/{menu.get('id')}/submenus/")
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get(ac, menu, submenu):
|
|
||||||
"""Получение меню по id"""
|
|
||||||
response = await ac.get(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}",
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def write(ac, menu, submenu):
|
|
||||||
"""создания меню"""
|
|
||||||
response = await ac.post(
|
|
||||||
f"/{menu.get('id')}/submenus/",
|
|
||||||
json=submenu,
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(ac, menu, submenu):
|
|
||||||
"""Обновление меню по id"""
|
|
||||||
response = await ac.patch(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}",
|
|
||||||
json=submenu,
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(ac, menu, submenu):
|
|
||||||
"""Удаление меню по id"""
|
|
||||||
response = await ac.delete(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}"
|
|
||||||
)
|
|
||||||
return response.status_code
|
|
||||||
|
|
||||||
class Dish:
|
|
||||||
@staticmethod
|
|
||||||
async def read_all(ac, menu, submenu):
|
|
||||||
"""чтение всех блюд"""
|
|
||||||
response = await ac.get(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/",
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get(ac, menu, submenu, dish):
|
|
||||||
"""Получение блюда по id"""
|
|
||||||
response = await ac.get(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}"
|
|
||||||
f"/dishes/{dish.get('id')}",
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def write(ac, menu, submenu, dish):
|
|
||||||
"""создания блюда"""
|
|
||||||
response = await ac.post(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/",
|
|
||||||
json=dish,
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update(ac, menu, submenu, dish):
|
|
||||||
"""Обновление блюда по id"""
|
|
||||||
response = await ac.patch(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}"
|
|
||||||
f"/dishes/{dish.get('id')}",
|
|
||||||
json=dish,
|
|
||||||
)
|
|
||||||
return response.status_code, response.json()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def delete(ac, menu, submenu, dish):
|
|
||||||
"""Удаление блюда по id"""
|
|
||||||
response = await ac.delete(
|
|
||||||
f"/{menu.get('id')}/submenus/{submenu.get('id')}"
|
|
||||||
f"/dishes/{dish.get('id')}"
|
|
||||||
)
|
|
||||||
return response.status_code
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_menu_crud(self, client):
|
|
||||||
"""Тестирование функций меню"""
|
|
||||||
code, rspn = await self.Menu.read_all(client)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
data = {"title": "Menu", "description": None}
|
|
||||||
code, rspn = await self.Menu.write(client, data)
|
|
||||||
assert code == 201
|
|
||||||
assert rspn["title"] == "Menu"
|
|
||||||
assert rspn["description"] is None
|
|
||||||
|
|
||||||
code, menu = await self.Menu.get(client, {"id": rspn.get("id")})
|
|
||||||
assert code == 200
|
|
||||||
assert menu["title"] == rspn["title"]
|
|
||||||
|
|
||||||
upd_data = {
|
|
||||||
"id": rspn.get("id"),
|
|
||||||
"title": "upd Menu",
|
|
||||||
"description": "",
|
|
||||||
}
|
|
||||||
code, upd_rspn = await self.Menu.update(client, upd_data)
|
|
||||||
assert code == 200
|
|
||||||
assert upd_rspn["title"] == "upd Menu"
|
|
||||||
|
|
||||||
code = await self.Menu.delete(client, rspn)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
code, menu = await self.Menu.get(client, {"id": rspn.get("id")})
|
|
||||||
assert code == 404
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_submenus(self, client):
|
|
||||||
# Создаем меню и проверяем ответ
|
|
||||||
menu = {"title": "Menu", "description": "main menu"}
|
|
||||||
code, rspn = await self.Menu.write(client, menu)
|
|
||||||
assert code == 201
|
|
||||||
menu.update(rspn)
|
|
||||||
|
|
||||||
# Проверяем наличие подменю
|
|
||||||
code, rspn = await self.Submenu.read_all(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn == []
|
|
||||||
|
|
||||||
# Создаем и проверяем подменю
|
|
||||||
submenu = {
|
|
||||||
"title": "Submenu",
|
|
||||||
"description": "submenu",
|
|
||||||
"parent_menu": menu["id"],
|
|
||||||
}
|
|
||||||
code, rspn = await self.Submenu.write(client, menu, submenu)
|
|
||||||
assert code == 201
|
|
||||||
submenu.update(rspn)
|
|
||||||
|
|
||||||
# Проверяем меню на наличие подменю
|
|
||||||
code, rspn = await self.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["submenus_count"] == 1
|
|
||||||
|
|
||||||
# Обновляем подменю и проверяем
|
|
||||||
submenu["title"] = "updated_submenu"
|
|
||||||
code, rspn = await self.Submenu.update(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
assert submenu["title"] == rspn["title"]
|
|
||||||
submenu.update(rspn)
|
|
||||||
|
|
||||||
# Удаляем подменю
|
|
||||||
code = await self.Submenu.delete(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
# Проверяем меню
|
|
||||||
code, rspn = await self.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["submenus_count"] == 0
|
|
||||||
|
|
||||||
# Проверяем удаленное подменю
|
|
||||||
code, rspn = await self.Submenu.get(client, menu, submenu)
|
|
||||||
assert code == 404
|
|
||||||
|
|
||||||
# удаляем сопутствующее
|
|
||||||
await self.Menu.delete(client, menu)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_dishes(self, client):
|
|
||||||
# Создаем меню и проверяем ответ
|
|
||||||
menu = {
|
|
||||||
"title": "Menu",
|
|
||||||
"description": "main menu",
|
|
||||||
}
|
|
||||||
code, rspn = await self.Menu.write(client, menu)
|
|
||||||
assert code == 201
|
|
||||||
menu.update(rspn)
|
|
||||||
|
|
||||||
# Создаем и проверяем подменю
|
|
||||||
submenu = {
|
|
||||||
"title": "Submenu",
|
|
||||||
"description": "submenu",
|
|
||||||
"parent_menu": menu["id"],
|
|
||||||
}
|
|
||||||
code, rspn = await self.Submenu.write(client, menu, submenu)
|
|
||||||
assert code == 201
|
|
||||||
submenu.update(rspn)
|
|
||||||
|
|
||||||
# Проверяем все блюда в подменю
|
|
||||||
code, rspn = await self.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 self.Dish.write(client, menu, submenu, dish)
|
|
||||||
assert code == 201
|
|
||||||
dish.update(rspn)
|
|
||||||
|
|
||||||
# Получаем блюдо
|
|
||||||
code, rspn = await self.Dish.get(client, menu, submenu, dish)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["title"] == dish["title"]
|
|
||||||
|
|
||||||
# Проверяем меню на количество блюд
|
|
||||||
code, rspn = await self.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["dishes_count"] == 1
|
|
||||||
|
|
||||||
# Проверяем подменю на наличие блюд
|
|
||||||
code, rspn = await self.Submenu.get(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["dishes_count"] == 1
|
|
||||||
|
|
||||||
# Обновляем блюдо и проверяем
|
|
||||||
dish["title"] = "updated_dish"
|
|
||||||
code, rspn = await self.Dish.update(client, menu, submenu, dish)
|
|
||||||
assert code == 200
|
|
||||||
assert dish["title"] == rspn["title"]
|
|
||||||
dish.update(rspn)
|
|
||||||
|
|
||||||
# Удаляем подменю
|
|
||||||
code = await self.Dish.delete(client, menu, submenu, dish)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
# Проверяем меню
|
|
||||||
code, rspn = await self.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["dishes_count"] == 0
|
|
||||||
|
|
||||||
# Проверяем подменю на наличие блюд
|
|
||||||
code, rspn = await self.Submenu.get(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn["dishes_count"] == 0
|
|
||||||
|
|
||||||
# Проверяем удаленное блюдо
|
|
||||||
code, rspn = await self.Dish.get(client, menu, submenu, dish)
|
|
||||||
assert code == 404
|
|
||||||
|
|
||||||
# удаляем сопутствующее
|
|
||||||
await self.Submenu.delete(client, menu, submenu)
|
|
||||||
await self.Menu.delete(client, menu)
|
|
||||||
|
|
||||||
|
|
||||||
class TestСontinuity:
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_postman_continuity(self, client):
|
|
||||||
# Создаем меню
|
|
||||||
menu = {
|
|
||||||
"title": "Menu",
|
|
||||||
"description": "main menu",
|
|
||||||
}
|
|
||||||
code, rspn = await TestBaseCrud.Menu.write(client, menu)
|
|
||||||
assert code == 201
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
menu.update(rspn)
|
|
||||||
|
|
||||||
# Создаем подменю
|
|
||||||
submenu = {
|
|
||||||
"title": "Submenu",
|
|
||||||
"description": "submenu",
|
|
||||||
"parent_menu": menu["id"],
|
|
||||||
}
|
|
||||||
code, rspn = await TestBaseCrud.Submenu.write(client, menu, submenu)
|
|
||||||
assert code == 201
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
submenu.update(rspn)
|
|
||||||
|
|
||||||
# Добавляем блюдо1
|
|
||||||
dish = {
|
|
||||||
"title": "dish1",
|
|
||||||
"description": "some dish1",
|
|
||||||
"price": "13.50",
|
|
||||||
"parent_submenu": submenu["id"],
|
|
||||||
}
|
|
||||||
code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish)
|
|
||||||
assert code == 201
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
dish.update(rspn)
|
|
||||||
|
|
||||||
# Добавляем блюдо2
|
|
||||||
dish = {
|
|
||||||
"title": "dish2",
|
|
||||||
"description": "some dish2",
|
|
||||||
"price": "12.50",
|
|
||||||
"parent_submenu": submenu["id"],
|
|
||||||
}
|
|
||||||
code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish)
|
|
||||||
assert code == 201
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
dish.update(rspn)
|
|
||||||
|
|
||||||
# Просматриваем конкретное меню
|
|
||||||
code, rspn = await TestBaseCrud.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
assert menu["id"] == rspn["id"]
|
|
||||||
assert "submenus_count" in rspn.keys()
|
|
||||||
assert rspn["submenus_count"] == 1
|
|
||||||
assert "dishes_count" in rspn.keys()
|
|
||||||
assert rspn["dishes_count"] == 2
|
|
||||||
|
|
||||||
# Просматриваем конкретное подменю
|
|
||||||
code, rspn = await TestBaseCrud.Submenu.get(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
assert "dishes_count" in rspn.keys()
|
|
||||||
assert rspn["dishes_count"] == 2
|
|
||||||
|
|
||||||
# Удаляем подменю
|
|
||||||
code = await TestBaseCrud.Submenu.delete(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
# Просматриваем список подменю
|
|
||||||
code, rspn = await TestBaseCrud.Submenu.read_all(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn == []
|
|
||||||
|
|
||||||
# Просматриваем список блюд
|
|
||||||
code, rspn = await TestBaseCrud.Dish.read_all(client, menu, submenu)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn == []
|
|
||||||
|
|
||||||
# Просматриваем конкретное меню
|
|
||||||
code, rspn = await TestBaseCrud.Menu.get(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
assert "id" in rspn.keys()
|
|
||||||
assert menu["id"] == rspn["id"]
|
|
||||||
assert "submenus_count" in rspn.keys()
|
|
||||||
assert rspn["submenus_count"] == 0
|
|
||||||
assert "dishes_count" in rspn.keys()
|
|
||||||
assert rspn["dishes_count"] == 0
|
|
||||||
|
|
||||||
# Удаляем меню
|
|
||||||
code = await TestBaseCrud.Menu.delete(client, menu)
|
|
||||||
assert code == 200
|
|
||||||
|
|
||||||
# Просматриваем все меню
|
|
||||||
code, rspn = await TestBaseCrud.Menu.read_all(client)
|
|
||||||
assert code == 200
|
|
||||||
assert rspn == []
|
|
@@ -1,100 +0,0 @@
|
|||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from fastfood.cruds.menu import MenuCrud
|
|
||||||
from fastfood.cruds.submenu import SubMenuCrud
|
|
||||||
from fastfood.models import Menu, SubMenu
|
|
||||||
from fastfood.schemas import Menu as menuschema
|
|
||||||
from fastfood.schemas import MenuBase as menubaseschema
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_menu(asession: AsyncSession) -> None:
|
|
||||||
async with asession:
|
|
||||||
# Создаем меню
|
|
||||||
menu: Menu = Menu(title="SomeMenu", description="SomeDescription")
|
|
||||||
menu: Menu = await MenuCrud.create_menu_item(
|
|
||||||
menubaseschema.model_validate(menu),
|
|
||||||
asession,
|
|
||||||
)
|
|
||||||
menu_id: UUID = menu.id
|
|
||||||
|
|
||||||
# Получаем его же
|
|
||||||
req_menu: Menu | None = await MenuCrud.get_menu_item(menu_id, asession)
|
|
||||||
assert menu == req_menu
|
|
||||||
|
|
||||||
# Получаем все меню и проверяем
|
|
||||||
req_menus = await MenuCrud.get_menus(asession)
|
|
||||||
assert menu == req_menus.scalars().all()[0]
|
|
||||||
|
|
||||||
# Обновляем
|
|
||||||
menu.title = "updatedMenu"
|
|
||||||
await MenuCrud.update_menu_item(
|
|
||||||
menu.id, menuschema.model_validate(menu), asession
|
|
||||||
)
|
|
||||||
# И сверяем
|
|
||||||
req_menu = await MenuCrud.get_menu_item(menu_id, asession)
|
|
||||||
assert menu == req_menu
|
|
||||||
|
|
||||||
# Удаляем и проверяем
|
|
||||||
await MenuCrud.delete_menu_item(menu_id, asession)
|
|
||||||
req_menus = await MenuCrud.get_menus(asession)
|
|
||||||
assert req_menus.all() == []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_submenu(asession: AsyncSession) -> None:
|
|
||||||
async with asession:
|
|
||||||
# Создаем меню напрямую
|
|
||||||
menu: Menu = Menu(title="SomeMenu", description="SomeDescription")
|
|
||||||
asession.add(menu)
|
|
||||||
await asession.commit()
|
|
||||||
await asession.refresh(menu)
|
|
||||||
menu_id: UUID = menu.id
|
|
||||||
|
|
||||||
# Создаем подменю
|
|
||||||
submenu: SubMenu = SubMenu(
|
|
||||||
title="submenu",
|
|
||||||
description="",
|
|
||||||
parent_menu=menu_id,
|
|
||||||
)
|
|
||||||
submenu = await SubMenuCrud.create_submenu_item(
|
|
||||||
menu_id,
|
|
||||||
menubaseschema.model_validate(submenu),
|
|
||||||
asession,
|
|
||||||
)
|
|
||||||
submenu_id = submenu.id
|
|
||||||
|
|
||||||
# Проверяем подменю
|
|
||||||
req_submenu = await SubMenuCrud.get_submenu_item(
|
|
||||||
menu_id, submenu.id, asession,
|
|
||||||
)
|
|
||||||
assert submenu == req_submenu
|
|
||||||
|
|
||||||
# Обновляем меню
|
|
||||||
submenu.title = "UpdatedSubmenu"
|
|
||||||
req_submenu = await SubMenuCrud.update_submenu_item(
|
|
||||||
submenu_id, menubaseschema.model_validate(submenu), asession,
|
|
||||||
)
|
|
||||||
assert submenu == req_submenu.scalar_one_or_none()
|
|
||||||
|
|
||||||
menu = await MenuCrud.get_menu_item(menu_id, asession)
|
|
||||||
assert 1 == menu.submenus_count
|
|
||||||
|
|
||||||
# Удаляем полменю
|
|
||||||
await SubMenuCrud.delete_submenu_item(submenu_id, asession)
|
|
||||||
|
|
||||||
menu = await MenuCrud.get_menu_item(menu_id, asession)
|
|
||||||
assert 0 == menu.submenus_count
|
|
||||||
|
|
||||||
await MenuCrud.delete_menu_item(menu_id, asession)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_dish(asession: AsyncSession):
|
|
||||||
"""Not Implemented yet"""
|
|
||||||
async with asession:
|
|
||||||
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