Compare commits

..

No commits in common. "749e37354d2ff75d6f472e42aad6b09f15233223" and "995be04dcb5673796ff27a046b0243c0733278cf" have entirely different histories.

39 changed files with 1414 additions and 2351 deletions

1
.gitignore vendored
View File

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

View File

@ -1,50 +0,0 @@
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,10 +8,6 @@ RUN mkdir -p /usr/src/fastfood
WORKDIR /usr/src/fastfood WORKDIR /usr/src/fastfood
COPY ./pyproject.toml . COPY . .
COPY ./poetry.lock .
RUN touch /usr/src/RUN_IN_DOCKER
RUN poetry install RUN poetry install

View File

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

View File

@ -1,60 +1,45 @@
version: "3.8" version: "3.8"
services: 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: db:
container_name: pgdb container_name: pgdb
image: postgres:15.1-alpine image: postgres:15.1-alpine
env_file: env_file:
- .env - .env
environment: environment:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports: ports:
- 6432:5432 - 6432:5432
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
app: app:
container_name: fastfood_app container_name: fastfood_app
build: build:
context: . context: .
env_file: env_file:
- .env - .env
ports: ports:
- 8000:8000 - 8000:8000
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
restart: always restart: always
command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server' command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server'

View File

@ -1,63 +1,45 @@
version: "3.8" version: "3.8"
services: 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: db:
container_name: pgdb_test container_name: pgdb_test
image: postgres:15.1-alpine image: postgres:15.1-alpine
env_file: env_file:
- .env - .env
environment: environment:
POSTGRES_DB: ${POSTGRES_DB_TEST} POSTGRES_DB: ${POSTGRES_DB_TEST}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports: ports:
- 6432:5432 - 6432:5432
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB_TEST}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB_TEST}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
app: app:
container_name: fastfood_app_test container_name: fastfood_app_test
build: build:
context: . context: .
env_file: env_file:
- .env - .env
ports: ports:
- 8000:8000 - 8000:8000
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
volumes:
- .:/usr/src/fastfood
restart: always restart: always
command: /bin/bash -c 'poetry run pytest -vv' command: /bin/bash -c 'poetry run pytest -vv'

View File

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

View File

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

View File

@ -1,36 +1,23 @@
import os
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings): class Settings(BaseSettings):
DB_HOST: str = '' DB_HOST: str = ""
DB_PORT: int = 5432 DB_PORT: int = 5432
POSTGRES_DB: str = '' POSTGRES_DB: str = ""
POSTGRES_PASSWORD: str = '' POSTGRES_PASSWORD: str = ""
POSTGRES_USER: str = '' POSTGRES_USER: str = ""
POSTGRES_DB_TEST: str = '' POSTGRES_DB_TEST: str = ""
REDIS_DB: str = ''
@property @property
def DATABASE_URL_asyncpg(self) -> str: def DATABASE_URL_asyncpg(self):
""" """
Возвращает строку подключения к БД необходимую для SQLAlchemy Возвращает строку подключения к БД необходимую для SQLAlchemy
""" """
# Проверяем, в DOCKER или нет
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return (
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@db:{self.DB_PORT}/{self.POSTGRES_DB}'
)
return ( return (
'postgresql+asyncpg://' "postgresql+asyncpg://"
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}' f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}"
) )
@property @property
@ -38,29 +25,13 @@ class Settings(BaseSettings):
""" """
Возвращает строку подключения к БД необходимую для SQLAlchemy Возвращает строку подключения к БД необходимую для SQLAlchemy
""" """
file_path = '/usr/src/RUN_IN_DOCKER'
if os.path.exists(file_path):
return (
'postgresql+asyncpg://'
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}'
f'@db:{self.DB_PORT}/{self.POSTGRES_DB_TEST}'
)
return ( return (
'postgresql+asyncpg://' "postgresql+asyncpg://"
f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}' f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}"
) )
@property model_config = SettingsConfigDict(env_file=".env")
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() settings = Settings()

View File

@ -1,9 +1,9 @@
from fastfood import models from fastfood import models
from fastfood.dbase import async_engine from fastfood.dbase import async_engine
from .dish import DishRepository from .dish import DishCrud
from .menu import MenuRepository from .menu import MenuCrud
from .submenu import SubMenuRepository from .submenu import SubMenuCrud
async def create_db_and_tables(): async def create_db_and_tables():
@ -12,8 +12,8 @@ async def create_db_and_tables():
await conn.run_sync(models.Base.metadata.create_all) await conn.run_sync(models.Base.metadata.create_all)
class Repository(MenuRepository, SubMenuRepository, DishRepository): class Crud(MenuCrud, SubMenuCrud, DishCrud):
pass pass
ropo = Repository() crud = Crud()

64
fastfood/cruds/dish.py Normal file
View File

@ -0,0 +1,64 @@
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()

74
fastfood/cruds/menu.py Normal file
View File

@ -0,0 +1,74 @@
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()

80
fastfood/cruds/submenu.py Normal file
View File

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

View File

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

View File

@ -1,69 +0,0 @@
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

@ -1,66 +0,0 @@
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

@ -1,65 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,137 +0,0 @@
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

View File

@ -1,111 +0,0 @@
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

View File

@ -1,120 +0,0 @@
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

3
fastfood/utils.py Normal file
View File

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

View File

@ -3,7 +3,7 @@ import sys
import uvicorn import uvicorn
from fastfood.repository import create_db_and_tables from fastfood.cruds import create_db_and_tables
def run_app(): def run_app():
@ -11,8 +11,8 @@ def run_app():
Запуск FastAPI Запуск FastAPI
""" """
uvicorn.run( uvicorn.run(
app='fastfood.app:create_app', app="fastfood.app:create_app",
host='0.0.0.0', host="0.0.0.0",
port=8000, port=8000,
reload=True, reload=True,
factory=True, factory=True,
@ -25,10 +25,10 @@ async def recreate():
await create_db_and_tables() await create_db_and_tables()
if __name__ == '__main__': if __name__ == "__main__":
if '--run-server' in sys.argv: if "--run-server" in sys.argv:
run_app() run_app()
if '--run-test-server' in sys.argv: if "--run-test-server" in sys.argv:
asyncio.run(recreate()) asyncio.run(recreate())
run_app() run_app()

File diff suppressed because one or more lines are too long

654
poetry.lock generated
View File

@ -103,88 +103,13 @@ test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2024.2.2" version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[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]] [[package]]
@ -279,71 +204,6 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
[package.extras] [package.extras]
toml = ["tomli"] 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]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.5.0" version = "2.5.0"
@ -412,22 +272,6 @@ typing-extensions = ">=4.8.0"
[package.extras] [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)"] 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]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.0.3" version = "3.0.3"
@ -555,20 +399,6 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"] http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] 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]] [[package]]
name = "idna" name = "idna"
version = "3.6" version = "3.6"
@ -591,78 +421,6 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {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]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "23.2"
@ -674,79 +432,35 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {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]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.4.0" version = "1.3.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
] ]
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.6.0" version = "2.5.3"
description = "Data validation using Python type hints" description = "Data validation using Python type hints"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"},
{file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"},
] ]
[package.dependencies] [package.dependencies]
annotated-types = ">=0.4.0" annotated-types = ">=0.4.0"
pydantic-core = "2.16.1" pydantic-core = "2.14.6"
typing-extensions = ">=4.6.1" typing-extensions = ">=4.6.1"
[package.extras] [package.extras]
@ -754,90 +468,116 @@ email = ["email-validator (>=2.0.0)"]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.16.1" version = "2.14.6"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"},
{file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"},
{file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"},
{file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"},
{file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"},
{file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"},
{file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"},
{file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"},
{file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"},
{file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"},
{file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"},
{file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"},
{file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"},
{file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"},
{file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"},
{file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"},
{file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"},
{file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"},
{file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"},
{file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"},
{file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"},
{file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"},
{file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"},
{file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"},
{file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"},
{file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"},
{file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"},
{file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"},
{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.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"},
{file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"},
{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.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"},
{file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, {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.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, {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"},
] ]
[package.dependencies] [package.dependencies]
@ -882,17 +622,17 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
[[package]] [[package]]
name = "pytest-asyncio" name = "pytest-asyncio"
version = "0.23.4" version = "0.23.3"
description = "Pytest support for asyncio" description = "Pytest support for asyncio"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-asyncio-0.23.4.tar.gz", hash = "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2"}, {file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"},
{file = "pytest_asyncio-0.23.4-py3-none-any.whl", hash = "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef"}, {file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"},
] ]
[package.dependencies] [package.dependencies]
pytest = ">=7.0.0,<8" pytest = ">=7.0.0"
[package.extras] [package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
@ -918,111 +658,18 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.1" version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables" description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
] ]
[package.extras] [package.extras]
cli = ["click (>=5.0)"] 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]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.0" version = "1.3.0"
@ -1149,35 +796,6 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.9.0" version = "4.9.0"
@ -1208,27 +826,7 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras] [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)"] 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "106e42984de924817e2dc083ad78699b3411f9aa60de5bb5c1a95ca94a21fda1" content-hash = "5bbc3cad36f6f40d10cb848918426b640f9e703bc2c6b22b5b8fe381a6251ded"

View File

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

View File

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

View File

@ -14,16 +14,12 @@ asyncpg = "^0.29.0"
pydantic-settings = "^2.1.0" pydantic-settings = "^2.1.0"
email-validator = "^2.1.0.post1" email-validator = "^2.1.0.post1"
pytest-asyncio = "^0.23.3" pytest-asyncio = "^0.23.3"
redis = "^4.6.0"
types-redis = "^4.6.0.3"
mypy = "^1.4.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.4.4" pytest = "^7.4.4"
pytest-cov = "^4.1.0" pytest-cov = "^4.1.0"
httpx = "^0.26.0" httpx = "^0.26.0"
pre-commit = "^3.6.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import AsyncGenerator from typing import AsyncGenerator, Dict, Generator
import pytest import pytest
import pytest_asyncio 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(): def event_loop():
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -30,7 +30,7 @@ def event_loop():
loop.close() loop.close()
@pytest_asyncio.fixture(scope='session', autouse=True) @pytest_asyncio.fixture(scope="function", autouse=True)
async def db_init(event_loop): async def db_init(event_loop):
async with async_engine.begin() as conn: async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.drop_all)
@ -45,13 +45,28 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]:
yield session yield session
@pytest_asyncio.fixture(scope='session') @pytest.fixture(scope="session")
async def client() -> AsyncGenerator[AsyncClient, None]: def app(event_loop) -> Generator[FastAPI, None, None]:
app: FastAPI = create_app() app: FastAPI = create_app()
app.dependency_overrides[get_async_session] = get_test_session app.dependency_overrides[get_async_session] = get_test_session
yield app
@pytest_asyncio.fixture(scope="function")
async def client(app) -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient( async with AsyncClient(
app=app, app=app,
base_url='http://localhost:8000/api/v1/menus', base_url="http://localhost:8000/api/v1/menus",
) as async_client: ) as async_client:
yield 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 {}

View File

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

179
tests/test_crud.py Normal file
View File

@ -0,0 +1,179 @@
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)

View File

@ -1,238 +0,0 @@
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 == []

View File

@ -1,18 +0,0 @@
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