develop
Сергей Ванюшкин 2024-02-05 20:02:32 +03:00
commit 749e37354d
39 changed files with 2341 additions and 1404 deletions

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

@ -8,6 +8,10 @@ RUN mkdir -p /usr/src/fastfood
WORKDIR /usr/src/fastfood
COPY . .
COPY ./pyproject.toml .
COPY ./poetry.lock .
RUN touch /usr/src/RUN_IN_DOCKER
RUN poetry install

View File

@ -53,19 +53,19 @@ Fastapi веб приложение реализующее api для общеп
Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish.
### Спринт 2
- 1й пункт ТЗ
Тесты реализованы в виде 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.
- 2й пункт ТЗ
Реализованы 3 контейнера(db, app, tests). В db написан блок "проверки здоровья", от которого зависят контейнеры app и test, который гарантирует, что зависимые контейнеры не будут запущены о полной готовности db.
- 3й пункт ТЗ
- 3й пункт ТЗ
см. функцию `get_menu_item` на 28 строке в файле
<base_dir>/fastfood/crud/menu.py
- 4й пункт ТЗ
- 4й пункт ТЗ
см. класс `TestContinuity` в файле
<base_dir>/tests/test_api.py
@ -101,12 +101,11 @@ Fastapi веб приложение реализующее api для общеп
- Запуск FAstAPI приложения
> `$ docker-compose -f compose_app.yml up `
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
После успешного запуска образов документация по API будет доступна по адресу <a href="http://localhost:8000/docs">http://localhost:8000</a>
По завершении работы остановите контейнеры
> `$ docker-compose -f compose_app.yml down`
- Запуск тестов
> `$ docker-compose -f compose_test.yml up`
@ -115,7 +114,7 @@ Fastapi веб приложение реализующее api для общеп
### Linux
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
Установите систему управления зависимостями
> `$ pip[x] install poetry`
@ -134,7 +133,7 @@ Fastapi веб приложение реализующее api для общеп
## Запуск
Запуск проекта возможен в 2х режимах:
- Запуск в режиме "prod" с ключем --run-server
Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются.
Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются.
- Запуск в режиме "dev" c ключем --run-test-server
В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей.
@ -163,5 +162,3 @@ Fastapi веб приложение реализующее api для общеп
## Лицензия
Распространяется под [MIT лицензией](https://mit-license.org/).

View File

@ -1,45 +1,60 @@
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
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server'

View File

@ -1,45 +1,63 @@
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
restart: always
command: /bin/bash -c 'poetry run pytest -vv'

View File

@ -4,3 +4,4 @@ POSTGRES_USER=testuser
POSTGRES_PASSWORD=test
POSTGRES_DB=fastfood_db
POSTGRES_DB_TEST=testdb
REDIS_DB=redis://localhost

View File

@ -1,82 +1,43 @@
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**.
## Приятного аппетита
"""
tags_metadata = [
{
"name": "menu",
"description": "Операции с меню.",
'name': 'menu',
'description': 'Операции с меню.',
},
{
"name": "submenu",
"description": "Подменю и работа с ним",
'name': 'submenu',
'description': 'Подменю и работа с ним',
},
{"name": "dish", "description": "Блюда и работа с ними"},
{'name': 'dish', 'description': 'Блюда и работа с ними'},
]
def create_app() -> FastAPI:
def create_app(redis=None) -> FastAPI:
"""
Фабрика FastAPI.
"""
with open('openapi.json') as f:
js = json.load(f)
app = FastAPI(
title="Fastfood-API",
description=description,
version="0.0.1",
title=js['info']['title'],
description=js['info']['description'],
version=js['info']['version'],
contact={
"name": "Sergey Vanyushkin",
"url": "http://pi3c.ru",
"email": "pi3c@yandex.ru",
'name': 'Sergey Vanyushkin',
'url': 'http://pi3c.ru',
'email': 'pi3c@yandex.ru',
},
license_info={
"name": "MIT license",
"url": "https://mit-license.org/",
'name': 'MIT license',
'url': 'https://mit-license.org/',
},
openapi_tags=tags_metadata,
)

View File

@ -1,23 +1,36 @@
import os
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DB_HOST: str = ""
DB_HOST: str = ''
DB_PORT: int = 5432
POSTGRES_DB: str = ""
POSTGRES_PASSWORD: str = ""
POSTGRES_USER: str = ""
POSTGRES_DB_TEST: str = ""
POSTGRES_DB: str = ''
POSTGRES_PASSWORD: str = ''
POSTGRES_USER: str = ''
POSTGRES_DB_TEST: str = ''
REDIS_DB: str = ''
@property
def DATABASE_URL_asyncpg(self):
def DATABASE_URL_asyncpg(self) -> str:
"""
Возвращает строку подключения к БД необходимую для SQLAlchemy
"""
# Проверяем, в DOCKER или нет
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return (
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@db:{self.DB_PORT}/{self.POSTGRES_DB}'
)
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'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}'
)
@property
@ -25,13 +38,29 @@ 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'@db:{self.DB_PORT}/{self.POSTGRES_DB_TEST}'
)
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'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}'
)
model_config = SettingsConfigDict(env_file=".env")
@property
def REDIS_URL(self):
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return 'redis://redis:6379/0'
return self.REDIS_DB
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,69 @@
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)):
self.db = session
async def get_dishes(self, menu_id: UUID, 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,
menu_id: UUID,
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,
menu_id: UUID,
submenu_id: UUID,
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,
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
dish_data: Dish_db,
) -> Dish:
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.scalars().one()
async def delete_dish_item(
self,
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
) -> int:
query = delete(Dish).where(Dish.id == dish_id)
await self.db.execute(query)
await self.db.commit()
return 200

View File

@ -0,0 +1,66 @@
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)):
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()
if menu is None:
return None
return menu
async def update_menu_item(
self,
menu_id: UUID,
menu: schemas.MenuBase,
) -> Menu:
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()
async def delete_menu_item(self, menu_id: UUID):
query = delete(Menu).where(Menu.id == menu_id)
await self.db.execute(query)
await self.db.commit()

View File

@ -0,0 +1,65 @@
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"{kwargs.get('menu_id')}"
case 'submenus':
return f"{kwargs.get('menu_id')}:SUBMENUS"
case 'submenu':
return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}"
case 'dishes':
return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES"
case 'dish':
return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}"
return 'abracadabra'
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)

View File

@ -0,0 +1,82 @@
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)):
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(menu_id, new_submenu.id)
if full_sub is None:
raise TypeError
return full_sub
async def get_submenu_item(
self,
menu_id: UUID,
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()
if submenu is None:
return None
return submenu
async def update_submenu_item(
self,
menu_id: UUID,
submenu_id: UUID,
submenu_data: MenuBase,
) -> SubMenu:
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()
async def delete_submenu_item(self, menu_id: UUID, submenu_id: UUID) -> int:
query = delete(SubMenu).where(
SubMenu.id == submenu_id,
)
await self.db.execute(query)
await self.db.commit()
return 200

View File

@ -1,80 +1,84 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
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(),
background_tasks: BackgroundTasks = BackgroundTasks(),
) -> 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),
dish_data: DishBase,
dish: DishService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.create_dish_item(
submenu_id=submenu_id,
dish=dish,
session=session,
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),
dish: DishService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.get_dish_item(
dish_id=dish_id,
session=session,
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),
dish_data: DishBase,
dish: DishService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.update_dish_item(
dish_id=dish_id,
dish=dish,
session=session,
result = await dish.update_dish(
menu_id,
submenu_id,
dish_id,
dish_data,
)
return price_converter(result)
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),
dish: DishService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
await crud.delete_dish_item(dish_id=dish_id, session=session)
await dish.del_dish(menu_id, submenu_id, dish_id)

View File

@ -1,66 +1,64 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
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.post("/", status_code=201, response_model=schemas.Menu)
async def add_menu(
menu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
@router.get('/', response_model=list[MenuRead])
async def get_menus(
menu: MenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.create_menu_item(
menu=menu,
session=session,
)
return result
return await menu.read_menus()
@router.get("/{menu_id}", response_model=schemas.MenuRead)
@router.post('/', status_code=201, response_model=MenuRead)
async def add_menu(
menu: MenuBase,
responce: MenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
return await responce.create_menu(menu)
@router.get('/{menu_id}', response_model=MenuRead)
async def get_menu(
menu_id: UUID,
session: AsyncSession = Depends(get_async_session),
responce: MenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.get_menu_item(menu_id=menu_id, session=session)
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),
menu: MenuBase,
responce: MenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.update_menu_item(
result = await responce.update_menu(
menu_id=menu_id,
menu=menu,
session=session,
menu_data=menu,
)
return result.scalars().one()
return result
@router.delete("/{menu_id}")
@router.delete('/{menu_id}')
async def delete_menu(
menu_id: UUID,
session: AsyncSession = Depends(get_async_session),
menu: MenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
await crud.delete_menu_item(menu_id=menu_id, session=session)
await menu.del_menu(menu_id)

View File

@ -1,76 +1,80 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
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)
menu_id: UUID,
submenu: SubmenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.get_submenus(menu_id=menu_id, session=session)
return result.scalars().all()
result = await submenu.read_submenus(menu_id=menu_id)
return result
@router.post("/", status_code=201)
@router.post('/', status_code=201, response_model=SubMenuRead)
async def create_submenu_item(
menu_id: UUID,
submenu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session),
submenu_data: MenuBase,
submenu: SubmenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.create_submenu_item(
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),
submenu: SubmenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.get_submenu_item(
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),
submenu_data: MenuBase,
submenu: SubmenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
result = await crud.update_submenu_item(
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()
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)
menu_id: UUID,
submenu_id: UUID,
submenu: SubmenuService = Depends(),
background_tasks: BackgroundTasks = BackgroundTasks(),
):
await crud.delete_submenu_item(submenu_id=submenu_id, session=session)
await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id)

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,12 @@ class SubMenuRead(Menu):
class DishBase(MenuBase):
price: float
price: str
class Dish(DishBase, Menu):
pass
class Dish_db(MenuBase):
price: float

137
fastfood/service/dish.py Normal file
View File

@ -0,0 +1,137 @@
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.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 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(menu_id, submenu_id)
response = []
for row in data:
dish = row.__dict__
dish['price'] = str(dish['price'])
response.append(Dish(**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(
menu_id,
submenu_id,
dish_db,
)
dish = data.__dict__
dish['price'] = str(dish['price'])
dish = Dish(**dish)
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(menu_id, submenu_id, dish_id)
if data is None:
return None
dish = data.__dict__
dish['price'] = str(dish['price'])
dish = Dish(**dish)
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:
dish_db = Dish_db(**dish_data.model_dump())
data = await self.dish_repo.update_dish_item(
menu_id, submenu_id, dish_id, dish_db
)
dish = data.__dict__
dish['price'] = str(dish['price'])
dish = Dish(**dish)
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, submenu_id: UUID, dish_id: UUID) -> int:
response = await self.dish_repo.delete_dish_item(
menu_id,
submenu_id,
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)
return response

111
fastfood/service/menu.py Normal file
View File

@ -0,0 +1,111 @@
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:
data = await self.menu_repo.update_menu_item(menu_id, 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
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):
data = 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)
return data

120
fastfood/service/submenu.py Normal file
View File

@ -0,0 +1,120 @@
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(menu_id, 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(menu_id, 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:
data = await self.submenu_repo.update_submenu_item(
menu_id, submenu_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_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) -> int:
code = await self.submenu_repo.delete_submenu_item(menu_id, 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)
return code

View File

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

View File

@ -3,7 +3,7 @@ import sys
import uvicorn
from fastfood.cruds import create_db_and_tables
from fastfood.repository import create_db_and_tables
def run_app():
@ -11,8 +11,8 @@ def run_app():
Запуск 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,
@ -25,10 +25,10 @@ async def recreate():
await create_db_and_tables()
if __name__ == "__main__":
if "--run-server" in sys.argv:
if __name__ == '__main__':
if '--run-server' in sys.argv:
run_app()
if "--run-test-server" in sys.argv:
if '--run-test-server' in sys.argv:
asyncio.run(recreate())
run_app()

1
openapi.json Normal file

File diff suppressed because one or more lines are too long

654
poetry.lock generated
View File

@ -103,13 +103,88 @@ test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
[[package]]
name = "certifi"
version = "2023.11.17"
version = "2024.2.2"
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"},
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
]
[[package]]
name = "cffi"
version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
@ -204,6 +279,71 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
[package.extras]
toml = ["tomli"]
[[package]]
name = "cryptography"
version = "42.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"},
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"},
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"},
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"},
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"},
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"},
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "dnspython"
version = "2.5.0"
@ -272,6 +412,22 @@ typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "greenlet"
version = "3.0.3"
@ -399,6 +555,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
[[package]]
name = "identify"
version = "2.5.33"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
{file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.6"
@ -421,6 +591,78 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "mypy"
version = "1.8.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
]
[package.dependencies]
setuptools = "*"
[[package]]
name = "packaging"
version = "23.2"
@ -432,35 +674,79 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "platformdirs"
version = "4.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
[[package]]
name = "pluggy"
version = "1.3.0"
version = "1.4.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.6.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"},
{file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
[[package]]
name = "pydantic"
version = "2.5.3"
version = "2.6.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"},
{file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"},
{file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"},
{file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.14.6"
pydantic-core = "2.16.1"
typing-extensions = ">=4.6.1"
[package.extras]
@ -468,116 +754,90 @@ email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.14.6"
version = "2.16.1"
description = ""
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"},
{file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"},
{file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"},
{file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"},
{file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"},
{file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"},
{file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"},
{file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"},
{file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"},
{file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"},
{file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"},
{file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"},
{file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"},
{file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"},
{file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"},
{file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"},
{file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"},
{file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"},
{file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"},
{file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"},
{file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"},
{file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"},
{file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"},
{file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"},
{file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"},
{file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"},
{file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"},
{file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"},
{file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"},
{file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"},
{file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"},
{file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"},
{file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"},
{file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"},
{file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"},
{file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"},
{file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"},
{file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"},
{file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"},
{file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"},
{file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"},
{file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"},
{file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"},
{file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"},
{file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"},
{file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"},
{file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"},
{file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"},
{file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"},
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"},
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"},
{file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"},
{file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"},
{file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"},
{file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"},
{file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"},
{file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"},
{file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"},
{file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"},
{file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"},
{file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"},
{file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"},
{file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"},
{file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"},
]
[package.dependencies]
@ -622,17 +882,17 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
[[package]]
name = "pytest-asyncio"
version = "0.23.3"
version = "0.23.4"
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"},
{file = "pytest-asyncio-0.23.4.tar.gz", hash = "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2"},
{file = "pytest_asyncio-0.23.4-py3-none-any.whl", hash = "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef"},
]
[package.dependencies]
pytest = ">=7.0.0"
pytest = ">=7.0.0,<8"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
@ -658,18 +918,111 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
[[package]]
name = "python-dotenv"
version = "1.0.0"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "redis"
version = "4.6.0"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.7"
files = [
{file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"},
{file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"},
]
[package.dependencies]
async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
[package.extras]
hiredis = ["hiredis (>=1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
name = "setuptools"
version = "69.0.3"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "sniffio"
version = "1.3.0"
@ -796,6 +1149,35 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "types-pyopenssl"
version = "24.0.0.20240130"
description = "Typing stubs for pyOpenSSL"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-pyOpenSSL-24.0.0.20240130.tar.gz", hash = "sha256:c812e5c1c35249f75ef5935708b2a997d62abf9745be222e5f94b9595472ab25"},
{file = "types_pyOpenSSL-24.0.0.20240130-py3-none-any.whl", hash = "sha256:24a255458b5b8a7fca8139cf56f2a8ad5a4f1a5f711b73a5bb9cb50dc688fab5"},
]
[package.dependencies]
cryptography = ">=35.0.0"
[[package]]
name = "types-redis"
version = "4.6.0.20240106"
description = "Typing stubs for redis"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-redis-4.6.0.20240106.tar.gz", hash = "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e"},
{file = "types_redis-4.6.0.20240106-py3-none-any.whl", hash = "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"},
]
[package.dependencies]
cryptography = ">=35.0.0"
types-pyOpenSSL = "*"
[[package]]
name = "typing-extensions"
version = "4.9.0"
@ -826,7 +1208,27 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "virtualenv"
version = "20.25.0"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "5bbc3cad36f6f40d10cb848918426b640f9e703bc2c6b22b5b8fe381a6251ded"
content-hash = "106e42984de924817e2dc083ad78699b3411f9aa60de5bb5c1a95ca94a21fda1"

View File

@ -3887,4 +3887,4 @@
]
}
]
}
}

View File

@ -90,4 +90,4 @@
"_postman_variable_scope": "environment",
"_postman_exported_at": "2023-01-12T16:22:10.333Z",
"_postman_exported_using": "Postman/10.6.7"
}
}

View File

@ -14,12 +14,16 @@ asyncpg = "^0.29.0"
pydantic-settings = "^2.1.0"
email-validator = "^2.1.0.post1"
pytest-asyncio = "^0.23.3"
redis = "^4.6.0"
types-redis = "^4.6.0.3"
mypy = "^1.4.1"
[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,5 +1,5 @@
import asyncio
from typing import AsyncGenerator, Dict, Generator
from typing import AsyncGenerator
import pytest
import pytest_asyncio
@ -20,7 +20,7 @@ async_session_maker = async_sessionmaker(
)
@pytest.fixture(scope="session", autouse=True)
@pytest.fixture(scope='session', autouse=True)
def event_loop():
try:
loop = asyncio.get_event_loop()
@ -30,7 +30,7 @@ def event_loop():
loop.close()
@pytest_asyncio.fixture(scope="function", autouse=True)
@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)
@ -45,28 +45,13 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
yield session
@pytest.fixture(scope="session")
def app(event_loop) -> Generator[FastAPI, None, None]:
@pytest_asyncio.fixture(scope='session')
async def client() -> 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) -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient(
app=app,
base_url="http://localhost:8000/api/v1/menus",
base_url='http://localhost:8000/api/v1/menus',
) as async_client:
yield async_client
@pytest_asyncio.fixture(scope="function")
async def asession(event_loop) -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
@pytest.fixture(scope="session")
def session_data() -> Dict:
return {}

188
tests/repository.py Normal file
View File

@ -0,0 +1,188 @@
from httpx import AsyncClient, Response
from .urls import reverse_url
class Repository:
class Menu:
@staticmethod
async def read_all(ac: AsyncClient) -> tuple[int, dict]:
"""чтение всех меню"""
response: Response = await ac.get(reverse_url('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_url('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_url('menus'), 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_url('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_url('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_url('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_url(
'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_url('submenu', 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_url(
'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_url(
'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_url(
'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_url(
'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_url(
'dishes',
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_url(
'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_url(
'dish',
menu_id=menu.get('id'),
submenu_id=submenu.get('id'),
dish_id=dish.get('id'),
),
)
return response.status_code

View File

@ -1,603 +1,360 @@
from typing import Dict, Tuple
import pytest
from httpx import AsyncClient, Response
class TestBaseCrud:
class Menu:
@staticmethod
async def read_all(ac: AsyncClient) -> Tuple[int, dict]:
"""чтение всех меню"""
response: Response = await ac.get("/")
return response.status_code, response.json()
@staticmethod
async def get(ac: AsyncClient, data: dict) -> Tuple[int, dict]:
"""Получение меню по id"""
response: Response = await ac.get(f"/{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("/", 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(
f"/{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(f"/{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(f"/{menu.get('id')}/submenus/")
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(
f"/{menu.get('id')}/submenus/{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(
f"/{menu.get('id')}/submenus/",
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(
f"/{menu.get('id')}/submenus/{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(
f"/{menu.get('id')}/submenus/{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(
f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/",
)
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(
f"/{menu.get('id')}/submenus/{submenu.get('id')}"
f"/dishes/{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(
f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/",
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(
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: AsyncClient, menu: dict, submenu: dict, dish: dict) -> int:
"""Удаление блюда по id"""
response: 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_empty(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
code, rspn = await self.Menu.read_all(client)
assert code == 200
assert rspn == []
@pytest.mark.asyncio
async def test_menu_crud_add(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
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
@pytest.mark.asyncio
async def test_menu_crud_get(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
data = {"title": "Menu", "description": None}
code, rspn = await self.Menu.write(client, data)
code, menu = await self.Menu.get(client, {"id": rspn.get("id")})
assert code == 200
assert menu["title"] == rspn["title"]
@pytest.mark.asyncio
async def test_menu_crud_update(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
data = {"title": "Menu", "description": None}
code, rspn = await self.Menu.write(client, data)
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"
@pytest.mark.asyncio
async def test_menu_crud_delete(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
data = {"title": "Menu", "description": None}
code, rspn = await self.Menu.write(client, data)
code = await self.Menu.delete(client, rspn)
assert code == 200
code, rspn = await self.Menu.get(client, {"id": rspn.get("id")})
assert code == 404
@pytest.mark.asyncio
async def test_menu_crud_get_all(self, client: AsyncClient) -> None:
"""Тестирование функций меню"""
code, rspn = await self.Menu.read_all(client)
assert code == 200
assert rspn == []
data = {"title": "Menu", "description": None}
code, rspn = await self.Menu.write(client, data)
code, upd_rspn = await self.Menu.read_all(client)
assert code == 200
assert upd_rspn == [rspn]
@pytest.mark.asyncio
async def test_submenus_get_all(self, client) -> None:
# Создаем меню и проверяем ответ
menu = {"title": "Menu", "description": "main menu"}
code, rspn = await self.Menu.write(client, menu)
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)
submenu.update(rspn)
# Проверяем наличие подменю
code, upd_rspn = await self.Submenu.read_all(client, menu)
assert code == 200
assert upd_rspn == [rspn]
# удаляем сопутствующее
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_submenus_add(self, client) -> None:
# Создаем меню и проверяем ответ
menu = {"title": "Menu", "description": "main menu"}
code, rspn = await self.Menu.write(client, menu)
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)
# удаляем сопутствующее
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_submenus_update(self, client) -> None:
# Создаем меню и проверяем ответ
menu = {"title": "Menu", "description": "main menu"}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
submenu.update(rspn)
# Обновляем подменю и проверяем
submenu["title"] = "updated_submenu"
code, rspn = await self.Submenu.update(client, menu, submenu)
assert code == 200
assert submenu["title"] == rspn["title"]
submenu.update(rspn)
# удаляем сопутствующее
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_submenus_delete(self, client) -> None:
# Создаем меню и проверяем ответ
menu = {"title": "Menu", "description": "main menu"}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
submenu.update(rspn)
# Удаляем подменю
code = await self.Submenu.delete(client, menu, submenu)
assert code == 200
# Проверяем удаленное подменю
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_get_all(self, client: AsyncClient) -> None:
# Создаем меню и проверяем ответ
menu = {
"title": "Menu",
"description": "main menu",
}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
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, upd_rspn = await self.Dish.read_all(client, menu, submenu)
assert code == 200
# удаляем сопутствующее
await self.Dish.delete(client, menu, submenu, dish)
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_dishes_add(self, client: AsyncClient) -> None:
# Создаем меню и проверяем ответ
menu = {
"title": "Menu",
"description": "main menu",
}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
submenu.update(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"]
# удаляем сопутствующее
await self.Dish.delete(client, menu, submenu, dish)
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_dishes_update(self, client: AsyncClient) -> None:
# Создаем меню и проверяем ответ
menu = {
"title": "Menu",
"description": "main menu",
}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
submenu.update(rspn)
# Добавляем блюдо
dish = {
"title": "dish",
"description": "some dish",
"price": "12.5",
"parent_submenu": submenu["id"],
}
code, rspn = await self.Dish.write(client, menu, submenu, dish)
dish.update(rspn)
# Обновляем блюдо и проверяем
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)
# удаляем сопутствующее
await self.Dish.delete(client, menu, submenu, dish)
await self.Submenu.delete(client, menu, submenu)
await self.Menu.delete(client, menu)
@pytest.mark.asyncio
async def test_dishes_delete(self, client: AsyncClient) -> None:
# Создаем меню и проверяем ответ
menu = {
"title": "Menu",
"description": "main menu",
}
code, rspn = await self.Menu.write(client, menu)
menu.update(rspn)
# Создаем и проверяем подменю
submenu = {
"title": "Submenu",
"description": "submenu",
"parent_menu": menu["id"],
}
code, rspn = await self.Submenu.write(client, menu, submenu)
submenu.update(rspn)
# Добавляем блюдо
dish = {
"title": "dish",
"description": "some dish",
"price": "12.5",
"parent_submenu": submenu["id"],
}
code, rspn = await self.Dish.write(client, menu, submenu, dish)
dish.update(rspn)
# Удаляем подменю
code = await self.Dish.delete(client, menu, submenu, dish)
assert code == 200
# Проверяем удаленное блюдо
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_01(self, client, session_data: Dict):
"""Проверяет создание меню"""
data = {"title": "Menu", "description": "some"}
code, rspn = await TestBaseCrud.Menu.write(client, data)
assert code == 201
code, rspn = await TestBaseCrud.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"
assert rspn.get("description") == "some"
@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 == []
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)
@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)
@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)

View File

@ -1,179 +0,0 @@
from uuid import UUID
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood.cruds.dish import DishCrud
from fastfood.cruds.menu import MenuCrud
from fastfood.cruds.submenu import SubMenuCrud
from fastfood.models import Dish, Menu, SubMenu
from fastfood.schemas import DishBase as dishschema
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
assert submenu.dishes_count == 0
# Обновляем меню
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.asyncio
async def test_dish(asession: AsyncSession):
"""Not Implemented yet"""
async with asession:
# Создаем меню напрямую
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,
)
asession.add(submenu)
await asession.commit()
await asession.refresh(submenu)
submenu_id = submenu.id
# Создаем блюдо
dish: Dish = Dish(
title="dish1",
description="dish number 1",
price="12.5",
parent_submenu=submenu_id,
)
dish = await DishCrud.create_dish_item(
submenu_id,
dishschema.model_validate(dish),
asession,
)
dish_id = dish.id
# Проверяем блюдо
req_dish = await DishCrud.get_dish_item(
dish_id,
asession,
)
assert dish == req_dish
menu = await MenuCrud.get_menu_item(menu_id, asession)
submenu = await SubMenuCrud.get_submenu_item(
menu_id,
submenu.id,
asession,
)
assert menu.submenus_count == 1
assert menu.dishes_count == 1
assert submenu.dishes_count == 1
# Обновляем блюдо
dish.price = 177
req_dish = await DishCrud.update_dish_item(
dish_id,
dishschema.model_validate(dish),
asession,
)
assert dish == req_dish
# Удаляем длюдо
await DishCrud.delete_dish_item(dish_id, asession)
menu = await MenuCrud.get_menu_item(menu_id, asession)
submenu = await SubMenuCrud.get_submenu_item(
menu_id,
submenu.id,
asession,
)
assert menu.dishes_count == 0
assert submenu.dishes_count == 0
await SubMenuCrud.delete_submenu_item(submenu_id, asession)
await MenuCrud.delete_menu_item(menu_id, asession)

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

18
tests/urls.py Normal file
View File

@ -0,0 +1,18 @@
def reverse_url(loc: str, **kwargs) -> str:
menu_pref = '/'
submenu_pref = menu_pref + str(kwargs.get('menu_id', '')) + '/submenus/'
dish_pref = submenu_pref + str(kwargs.get('submenu_id', '')) + '/dishes/'
match loc:
case 'menus':
return menu_pref
case 'menu':
return menu_pref + str(kwargs.get('menu_id', ''))
case 'submenus':
return submenu_pref
case 'submenu':
return submenu_pref + str(kwargs.get('submenu_id', ''))
case 'dishes':
return dish_pref
case 'dish':
return dish_pref + str(kwargs.get('dish_id', ''))
return menu_pref