Compare commits
19 Commits
914814e267
...
bde9581090
Author | SHA1 | Date |
---|---|---|
Сергей Ванюшкин | bde9581090 | |
Сергей Ванюшкин | c27858e4fb | |
Сергей Ванюшкин | 479a997844 | |
Сергей Ванюшкин | 08f3297297 | |
Сергей Ванюшкин | b5da5736e9 | |
Сергей Ванюшкин | 1b5182b41a | |
Сергей Ванюшкин | bab8008ec8 | |
Сергей Ванюшкин | cae407a5f4 | |
Сергей Ванюшкин | dce3841d5a | |
Сергей Ванюшкин | e2428d7cdc | |
Сергей Ванюшкин | 51b5b909c9 | |
Сергей Ванюшкин | b282ceebe7 | |
Сергей Ванюшкин | 5ced7acef8 | |
Сергей Ванюшкин | 8f48352600 | |
Сергей Ванюшкин | d6f1347fab | |
Сергей Ванюшкин | b474e21f0f | |
Сергей Ванюшкин | b20ff8bceb | |
Сергей Ванюшкин | f09b5b57b2 | |
Сергей Ванюшкин | 0ae3293730 |
|
@ -0,0 +1,15 @@
|
|||
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 chmod a+x scripts/*.sh
|
75
README.md
75
README.md
|
@ -2,9 +2,10 @@
|
|||
Fastapi веб приложение реализующее api для общепита.
|
||||
|
||||
## Описание
|
||||
Данный проект, это результат выполнения практического домашнего задания интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||
Данный проект, это результат выполнения практических домашних заданий интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
|
||||
|
||||
### Техническое задание
|
||||
## Техническое задание
|
||||
### Спринт 1 - Создание API
|
||||
Написать проект на FastAPI с использованием PostgreSQL в качестве БД. В проекте следует реализовать REST API по работе с меню ресторана, все CRUD операции. Для проверки задания, к презентаций будет приложена Postman коллекция с тестами. Задание выполнено, если все тесты проходят успешно.
|
||||
Даны 3 сущности: Меню, Подменю, Блюдо.
|
||||
|
||||
|
@ -25,18 +26,85 @@ Fastapi веб приложение реализующее api для общеп
|
|||
|
||||
В папке ./postman_scripts находятся фалы тестов Postman, для тестирования функционала проекта.
|
||||
|
||||
### Спринт 2 - Docker && pytest
|
||||
В этом домашнем задании надо написать тесты для ранее разработанных ендпоинтов вашего API после Вебинара №1.
|
||||
|
||||
Обернуть программные компоненты в контейнеры. Контейнеры должны запускаться по одной команде “docker-compose up -d” или той которая описана вами в readme.md.
|
||||
|
||||
Образы для Docker:
|
||||
(API) python:3.10-slim
|
||||
(DB) postgres:15.1-alpine
|
||||
|
||||
1.Написать CRUD тесты для ранее разработанного API с помощью библиотеки pytest
|
||||
2.Подготовить отдельный контейнер для запуска тестов. Команду для запуска указать в README.md
|
||||
3.* Реализовать вывод количества подменю и блюд для Меню через один (сложный) ORM запрос.
|
||||
4.** Реализовать тестовый сценарий «Проверка кол-ва блюд и подменю в меню» из Postman с помощью pytest
|
||||
Если FastAPI синхронное - тесты синхронные, Если асинхронное - тесты асинхронные
|
||||
|
||||
|
||||
*Оборачиваем приложение в докер.
|
||||
**CRUD – create/update/retrieve/delete.
|
||||
|
||||
<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`
|
||||
|
||||
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
|
||||
|
||||
Для запуска тестов pytest поднимаем контейнер tests
|
||||
> `$ docker-compose up tests`
|
||||
|
||||
### Linux
|
||||
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
|
||||
|
||||
|
@ -56,7 +124,7 @@ Fastapi веб приложение реализующее api для общеп
|
|||
|
||||
Файл example.env является образцом файла .env, который необходимо создать перед запуском проекта.
|
||||
В нем указанны переменные необходимые для подключения к БД.
|
||||
Созданим файл .env
|
||||
Создадим файл .env
|
||||
|
||||
>`$ cp ./example.env ./.env`
|
||||
|
||||
|
@ -85,6 +153,7 @@ Fastapi веб приложение реализующее api для общеп
|
|||
|
||||
|
||||
## TODO
|
||||
- Написать тесты для кривых данных
|
||||
- Добавить миграции
|
||||
- Провести рефакторинг, много дублирующего кода
|
||||
- Много чего другого :)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres:15.1-alpine
|
||||
env_file:
|
||||
- .env
|
||||
container_name: pgdatabase
|
||||
ports:
|
||||
- 6432:5432
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./scripts/db_prepare.sql:/docker-entrypoint-initdb.d/db_prepare.sql
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
container_name: fastfood_app
|
||||
env_file:
|
||||
- .env
|
||||
command: ["/fastfood/scripts/migrate_and_run.sh"]
|
||||
ports:
|
||||
- 8000:8000
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
|
||||
tests:
|
||||
build:
|
||||
context: .
|
||||
container_name: tests
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
app:
|
||||
condition: service_started
|
||||
command: ["/fastfood/scripts/testing.sh"]
|
|
@ -1,5 +1,5 @@
|
|||
DB_HOST=localhost
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASS=postgres
|
||||
DB_NAME=postgres
|
||||
POSTGRES_USER=postges
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=fastfood_db
|
|
@ -2,11 +2,11 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
DB_HOST: str = "localhost"
|
||||
DB_HOST: str = "db"
|
||||
DB_PORT: int = 5432
|
||||
DB_USER: str = "postrges"
|
||||
DB_PASS: str = "postgres"
|
||||
DB_NAME: str = "postgres"
|
||||
POSTGRES_DB: str = "fastfod_db"
|
||||
POSTGRES_PASSWORD: str = "postgres"
|
||||
POSTGRES_USER: str = "postgres"
|
||||
|
||||
@property
|
||||
def DATABASE_URL_asyncpg(self):
|
||||
|
@ -14,8 +14,20 @@ class Settings(BaseSettings):
|
|||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
||||
"""
|
||||
return (
|
||||
f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}"
|
||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
"postgresql+asyncpg://"
|
||||
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}"
|
||||
)
|
||||
|
||||
@property
|
||||
def TESTDATABASE_URL_asyncpg(self):
|
||||
"""
|
||||
Возвращает строку подключения к БД необходимую для SQLAlchemy
|
||||
"""
|
||||
return (
|
||||
"postgresql+asyncpg://"
|
||||
f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||
f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}_test"
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy import delete, distinct, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from fastfood import models, schemas
|
||||
|
||||
|
@ -11,8 +12,8 @@ class MenuCrud:
|
|||
async def get_menus(session: AsyncSession):
|
||||
async with session:
|
||||
query = select(models.Menu)
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
menus = await session.execute(query)
|
||||
return menus
|
||||
|
||||
@staticmethod
|
||||
async def create_menu_item(menu: schemas.MenuBase, session: AsyncSession):
|
||||
|
@ -26,26 +27,26 @@ class MenuCrud:
|
|||
@staticmethod
|
||||
async def get_menu_item(menu_id: UUID, session: AsyncSession):
|
||||
async with session:
|
||||
query = select(models.Menu).where(models.Menu.id == menu_id)
|
||||
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
|
||||
submenu_query = select(
|
||||
func.count(models.SubMenu.id).label("counter")
|
||||
).filter(models.SubMenu.parent_menu == menu_id)
|
||||
counter = await session.execute(submenu_query)
|
||||
|
||||
dish_query = (
|
||||
select(func.count(models.Dish.id))
|
||||
.join(models.SubMenu)
|
||||
.filter(models.Dish.parent_submenu == models.SubMenu.id)
|
||||
.filter(models.SubMenu.parent_menu == menu_id)
|
||||
)
|
||||
dishes = await session.execute(dish_query)
|
||||
menu.submenus_count = counter.scalars().one_or_none()
|
||||
menu.dishes_count = dishes.scalars().one_or_none()
|
||||
return menu
|
||||
return menu
|
||||
|
||||
@staticmethod
|
||||
async def update_menu_item(
|
||||
|
@ -63,7 +64,7 @@ class MenuCrud:
|
|||
await session.commit()
|
||||
qr = select(models.Menu).where(models.Menu.id == menu_id)
|
||||
updated_menu = await session.execute(qr)
|
||||
return updated_menu.scalars().one()
|
||||
return updated_menu
|
||||
|
||||
@staticmethod
|
||||
async def delete_menu_item(menu_id: UUID, session: AsyncSession):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy import delete, distinct, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from fastfood import models, schemas
|
||||
|
||||
|
@ -10,9 +11,11 @@ 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)
|
||||
query = select(models.SubMenu).where(
|
||||
models.SubMenu.parent_menu == menu_id,
|
||||
)
|
||||
submenus = await session.execute(query)
|
||||
return submenus.scalars().all()
|
||||
return submenus
|
||||
|
||||
@staticmethod
|
||||
async def create_submenu_item(
|
||||
|
@ -24,9 +27,9 @@ class SubMenuCrud:
|
|||
new_submenu = models.SubMenu(**submenu.model_dump())
|
||||
new_submenu.parent_menu = menu_id
|
||||
session.add(new_submenu)
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
return new_submenu
|
||||
await session.refresh(new_submenu)
|
||||
return new_submenu
|
||||
|
||||
@staticmethod
|
||||
async def get_submenu_item(
|
||||
|
@ -35,21 +38,19 @@ class SubMenuCrud:
|
|||
session: AsyncSession,
|
||||
):
|
||||
async with session:
|
||||
query = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
|
||||
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
|
||||
|
||||
dish_query = (
|
||||
select(func.count(models.Dish.id))
|
||||
.join(models.SubMenu)
|
||||
.filter(models.Dish.parent_submenu == models.SubMenu.id)
|
||||
)
|
||||
dishes = await session.execute(dish_query)
|
||||
submenu.dishes_count = dishes.scalars().one_or_none()
|
||||
|
||||
return submenu
|
||||
return submenu
|
||||
|
||||
@staticmethod
|
||||
async def update_submenu_item(
|
||||
|
@ -67,11 +68,13 @@ class SubMenuCrud:
|
|||
await session.commit()
|
||||
qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
|
||||
updated_submenu = await session.execute(qr)
|
||||
return updated_submenu.scalars().one()
|
||||
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)
|
||||
query = delete(models.SubMenu).where(
|
||||
models.SubMenu.id == submenu_id,
|
||||
)
|
||||
await session.execute(query)
|
||||
await session.commit()
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import uuid
|
||||
from copy import deepcopy
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy.util import hybridproperty
|
||||
|
||||
uuidpk = Annotated[
|
||||
uuid.UUID,
|
||||
|
@ -21,6 +23,17 @@ class Base(DeclarativeBase):
|
|||
title: Mapped[str_25]
|
||||
description: Mapped[Optional[str]]
|
||||
|
||||
def __eq__(self, other):
|
||||
classes_match = isinstance(other, self.__class__)
|
||||
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
|
||||
a.pop("_sa_instance_state", None)
|
||||
b.pop("_sa_instance_state", None)
|
||||
attrs_match = a == b
|
||||
return classes_match and attrs_match
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class Menu(Base):
|
||||
__tablename__ = "menu"
|
||||
|
@ -28,10 +41,21 @@ class Menu(Base):
|
|||
submenus: Mapped[List["SubMenu"]] = relationship(
|
||||
"SubMenu",
|
||||
backref="menu",
|
||||
lazy="dynamic",
|
||||
lazy="selectin",
|
||||
cascade="all, delete",
|
||||
)
|
||||
|
||||
@hybridproperty
|
||||
def submenus_count(self):
|
||||
return len(self.submenus)
|
||||
|
||||
@hybridproperty
|
||||
def dishes_count(self):
|
||||
counter = 0
|
||||
for sub in self.submenus:
|
||||
counter += len(sub.dishes)
|
||||
return counter
|
||||
|
||||
|
||||
class SubMenu(Base):
|
||||
__tablename__ = "submenu"
|
||||
|
@ -42,10 +66,14 @@ class SubMenu(Base):
|
|||
dishes: Mapped[List["Dish"]] = relationship(
|
||||
"Dish",
|
||||
backref="submenu",
|
||||
lazy="dynamic",
|
||||
lazy="selectin",
|
||||
cascade="all, delete",
|
||||
)
|
||||
|
||||
@hybridproperty
|
||||
def dishes_count(self):
|
||||
return len(self.dishes)
|
||||
|
||||
|
||||
class Dish(Base):
|
||||
__tablename__ = "dish"
|
||||
|
|
|
@ -17,7 +17,7 @@ router = APIRouter(
|
|||
@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
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/", status_code=201, response_model=schemas.Menu)
|
||||
|
@ -38,6 +38,7 @@ async def get_menu(
|
|||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
result = await crud.get_menu_item(menu_id=menu_id, session=session)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="menu not found")
|
||||
return result
|
||||
|
@ -54,7 +55,7 @@ async def update_menu(
|
|||
menu=menu,
|
||||
session=session,
|
||||
)
|
||||
return result
|
||||
return result.scalars().one()
|
||||
|
||||
|
||||
@router.delete("/{menu_id}")
|
||||
|
|
|
@ -18,7 +18,7 @@ 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
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
|
@ -66,7 +66,7 @@ async def update_submenu(
|
|||
submenu=submenu,
|
||||
session=session,
|
||||
)
|
||||
return result
|
||||
return result.scalars().one()
|
||||
|
||||
|
||||
@router.delete("/{submenu_id}")
|
||||
|
|
|
@ -12,8 +12,11 @@ def run_app():
|
|||
"""
|
||||
uvicorn.run(
|
||||
app="fastfood.app:create_app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
factory=True,
|
||||
workers=1,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.13.1"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"},
|
||||
{file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
typing-extensions = ">=4"
|
||||
|
||||
[package.extras]
|
||||
tz = ["backports.zoneinfo"]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.6.0"
|
||||
|
@ -101,6 +120,17 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
|
|||
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2023.11.17"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
|
||||
{file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
|
@ -126,6 +156,73 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.4.1"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"},
|
||||
{file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"},
|
||||
{file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"},
|
||||
{file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"},
|
||||
{file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"},
|
||||
{file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"},
|
||||
{file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"},
|
||||
{file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.5.0"
|
||||
|
@ -276,6 +373,51 @@ files = [
|
|||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.2"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"},
|
||||
{file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.23.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.26.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
|
@ -298,6 +440,94 @@ files = [
|
|||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.0"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"},
|
||||
{file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["Babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.4"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"},
|
||||
{file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"},
|
||||
{file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"},
|
||||
{file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"},
|
||||
{file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"},
|
||||
{file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"},
|
||||
{file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"},
|
||||
{file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
|
@ -578,6 +808,42 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
|||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.23.3"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"},
|
||||
{file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=7.0.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.1.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
|
||||
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.0"
|
||||
|
@ -751,4 +1017,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "84fe024aa665ddad3077ca7e73054d1a5cb019a9a3e78af917922433ff4b3d8c"
|
||||
content-hash = "21d68c7a50ac5fb0af89a33a484094ea6a69f1e9a03d0f8cb5c6dc35400c760a"
|
||||
|
|
|
@ -14,6 +14,10 @@ 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"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
@ -25,3 +29,7 @@ build-backend = "poetry.core.masonry.api"
|
|||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ". fastfood"
|
||||
filterwarnings = [
|
||||
"ignore::UserWarning",
|
||||
"ignore::DeprecationWarning"
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
CREATE DATABASE fastfood_db_test WITH OWNER postgres;
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Тут можно выполнить миграции или дополнительные перед запуском приложения
|
||||
#
|
||||
poetry run python manage.py --run-test-server
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
poetry run pytest -vv
|
|
@ -0,0 +1,69 @@
|
|||
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 fastfood.config import settings
|
||||
from fastfood.dbase import get_async_session
|
||||
from fastfood.models import Base
|
||||
|
||||
|
||||
async_engine = create_async_engine(settings.TESTDATABASE_URL_asyncpg)
|
||||
async_session_maker = async_sessionmaker(
|
||||
async_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function", autouse=True)
|
||||
async def db_init():
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app():
|
||||
app = 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",
|
||||
) 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
|
|
@ -0,0 +1,383 @@
|
|||
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 == []
|
|
@ -0,0 +1,100 @@
|
|||
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
|
Loading…
Reference in New Issue