diff --git a/.gitignore b/.gitignore index dc7ea06..20abbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -217,4 +217,3 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..45e9c60 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: trailing-whitespace # убирает лишние пробелы + - id: check-added-large-files # проверяет тяжелые файлы на изменения + - id: check-yaml # проверяет синтаксис .yaml файлов + - id: check-json # проверяет синтаксис .json файлов + exclude: launch.json + - id: check-case-conflict # проверяет файлы, которые могут конфликтовать в файловых системах без учета регистра. + - id: check-merge-conflict # проверяет файлы, содержащие конфликтные строки слияния. + - id: double-quote-string-fixer # заменяет " на ' + - id: end-of-file-fixer # добавляет пустую строку в конце файла + +# Отсортировывает импорты в проекте +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + exclude: __init__.py + args: [ --profile, black, --filter-files ] + +# Обновляет синтаксис Python кода в соответствии с последними версиями +- repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 + hooks: + - id: pyupgrade + args: [--py310-plus] + +# Форматирует код под PEP8 +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.1 + hooks: + - id: autopep8 + args: [--max-line-length=120, --in-place] + +# Сканер стилистических ошибок, нарушающие договоренности PEP8 +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + exclude: "__init__.py" + args: ["--ignore=E501,F821", "--max-line-length=120"] + +# Проверка статических типов с помощью mypy +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.991 + hooks: + - id: mypy + exclude: 'migrations' diff --git a/Dockerfile b/Dockerfile index 634d067..dc34eac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,10 @@ RUN mkdir -p /usr/src/fastfood WORKDIR /usr/src/fastfood -COPY . . +COPY ./pyproject.toml . + +COPY ./poetry.lock . + +RUN touch /usr/src/RUN_IN_DOCKER RUN poetry install diff --git a/README.md b/README.md index 3b68ba8..04456a3 100644 --- a/README.md +++ b/README.md @@ -53,19 +53,19 @@ Fastapi веб приложение реализующее api для общеп Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish. ### Спринт 2 -- 1й пункт ТЗ -Тесты реализованы в виде 2х классов +- 1й пункт ТЗ +Тесты реализованы в виде 2х классов `TastBaseCrud` включает 3 подкласса `Menu`, `Submenu`, `Dish` которые реализуют интерфейсы взаимодействия с endpoint'ами реализованных на предыдущем спринте сущностей. Каждый подкласс реализует методы GET(получение всех сущностей), Get(получение конкректной сущности), Post(создание), Patch(обновление), Delete(удаления). Так же в классе реализованы 3 тестовых функции, которые осуществляют тестирование соответствующих endpoint'ов `TestContinuity` реализует последовательность сценария «Проверка кол-ва блюд и подменю в меню» из Postman -- 2й пункт ТЗ -Реализованы 3 контейнера(db, app, tests). В db написан блок "проверки здоровья", от которого зависят контейнеры app и test, который гарантирует, что зависимые контейнеры не будут запущены о полной готовности db. +- 2й пункт ТЗ +Реализованы 3 контейнера(db, app, tests). В db написан блок "проверки здоровья", от которого зависят контейнеры app и test, который гарантирует, что зависимые контейнеры не будут запущены о полной готовности db. -- 3й пункт ТЗ +- 3й пункт ТЗ см. функцию `get_menu_item` на 28 строке в файле /fastfood/crud/menu.py -- 4й пункт ТЗ +- 4й пункт ТЗ см. класс `TestContinuity` в файле /tests/test_api.py @@ -101,12 +101,11 @@ Fastapi веб приложение реализующее api для общеп - Запуск FAstAPI приложения > `$ docker-compose -f compose_app.yml up ` -После успешного запуска образов документация по API будет доступна по адресу http://localhost:8000 +После успешного запуска образов документация по API будет доступна по адресу http://localhost:8000 По завершении работы остановите контейнеры > `$ docker-compose -f compose_app.yml down` - - Запуск тестов > `$ docker-compose -f compose_test.yml up` @@ -115,7 +114,7 @@ Fastapi веб приложение реализующее api для общеп ### Linux -Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд. +Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд. Установите систему управления зависимостями > `$ pip[x] install poetry` @@ -134,7 +133,7 @@ Fastapi веб приложение реализующее api для общеп ## Запуск Запуск проекта возможен в 2х режимах: - Запуск в режиме "prod" с ключем --run-server - Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются. + Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются. - Запуск в режиме "dev" c ключем --run-test-server В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей. @@ -163,5 +162,3 @@ Fastapi веб приложение реализующее api для общеп ## Лицензия Распространяется под [MIT лицензией](https://mit-license.org/). - - diff --git a/compose_app.yml b/compose_app.yml index 2028dc8..a8ad9a8 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -1,45 +1,60 @@ version: "3.8" services: + redis: + container_name: redis_test + + image: redis:7.2.4-alpine3.19 + + ports: + - '6380:6379' + + healthcheck: + test: [ "CMD", "redis-cli","ping" ] + interval: 10s + timeout: 5s + retries: 5 + db: container_name: pgdb - + image: postgres:15.1-alpine - + env_file: - .env - + environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - + ports: - 6432:5432 - + healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 - + app: container_name: fastfood_app - + build: context: . - + env_file: - .env - + ports: - 8000:8000 - + depends_on: db: condition: service_healthy - + redis: + condition: service_healthy + restart: always command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server' - diff --git a/compose_test.yml b/compose_test.yml index 0b706f2..43cea1b 100644 --- a/compose_test.yml +++ b/compose_test.yml @@ -1,45 +1,63 @@ version: "3.8" services: + redis: + container_name: redis_test + + image: redis:7.2.4-alpine3.19 + + ports: + - '6380:6379' + + healthcheck: + test: [ "CMD", "redis-cli","ping" ] + interval: 10s + timeout: 5s + retries: 5 + db: container_name: pgdb_test - + image: postgres:15.1-alpine - + env_file: - .env - + environment: POSTGRES_DB: ${POSTGRES_DB_TEST} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - + ports: - 6432:5432 - + healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB_TEST}"] interval: 10s timeout: 5s retries: 5 - + app: container_name: fastfood_app_test - + build: context: . - + env_file: - .env - + ports: - 8000:8000 - + depends_on: db: condition: service_healthy - + redis: + condition: service_healthy + + volumes: + - .:/usr/src/fastfood + restart: always command: /bin/bash -c 'poetry run pytest -vv' - diff --git a/example.env b/example.env index d62dc7a..73ef17e 100644 --- a/example.env +++ b/example.env @@ -4,3 +4,4 @@ POSTGRES_USER=testuser POSTGRES_PASSWORD=test POSTGRES_DB=fastfood_db POSTGRES_DB_TEST=testdb +REDIS_DB=redis://localhost diff --git a/fastfood/app.py b/fastfood/app.py index 0bed41c..202ef5f 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -1,82 +1,43 @@ +import json + from fastapi import FastAPI from fastfood.routers.dish import router as dish_router from fastfood.routers.menu import router as menu_router from fastfood.routers.submenu import router as submenu_router -description = """ -# 🔥🔥🔥Fastfood-API поможет тебе подкрепиться 🔥🔥🔥 - -### У нас есть Menu. Ты можеш выбрать блюда из кухни, которая тебе нравится - -## Menu - -Ты можешь **add menu**. - -Ты можешь **read menu**. - -Ты можешь **patch menu**. - -Ты можешь **delete menu**. - -### У нас есть в SubMenu, где ты сможешь найти -десерты/напитки/супчики/прочие вкусности - -# SubMenu - -Ты можешь **add submenu into menu**. - -Ты можешь **read submenu**. - -Ты можешь **patch submenu**. - -Ты можешь **delete menu**. - -### У нас есть в Dish, где ты сможешь найти блюдо по вкусу - -# Dish - -Ты можешь **add dish into submenu**. - -Ты можешь **read dish**. - -Ты можешь **patch dish**. - -Ты можешь **delete dish**. - -## Приятного аппетита -""" - - tags_metadata = [ { - "name": "menu", - "description": "Операции с меню.", + 'name': 'menu', + 'description': 'Операции с меню.', }, { - "name": "submenu", - "description": "Подменю и работа с ним", + 'name': 'submenu', + 'description': 'Подменю и работа с ним', }, - {"name": "dish", "description": "Блюда и работа с ними"}, + {'name': 'dish', 'description': 'Блюда и работа с ними'}, ] -def create_app() -> FastAPI: +def create_app(redis=None) -> FastAPI: """ Фабрика FastAPI. """ + with open('openapi.json') as f: + js = json.load(f) + app = FastAPI( - title="Fastfood-API", - description=description, - version="0.0.1", + title=js['info']['title'], + description=js['info']['description'], + version=js['info']['version'], contact={ - "name": "Sergey Vanyushkin", - "url": "http://pi3c.ru", - "email": "pi3c@yandex.ru", + 'name': 'Sergey Vanyushkin', + 'url': 'http://pi3c.ru', + 'email': 'pi3c@yandex.ru', }, license_info={ - "name": "MIT license", - "url": "https://mit-license.org/", + 'name': 'MIT license', + 'url': 'https://mit-license.org/', }, openapi_tags=tags_metadata, ) diff --git a/fastfood/config.py b/fastfood/config.py index 90394d3..e180dc7 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -1,23 +1,36 @@ +import os + from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - DB_HOST: str = "" + DB_HOST: str = '' DB_PORT: int = 5432 - POSTGRES_DB: str = "" - POSTGRES_PASSWORD: str = "" - POSTGRES_USER: str = "" - POSTGRES_DB_TEST: str = "" + POSTGRES_DB: str = '' + POSTGRES_PASSWORD: str = '' + POSTGRES_USER: str = '' + POSTGRES_DB_TEST: str = '' + REDIS_DB: str = '' @property - def DATABASE_URL_asyncpg(self): + def DATABASE_URL_asyncpg(self) -> str: """ Возвращает строку подключения к БД необходимую для SQLAlchemy """ + # Проверяем, в DOCKER или нет + + file_path = '/usr/src/RUN_IN_DOCKER' + if os.path.exists(file_path): + return ( + 'postgresql+asyncpg://' + f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' + f'@db:{self.DB_PORT}/{self.POSTGRES_DB}' + ) + return ( - "postgresql+asyncpg://" - f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" - f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}" + 'postgresql+asyncpg://' + f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' + f'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}' ) @property @@ -25,13 +38,29 @@ class Settings(BaseSettings): """ Возвращает строку подключения к БД необходимую для SQLAlchemy """ + file_path = '/usr/src/RUN_IN_DOCKER' + if os.path.exists(file_path): + return ( + 'postgresql+asyncpg://' + f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' + f'@db:{self.DB_PORT}/{self.POSTGRES_DB_TEST}' + ) + return ( - "postgresql+asyncpg://" - f"{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}" - f"@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}" + 'postgresql+asyncpg://' + f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' + f'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB_TEST}' ) - model_config = SettingsConfigDict(env_file=".env") + @property + def REDIS_URL(self): + file_path = '/usr/src/RUN_IN_DOCKER' + if os.path.exists(file_path): + return 'redis://redis:6379/0' + + return self.REDIS_DB + + model_config = SettingsConfigDict(env_file='.env') settings = Settings() diff --git a/fastfood/cruds/dish.py b/fastfood/cruds/dish.py deleted file mode 100644 index 18cb8c4..0000000 --- a/fastfood/cruds/dish.py +++ /dev/null @@ -1,64 +0,0 @@ -from uuid import UUID - -from sqlalchemy import delete, select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from fastfood import models, schemas - - -class DishCrud: - @staticmethod - async def get_dishes(submenu_id: UUID, session: AsyncSession): - async with session: - query = select(models.Dish).where(models.Dish.parent_submenu == submenu_id) - dishes = await session.execute(query) - return dishes.scalars().all() - - @staticmethod - async def create_dish_item( - submenu_id: UUID, - dish: schemas.DishBase, - session: AsyncSession, - ): - async with session: - new_dish = models.Dish(**dish.model_dump()) - new_dish.parent_submenu = submenu_id - session.add(new_dish) - await session.flush() - await session.commit() - return new_dish - - @staticmethod - async def get_dish_item( - dish_id: UUID, - session: AsyncSession, - ): - async with session: - query = select(models.Dish).where(models.Dish.id == dish_id) - submenu = await session.execute(query) - return submenu.scalars().one_or_none() - - @staticmethod - async def update_dish_item( - dish_id: UUID, - dish: schemas.DishBase, - session: AsyncSession, - ): - async with session: - query = ( - update(models.Dish) - .where(models.Dish.id == dish_id) - .values(**dish.model_dump()) - ) - await session.execute(query) - await session.commit() - qr = select(models.Dish).where(models.Dish.id == dish_id) - updated_submenu = await session.execute(qr) - return updated_submenu.scalars().one() - - @staticmethod - async def delete_dish_item(dish_id: UUID, session: AsyncSession): - async with session: - query = delete(models.Dish).where(models.Dish.id == dish_id) - await session.execute(query) - await session.commit() diff --git a/fastfood/cruds/menu.py b/fastfood/cruds/menu.py deleted file mode 100644 index 0d3c479..0000000 --- a/fastfood/cruds/menu.py +++ /dev/null @@ -1,74 +0,0 @@ -from uuid import UUID - -from sqlalchemy import delete, distinct, func, select, update -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import aliased - -from fastfood import models, schemas - - -class MenuCrud: - @staticmethod - async def get_menus(session: AsyncSession): - async with session: - query = select(models.Menu) - menus = await session.execute(query) - return menus - - @staticmethod - async def create_menu_item(menu: schemas.MenuBase, session: AsyncSession): - async with session: - new_menu = models.Menu(**menu.model_dump()) - session.add(new_menu) - await session.commit() - await session.refresh(new_menu) - return new_menu - - @staticmethod - async def get_menu_item(menu_id: UUID, session: AsyncSession): - async with session: - m = aliased(models.Menu) - s = aliased(models.SubMenu) - d = aliased(models.Dish) - - query = ( - select( - m, - func.count(distinct(s.id)).label("submenus_count"), - func.count(distinct(d.id)).label("dishes_count"), - ) - .join(s, s.parent_menu == m.id, isouter=True) - .join(d, d.parent_submenu == s.id, isouter=True) - .group_by(m.id) - .where(m.id == menu_id) - ) - menu = await session.execute(query) - menu = menu.scalars().one_or_none() - if menu is None: - return None - return menu - - @staticmethod - async def update_menu_item( - menu_id: UUID, - menu: schemas.MenuBase, - session: AsyncSession, - ): - async with session: - query = ( - update(models.Menu) - .where(models.Menu.id == menu_id) - .values(**menu.model_dump()) - ) - await session.execute(query) - await session.commit() - qr = select(models.Menu).where(models.Menu.id == menu_id) - updated_menu = await session.execute(qr) - return updated_menu - - @staticmethod - async def delete_menu_item(menu_id: UUID, session: AsyncSession): - async with session: - query = delete(models.Menu).where(models.Menu.id == menu_id) - await session.execute(query) - await session.commit() diff --git a/fastfood/cruds/submenu.py b/fastfood/cruds/submenu.py deleted file mode 100644 index db562d9..0000000 --- a/fastfood/cruds/submenu.py +++ /dev/null @@ -1,80 +0,0 @@ -from uuid import UUID - -from sqlalchemy import delete, distinct, func, select, update -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import aliased - -from fastfood import models, schemas - - -class SubMenuCrud: - @staticmethod - async def get_submenus(menu_id: UUID, session: AsyncSession): - async with session: - query = select(models.SubMenu).where( - models.SubMenu.parent_menu == menu_id, - ) - submenus = await session.execute(query) - return submenus - - @staticmethod - async def create_submenu_item( - menu_id: UUID, - submenu: schemas.MenuBase, - session: AsyncSession, - ): - async with session: - new_submenu = models.SubMenu(**submenu.model_dump()) - new_submenu.parent_menu = menu_id - session.add(new_submenu) - await session.commit() - await session.refresh(new_submenu) - return new_submenu - - @staticmethod - async def get_submenu_item( - menu_id: UUID, - submenu_id: UUID, - session: AsyncSession, - ): - async with session: - s = aliased(models.SubMenu) - d = aliased(models.Dish) - query = ( - select(s, func.count(distinct(d.id))) - .join(d, s.id == d.parent_submenu, isouter=True) - .group_by(s.id) - .where(s.id == submenu_id) - ) - submenu = await session.execute(query) - submenu = submenu.scalars().one_or_none() - if submenu is None: - return None - return submenu - - @staticmethod - async def update_submenu_item( - submenu_id: UUID, - submenu: schemas.MenuBase, - session: AsyncSession, - ): - async with session: - query = ( - update(models.SubMenu) - .where(models.SubMenu.id == submenu_id) - .values(**submenu.model_dump()) - ) - await session.execute(query) - await session.commit() - qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id) - updated_submenu = await session.execute(qr) - return updated_submenu - - @staticmethod - async def delete_submenu_item(submenu_id: UUID, session: AsyncSession): - async with session: - query = delete(models.SubMenu).where( - models.SubMenu.id == submenu_id, - ) - await session.execute(query) - await session.commit() diff --git a/fastfood/dbase.py b/fastfood/dbase.py index eb49cca..73bc929 100644 --- a/fastfood/dbase.py +++ b/fastfood/dbase.py @@ -1,7 +1,8 @@ from typing import AsyncGenerator -from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker, - create_async_engine) +import redis.asyncio as redis # type: ignore +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from fastfood.config import settings @@ -16,3 +17,13 @@ async_session_maker = async_sessionmaker( async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async with async_session_maker() as session: yield session + + +def get_redis_pool(): + return redis.from_url(settings.REDIS_URL, decode_responses=False) + + +async def get_async_redis_client( + redis_pool: redis.Redis = Depends(get_redis_pool), +): + return redis_pool diff --git a/fastfood/models.py b/fastfood/models.py index 98d2d10..bc8e12a 100644 --- a/fastfood/models.py +++ b/fastfood/models.py @@ -1,6 +1,6 @@ import uuid from copy import deepcopy -from typing import Annotated, List, Optional +from typing import Annotated from sqlalchemy import ForeignKey from sqlalchemy.dialects.postgresql import UUID @@ -21,13 +21,13 @@ str_25 = Annotated[str, 25] class Base(DeclarativeBase): id: Mapped[uuidpk] title: Mapped[str_25] - description: Mapped[Optional[str]] + description: Mapped[str | None] def __eq__(self, other): classes_match = isinstance(other, self.__class__) a, b = deepcopy(self.__dict__), deepcopy(other.__dict__) - a.pop("_sa_instance_state", None) - b.pop("_sa_instance_state", None) + a.pop('_sa_instance_state', None) + b.pop('_sa_instance_state', None) attrs_match = a == b return classes_match and attrs_match @@ -36,13 +36,13 @@ class Base(DeclarativeBase): class Menu(Base): - __tablename__ = "menu" + __tablename__ = 'menu' - submenus: Mapped[List["SubMenu"]] = relationship( - "SubMenu", - backref="menu", - lazy="selectin", - cascade="all, delete", + submenus: Mapped[list['SubMenu']] = relationship( + 'SubMenu', + backref='menu', + lazy='selectin', + cascade='all, delete', ) @hybridproperty @@ -58,16 +58,16 @@ class Menu(Base): class SubMenu(Base): - __tablename__ = "submenu" + __tablename__ = 'submenu' parent_menu: Mapped[uuid.UUID] = mapped_column( - ForeignKey("menu.id", ondelete="CASCADE") + ForeignKey('menu.id', ondelete='CASCADE') ) - dishes: Mapped[List["Dish"]] = relationship( - "Dish", - backref="submenu", - lazy="selectin", - cascade="all, delete", + dishes: Mapped[list['Dish']] = relationship( + 'Dish', + backref='submenu', + lazy='selectin', + cascade='all, delete', ) @hybridproperty @@ -76,9 +76,9 @@ class SubMenu(Base): class Dish(Base): - __tablename__ = "dish" + __tablename__ = 'dish' price: Mapped[float] parent_submenu: Mapped[uuid.UUID] = mapped_column( - ForeignKey("submenu.id", ondelete="CASCADE") + ForeignKey('submenu.id', ondelete='CASCADE') ) diff --git a/fastfood/cruds/__init__.py b/fastfood/repository/__init__.py similarity index 59% rename from fastfood/cruds/__init__.py rename to fastfood/repository/__init__.py index 155d4be..6b9f335 100644 --- a/fastfood/cruds/__init__.py +++ b/fastfood/repository/__init__.py @@ -1,9 +1,9 @@ from fastfood import models from fastfood.dbase import async_engine -from .dish import DishCrud -from .menu import MenuCrud -from .submenu import SubMenuCrud +from .dish import DishRepository +from .menu import MenuRepository +from .submenu import SubMenuRepository async def create_db_and_tables(): @@ -12,8 +12,8 @@ async def create_db_and_tables(): await conn.run_sync(models.Base.metadata.create_all) -class Crud(MenuCrud, SubMenuCrud, DishCrud): +class Repository(MenuRepository, SubMenuRepository, DishRepository): pass -crud = Crud() +ropo = Repository() diff --git a/fastfood/repository/dish.py b/fastfood/repository/dish.py new file mode 100644 index 0000000..7935d49 --- /dev/null +++ b/fastfood/repository/dish.py @@ -0,0 +1,69 @@ +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from fastfood.dbase import get_async_session +from fastfood.models import Dish +from fastfood.schemas import Dish_db + + +class DishRepository: + def __init__(self, session: AsyncSession = Depends(get_async_session)): + self.db = session + + async def get_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]: + query = select(Dish).where( + Dish.parent_submenu == submenu_id, + ) + dishes = await self.db.execute(query) + return [x for x in dishes.scalars().all()] + + async def create_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_data: Dish_db, + ) -> Dish: + new_dish = Dish(**dish_data.model_dump()) + new_dish.parent_submenu = submenu_id + self.db.add(new_dish) + await self.db.commit() + await self.db.refresh(new_dish) + return new_dish + + async def get_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_id: UUID, + ) -> Dish | None: + query = select(Dish).where(Dish.id == dish_id) + submenu = await self.db.execute(query) + return submenu.scalars().one_or_none() + + async def update_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_id: UUID, + dish_data: Dish_db, + ) -> Dish: + query = update(Dish).where(Dish.id == dish_id).values(**dish_data.model_dump()) + await self.db.execute(query) + await self.db.commit() + qr = select(Dish).where(Dish.id == dish_id) + updated_submenu = await self.db.execute(qr) + return updated_submenu.scalars().one() + + async def delete_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_id: UUID, + ) -> int: + query = delete(Dish).where(Dish.id == dish_id) + await self.db.execute(query) + await self.db.commit() + return 200 diff --git a/fastfood/repository/menu.py b/fastfood/repository/menu.py new file mode 100644 index 0000000..a7937a8 --- /dev/null +++ b/fastfood/repository/menu.py @@ -0,0 +1,66 @@ +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import delete, distinct, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import aliased + +from fastfood import schemas +from fastfood.dbase import get_async_session +from fastfood.models import Dish, Menu, SubMenu + + +class MenuRepository: + def __init__(self, session: AsyncSession = Depends(get_async_session)): + self.db = session + + async def get_menus(self) -> list[Menu]: + query = select(Menu) + menus = await self.db.execute(query) + return [x for x in menus.scalars().all()] + + async def create_menu_item(self, menu: schemas.MenuBase) -> Menu: + new_menu = Menu(**menu.model_dump()) + self.db.add(new_menu) + await self.db.commit() + await self.db.refresh(new_menu) + return new_menu + + async def get_menu_item(self, menu_id: UUID) -> Menu | None: + m = aliased(Menu) + s = aliased(SubMenu) + d = aliased(Dish) + + query = ( + select( + m, + func.count(distinct(s.id)).label('submenus_count'), + func.count(distinct(d.id)).label('dishes_count'), + ) + .join(s, s.parent_menu == m.id, isouter=True) + .join(d, d.parent_submenu == s.id, isouter=True) + .group_by(m.id) + .where(m.id == menu_id) + ) + menu = await self.db.execute(query) + menu = menu.scalars().one_or_none() + if menu is None: + return None + return menu + + async def update_menu_item( + self, + menu_id: UUID, + menu: schemas.MenuBase, + ) -> Menu: + query = update(Menu).where(Menu.id == menu_id).values(**menu.model_dump()) + await self.db.execute(query) + await self.db.commit() + qr = select(Menu).where(Menu.id == menu_id) + updated_menu = await self.db.execute(qr) + return updated_menu.scalar_one() + + async def delete_menu_item(self, menu_id: UUID): + query = delete(Menu).where(Menu.id == menu_id) + await self.db.execute(query) + await self.db.commit() diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py new file mode 100644 index 0000000..a560efd --- /dev/null +++ b/fastfood/repository/redis.py @@ -0,0 +1,65 @@ +import pickle +from typing import Any + +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_redis_pool + + +def get_key(level: str, **kwargs) -> str: + match level: + case 'menus': + return 'MENUS' + case 'menu': + return f"{kwargs.get('menu_id')}" + case 'submenus': + return f"{kwargs.get('menu_id')}:SUBMENUS" + case 'submenu': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" + case 'dishes': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES" + case 'dish': + return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" + + return 'abracadabra' + + +class RedisRepository: + def __init__( + self, + pool: redis.Redis = Depends(get_redis_pool), + ) -> None: + self.pool = pool + self.ttl = 1800 + + async def get(self, key: str) -> Any | None: + data = await self.pool.get(key) + if data is not None: + return pickle.loads(data) + return None + + async def set(self, key: str, value: Any, bg_task: BackgroundTasks) -> None: + data = pickle.dumps(value) + bg_task.add_task(self._set_cache, key, data) + + async def _set_cache(self, key: str, data: Any) -> None: + await self.pool.setex(key, self.ttl, data) + + async def delete(self, key: str, bg_task: BackgroundTasks) -> None: + bg_task.add_task(self._delete_cache, key) + + async def _delete_cache(self, key: str) -> None: + await self.pool.delete(key) + + async def clear_cache(self, pattern: str, bg_task: BackgroundTasks) -> None: + keys = [key async for key in self.pool.scan_iter(pattern)] + if keys: + bg_task.add_task(self._clear_keys, keys) + + async def _clear_keys(self, keys: list[str]) -> None: + await self.pool.delete(*keys) + + async def invalidate(self, key: str, bg_task: BackgroundTasks) -> None: + await self.clear_cache(f'{key}*', bg_task) + await self.clear_cache(f'{get_key("menus")}*', bg_task) diff --git a/fastfood/repository/submenu.py b/fastfood/repository/submenu.py new file mode 100644 index 0000000..523ad89 --- /dev/null +++ b/fastfood/repository/submenu.py @@ -0,0 +1,82 @@ +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import delete, distinct, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import aliased + +from fastfood.dbase import get_async_session +from fastfood.models import Dish, SubMenu +from fastfood.schemas import MenuBase + + +class SubMenuRepository: + def __init__(self, session: AsyncSession = Depends(get_async_session)): + self.db = session + + async def get_submenus(self, menu_id: UUID) -> list[SubMenu]: + query = select(SubMenu).where( + SubMenu.parent_menu == menu_id, + ) + submenus = await self.db.execute(query) + return [x for x in submenus.scalars().all()] + + async def create_submenu_item( + self, + menu_id: UUID, + submenu: MenuBase, + ) -> SubMenu: + new_submenu = SubMenu(**submenu.model_dump()) + new_submenu.parent_menu = menu_id + self.db.add(new_submenu) + await self.db.commit() + await self.db.refresh(new_submenu) + + full_sub = await self.get_submenu_item(menu_id, new_submenu.id) + if full_sub is None: + raise TypeError + return full_sub + + async def get_submenu_item( + self, + menu_id: UUID, + submenu_id: UUID, + ) -> SubMenu | None: + s = aliased(SubMenu) + d = aliased(Dish) + query = ( + select(s, func.count(distinct(d.id)).label('dishes_count')) + .join(d, s.id == d.parent_submenu, isouter=True) + .group_by(s.id) + .where(s.id == submenu_id) + ) + submenu = await self.db.execute(query) + submenu = submenu.scalars().one_or_none() + if submenu is None: + return None + return submenu + + async def update_submenu_item( + self, + menu_id: UUID, + submenu_id: UUID, + submenu_data: MenuBase, + ) -> SubMenu: + query = ( + update(SubMenu) + .where(SubMenu.id == submenu_id) + .values(**submenu_data.model_dump()) + ) + await self.db.execute(query) + await self.db.commit() + qr = select(SubMenu).where(SubMenu.id == submenu_id) + updated_submenu = await self.db.execute(qr) + return updated_submenu.scalar_one() + + async def delete_submenu_item(self, menu_id: UUID, submenu_id: UUID) -> int: + query = delete(SubMenu).where( + SubMenu.id == submenu_id, + ) + await self.db.execute(query) + await self.db.commit() + return 200 diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index 9007cef..b23ac1b 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -1,80 +1,84 @@ -from typing import List, Optional from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastfood import schemas -from fastfood.cruds import crud -from fastfood.dbase import get_async_session -from fastfood.utils import price_converter +from fastfood.schemas import Dish, DishBase +from fastfood.service.dish import DishService router = APIRouter( - prefix="/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes", - tags=["dish"], + prefix='/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes', + tags=['dish'], ) -@router.get("/") +@router.get('/', response_model=list[Dish]) async def get_dishes( - menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session) -): - result = await crud.get_dishes(submenu_id=submenu_id, session=session) + menu_id: UUID, + submenu_id: UUID, + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), +) -> list[Dish]: + result = await dish.read_dishes(menu_id, submenu_id) return result -@router.post("/", status_code=201) +@router.post('/', status_code=201, response_model=Dish) async def create_dish( menu_id: UUID, submenu_id: UUID, - dish: schemas.DishBase, - session: AsyncSession = Depends(get_async_session), + dish_data: DishBase, + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.create_dish_item( - submenu_id=submenu_id, - dish=dish, - session=session, + return await dish.create_dish( + menu_id, + submenu_id, + dish_data, ) - return price_converter(result) -@router.get("/{dish_id}") +@router.get('/{dish_id}', response_model=Dish) async def get_dish( menu_id: UUID, submenu_id: UUID, dish_id: UUID, - session: AsyncSession = Depends(get_async_session), + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.get_dish_item( - dish_id=dish_id, - session=session, + result = await dish.read_dish( + menu_id, + submenu_id, + dish_id, ) if not result: - raise HTTPException(status_code=404, detail="dish not found") - return price_converter(result) + raise HTTPException(status_code=404, detail='dish not found') + return result -@router.patch("/{dish_id}") +@router.patch('/{dish_id}', response_model=Dish) async def update_dish( menu_id: UUID, submenu_id: UUID, dish_id: UUID, - dish: schemas.DishBase, - session: AsyncSession = Depends(get_async_session), + dish_data: DishBase, + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.update_dish_item( - dish_id=dish_id, - dish=dish, - session=session, + result = await dish.update_dish( + menu_id, + submenu_id, + dish_id, + dish_data, ) - return price_converter(result) + return result -@router.delete("/{dish_id}") +@router.delete('/{dish_id}') async def delete_dish( menu_id: UUID, submenu_id: UUID, dish_id: UUID, - session: AsyncSession = Depends(get_async_session), + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - await crud.delete_dish_item(dish_id=dish_id, session=session) + await dish.del_dish(menu_id, submenu_id, dish_id) diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index 0b9b324..5c638e7 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -1,66 +1,64 @@ -from typing import List, Optional from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastfood import schemas -from fastfood.cruds import crud -from fastfood.dbase import get_async_session +from fastfood.schemas import MenuBase, MenuRead +from fastfood.service.menu import MenuService router = APIRouter( - prefix="/api/v1/menus", - tags=["menu"], + prefix='/api/v1/menus', + tags=['menu'], ) -@router.get("/", response_model=Optional[List[schemas.Menu]]) -async def get_menus(session: AsyncSession = Depends(get_async_session)): - result = await crud.get_menus(session=session) - return result.scalars().all() - - -@router.post("/", status_code=201, response_model=schemas.Menu) -async def add_menu( - menu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), +@router.get('/', response_model=list[MenuRead]) +async def get_menus( + menu: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.create_menu_item( - menu=menu, - session=session, - ) - return result + return await menu.read_menus() -@router.get("/{menu_id}", response_model=schemas.MenuRead) +@router.post('/', status_code=201, response_model=MenuRead) +async def add_menu( + menu: MenuBase, + responce: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), +): + return await responce.create_menu(menu) + + +@router.get('/{menu_id}', response_model=MenuRead) async def get_menu( menu_id: UUID, - session: AsyncSession = Depends(get_async_session), + responce: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.get_menu_item(menu_id=menu_id, session=session) + result = await responce.read_menu(menu_id=menu_id) if not result: - raise HTTPException(status_code=404, detail="menu not found") + raise HTTPException(status_code=404, detail='menu not found') return result -@router.patch("/{menu_id}", response_model=schemas.MenuBase) +@router.patch('/{menu_id}', response_model=MenuRead) async def update_menu( menu_id: UUID, - menu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), + menu: MenuBase, + responce: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.update_menu_item( + result = await responce.update_menu( menu_id=menu_id, - menu=menu, - session=session, + menu_data=menu, ) - return result.scalars().one() + return result -@router.delete("/{menu_id}") +@router.delete('/{menu_id}') async def delete_menu( menu_id: UUID, - session: AsyncSession = Depends(get_async_session), + menu: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - await crud.delete_menu_item(menu_id=menu_id, session=session) + await menu.del_menu(menu_id) diff --git a/fastfood/routers/submenu.py b/fastfood/routers/submenu.py index b322532..6206e78 100644 --- a/fastfood/routers/submenu.py +++ b/fastfood/routers/submenu.py @@ -1,76 +1,80 @@ from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastfood import schemas -from fastfood.cruds import crud -from fastfood.dbase import get_async_session +from fastfood.schemas import MenuBase, SubMenuRead +from fastfood.service.submenu import SubmenuService router = APIRouter( - prefix="/api/v1/menus/{menu_id}/submenus", - tags=["submenu"], + prefix='/api/v1/menus/{menu_id}/submenus', + tags=['submenu'], ) -@router.get("/") +@router.get('/', response_model=list[SubMenuRead]) async def get_submenus( - menu_id: UUID, session: AsyncSession = Depends(get_async_session) + menu_id: UUID, + submenu: SubmenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.get_submenus(menu_id=menu_id, session=session) - return result.scalars().all() + result = await submenu.read_submenus(menu_id=menu_id) + return result -@router.post("/", status_code=201) +@router.post('/', status_code=201, response_model=SubMenuRead) async def create_submenu_item( menu_id: UUID, - submenu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), + submenu_data: MenuBase, + submenu: SubmenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.create_submenu_item( + result = await submenu.create_submenu( menu_id=menu_id, - submenu=submenu, - session=session, + submenu_data=submenu_data, ) return result -@router.get("/{submenu_id}", response_model=schemas.SubMenuRead) +@router.get('/{submenu_id}', response_model=SubMenuRead) async def get_submenu( menu_id: UUID, submenu_id: UUID, - session: AsyncSession = Depends(get_async_session), + submenu: SubmenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.get_submenu_item( + result = await submenu.read_menu( menu_id=menu_id, submenu_id=submenu_id, - session=session, ) if not result: - raise HTTPException(status_code=404, detail="submenu not found") + raise HTTPException(status_code=404, detail='submenu not found') return result @router.patch( - "/{submenu_id}", - response_model=schemas.MenuBase, + '/{submenu_id}', + response_model=SubMenuRead, ) async def update_submenu( menu_id: UUID, submenu_id: UUID, - submenu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), + submenu_data: MenuBase, + submenu: SubmenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.update_submenu_item( + result = await submenu.update_submenu( + menu_id=menu_id, submenu_id=submenu_id, - submenu=submenu, - session=session, + submenu_data=submenu_data, ) - return result.scalars().one() + return result -@router.delete("/{submenu_id}") +@router.delete('/{submenu_id}') async def delete_submenu( - menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session) + menu_id: UUID, + submenu_id: UUID, + submenu: SubmenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - await crud.delete_submenu_item(submenu_id=submenu_id, session=session) + await submenu.del_menu(menu_id=menu_id, submenu_id=submenu_id) diff --git a/fastfood/schemas.py b/fastfood/schemas.py index 8fab6a1..cf14ecb 100644 --- a/fastfood/schemas.py +++ b/fastfood/schemas.py @@ -1,4 +1,3 @@ -from typing import Optional from uuid import UUID from pydantic import BaseModel @@ -6,7 +5,7 @@ from pydantic import BaseModel class MenuBase(BaseModel): title: str - description: Optional[str] + description: str | None class Config: from_attributes = True @@ -26,8 +25,12 @@ class SubMenuRead(Menu): class DishBase(MenuBase): - price: float + price: str class Dish(DishBase, Menu): pass + + +class Dish_db(MenuBase): + price: float diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py new file mode 100644 index 0000000..5b921ed --- /dev/null +++ b/fastfood/service/dish.py @@ -0,0 +1,137 @@ +from uuid import UUID + +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_async_redis_client +from fastfood.repository.dish import DishRepository +from fastfood.repository.redis import RedisRepository, get_key +from fastfood.schemas import Dish, Dish_db, DishBase + + +class DishService: + def __init__( + self, + dish_repo: DishRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ) -> None: + self.dish_repo = dish_repo + self.cache = RedisRepository(redis_client) + self.bg_tasks = background_tasks + self.key = get_key + + async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]: + cached_dishes = await self.cache.get( + self.key('dishes', menu_id=str(menu_id), submenu_id=str(submenu_id)) + ) + if cached_dishes is not None: + return cached_dishes + + data = await self.dish_repo.get_dishes(menu_id, submenu_id) + response = [] + for row in data: + dish = row.__dict__ + dish['price'] = str(dish['price']) + response.append(Dish(**dish)) + await self.cache.set( + self.key( + 'dishes', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ), + response, + self.bg_tasks, + ) + return response + + async def create_dish( + self, + menu_id: UUID, + submenu_id: UUID, + dish_data: DishBase, + ) -> Dish: + dish_db = Dish_db(**dish_data.model_dump()) + data = await self.dish_repo.create_dish_item( + menu_id, + submenu_id, + dish_db, + ) + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key('dish', menu_id=str(menu_id), submenu_id=str(submenu_id)), + dish, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return dish + + async def read_dish( + self, menu_id: UUID, submenu_id: UUID, dish_id: UUID + ) -> Dish | None: + cached_dish = await self.cache.get( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ) + ) + if cached_dish is not None: + return cached_dish + + data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) + if data is None: + return None + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ), + dish, + self.bg_tasks, + ) + return dish + + async def update_dish( + self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase + ) -> Dish: + dish_db = Dish_db(**dish_data.model_dump()) + data = await self.dish_repo.update_dish_item( + menu_id, submenu_id, dish_id, dish_db + ) + dish = data.__dict__ + dish['price'] = str(dish['price']) + dish = Dish(**dish) + await self.cache.set( + self.key( + 'dish', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + dish_id=str(dish_id), + ), + dish, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return dish + + async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> int: + response = await self.dish_repo.delete_dish_item( + menu_id, + submenu_id, + dish_id, + ) + await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return response diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py new file mode 100644 index 0000000..48147e6 --- /dev/null +++ b/fastfood/service/menu.py @@ -0,0 +1,111 @@ +from uuid import UUID + +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_async_redis_client +from fastfood.repository.menu import MenuRepository +from fastfood.repository.redis import RedisRepository, get_key +from fastfood.schemas import MenuBase, MenuRead + + +class MenuService: + def __init__( + self, + menu_repo: MenuRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ) -> None: + self.menu_repo = menu_repo + self.cache = RedisRepository(redis_client) + self.key = get_key + self.bg_tasks = background_tasks + + async def read_menus(self) -> list[MenuRead]: + cached_menus = await self.cache.get(self.key('menus')) + if cached_menus is not None: + return cached_menus + + data = await self.menu_repo.get_menus() + menus = [] + for r in data: + menu = r.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 + for sub in r.submenus: + dishes_conter += len(sub.dishes) + + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + menu = MenuRead(**menu) + menus.append(menu) + + await self.cache.set(self.key('menus'), menus, self.bg_tasks) + return menus + + async def create_menu(self, menu_data: MenuBase) -> MenuRead: + data = await self.menu_repo.create_menu_item(menu_data) + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 + + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + await self.cache.set( + key=get_key('menu', menu_id=str(menu.get('id'))), + value=menu, + bg_task=self.bg_tasks, + ) + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu.id), bg_task=self.bg_tasks) + return menu + + async def read_menu(self, menu_id: UUID) -> MenuRead | None: + cached_menu = await self.cache.get(self.key('menu', menu_id=str(menu_id))) + if cached_menu is not None: + return cached_menu + + data = await self.menu_repo.get_menu_item(menu_id) + if data is None: + return None + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 + + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + return menu + + async def update_menu(self, menu_id: UUID, menu_data) -> MenuRead: + data = await self.menu_repo.update_menu_item(menu_id, menu_data) + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 + + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + menu = MenuRead(**menu) + await self.cache.set( + self.key('menu', menu_id=str(menu.id)), menu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return menu + + async def del_menu(self, menu_id: UUID): + data = await self.menu_repo.delete_menu_item(menu_id) + await self.cache.delete(key=str(menu_id), bg_task=self.bg_tasks) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return data diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py new file mode 100644 index 0000000..db23343 --- /dev/null +++ b/fastfood/service/submenu.py @@ -0,0 +1,120 @@ +from uuid import UUID + +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_async_redis_client +from fastfood.repository.redis import RedisRepository, get_key +from fastfood.repository.submenu import SubMenuRepository +from fastfood.schemas import MenuBase, SubMenuRead + + +class SubmenuService: + def __init__( + self, + submenu_repo: SubMenuRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ) -> None: + + self.submenu_repo = submenu_repo + self.cache = RedisRepository(redis_client) + self.bg_tasks = background_tasks + self.key = get_key + + async def read_submenus(self, menu_id: UUID) -> list[SubMenuRead]: + cached_submenus = await self.cache.get( + self.key('submenus', menu_id=str(menu_id)) + ) + if cached_submenus is not None: + return cached_submenus + + data = await self.submenu_repo.get_submenus(menu_id=menu_id) + submenus = [] + for r in data: + submenu = r.__dict__ + subq = await self.submenu_repo.get_submenu_item(menu_id, r.id) + if subq is not None: + submenu['dishes_count'] = len(subq.dishes) + submenu = SubMenuRead(**submenu) + submenus.append(submenu) + + await self.cache.set( + self.key('submenus', menu_id=str(menu_id)), submenus, self.bg_tasks + ) + return submenus + + async def create_submenu( + self, menu_id: UUID, submenu_data: MenuBase + ) -> SubMenuRead: + data = await self.submenu_repo.create_submenu_item( + menu_id, + submenu_data, + ) + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + submenu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id)), submenu, self.bg_tasks + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return submenu + + async def read_menu(self, menu_id: UUID, submenu_id: UUID) -> SubMenuRead | None: + cached_submenu = await self.cache.get( + self.key( + 'submenu', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ) + ) + if cached_submenu is not None: + return cached_submenu + + data = await self.submenu_repo.get_submenu_item(menu_id, submenu_id) + if data is None: + return None + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + menu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)), + submenu, + self.bg_tasks, + ) + return menu + + async def update_submenu( + self, menu_id: UUID, submenu_id: UUID, submenu_data: MenuBase + ) -> SubMenuRead: + data = await self.submenu_repo.update_submenu_item( + menu_id, submenu_id, submenu_data + ) + submenu = data.__dict__ + submenu = {k: v for k, v in submenu.items() if not k.startswith('_')} + submenu['dishes_count'] = len(submenu.pop('dishes')) + submenu = SubMenuRead(**submenu) + await self.cache.set( + self.key('submenu', menu_id=str(menu_id), submenu_id=str(submenu_id)), + submenu, + self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + + return submenu + + async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> int: + code = await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) + await self.cache.delete( + key=self.key( + 'submenu', + menu_id=str(menu_id), + submenu_id=str(submenu_id), + ), + bg_task=self.bg_tasks, + ) + await self.cache.invalidate(key=str(menu_id), bg_task=self.bg_tasks) + return code diff --git a/fastfood/utils.py b/fastfood/utils.py deleted file mode 100644 index aa1205f..0000000 --- a/fastfood/utils.py +++ /dev/null @@ -1,3 +0,0 @@ -def price_converter(dish: dict) -> dict: - dish.price = str(dish.price) - return dish diff --git a/manage.py b/manage.py index 7a44d5f..f204f65 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys import uvicorn -from fastfood.cruds import create_db_and_tables +from fastfood.repository import create_db_and_tables def run_app(): @@ -11,8 +11,8 @@ def run_app(): Запуск FastAPI """ uvicorn.run( - app="fastfood.app:create_app", - host="0.0.0.0", + app='fastfood.app:create_app', + host='0.0.0.0', port=8000, reload=True, factory=True, @@ -25,10 +25,10 @@ async def recreate(): await create_db_and_tables() -if __name__ == "__main__": - if "--run-server" in sys.argv: +if __name__ == '__main__': + if '--run-server' in sys.argv: run_app() - if "--run-test-server" in sys.argv: + if '--run-test-server' in sys.argv: asyncio.run(recreate()) run_app() diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..e41830d --- /dev/null +++ b/openapi.json @@ -0,0 +1 @@ +{"openapi": "3.1.0", "info": {"title": "Fastfood-API", "description": "\n# \ud83d\udd25\ud83d\udd25\ud83d\udd25Fastfood-API \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0442\u0435\u0431\u0435 \u043f\u043e\u0434\u043a\u0440\u0435\u043f\u0438\u0442\u044c\u0441\u044f \ud83d\udd25\ud83d\udd25\ud83d\udd25\n\n### \u0423 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c Menu. \u0422\u044b \u043c\u043e\u0436\u0435\u0448 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u0431\u043b\u044e\u0434\u0430 \u0438\u0437 \u043a\u0443\u0445\u043d\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0442\u0435\u0431\u0435 \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f\n\n## Menu\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **add menu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **read menu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **patch menu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **delete menu**.\n\n### \u0423 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432 SubMenu, \u0433\u0434\u0435 \u0442\u044b \u0441\u043c\u043e\u0436\u0435\u0448\u044c \u043d\u0430\u0439\u0442\u0438\n\u0434\u0435\u0441\u0435\u0440\u0442\u044b/\u043d\u0430\u043f\u0438\u0442\u043a\u0438/\u0441\u0443\u043f\u0447\u0438\u043a\u0438/\u043f\u0440\u043e\u0447\u0438\u0435 \u0432\u043a\u0443\u0441\u043d\u043e\u0441\u0442\u0438\n\n# SubMenu\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **add submenu into menu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **read submenu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **patch submenu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **delete menu**.\n\n### \u0423 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0432 Dish, \u0433\u0434\u0435 \u0442\u044b \u0441\u043c\u043e\u0436\u0435\u0448\u044c \u043d\u0430\u0439\u0442\u0438 \u0431\u043b\u044e\u0434\u043e \u043f\u043e \u0432\u043a\u0443\u0441\u0443\n\n# Dish\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **add dish into submenu**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **read dish**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **patch dish**.\n\n\u0422\u044b \u043c\u043e\u0436\u0435\u0448\u044c **delete dish**.\n\n## \u041f\u0440\u0438\u044f\u0442\u043d\u043e\u0433\u043e \u0430\u043f\u043f\u0435\u0442\u0438\u0442\u0430\n", "version": "0.0.1"}, "paths": {"/api/v1/menus/": {"get": {"tags": ["menu"], "summary": "Get Menus", "operationId": "get_menus_api_v1_menus__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"anyOf": [{"items": {"$ref": "#/components/schemas/Menu"}, "type": "array"}, {"type": "null"}], "title": "Response Get Menus Api V1 Menus Get"}}}}}}, "post": {"tags": ["menu"], "summary": "Add Menu", "operationId": "add_menu_api_v1_menus__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuBase"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Menu"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}": {"get": {"tags": ["menu"], "summary": "Get Menu", "operationId": "get_menu_api_v1_menus__menu_id__get", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuRead"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["menu"], "summary": "Update Menu", "operationId": "update_menu_api_v1_menus__menu_id__patch", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuBase"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuRead"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["menu"], "summary": "Delete Menu", "operationId": "delete_menu_api_v1_menus__menu_id__delete", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}/submenus/": {"get": {"tags": ["submenu"], "summary": "Get Submenus", "operationId": "get_submenus_api_v1_menus__menu_id__submenus__get", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/SubMenuRead"}}, {"type": "null"}], "title": "Response Get Submenus Api V1 Menus Menu Id Submenus Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "post": {"tags": ["submenu"], "summary": "Create Submenu Item", "operationId": "create_submenu_item_api_v1_menus__menu_id__submenus__post", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuBase"}}}}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubMenuRead"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}/submenus/{submenu_id}": {"get": {"tags": ["submenu"], "summary": "Get Submenu", "operationId": "get_submenu_api_v1_menus__menu_id__submenus__submenu_id__get", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubMenuRead"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["submenu"], "summary": "Update Submenu", "operationId": "update_submenu_api_v1_menus__menu_id__submenus__submenu_id__patch", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuBase"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MenuBase"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["submenu"], "summary": "Delete Submenu", "operationId": "delete_submenu_api_v1_menus__menu_id__submenus__submenu_id__delete", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes/": {"get": {"tags": ["dish"], "summary": "Get Dishes", "operationId": "get_dishes_api_v1_menus__menu_id__submenus__submenu_id__dishes__get", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "post": {"tags": ["dish"], "summary": "Create Dish", "operationId": "create_dish_api_v1_menus__menu_id__submenus__submenu_id__dishes__post", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DishBase"}}}}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes/{dish_id}": {"get": {"tags": ["dish"], "summary": "Get Dish", "operationId": "get_dish_api_v1_menus__menu_id__submenus__submenu_id__dishes__dish_id__get", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}, {"name": "dish_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Dish Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["dish"], "summary": "Update Dish", "operationId": "update_dish_api_v1_menus__menu_id__submenus__submenu_id__dishes__dish_id__patch", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}, {"name": "dish_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Dish Id"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DishBase"}}}}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["dish"], "summary": "Delete Dish", "operationId": "delete_dish_api_v1_menus__menu_id__submenus__submenu_id__dishes__dish_id__delete", "parameters": [{"name": "menu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Menu Id"}}, {"name": "submenu_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Submenu Id"}}, {"name": "dish_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Dish Id"}}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"DishBase": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "price": {"type": "number", "title": "Price"}}, "type": "object", "required": ["title", "description", "price"], "title": "DishBase"}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "Menu": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "id": {"type": "string", "format": "uuid", "title": "Id"}}, "type": "object", "required": ["title", "description", "id"], "title": "Menu"}, "MenuBase": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}}, "type": "object", "required": ["title", "description"], "title": "MenuBase"}, "MenuRead": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "id": {"type": "string", "format": "uuid", "title": "Id"}, "submenus_count": {"type": "integer", "title": "Submenus Count"}, "dishes_count": {"type": "integer", "title": "Dishes Count"}}, "type": "object", "required": ["title", "description", "id", "submenus_count", "dishes_count"], "title": "MenuRead"}, "SubMenuRead": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "id": {"type": "string", "format": "uuid", "title": "Id"}, "dishes_count": {"type": "integer", "title": "Dishes Count"}}, "type": "object", "required": ["title", "description", "id", "dishes_count"], "title": "SubMenuRead"}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}}}} diff --git a/poetry.lock b/poetry.lock index eef5531..dc0ad01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -103,13 +103,88 @@ test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] @@ -204,6 +279,71 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "42.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "dnspython" version = "2.5.0" @@ -272,6 +412,22 @@ typing-extensions = ">=4.8.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "greenlet" version = "3.0.3" @@ -399,6 +555,20 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.6" @@ -421,6 +591,78 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "23.2" @@ -432,35 +674,79 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" -version = "2.5.3" +version = "2.6.0" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, - {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, + {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, + {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.14.6" +pydantic-core = "2.16.1" typing-extensions = ">=4.6.1" [package.extras] @@ -468,116 +754,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.6" +version = "2.16.1" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, - {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, - {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, - {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, - {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, - {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, - {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, - {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, - {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, - {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, - {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, - {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, - {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, - {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, - {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, - {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, + {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, + {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, + {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, + {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, + {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, + {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, + {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, + {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, + {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, + {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, + {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, + {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, + {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, ] [package.dependencies] @@ -622,17 +882,17 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.23.3" +version = "0.23.4" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.3.tar.gz", hash = "sha256:af313ce900a62fbe2b1aed18e37ad757f1ef9940c6b6a88e2954de38d6b1fb9f"}, - {file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"}, + {file = "pytest-asyncio-0.23.4.tar.gz", hash = "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2"}, + {file = "pytest_asyncio-0.23.4-py3-none-any.whl", hash = "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef"}, ] [package.dependencies] -pytest = ">=7.0.0" +pytest = ">=7.0.0,<8" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -658,18 +918,111 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "redis" +version = "4.6.0" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "setuptools" +version = "69.0.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "sniffio" version = "1.3.0" @@ -796,6 +1149,35 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "types-pyopenssl" +version = "24.0.0.20240130" +description = "Typing stubs for pyOpenSSL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pyOpenSSL-24.0.0.20240130.tar.gz", hash = "sha256:c812e5c1c35249f75ef5935708b2a997d62abf9745be222e5f94b9595472ab25"}, + {file = "types_pyOpenSSL-24.0.0.20240130-py3-none-any.whl", hash = "sha256:24a255458b5b8a7fca8139cf56f2a8ad5a4f1a5f711b73a5bb9cb50dc688fab5"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" + +[[package]] +name = "types-redis" +version = "4.6.0.20240106" +description = "Typing stubs for redis" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-redis-4.6.0.20240106.tar.gz", hash = "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e"}, + {file = "types_redis-4.6.0.20240106-py3-none-any.whl", hash = "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-pyOpenSSL = "*" + [[package]] name = "typing-extensions" version = "4.9.0" @@ -826,7 +1208,27 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5bbc3cad36f6f40d10cb848918426b640f9e703bc2c6b22b5b8fe381a6251ded" +content-hash = "106e42984de924817e2dc083ad78699b3411f9aa60de5bb5c1a95ca94a21fda1" diff --git a/postman_scripts/menu app.postman_collection.json b/postman_scripts/menu app.postman_collection.json index f60b345..7898913 100644 --- a/postman_scripts/menu app.postman_collection.json +++ b/postman_scripts/menu app.postman_collection.json @@ -3887,4 +3887,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/postman_scripts/menu app.postman_environment.json b/postman_scripts/menu app.postman_environment.json index f85cfa5..9017bd3 100644 --- a/postman_scripts/menu app.postman_environment.json +++ b/postman_scripts/menu app.postman_environment.json @@ -90,4 +90,4 @@ "_postman_variable_scope": "environment", "_postman_exported_at": "2023-01-12T16:22:10.333Z", "_postman_exported_using": "Postman/10.6.7" -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 560b0d5..a9dc80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,12 +14,16 @@ asyncpg = "^0.29.0" pydantic-settings = "^2.1.0" email-validator = "^2.1.0.post1" pytest-asyncio = "^0.23.3" +redis = "^4.6.0" +types-redis = "^4.6.0.3" +mypy = "^1.4.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4.4" pytest-cov = "^4.1.0" httpx = "^0.26.0" +pre-commit = "^3.6.0" [build-system] requires = ["poetry-core"] diff --git a/tests/conftest.py b/tests/conftest.py index a90680a..30f1296 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import asyncio -from typing import AsyncGenerator, Dict, Generator +from typing import AsyncGenerator import pytest import pytest_asyncio @@ -20,7 +20,7 @@ async_session_maker = async_sessionmaker( ) -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope='session', autouse=True) def event_loop(): try: loop = asyncio.get_event_loop() @@ -30,7 +30,7 @@ def event_loop(): loop.close() -@pytest_asyncio.fixture(scope="function", autouse=True) +@pytest_asyncio.fixture(scope='session', autouse=True) async def db_init(event_loop): async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) @@ -45,28 +45,13 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]: yield session -@pytest.fixture(scope="session") -def app(event_loop) -> Generator[FastAPI, None, None]: +@pytest_asyncio.fixture(scope='session') +async def client() -> AsyncGenerator[AsyncClient, None]: app: FastAPI = create_app() app.dependency_overrides[get_async_session] = get_test_session - yield app - -@pytest_asyncio.fixture(scope="function") -async def client(app) -> AsyncGenerator[AsyncClient, None]: async with AsyncClient( app=app, - base_url="http://localhost:8000/api/v1/menus", + base_url='http://localhost:8000/api/v1/menus', ) as async_client: yield async_client - - -@pytest_asyncio.fixture(scope="function") -async def asession(event_loop) -> AsyncGenerator[AsyncSession, None]: - async with async_session_maker() as session: - yield session - - -@pytest.fixture(scope="session") -def session_data() -> Dict: - return {} diff --git a/tests/repository.py b/tests/repository.py new file mode 100644 index 0000000..f4aca24 --- /dev/null +++ b/tests/repository.py @@ -0,0 +1,188 @@ +from httpx import AsyncClient, Response + +from .urls import reverse_url + + +class Repository: + class Menu: + @staticmethod + async def read_all(ac: AsyncClient) -> tuple[int, dict]: + """чтение всех меню""" + + response: Response = await ac.get(reverse_url('menus')) + return response.status_code, response.json() + + @staticmethod + async def get(ac: AsyncClient, data: dict) -> tuple[int, dict]: + """Получение меню по id""" + response: Response = await ac.get( + reverse_url('menu', menu_id=data.get('id')) + ) + return response.status_code, response.json() + + @staticmethod + async def write(ac: AsyncClient, data: dict) -> tuple[int, dict]: + """создания меню""" + response: Response = await ac.post(reverse_url('menus'), json=data) + return response.status_code, response.json() + + @staticmethod + async def update(ac: AsyncClient, data: dict) -> tuple[int, dict]: + """Обновление меню по id""" + response: Response = await ac.patch( + reverse_url('menu', menu_id=data.get('id')), + json=data, + ) + return response.status_code, response.json() + + @staticmethod + async def delete(ac: AsyncClient, data: dict) -> int: + """Удаление меню по id""" + response: Response = await ac.delete( + reverse_url('menu', menu_id=data.get('id')), + ) + return response.status_code + + class Submenu: + @staticmethod + async def read_all(ac: AsyncClient, menu: dict) -> tuple[int, dict]: + """чтение всех меню""" + response: Response = await ac.get( + reverse_url('submenus', menu_id=menu.get('id')), + ) + return response.status_code, response.json() + + @staticmethod + async def get( + ac: AsyncClient, + menu: dict, + submenu: dict, + ) -> tuple[int, dict]: + """Получение меню по id""" + response: Response = await ac.get( + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), + ) + return response.status_code, response.json() + + @staticmethod + async def write( + ac: AsyncClient, + menu: dict, + submenu: dict, + ) -> tuple[int, dict]: + """создания меню""" + response: Response = await ac.post( + reverse_url('submenu', menu_id=menu.get('id')), + json=submenu, + ) + return response.status_code, response.json() + + @staticmethod + async def update( + ac: AsyncClient, menu: dict, submenu: dict + ) -> tuple[int, dict]: + """Обновление меню по id""" + response: Response = await ac.patch( + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), + json=submenu, + ) + return response.status_code, response.json() + + @staticmethod + async def delete(ac: AsyncClient, menu: dict, submenu: dict) -> int: + """Удаление меню по id""" + response: Response = await ac.delete( + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), + ) + return response.status_code + + class Dish: + @staticmethod + async def read_all( + ac: AsyncClient, menu: dict, submenu: dict + ) -> tuple[int, dict]: + """чтение всех блюд""" + response: Response = await ac.get( + reverse_url( + 'dishes', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), + ) + return response.status_code, response.json() + + @staticmethod + async def get( + ac: AsyncClient, menu: dict, submenu: dict, dish: dict + ) -> tuple[int, dict]: + """Получение блюда по id""" + response: Response = await ac.get( + reverse_url( + 'dish', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + dish_id=dish.get('id'), + ), + ) + return response.status_code, response.json() + + @staticmethod + async def write( + ac: AsyncClient, menu: dict, submenu: dict, dish: dict + ) -> tuple[int, dict]: + """создания блюда""" + response: Response = await ac.post( + reverse_url( + 'dishes', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), + json=dish, + ) + return response.status_code, response.json() + + @staticmethod + async def update( + ac: AsyncClient, menu: dict, submenu: dict, dish: dict + ) -> tuple[int, dict]: + """Обновление блюда по id""" + response: Response = await ac.patch( + reverse_url( + 'dish', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + dish_id=dish.get('id'), + ), + json=dish, + ) + return response.status_code, response.json() + + @staticmethod + async def delete( + ac: AsyncClient, + menu: dict, + submenu: dict, + dish: dict, + ) -> int: + """Удаление блюда по id""" + response: Response = await ac.delete( + reverse_url( + 'dish', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + dish_id=dish.get('id'), + ), + ) + return response.status_code diff --git a/tests/test_api.py b/tests/test_api.py index fb16202..d57f408 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,603 +1,360 @@ -from typing import Dict, Tuple - import pytest -from httpx import AsyncClient, Response - - -class TestBaseCrud: - class Menu: - @staticmethod - async def read_all(ac: AsyncClient) -> Tuple[int, dict]: - """чтение всех меню""" - response: Response = await ac.get("/") - return response.status_code, response.json() - - @staticmethod - async def get(ac: AsyncClient, data: dict) -> Tuple[int, dict]: - """Получение меню по id""" - response: Response = await ac.get(f"/{data.get('id')}") - return response.status_code, response.json() - - @staticmethod - async def write(ac: AsyncClient, data: dict) -> Tuple[int, dict]: - """создания меню""" - response: Response = await ac.post("/", json=data) - return response.status_code, response.json() - - @staticmethod - async def update(ac: AsyncClient, data: dict) -> Tuple[int, dict]: - """Обновление меню по id""" - response: Response = await ac.patch( - f"/{data.get('id')}", - json=data, - ) - return response.status_code, response.json() - - @staticmethod - async def delete(ac: AsyncClient, data: dict) -> int: - """Удаление меню по id""" - response: Response = await ac.delete(f"/{data.get('id')}") - return response.status_code - - class Submenu: - @staticmethod - async def read_all(ac: AsyncClient, menu: dict) -> Tuple[int, dict]: - """чтение всех меню""" - response: Response = await ac.get(f"/{menu.get('id')}/submenus/") - return response.status_code, response.json() - - @staticmethod - async def get( - ac: AsyncClient, - menu: dict, - submenu: dict, - ) -> Tuple[int, dict]: - """Получение меню по id""" - response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}", - ) - return response.status_code, response.json() - - @staticmethod - async def write( - ac: AsyncClient, - menu: dict, - submenu: dict, - ) -> Tuple[int, dict]: - """создания меню""" - response: Response = await ac.post( - f"/{menu.get('id')}/submenus/", - json=submenu, - ) - return response.status_code, response.json() - - @staticmethod - async def update( - ac: AsyncClient, menu: dict, submenu: dict - ) -> Tuple[int, dict]: - """Обновление меню по id""" - response: Response = await ac.patch( - f"/{menu.get('id')}/submenus/{submenu.get('id')}", - json=submenu, - ) - return response.status_code, response.json() - - @staticmethod - async def delete(ac: AsyncClient, menu: dict, submenu: dict) -> int: - """Удаление меню по id""" - response: Response = await ac.delete( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - ) - return response.status_code - - class Dish: - @staticmethod - async def read_all( - ac: AsyncClient, menu: dict, submenu: dict - ) -> Tuple[int, dict]: - """чтение всех блюд""" - response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", - ) - return response.status_code, response.json() - - @staticmethod - async def get( - ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: - """Получение блюда по id""" - response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}", - ) - return response.status_code, response.json() - - @staticmethod - async def write( - ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: - """создания блюда""" - response: Response = await ac.post( - f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", - json=dish, - ) - return response.status_code, response.json() - - @staticmethod - async def update( - ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: - """Обновление блюда по id""" - response: Response = await ac.patch( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}", - json=dish, - ) - return response.status_code, response.json() - - @staticmethod - async def delete(ac: AsyncClient, menu: dict, submenu: dict, dish: dict) -> int: - """Удаление блюда по id""" - response: Response = await ac.delete( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}" - ) - return response.status_code - - @pytest.mark.asyncio - async def test_menu_crud_empty(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - code, rspn = await self.Menu.read_all(client) - assert code == 200 - assert rspn == [] - - @pytest.mark.asyncio - async def test_menu_crud_add(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - assert code == 201 - assert rspn["title"] == "Menu" - assert rspn["description"] is None - - @pytest.mark.asyncio - async def test_menu_crud_get(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) - assert code == 200 - assert menu["title"] == rspn["title"] - - @pytest.mark.asyncio - async def test_menu_crud_update(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - - upd_data = { - "id": rspn.get("id"), - "title": "upd Menu", - "description": "", - } - code, upd_rspn = await self.Menu.update(client, upd_data) - assert code == 200 - assert upd_rspn["title"] == "upd Menu" - - @pytest.mark.asyncio - async def test_menu_crud_delete(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - - code = await self.Menu.delete(client, rspn) - assert code == 200 - - code, rspn = await self.Menu.get(client, {"id": rspn.get("id")}) - assert code == 404 - - @pytest.mark.asyncio - async def test_menu_crud_get_all(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - code, rspn = await self.Menu.read_all(client) - assert code == 200 - assert rspn == [] - - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - - code, upd_rspn = await self.Menu.read_all(client) - assert code == 200 - assert upd_rspn == [rspn] - - @pytest.mark.asyncio - async def test_submenus_get_all(self, client) -> None: - # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Проверяем наличие подменю - code, rspn = await self.Submenu.read_all(client, menu) - assert code == 200 - assert rspn == [] - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Проверяем наличие подменю - code, upd_rspn = await self.Submenu.read_all(client, menu) - assert code == 200 - assert upd_rspn == [rspn] - - # удаляем сопутствующее - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_submenus_add(self, client) -> None: - # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - assert code == 201 - submenu.update(rspn) - - # удаляем сопутствующее - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_submenus_update(self, client) -> None: - # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Обновляем подменю и проверяем - submenu["title"] = "updated_submenu" - code, rspn = await self.Submenu.update(client, menu, submenu) - assert code == 200 - assert submenu["title"] == rspn["title"] - submenu.update(rspn) - - # удаляем сопутствующее - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_submenus_delete(self, client) -> None: - # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Удаляем подменю - code = await self.Submenu.delete(client, menu, submenu) - assert code == 200 - - # Проверяем удаленное подменю - code, rspn = await self.Submenu.get(client, menu, submenu) - assert code == 404 - - # удаляем сопутствующее - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_dishes_get_all(self, client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Проверяем все блюда в подменю - code, rspn = await self.Dish.read_all(client, menu, submenu) - assert code == 200 - assert rspn == [] - - # Добавляем блюдо - dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], - } - code, rspn = await self.Dish.write(client, menu, submenu, dish) - assert code == 201 - dish.update(rspn) - - code, upd_rspn = await self.Dish.read_all(client, menu, submenu) - - assert code == 200 - - # удаляем сопутствующее - await self.Dish.delete(client, menu, submenu, dish) - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_dishes_add(self, client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], - } - code, rspn = await self.Dish.write(client, menu, submenu, dish) - assert code == 201 - dish.update(rspn) - - # Получаем блюдо - code, rspn = await self.Dish.get(client, menu, submenu, dish) - assert code == 200 - assert rspn["title"] == dish["title"] - - # удаляем сопутствующее - await self.Dish.delete(client, menu, submenu, dish) - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_dishes_update(self, client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], - } - code, rspn = await self.Dish.write(client, menu, submenu, dish) - dish.update(rspn) - - # Обновляем блюдо и проверяем - dish["title"] = "updated_dish" - code, rspn = await self.Dish.update(client, menu, submenu, dish) - assert code == 200 - assert dish["title"] == rspn["title"] - dish.update(rspn) - - # удаляем сопутствующее - await self.Dish.delete(client, menu, submenu, dish) - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_dishes_delete(self, client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await self.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], - } - code, rspn = await self.Dish.write(client, menu, submenu, dish) - dish.update(rspn) - - # Удаляем подменю - code = await self.Dish.delete(client, menu, submenu, dish) - assert code == 200 - - # Проверяем удаленное блюдо - code, rspn = await self.Dish.get(client, menu, submenu, dish) - assert code == 404 - - # удаляем сопутствующее - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) - - -class TestСontinuity: - @pytest.mark.asyncio - async def test_01(self, client, session_data: Dict): - """Проверяет создание меню""" - data = {"title": "Menu", "description": "some"} - code, rspn = await TestBaseCrud.Menu.write(client, data) - - assert code == 201 - code, rspn = await TestBaseCrud.Menu.get(client, rspn) - session_data["target_menu_id"] = rspn.get("id") - session_data["target_menu_title"] = rspn.get("title") - session_data["target_menu_description"] = rspn.get("description") - - assert code == 200 - assert "id" in rspn - assert "title" in rspn - assert "description" in rspn - assert "submenus_count" in rspn - assert "dishes_count" in rspn - - assert rspn["title"] == "Menu" - assert rspn.get("description") == "some" - - @pytest.mark.asyncio - async def test_postman_continuity(self, client): - # Создаем меню - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await TestBaseCrud.Menu.write(client, menu) - assert code == 201 - assert "id" in rspn.keys() - menu.update(rspn) - - # Создаем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await TestBaseCrud.Submenu.write(client, menu, submenu) - assert code == 201 - assert "id" in rspn.keys() - submenu.update(rspn) - - # Добавляем блюдо1 - dish = { - "title": "dish1", - "description": "some dish1", - "price": "13.50", - "parent_submenu": submenu["id"], - } - code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) - assert code == 201 - assert "id" in rspn.keys() - dish.update(rspn) - - # Добавляем блюдо2 - dish = { - "title": "dish2", - "description": "some dish2", - "price": "12.50", - "parent_submenu": submenu["id"], - } - code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) - assert code == 201 - assert "id" in rspn.keys() - dish.update(rspn) - - # Просматриваем конкретное меню - code, rspn = await TestBaseCrud.Menu.get(client, menu) - assert code == 200 - assert "id" in rspn.keys() - assert menu["id"] == rspn["id"] - assert "submenus_count" in rspn.keys() - assert rspn["submenus_count"] == 1 - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 2 - - # Просматриваем конкретное подменю - code, rspn = await TestBaseCrud.Submenu.get(client, menu, submenu) - assert code == 200 - assert "id" in rspn.keys() - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 2 - - # Удаляем подменю - code = await TestBaseCrud.Submenu.delete(client, menu, submenu) - assert code == 200 - - # Просматриваем список подменю - code, rspn = await TestBaseCrud.Submenu.read_all(client, menu) - assert code == 200 - assert rspn == [] - - # Просматриваем список блюд - code, rspn = await TestBaseCrud.Dish.read_all(client, menu, submenu) - assert code == 200 - assert rspn == [] - - # Просматриваем конкретное меню - code, rspn = await TestBaseCrud.Menu.get(client, menu) - assert code == 200 - assert "id" in rspn.keys() - assert menu["id"] == rspn["id"] - assert "submenus_count" in rspn.keys() - assert rspn["submenus_count"] == 0 - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 0 - - # Удаляем меню - code = await TestBaseCrud.Menu.delete(client, menu) - assert code == 200 - - # Просматриваем все меню - code, rspn = await TestBaseCrud.Menu.read_all(client) - assert code == 200 - assert rspn == [] +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@pytest.mark.asyncio +async def test_menu_crud_empty(client: AsyncClient) -> None: + """Тестирование функций меню""" + code, rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert rspn == [] + + +@pytest.mark.asyncio +async def test_menu_crud_add(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + assert code == 201 + assert rspn['title'] == 'Menu' + assert rspn['description'] is None + await Repo.Menu.delete(client, rspn) + + +@pytest.mark.asyncio +async def test_menu_crud_get(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + code, menu = await Repo.Menu.get(client, {'id': rspn.get('id')}) + assert code == 200 + assert menu['title'] == rspn['title'] + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_menu_crud_update(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + upd_data = { + 'id': rspn.get('id'), + 'title': 'upd Menu', + 'description': '', + } + code, upd_rspn = await Repo.Menu.update(client, upd_data) + assert code == 200 + assert upd_rspn['title'] == 'upd Menu' + await Repo.Menu.delete(client, upd_rspn) + + +@pytest.mark.asyncio +async def test_menu_crud_delete(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + code = await Repo.Menu.delete(client, rspn) + assert code == 200 + + code, rspn = await Repo.Menu.get(client, {'id': rspn.get('id')}) + assert code == 404 + + +@pytest.mark.asyncio +async def test_menu_crud_get_all(client: AsyncClient) -> None: + """Тестирование функций меню""" + code, rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert rspn == [] + + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + code, upd_rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert upd_rspn == [rspn] + await Repo.Menu.delete(client, rspn) + + +@pytest.mark.asyncio +async def test_submenus_get_all(client) -> None: + # Создаем меню и проверяем ответ + menu = {'title': 'Menu', 'description': 'main menu'} + code, rspn = await Repo.Menu.write(client, menu) + assert code == 201 + menu.update(rspn) + + # Проверяем наличие подменю + code, rspn = await Repo.Submenu.read_all(client, menu) + assert code == 200 + assert rspn == [] + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Проверяем наличие подменю + code, upd_rspn = await Repo.Submenu.read_all(client, menu) + assert code == 200 + assert upd_rspn == [rspn] + + # удаляем сопутствующее + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_submenus_add(client) -> None: + # Создаем меню и проверяем ответ + menu = {'title': 'Menu', 'description': 'main menu'} + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + assert code == 201 + submenu.update(rspn) + + # удаляем сопутствующее + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_submenus_update(client) -> None: + # Создаем меню и проверяем ответ + menu = {'title': 'Menu', 'description': 'main menu'} + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Обновляем подменю и проверяем + submenu['title'] = 'updated_submenu' + code, rspn = await Repo.Submenu.update(client, menu, submenu) + assert code == 200 + assert submenu['title'] == rspn['title'] + submenu.update(rspn) + + # удаляем сопутствующее + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_submenus_delete(client) -> None: + # Создаем меню и проверяем ответ + menu = {'title': 'Menu', 'description': 'main menu'} + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Удаляем подменю + code = await Repo.Submenu.delete(client, menu, submenu) + assert code == 200 + + # Проверяем удаленное подменю + code, rspn = await Repo.Submenu.get(client, menu, submenu) + assert code == 404 + + # удаляем сопутствующее + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_dishes_get_all(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Проверяем все блюда в подменю + code, rspn = await Repo.Dish.read_all(client, menu, submenu) + assert code == 200 + assert rspn == [] + + # Добавляем блюдо + dish = { + 'title': 'dish', + 'description': 'some dish', + 'price': '12.5', + 'parent_submenu': submenu['id'], + } + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + assert code == 201 + dish.update(rspn) + + code, upd_rspn = await Repo.Dish.read_all(client, menu, submenu) + + assert code == 200 + + # удаляем сопутствующее + await Repo.Dish.delete(client, menu, submenu, dish) + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_dishes_add(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Добавляем блюдо + dish = { + 'title': 'dish', + 'description': 'some dish', + 'price': '12.5', + 'parent_submenu': submenu['id'], + } + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + assert code == 201 + dish.update(rspn) + + # Получаем блюдо + code, rspn = await Repo.Dish.get(client, menu, submenu, dish) + assert code == 200 + assert rspn['title'] == dish['title'] + + # удаляем сопутствующее + await Repo.Dish.delete(client, menu, submenu, dish) + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_dishes_update(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Добавляем блюдо + dish = { + 'title': 'dish', + 'description': 'some dish', + 'price': '12.5', + 'parent_submenu': submenu['id'], + } + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + dish.update(rspn) + + # Обновляем блюдо и проверяем + dish['title'] = 'updated_dish' + code, rspn = await Repo.Dish.update(client, menu, submenu, dish) + assert code == 200 + assert dish['title'] == rspn['title'] + dish.update(rspn) + + # удаляем сопутствующее + await Repo.Dish.delete(client, menu, submenu, dish) + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_dishes_delete(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + + # Добавляем блюдо + dish = { + 'title': 'dish', + 'description': 'some dish', + 'price': '12.5', + 'parent_submenu': submenu['id'], + } + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + dish.update(rspn) + + # Удаляем подменю + code = await Repo.Dish.delete(client, menu, submenu, dish) + assert code == 200 + + # Проверяем удаленное блюдо + code, rspn = await Repo.Dish.get(client, menu, submenu, dish) + assert code == 404 + + # удаляем сопутствующее + await Repo.Submenu.delete(client, menu, submenu) + await Repo.Menu.delete(client, menu) diff --git a/tests/test_crud.py b/tests/test_crud.py deleted file mode 100644 index 309cc83..0000000 --- a/tests/test_crud.py +++ /dev/null @@ -1,179 +0,0 @@ -from uuid import UUID - -import pytest -from sqlalchemy.ext.asyncio import AsyncSession - -from fastfood.cruds.dish import DishCrud -from fastfood.cruds.menu import MenuCrud -from fastfood.cruds.submenu import SubMenuCrud -from fastfood.models import Dish, Menu, SubMenu -from fastfood.schemas import DishBase as dishschema -from fastfood.schemas import Menu as menuschema -from fastfood.schemas import MenuBase as menubaseschema - - -@pytest.mark.asyncio -async def test_menu(asession: AsyncSession) -> None: - async with asession: - # Создаем меню - menu: Menu = Menu(title="SomeMenu", description="SomeDescription") - menu: Menu = await MenuCrud.create_menu_item( - menubaseschema.model_validate(menu), - asession, - ) - menu_id: UUID = menu.id - - # Получаем его же - req_menu: Menu | None = await MenuCrud.get_menu_item(menu_id, asession) - assert menu == req_menu - - # Получаем все меню и проверяем - req_menus = await MenuCrud.get_menus(asession) - assert menu == req_menus.scalars().all()[0] - - # Обновляем - menu.title = "updatedMenu" - await MenuCrud.update_menu_item( - menu.id, menuschema.model_validate(menu), asession - ) - # И сверяем - req_menu = await MenuCrud.get_menu_item(menu_id, asession) - assert menu == req_menu - - # Удаляем и проверяем - await MenuCrud.delete_menu_item(menu_id, asession) - req_menus = await MenuCrud.get_menus(asession) - assert req_menus.all() == [] - - -@pytest.mark.asyncio -async def test_submenu(asession: AsyncSession) -> None: - async with asession: - # Создаем меню напрямую - menu: Menu = Menu(title="SomeMenu", description="SomeDescription") - asession.add(menu) - await asession.commit() - await asession.refresh(menu) - menu_id: UUID = menu.id - - # Создаем подменю - submenu: SubMenu = SubMenu( - title="submenu", - description="", - parent_menu=menu_id, - ) - submenu = await SubMenuCrud.create_submenu_item( - menu_id, - menubaseschema.model_validate(submenu), - asession, - ) - submenu_id = submenu.id - - # Проверяем подменю - req_submenu = await SubMenuCrud.get_submenu_item( - menu_id, - submenu.id, - asession, - ) - assert submenu == req_submenu - assert submenu.dishes_count == 0 - - # Обновляем меню - submenu.title = "UpdatedSubmenu" - req_submenu = await SubMenuCrud.update_submenu_item( - submenu_id, - menubaseschema.model_validate(submenu), - asession, - ) - assert submenu == req_submenu.scalar_one_or_none() - - menu = await MenuCrud.get_menu_item(menu_id, asession) - assert 1 == menu.submenus_count - - # Удаляем полменю - await SubMenuCrud.delete_submenu_item(submenu_id, asession) - - menu = await MenuCrud.get_menu_item(menu_id, asession) - assert 0 == menu.submenus_count - - await MenuCrud.delete_menu_item(menu_id, asession) - - -@pytest.mark.asyncio -async def test_dish(asession: AsyncSession): - """Not Implemented yet""" - async with asession: - # Создаем меню напрямую - menu = Menu(title="SomeMenu", description="SomeDescription") - asession.add(menu) - await asession.commit() - await asession.refresh(menu) - menu_id: UUID = menu.id - - # Создаем подменю - submenu: SubMenu = SubMenu( - title="submenu", - description="", - parent_menu=menu_id, - ) - asession.add(submenu) - await asession.commit() - await asession.refresh(submenu) - submenu_id = submenu.id - - # Создаем блюдо - dish: Dish = Dish( - title="dish1", - description="dish number 1", - price="12.5", - parent_submenu=submenu_id, - ) - dish = await DishCrud.create_dish_item( - submenu_id, - dishschema.model_validate(dish), - asession, - ) - dish_id = dish.id - - # Проверяем блюдо - req_dish = await DishCrud.get_dish_item( - dish_id, - asession, - ) - assert dish == req_dish - - menu = await MenuCrud.get_menu_item(menu_id, asession) - submenu = await SubMenuCrud.get_submenu_item( - menu_id, - submenu.id, - asession, - ) - - assert menu.submenus_count == 1 - assert menu.dishes_count == 1 - assert submenu.dishes_count == 1 - - # Обновляем блюдо - dish.price = 177 - req_dish = await DishCrud.update_dish_item( - dish_id, - dishschema.model_validate(dish), - asession, - ) - assert dish == req_dish - - # Удаляем длюдо - await DishCrud.delete_dish_item(dish_id, asession) - - menu = await MenuCrud.get_menu_item(menu_id, asession) - submenu = await SubMenuCrud.get_submenu_item( - menu_id, - submenu.id, - asession, - ) - - assert menu.dishes_count == 0 - assert submenu.dishes_count == 0 - - await SubMenuCrud.delete_submenu_item(submenu_id, asession) - await MenuCrud.delete_menu_item(menu_id, asession) diff --git a/tests/test_postman.py b/tests/test_postman.py new file mode 100644 index 0000000..da61dbf --- /dev/null +++ b/tests/test_postman.py @@ -0,0 +1,238 @@ +import pytest +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@pytest.fixture(scope='module', autouse=True) +def session_data() -> dict: + return {} + + +@pytest.mark.asyncio +async def test_01(client: AsyncClient, session_data: dict): + """Проверяет создание меню""" + menu = {'title': 'Menu', 'description': 'some_menu_desc'} + code, rspn = await Repo.Menu.write(client, menu) + + assert code == 201 + code, rspn = await Repo.Menu.get(client, rspn) + session_data['target_menu_id'] = rspn.get('id') + session_data['target_menu_title'] = rspn.get('title') + session_data['target_menu_description'] = rspn.get('description') + + assert code == 200 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert 'submenus_count' in rspn + assert 'dishes_count' in rspn + assert rspn['title'] == menu.get('title') + assert rspn.get('description') == menu.get('description') + + +@pytest.mark.asyncio +async def test_02(client: AsyncClient, session_data: dict): + submenu = {'title': 'Submenu', 'description': 'submenu_descr'} + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + + code, rspn = await Repo.Submenu.write(client, menu, submenu) + + assert code == 201 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert 'dishes_count' in rspn + assert rspn['title'] == submenu.get('title') + assert rspn.get('description') == submenu.get('description') + + session_data['target_submenu_id'] = rspn.get('id') + session_data['target_submenu_title'] = rspn.get('title') + session_data['target_submenu_description'] = rspn.get('description') + + +@pytest.mark.asyncio +async def test_03_dish1(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + submenu = { + 'id': session_data.get('target_submenu_id'), + 'title': session_data.get('target_submenu_title'), + 'description': session_data.get('target_submenu_description'), + } + dish = {'title': 'dish_1', 'description': 'dish 1 descr', 'price': '12.5'} + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + + assert code == 201 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert 'price' in rspn + assert rspn['title'] == dish.get('title') + assert rspn.get('description') == dish.get('description') + assert rspn.get('price') == dish.get('price') + + session_data['target_dish_id'] = rspn.get('id') + session_data['target_dish_title'] = rspn.get('title') + session_data['target_dish_description'] = rspn.get('description') + session_data['target_dish_price'] = rspn.get('price') + + +@pytest.mark.asyncio +async def test_04_dish2(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + submenu = { + 'id': session_data.get('target_submenu_id'), + 'title': session_data.get('target_submenu_title'), + 'description': session_data.get('target_submenu_description'), + } + dish = {'title': 'dish_2', 'description': 'dish 2 descr', 'price': '13.5'} + code, rspn = await Repo.Dish.write(client, menu, submenu, dish) + + assert code == 201 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert 'price' in rspn + assert rspn['title'] == dish.get('title') + assert rspn.get('description') == dish.get('description') + assert rspn.get('price') == dish.get('price') + + session_data['target_dish1_id'] = rspn.get('id') + session_data['target_dish1_title'] = rspn.get('title') + session_data['target_dish1_description'] = rspn.get('description') + session_data['target_dish1_price'] = rspn.get('price') + + +@pytest.mark.asyncio +async def test_05_check_menu(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + code, rspn = await Repo.Menu.get(client, menu) + + assert code == 200 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert rspn.get('submenus_count') == 1 + assert rspn.get('dishes_count') == 2 + + +@pytest.mark.asyncio +async def test_06_check_submenu(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + submenu = { + 'id': session_data.get('target_submenu_id'), + 'title': session_data.get('target_submenu_title'), + 'description': session_data.get('target_submenu_description'), + } + code, rspn = await Repo.Submenu.get(client, menu, submenu) + + assert code == 200 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert rspn.get('dishes_count') == 2 + + +@pytest.mark.asyncio +async def test_07_del_submenu(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + submenu = { + 'id': session_data.get('target_submenu_id'), + 'title': session_data.get('target_submenu_title'), + 'description': session_data.get('target_submenu_description'), + } + code = await Repo.Submenu.delete(client, menu, submenu) + + assert code == 200 + + +@pytest.mark.asyncio +async def test_07_check_submenus(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + code, rspn = await Repo.Submenu.read_all(client, menu) + + assert code == 200 + assert rspn == [] + + +@pytest.mark.asyncio +async def test_08_check_dishes(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + submenu = { + 'id': session_data.get('target_submenu_id'), + 'title': session_data.get('target_submenu_title'), + 'description': session_data.get('target_submenu_description'), + } + code, rspn = await Repo.Dish.read_all(client, menu, submenu) + + assert code == 200 + assert rspn == [] + + +@pytest.mark.asyncio +async def test_09_check_menu(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + code, rspn = await Repo.Menu.get(client, menu) + + assert code == 200 + assert 'id' in rspn + assert 'title' in rspn + assert 'description' in rspn + assert rspn.get('submenus_count') == 0 + assert rspn.get('dishes_count') == 0 + + +@pytest.mark.asyncio +async def test_10_del_menu(client: AsyncClient, session_data: dict): + menu = { + 'id': session_data.get('target_menu_id'), + 'title': session_data.get('target_menu_title'), + 'description': session_data.get('target_menu_description'), + } + code = await Repo.Menu.delete(client, menu) + + assert code == 200 + + +@pytest.mark.asyncio +async def test_11_check_menus(client: AsyncClient, session_data: dict): + code, rspn = await Repo.Menu.read_all(client) + + assert code == 200 + assert rspn == [] diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..bc16268 --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,18 @@ +def reverse_url(loc: str, **kwargs) -> str: + menu_pref = '/' + submenu_pref = menu_pref + str(kwargs.get('menu_id', '')) + '/submenus/' + dish_pref = submenu_pref + str(kwargs.get('submenu_id', '')) + '/dishes/' + match loc: + case 'menus': + return menu_pref + case 'menu': + return menu_pref + str(kwargs.get('menu_id', '')) + case 'submenus': + return submenu_pref + case 'submenu': + return submenu_pref + str(kwargs.get('submenu_id', '')) + case 'dishes': + return dish_pref + case 'dish': + return dish_pref + str(kwargs.get('dish_id', '')) + return menu_pref