Compare commits

...

78 Commits

Author SHA1 Message Date
8189aaedd4 fix: Поправил TypeHints и убраз неиспользуемые сущности 2024-02-14 15:34:24 +03:00
5ef6aaeb6f маленькие правки 2024-02-13 13:09:09 +03:00
f75415d9d9 Удаление блюда 2024-02-13 12:38:13 +03:00
4c3779776d Readme 2024-02-13 02:44:24 +03:00
d54e704dfb fix: volumes не примонтировал 2024-02-13 00:02:22 +03:00
68594eb7f0 слияние веток 2024-02-12 23:52:21 +03:00
8bfa166987 слияние веток 2024-02-12 23:09:50 +03:00
e0a81cf126 google sheets docker образ 2024-02-12 23:09:01 +03:00
a4f8bce657 google синхронизация 2024-02-12 23:09:01 +03:00
9ba42aae9f upd фоновая задача теперь не дропает базу 2024-02-12 23:09:01 +03:00
afdf1c5e2b fix 2024-02-12 23:09:01 +03:00
74c0ccae2a fix 2024-02-12 23:09:01 +03:00
2c48529a02 fix 2024-02-12 23:09:01 +03:00
cedf27a04d fix 2024-02-12 23:09:01 +03:00
e0798de713 fix 2024-02-12 23:09:01 +03:00
5a133a05e1 fix 2024-02-12 23:09:01 +03:00
3df3c67e7c fix: правка урла кролика 2024-02-12 23:09:01 +03:00
a0ebe9bdb9 upd: Контейнеры для celery & rabbitmq 2024-02-12 23:09:01 +03:00
ed3d7d9352 upd Разнес тесты, уменьшив портянку
upd Тест для summary роута
2024-02-12 23:09:01 +03:00
3dbefda936 upd: Применение скидки в выводе API 2024-02-12 23:09:01 +03:00
5a95b06300 upd: Добавил bg_task xlsx>>DBase 2024-02-12 23:09:01 +03:00
ebe75b6dc3 upd: Добавил роут summary с выводом вмего меню со вложением 2024-02-12 23:09:01 +03:00
22a876d3ce google sheets docker образ 2024-02-12 23:03:28 +03:00
6a0776557d google синхронизация 2024-02-12 22:49:16 +03:00
b2a284d791 upd фоновая задача теперь не дропает базу 2024-02-12 22:22:59 +03:00
5e213e759d fix 2024-02-12 03:03:24 +03:00
f28637f5dd fix 2024-02-12 02:42:46 +03:00
e6d1070d9a fix 2024-02-12 01:42:53 +03:00
47cb0e08c7 fix 2024-02-12 01:29:06 +03:00
e6576e9e58 fix 2024-02-12 01:11:00 +03:00
02134d247a fix 2024-02-12 01:06:45 +03:00
68db31a033 fix: правка урла кролика 2024-02-12 00:54:53 +03:00
fc9577c538 upd: Контейнеры для celery & rabbitmq 2024-02-12 00:39:51 +03:00
550a058b6f upd Разнес тесты, уменьшив портянку
upd Тест для summary роута
2024-02-11 23:17:57 +03:00
ffb5b855c4 upd: Применение скидки в выводе API 2024-02-11 20:10:25 +03:00
d9633dcfbd upd: Добавил bg_task xlsx>>DBase 2024-02-11 03:14:17 +03:00
e4656825cb upd: Добавил роут summary с выводом вмего меню со вложением 2024-02-09 02:57:34 +03:00
3120910552 Fix .env для локального запуска 2024-02-07 12:44:42 +03:00
3b1a1614cf fix: .env for local run 2024-02-07 12:37:43 +03:00
aa7db7cd35 Обновить README.md 2024-02-06 23:16:18 +03:00
27904e0c6a .env
добавил шаблон, чтоб не копировать файл постоянно
2024-02-06 22:46:25 +03:00
ee709a489e flow using openapi.json 2024-02-06 22:41:29 +03:00
f8cca4b861 flow dump openapi.json 2024-02-06 22:07:25 +03:00
7d4c4d9be3 fix: typehint в serv/repos 2024-02-06 15:50:02 +03:00
095ab07ebb fix: typehint в routes 2024-02-06 15:25:19 +03:00
f72c6fe4d7 fix: reverse() получает урл из имени ендпоинта 2024-02-06 15:12:32 +03:00
a2ed5a6732 fix 2024-02-05 23:30:18 +03:00
b3509d698d fix 2024-02-05 23:14:23 +03:00
5c8c3f16ae Merge branch 'develop' 2024-02-05 20:31:24 +03:00
c6e8e78c95 fix 2024-02-05 20:30:02 +03:00
749e37354d f 2024-02-05 20:02:32 +03:00
a5eebd15ba тесты 2024-02-05 19:13:40 +03:00
43eca19d91 REDIS кэширование, на локалке 2024-02-05 03:40:12 +03:00
291c61f873 reverse_url в тестах 2024-02-04 18:40:58 +00:00
09d0627d70 service.menu typehint 2024-02-04 18:26:17 +03:00
5173fcd36c repo.menu typehint 2024-02-04 17:59:20 +03:00
2754b82b5d service/repo.submenu typehint 2024-02-04 17:49:55 +03:00
35659529b4 service/repo.submenu typehint 2024-02-04 17:13:37 +03:00
181c6f10af service.dish typehint 2024-02-04 02:49:06 +03:00
015a0bcc87 service.dish typehint 2024-02-04 02:41:12 +03:00
628babc295 repo.dish typehint 2024-02-04 02:28:12 +03:00
f807bdd275 fix: Поправил pydantic валидацию, убрал костыль 2024-02-04 00:24:09 +03:00
2afba14e44 fix: Поправил pydantic валидацию, убрал костыль 2024-02-04 00:14:13 +03:00
45dd8dc73e конфиг из openapi.json 2024-02-03 02:58:06 +03:00
f667026d62 add .pre-commit-config.yaml Поправил проблемы 2024-02-03 01:08:04 +03:00
0ba422397a Рефакт. Добавил слой сервис, подготовил к кэшированию 2024-02-02 23:38:12 +03:00
b223053cf6 sync 2024-02-02 13:01:37 +00:00
58ecd82bb6 sinc 2024-02-02 08:04:02 +03:00
995be04dcb правки 2024-01-31 14:52:14 +03:00
e2185cc904 ридмишечку поправил 2024-01-31 02:21:12 +03:00
7eefa8e5db ридмишечку поправил 2024-01-31 02:14:43 +03:00
75e3036e13 dep remove 2024-01-31 01:55:12 +03:00
f86a783d1c env 2024-01-31 01:30:18 +03:00
64bc03b7fa dishcrud test 2024-01-31 01:05:28 +03:00
ead24d9f28 FIXDOCKER и typehint в тестах 2024-01-30 23:11:40 +03:00
f61cb3a2ee Merge branch 'develop' of https://git.pi3c.ru/pi3c/fastfood into develop
Объединение изменений с разных устройств
2024-01-30 13:09:54 +03:00
e378bf1da1 typehint в app и conftest 2024-01-30 13:05:15 +03:00
732bf9928c typehint в test_api 2024-01-30 09:50:46 +00:00
57 changed files with 5154 additions and 1450 deletions

14
.env Normal file
View 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
View File

@@ -217,4 +217,3 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

50
.pre-commit-config.yaml Normal file
View 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'

View File

@@ -1,15 +1,19 @@
FROM python:3.10-slim
RUN mkdir /fastfood
WORKDIR /fastfood
COPY . .
RUN pip install poetry
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

172
README.md
View File

@@ -5,157 +5,87 @@ Fastapi веб приложение реализующее api для общеп
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
## Техническое задание
### Спринт 1 - Создание API
Написать проект на FastAPI с использованием PostgreSQL в качестве БД. В проекте следует реализовать REST API по работе с меню ресторана, все CRUD операции. Для проверки задания, к презентаций будет приложена Postman коллекция с тестами. Задание выполнено, если все тесты проходят успешно.
Даны 3 сущности: Меню, Подменю, Блюдо.
### Спринт 4 - Многопроцессорность, асинхронность
В этом домашнем задании необходимо:
1.Переписать текущее FastAPI приложение на асинхронное выполнение
2.Добавить в проект фоновую задачу с помощью Celery + RabbitMQ.
3.Добавить эндпоинт (GET) для вывода всех меню со всеми связанными подменю и со всеми связанными блюдами.
4.Реализовать инвалидация кэша в background task (встроено в FastAPI)
5.* Обновление меню из google sheets раз в 15 сек.
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
Зависимости:
- У меню есть подменю, которые к ней привязаны.
- У подменю есть блюда.
Фоновая задача: синхронизация Excel документа и БД.
В проекте создаем папку admin. В эту папку кладем файл Menu.xlsx (будет прикреплен к ДЗ). Не забываем запушить в гит.
При внесении изменений в файл все изменения должны отображаться в БД. Периодичность обновления 15 сек. Удалять БД при каждом обновлении нельзя.
Условия:
- Блюдо не может быть привязано напрямую к меню, минуя подменю.
- Блюдо не может находиться в 2-х подменю одновременно.
- Подменю не может находиться в 2-х меню одновременно.
- Если удалить меню, должны удалиться все подменю и блюда этого меню.
- Если удалить подменю, должны удалиться все блюда этого подменю.
- Цены блюд выводить с округлением до 2 знаков после запятой.
- Во время выдачи списка меню, для каждого меню добавлять кол-во подменю и блюд в этом меню.
- Во время выдачи списка подменю, для каждого подменю добавлять кол-во блюд в этом подменю.
- Во время запуска тестового сценария БД должна быть пуста.
В папке ./postman_scripts находятся фалы тестов Postman, для тестирования функционала проекта.
Требования:
●Данные меню, подменю, блюд для нового эндпоинта должны доставаться одним ORM-запросом в БД (использовать подзапросы и агрегирующие функций SQL).
●Проект должен запускаться одной командой
●Проект должен соответствовать требованиям всех предыдущих вебинаров. (Не забыть добавить тесты для нового API эндпоинта)
### Спринт 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
### Выполненные доп задания со *
Спринт 2
3.* Реализовать вывод количества подменю и блюд для Меню через один (сложный) ORM запрос.
`./fastfood/repository/menu.py` Метод `get_menu_item`
4.** Реализовать тестовый сценарий «Проверка кол-ва блюд и подменю в меню» из Postman с помощью pytest
Если FastAPI синхронное - тесты синхронные, Если асинхронное - тесты асинхронные
`./tests/test_postman.py`
Спринт 3
5.* Описать ручки API в соответствий c OpenAPI
'./openapi.json'
6.** Реализовать в тестах аналог Django reverse() для FastAPI
'./tests/urls.py'
Спринт 4
5.* Обновление меню из google sheets раз в 15 сек.
`./bg_tasks/` Реализовано чтение как локальной, так и удаленной таблицы.
В зависимости какой compose поднять, тот и будет использоваться
6.** Блюда по акции. Размер скидки (%) указывается в столбце G файла Menu.xlsx
`./fastfood/service/dish.py`, метод _get_discont, подменяет сумму в выдаче,
скидка хранится в REDIS под ключами вида DISCONT:{UUID блюда}
*Оборачиваем приложение в докер.
**CRUD create/update/retrieve/delete.
<a href="https://drive.google.com/drive/folders/13t6fsMO0B6Ls0qYl-uVgAHWOyhFTFv4Z?usp=sharing">Дополнительные материалы</a>
## Возможности
### Спринт 1
В проекте реализованы 3 сущности: Menu, SubMenu и Dish. Для каждого них реализованы 4 метода http запросов: GET, POST, PATCH и DELETE c помощью которых можно управлять данными.
Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish.
### Спринт 2
- 1й пункт ТЗ
Тесты реализованы в виде 2х классов
`TastBaseCrud` включает 3 подкласса `Menu`, `Submenu`, `Dish` которые реализуют интерфейсы взаимодействия с endpoint'ами реализованных на предыдущем спринте сущностей. Каждый подкласс реализует методы GET(получение всех сущностей), Get(получение конкректной сущности), Post(создание), Patch(обновление), Delete(удаления). Так же в классе реализованы 3 тестовых функции, которые осуществляют тестирование соответствующих endpoint'ов
`TestContinuity` реализует последовательность сценария «Проверка кол-ва блюд и подменю в меню» из Postman
- 2й пункт ТЗ
Реализованы 3 контейнера(db, app, tests). В db написан блок "проверки здоровья", от которого зависят контейнеры app и test, который гарантирует, что зависимые контейнеры не будут запущены о полной готовности db.
- 3й пункт ТЗ
см. функцию `get_menu_item` на 28 строке в файле
<base_dir>/fastfood/crud/menu.py
- 4й пункт ТЗ
см. класс `TestContinuity` в файле
<base_dir>/tests/test_api.py
## Зависимости
Для локальной установки
- postgresql Для работы сервиса необходима установленная СУБД. Должна быть создана база данных и пользователь с правами на нее.
- poetry - Система управления зависимостями в Python.
Остальное добавится автоматически на этапе установки.
Для запуска в контейнере
- docker
- docker-compose
## Установка
### Docker
Для запуска необходимы установленные приложения docker и docker-compose
Клонируйте репозиторий
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
Перейдите в каталог
> `$ 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`
Запуск/остановка образов:
Создайте и запустите образы
> `$ docker-compose up -d --build`
- Запуск FAstAPI приложения c локальным файлом для фоновой задачи
> `$ docker-compose -f compose_app.yml up`
- Запуск FAstAPI приложения c Google Sheets для фоновой задачи
> `$ docker-compose -f compose_google.yml up`
(ЧИТАЙТЕ СООБЩЕНИЕ В ЧАТЕ)
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
Для запуска тестов pytest поднимаем контейнер tests
> `$ docker-compose up tests`
По завершении работы остановите контейнеры
> `$ docker-compose -f compose_app.yml down`
### Linux
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
- Запуск тестов
> `$ docker-compose -f compose_test.yml up`
Установите систему управления зависимостями
> `$ pip[x] install poetry`
По завершении работы остановите контейнеры
> `$ docker-compose -f compose_test.yml down`
Клонируйте репозиторий
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
Перейдите в каталог
> `$ 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
- Написать тесты для кривых данных
- Добавить миграции
- Провести рефакторинг, много дублирующего кода
- Много чего другого :)
## Авторы
@@ -163,5 +93,3 @@ Fastapi веб приложение реализующее api для общеп
## Лицензия
Распространяется под [MIT лицензией](https://mit-license.org/).

BIN
admin/Menu.xlsx Normal file

Binary file not shown.

0
bg_tasks/__init__.py Normal file
View File

50
bg_tasks/bg_task.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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'

View File

@@ -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"]

View File

@@ -1,5 +0,0 @@
DB_HOST=db
DB_PORT=5432
POSTGRES_USER=postges
POSTGRES_PASSWORD=postgres
POSTGRES_DB=fastfood_db

View File

@@ -1,87 +1,27 @@
import json
from fastapi import FastAPI
from fastfood.routers.dish import router as dish_router
from fastfood.routers.menu import router as menu_router
from fastfood.routers.submenu import router as submenu_router
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**.
## Приятного аппетита
"""
from fastfood.routers.summary import router as summary_router
tags_metadata = [
{
"name": "menu",
"description": "Операции с меню.",
},
{
"name": "submenu",
"description": "Подменю и работа с ним",
},
{"name": "dish", "description": "Блюда и работа с ними"},
]
def create_app():
def create_app() -> FastAPI:
"""
Фабрика 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 = FastAPI()
app.include_router(menu_router)
app.include_router(submenu_router)
app.include_router(dish_router)
app.include_router(summary_router)
def custom_openapi():
with open('openapi.json') as openapi:
return json.load(openapi)
app.openapi = custom_openapi
return app

View File

@@ -1,22 +1,39 @@
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DB_HOST: str = "db"
DB_PORT: int = 5432
POSTGRES_DB: str = "fastfod_db"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_USER: str = "postgres"
# Конфиг PostgreSql
POSTGRES_HOST: str = ''
POSTGRES_PORT: int = 5432
POSTGRES_DB: str = ''
POSTGRES_PASSWORD: str = ''
POSTGRES_USER: str = ''
POSTGRES_DB_TEST: str = ''
# Конфиг Redis
REDIS_HOST: str = ''
REDIS_PORT: int = 6379
REDIS_DB: int = 0
@property
def DATABASE_URL_asyncpg(self):
def DATABASE_URL_asyncpg(self) -> str:
"""
Возвращает строку подключения к БД необходимую для SQLAlchemy
"""
# Проверяем, в DOCKER или нет
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return (
"postgresql+asyncpg://"
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}"
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@db:5432/{self.POSTGRES_DB}'
)
return (
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}'
)
@property
@@ -24,13 +41,43 @@ class Settings(BaseSettings):
"""
Возвращает строку подключения к БД необходимую для 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"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}_test"
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@db:5432/{self.POSTGRES_DB_TEST}'
)
model_config = SettingsConfigDict(env_file=".env")
return (
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB_TEST}'
)
@property
def REDIS_URL(self):
"""
Возвращает строку подключения к REDIS
"""
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return 'redis://redis:6379/0'
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
@property
def REBBITMQ_URL(self):
"""
Возвращает строку подключения к REBBITMQ
"""
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return 'amqp://guest:guest@rabbitmq'
return 'amqp://guest:guest@127.0.0.1'
model_config = SettingsConfigDict(env_file='.env')
settings = Settings()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -1,7 +1,8 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker,
create_async_engine)
import redis.asyncio as redis # type: ignore
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastfood.config import settings
@@ -16,3 +17,13 @@ async_session_maker = async_sessionmaker(
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
def get_redis_pool():
return redis.from_url(settings.REDIS_URL, decode_responses=False)
async def get_async_redis_client(
redis_pool: redis.Redis = Depends(get_redis_pool),
):
return redis_pool

View File

@@ -1,6 +1,6 @@
import uuid
from copy import deepcopy
from typing import Annotated, List, Optional
from typing import Annotated
from sqlalchemy import ForeignKey
from sqlalchemy.dialects.postgresql import UUID
@@ -21,13 +21,13 @@ str_25 = Annotated[str, 25]
class Base(DeclarativeBase):
id: Mapped[uuidpk]
title: Mapped[str_25]
description: Mapped[Optional[str]]
description: Mapped[str | None]
def __eq__(self, other):
classes_match = isinstance(other, self.__class__)
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
a.pop("_sa_instance_state", None)
b.pop("_sa_instance_state", None)
a.pop('_sa_instance_state', None)
b.pop('_sa_instance_state', None)
attrs_match = a == b
return classes_match and attrs_match
@@ -36,13 +36,13 @@ class Base(DeclarativeBase):
class Menu(Base):
__tablename__ = "menu"
__tablename__ = 'menu'
submenus: Mapped[List["SubMenu"]] = relationship(
"SubMenu",
backref="menu",
lazy="selectin",
cascade="all, delete",
submenus: Mapped[list['SubMenu']] = relationship(
'SubMenu',
backref='menu',
lazy='selectin',
cascade='all, delete',
)
@hybridproperty
@@ -58,16 +58,16 @@ class Menu(Base):
class SubMenu(Base):
__tablename__ = "submenu"
__tablename__ = 'submenu'
parent_menu: Mapped[uuid.UUID] = mapped_column(
ForeignKey("menu.id", ondelete="CASCADE")
ForeignKey('menu.id', ondelete='CASCADE')
)
dishes: Mapped[List["Dish"]] = relationship(
"Dish",
backref="submenu",
lazy="selectin",
cascade="all, delete",
dishes: Mapped[list['Dish']] = relationship(
'Dish',
backref='submenu',
lazy='selectin',
cascade='all, delete',
)
@hybridproperty
@@ -76,9 +76,9 @@ class SubMenu(Base):
class Dish(Base):
__tablename__ = "dish"
__tablename__ = 'dish'
price: Mapped[float]
parent_submenu: Mapped[uuid.UUID] = mapped_column(
ForeignKey("submenu.id", ondelete="CASCADE")
ForeignKey('submenu.id', ondelete='CASCADE')
)

View File

@@ -1,9 +1,9 @@
from fastfood import models
from fastfood.dbase import async_engine
from .dish import DishCrud
from .menu import MenuCrud
from .submenu import SubMenuCrud
from .dish import DishRepository
from .menu import MenuRepository
from .submenu import SubMenuRepository
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)
class Crud(MenuCrud, SubMenuCrud, DishCrud):
class Repository(MenuRepository, SubMenuRepository, DishRepository):
pass
crud = Crud()
ropo = Repository()

View 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()

View 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()

View 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)

View 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()

View 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()]

View File

@@ -1,80 +1,102 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import schemas
from fastfood.cruds import crud
from fastfood.dbase import get_async_session
from fastfood.utils import price_converter
from fastfood.schemas import Dish, DishBase
from fastfood.service.dish import DishService
router = APIRouter(
prefix="/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes",
tags=["dish"],
prefix='/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes',
tags=['dish'],
)
@router.get("/")
@router.get(
'/',
response_model=list[Dish],
)
async def get_dishes(
menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session)
):
result = await crud.get_dishes(submenu_id=submenu_id, session=session)
menu_id: UUID,
submenu_id: UUID,
dish: DishService = Depends(),
) -> list[Dish]:
result = await dish.read_dishes(menu_id, submenu_id)
return result
@router.post("/", status_code=201)
@router.post(
'/',
status_code=201,
response_model=Dish,
)
async def create_dish(
menu_id: UUID,
submenu_id: UUID,
dish: schemas.DishBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.create_dish_item(
submenu_id=submenu_id,
dish=dish,
session=session,
dish_data: DishBase,
dish: DishService = Depends(),
) -> Dish:
return await dish.create_dish(
menu_id,
submenu_id,
dish_data,
)
return price_converter(result)
@router.get("/{dish_id}")
@router.get(
'/{dish_id}',
response_model=Dish,
)
async def get_dish(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.get_dish_item(
dish_id=dish_id,
session=session,
dish: DishService = Depends(),
) -> Dish | None:
result = await dish.read_dish(
menu_id,
submenu_id,
dish_id,
)
if not result:
raise HTTPException(status_code=404, detail="dish not found")
return price_converter(result)
raise HTTPException(
status_code=404,
detail='dish not found',
)
return result
@router.patch("/{dish_id}")
@router.patch(
'/{dish_id}',
response_model=Dish,
)
async def update_dish(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
dish: schemas.DishBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.update_dish_item(
dish_id=dish_id,
dish=dish,
session=session,
dish_data: DishBase,
dish: DishService = Depends(),
) -> Dish:
result = await dish.update_dish(
menu_id,
submenu_id,
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(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
await crud.delete_dish_item(dish_id=dish_id, session=session)
dish: DishService = Depends(),
) -> None:
await dish.del_dish(menu_id, dish_id)

View File

@@ -1,66 +1,85 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import schemas
from fastfood.cruds import crud
from fastfood.dbase import get_async_session
from fastfood.schemas import MenuBase, MenuRead
from fastfood.service.menu import MenuService
router = APIRouter(
prefix="/api/v1/menus",
tags=["menu"],
prefix='/api/v1/menus',
tags=['menu'],
)
@router.get("/", response_model=Optional[List[schemas.Menu]])
async def get_menus(session: AsyncSession = Depends(get_async_session)):
result = await crud.get_menus(session=session)
return result.scalars().all()
@router.get(
'/',
status_code=200,
response_model=list[MenuRead],
)
async def get_menus(
menu: MenuService = Depends(),
) -> list[MenuRead]:
return await menu.read_menus()
@router.post("/", status_code=201, response_model=schemas.Menu)
@router.post(
'/',
status_code=201,
response_model=MenuRead,
)
async def add_menu(
menu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.create_menu_item(
menu=menu,
session=session,
menu: MenuBase,
responce: MenuService = Depends(),
) -> MenuRead:
return await responce.create_menu(menu)
@router.get(
'/{menu_id}',
response_model=MenuRead,
)
return result
@router.get("/{menu_id}", response_model=schemas.MenuRead)
async def get_menu(
menu_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.get_menu_item(menu_id=menu_id, session=session)
responce: MenuService = Depends(),
) -> MenuRead:
result = await responce.read_menu(menu_id=menu_id)
if not result:
raise HTTPException(status_code=404, detail="menu not found")
raise HTTPException(
status_code=404,
detail='menu not found',
)
return result
@router.patch("/{menu_id}", response_model=schemas.MenuBase)
@router.patch(
'/{menu_id}',
response_model=MenuRead,
)
async def update_menu(
menu_id: UUID,
menu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.update_menu_item(
menu: MenuBase,
responce: MenuService = Depends(),
) -> MenuRead:
result = await responce.update_menu(
menu_id=menu_id,
menu=menu,
session=session,
menu_data=menu,
)
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(
menu_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
await crud.delete_menu_item(menu_id=menu_id, session=session)
menu: MenuService = Depends(),
) -> None:
await menu.del_menu(menu_id)

View File

@@ -1,76 +1,96 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import schemas
from fastfood.cruds import crud
from fastfood.dbase import get_async_session
from fastfood.schemas import MenuBase, SubMenuRead
from fastfood.service.submenu import SubmenuService
router = APIRouter(
prefix="/api/v1/menus/{menu_id}/submenus",
tags=["submenu"],
prefix='/api/v1/menus/{menu_id}/submenus',
tags=['submenu'],
)
@router.get("/")
@router.get(
'/',
response_model=list[SubMenuRead],
)
async def get_submenus(
menu_id: UUID, session: AsyncSession = Depends(get_async_session)
):
result = await crud.get_submenus(menu_id=menu_id, session=session)
return result.scalars().all()
menu_id: UUID,
submenu: SubmenuService = Depends(),
) -> list[SubMenuRead]:
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(
menu_id: UUID,
submenu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.create_submenu_item(
submenu_data: MenuBase,
submenu: SubmenuService = Depends(),
) -> SubMenuRead:
result = await submenu.create_submenu(
menu_id=menu_id,
submenu=submenu,
session=session,
submenu_data=submenu_data,
)
return result
@router.get("/{submenu_id}", response_model=schemas.SubMenuRead)
@router.get(
'/{submenu_id}',
response_model=SubMenuRead,
)
async def get_submenu(
menu_id: UUID,
submenu_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.get_submenu_item(
submenu: SubmenuService = Depends(),
) -> SubMenuRead:
result = await submenu.read_menu(
menu_id=menu_id,
submenu_id=submenu_id,
session=session,
)
if not result:
raise HTTPException(status_code=404, detail="submenu not found")
raise HTTPException(
status_code=404,
detail='submenu not found',
)
return result
@router.patch(
"/{submenu_id}",
response_model=schemas.MenuBase,
'/{submenu_id}',
response_model=SubMenuRead,
)
async def update_submenu(
menu_id: UUID,
submenu_id: UUID,
submenu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.update_submenu_item(
submenu_data: MenuBase,
submenu: SubmenuService = Depends(),
) -> SubMenuRead:
result = await submenu.update_submenu(
menu_id=menu_id,
submenu_id=submenu_id,
submenu=submenu,
session=session,
submenu_data=submenu_data,
)
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(
menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session)
):
await crud.delete_submenu_item(submenu_id=submenu_id, session=session)
menu_id: UUID,
submenu_id: UUID,
submenu: SubmenuService = Depends(),
) -> None:
await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id)

View 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()

View File

@@ -1,4 +1,3 @@
from typing import Optional
from uuid import UUID
from pydantic import BaseModel
@@ -6,7 +5,7 @@ from pydantic import BaseModel
class MenuBase(BaseModel):
title: str
description: Optional[str]
description: str | None
class Config:
from_attributes = True
@@ -26,8 +25,20 @@ class SubMenuRead(Menu):
class DishBase(MenuBase):
price: float
price: str
class Dish(DishBase, Menu):
pass
class Dish_db(MenuBase):
price: float
class SubMenuSummary(Menu):
dishes: list[Dish_db]
class MenuSummary(Menu):
submenus: list[SubMenuSummary]

144
fastfood/service/dish.py Normal file
View 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
View 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
View 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)

View 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

View File

@@ -1,3 +0,0 @@
def price_converter(dish: dict) -> dict:
dish.price = str(dish.price)
return dish

View File

@@ -1,18 +1,34 @@
import asyncio
import multiprocessing
import sys
from subprocess import Popen
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
"""
uvicorn.run(
app="fastfood.app:create_app",
host="0.0.0.0",
app='fastfood.app:create_app',
host='0.0.0.0',
port=8000,
reload=True,
factory=True,
@@ -20,15 +36,24 @@ def run_app():
)
async def recreate():
async def recreate() -> None:
"""Удаление и создание таблиц в базе данных для тестирования"""
await create_db_and_tables()
if __name__ == "__main__":
if "--run-server" in sys.argv:
run_app()
if __name__ == '__main__':
if '--run-docker-server' in sys.argv:
"""Запуск FastAPI в докере. Celery запускается в отдельном контейнере"""
loop.run_until_complete(recreate())
loop.run_until_complete(run_app())
if "--run-test-server" in sys.argv:
asyncio.run(recreate())
run_app()
if '--run-local-server' in sys.argv:
"""Локальный запуск FastAPI с запуском Celery в отдельных процессах"""
celery_worker_process.start()
celery_beat_process.start()
loop.run_until_complete(recreate())
loop.run_until_complete(run_app())
celery_beat_process.kill()
celery_worker_process.kill()

1203
openapi.json Normal file

File diff suppressed because it is too large Load Diff

1340
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,16 +12,21 @@ fastapi = "^0.109.0"
uvicorn = "^0.26.0"
asyncpg = "^0.29.0"
pydantic-settings = "^2.1.0"
psycopg2-binary = "^2.9.9"
email-validator = "^2.1.0.post1"
pytest-asyncio = "^0.23.3"
httpx = "^0.26.0"
pytest-cov = "^4.1.0"
alembic = "^1.13.1"
redis = "^4.6.0"
types-redis = "^4.6.0.3"
mypy = "^1.4.1"
celery = "^5.3.6"
openpyxl = "^3.1.2"
gspread = "^6.0.1"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.4"
pytest-cov = "^4.1.0"
httpx = "^0.26.0"
pre-commit = "^3.6.0"
[build-system]
requires = ["poetry-core"]

View File

@@ -1 +0,0 @@
CREATE DATABASE fastfood_db_test WITH OWNER postgres;

View File

@@ -1,5 +0,0 @@
#!/bin/bash
#
# Тут можно выполнить миграции или дополнительные перед запуском приложения
#
poetry run python manage.py --run-test-server

View File

@@ -1,2 +0,0 @@
#!/bin/bash
poetry run pytest -vv

View File

@@ -1,20 +1,17 @@
import asyncio
from typing import AsyncGenerator
from httpx import AsyncClient
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from fastfood.app import create_app
from fastapi import FastAPI
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastfood.app import create_app
from fastfood.config import settings
from fastfood.dbase import get_async_session
from fastfood.models import Base
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
async_session_maker = async_sessionmaker(
async_engine,
@@ -23,7 +20,7 @@ async_session_maker = async_sessionmaker(
)
@pytest.fixture(scope="session")
@pytest.fixture(scope='session', autouse=True)
def event_loop():
try:
loop = asyncio.get_event_loop()
@@ -33,8 +30,8 @@ def event_loop():
loop.close()
@pytest_asyncio.fixture(scope="function", autouse=True)
async def db_init():
@pytest_asyncio.fixture(scope='session', autouse=True)
async def db_init(event_loop):
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
@@ -48,22 +45,13 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
yield session
@pytest.fixture(scope="session")
def app():
app = create_app()
@pytest_asyncio.fixture(scope='session', autouse=True)
async def client(event_loop) -> AsyncGenerator[AsyncClient, None]:
app: FastAPI = create_app()
app.dependency_overrides[get_async_session] = get_test_session
yield app
@pytest_asyncio.fixture(scope="function")
async def client(app):
async with AsyncClient(
app=app, base_url="http://localhost:8000/api/v1/menus",
app=app,
base_url='http://localhost:8000',
) as 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
View 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()

View File

@@ -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 == []

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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