From 58ecd82bb6ca625e0119088c8a2dda8c05ed26db Mon Sep 17 00:00:00 2001 From: pi3c Date: Fri, 2 Feb 2024 08:04:02 +0300 Subject: [PATCH 01/17] sinc --- fastfood/app.py | 6 +- fastfood/cruds/menu.py | 42 +-- fastfood/cruds/redis_cache.py | 59 ++++ fastfood/routers/menu.py | 22 +- fastfood/service/menu.py | 65 +++++ poetry.lock | 20 +- pyproject.toml | 1 + tests/conftest.py | 8 +- tests/test_api.py | 524 +++++++++++++++++----------------- tests/test_crud.py | 179 ------------ 10 files changed, 450 insertions(+), 476 deletions(-) create mode 100644 fastfood/cruds/redis_cache.py create mode 100644 fastfood/service/menu.py delete mode 100644 tests/test_crud.py diff --git a/fastfood/app.py b/fastfood/app.py index 0bed41c..25e3d66 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -1,4 +1,6 @@ -from fastapi import FastAPI +import aioredis +from fastapi import FastAPI, Request +from starlette.responses import JSONResponse from fastfood.routers.dish import router as dish_router from fastfood.routers.menu import router as menu_router @@ -61,7 +63,7 @@ tags_metadata = [ ] -def create_app() -> FastAPI: +def create_app(redis=None) -> FastAPI: """ Фабрика FastAPI. """ diff --git a/fastfood/cruds/menu.py b/fastfood/cruds/menu.py index 0d3c479..d1e31ea 100644 --- a/fastfood/cruds/menu.py +++ b/fastfood/cruds/menu.py @@ -1,31 +1,35 @@ +from typing import Annotated 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 models, schemas +from fastfood.dbase import get_async_session class MenuCrud: - @staticmethod - async def get_menus(session: AsyncSession): - async with session: - query = select(models.Menu) - menus = await session.execute(query) - return menus + def __init__(self, session: AsyncSession = Depends(get_async_session)): + self.db = session + + async def get_menus(self): + query = select(models.Menu) + menus = await self.db.execute(query) + return menus.scalars().all() + + async def create_menu_item(self, menu: schemas.MenuBase): + new_menu = models.Menu(**menu.model_dump()) + self.db.add(new_menu) + await self.db.commit() + await self.db.refresh(new_menu) + return new_menu @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 def get_menu_item( + menu_id: UUID, session: AsyncSession = Depends(get_async_session) + ): async with session: m = aliased(models.Menu) s = aliased(models.SubMenu) @@ -52,7 +56,7 @@ class MenuCrud: async def update_menu_item( menu_id: UUID, menu: schemas.MenuBase, - session: AsyncSession, + session: AsyncSession = Depends(get_async_session), ): async with session: query = ( @@ -67,7 +71,9 @@ class MenuCrud: return updated_menu @staticmethod - async def delete_menu_item(menu_id: UUID, session: AsyncSession): + async def delete_menu_item( + menu_id: UUID, session: AsyncSession = Depends(get_async_session) + ): async with session: query = delete(models.Menu).where(models.Menu.id == menu_id) await session.execute(query) diff --git a/fastfood/cruds/redis_cache.py b/fastfood/cruds/redis_cache.py new file mode 100644 index 0000000..55b01e2 --- /dev/null +++ b/fastfood/cruds/redis_cache.py @@ -0,0 +1,59 @@ +import pickle +from typing import Any + +import redis.asyncio as redis +from fastapi import BackgroundTasks, Depends + + +def get_async_redis_pool(): + redis_url = "redis://localhost" + return redis.from_url(redis_url, decode_responses=False) + + +async def get_async_redis_client( + redis_pool: redis.Redis = Depends(get_async_redis_pool), +): + return redis_pool + + +class AsyncRedisCache: + def __init__(self, redis_pool: redis.Redis = Depends(get_async_redis_pool)) -> None: + self.redis_pool = redis_pool + self.ttl = 1800 + + async def get(self, key: str) -> Any | None: + data = await self.redis_pool.get(key) + if data is not None: + return pickle.loads(data) + return None + + async def set( + self, key: str, value: Any, background_tasks: BackgroundTasks + ) -> None: + data = pickle.dumps(value) + background_tasks.add_task(self._set_cache, key, data) + + async def _set_cache(self, key: str, data: Any) -> None: + await self.redis_pool.setex(key, self.ttl, data) + + async def delete(self, key: str, background_tasks: BackgroundTasks) -> None: + background_tasks.add_task(self._delete_cache, key) + + async def _delete_cache(self, key: str) -> None: + await self.redis_pool.delete(key) + + async def clear_cache( + self, pattern: str, background_tasks: BackgroundTasks + ) -> None: + keys = [key async for key in self.redis_pool.scan_iter(pattern)] + if keys: + background_tasks.add_task(self._clear_keys, keys) + + async def _clear_keys(self, keys: list[str]) -> None: + await self.redis_pool.delete(*keys) + + async def clear_after_change( + self, menu_id: int | str, background_tasks: BackgroundTasks + ) -> None: + await self.clear_cache(f"{str(menu_id)}*", background_tasks) + await self.clear_cache("all*", background_tasks) diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index 0b9b324..fb5b2b7 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -1,12 +1,13 @@ from typing import List, Optional from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from fastfood import schemas from fastfood.cruds import crud from fastfood.dbase import get_async_session +from fastfood.service.menu import MenuService router = APIRouter( prefix="/api/v1/menus", @@ -15,21 +16,22 @@ router = APIRouter( @router.get("/", response_model=Optional[List[schemas.Menu]]) -async def get_menus(session: AsyncSession = Depends(get_async_session)): - result = await crud.get_menus(session=session) - return result.scalars().all() +async def get_menus( + menu: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), +): + result = await menu.read_menus() + return result @router.post("/", status_code=201, response_model=schemas.Menu) async def add_menu( menu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), + responce: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.create_menu_item( - menu=menu, - session=session, - ) - return result + rspn = await responce.create_menu(menu) + return rspn @router.get("/{menu_id}", response_model=schemas.MenuRead) diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py new file mode 100644 index 0000000..2faf96c --- /dev/null +++ b/fastfood/service/menu.py @@ -0,0 +1,65 @@ +import pickle + +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.cruds.menu import MenuCrud +from fastfood.cruds.redis_cache import AsyncRedisCache, get_async_redis_client +from fastfood.schemas import MenuRead + + +class MenuService: + def __init__( + self, + menu_crud: MenuCrud = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ): + self.menu_crud = menu_crud + self.cache_client = AsyncRedisCache(redis_client) + self.background_tasks = background_tasks + + async def read_menus(self): + cached = await self.cache_client.get("all") + + if cached: + return cached + + data = await self.menu_crud.get_menus() + print("not cached", data) + await self.cache_client.set("all", data, self.background_tasks) + return data + + async def create_menu(self, menu_data): + data = await self.menu_crud.create_menu_item(menu_data) + await self.cache_client.set(str(data.id), data, self.background_tasks) + await self.cache_client.clear_after_change(str(data.id), self.background_tasks) + return data + + # async def read_menu(self, menu_id: int | str): + # cached = await self.cache_client.get(f'{menu_id}') + # if cached is not None: + # return cached + # + # data = await self.menu_crud.read_menu(menu_id) + # await self.cache_client.set(f'{menu_id}', data, self.background_tasks) + # return data + # + # async def update_menu(self, menu_id: int | str, menu_data): + # data = await self.menu_crud.update_menu(menu_id, menu_data) + # await self.cache_client.set(f'{menu_id}', data, self.background_tasks) + # await self.cache_client.clear_after_change(menu_id, self.background_tasks) + # return data + # + # async def del_menu(self, menu_id: int | str): + # data = await self.menu_crud.del_menu(menu_id) + # await self.cache_client.delete(f'{menu_id}', self.background_tasks) + # await self.cache_client.clear_after_change(menu_id, self.background_tasks) + # return data + # + # async def orm_read_menu(self, menu_id: int | str): + # return await self.menu_crud.orm_read_menu(menu_id) + # + # async def read_full_menus(self): + # menu_data = await self.menu_crud.get_full_menus() + # return FullMenuListResponse(menus=menu_data) diff --git a/poetry.lock b/poetry.lock index eef5531..0bae181 100644 --- a/poetry.lock +++ b/poetry.lock @@ -670,6 +670,24 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "redis" +version = "5.0.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, + {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, +] + +[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 = "sniffio" version = "1.3.0" @@ -829,4 +847,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5bbc3cad36f6f40d10cb848918426b640f9e703bc2c6b22b5b8fe381a6251ded" +content-hash = "8da16a83882a9b35a5a05441a33e2296b04a5f664dbb090fec0f384c709fb7ef" diff --git a/pyproject.toml b/pyproject.toml index 560b0d5..38fa2dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ asyncpg = "^0.29.0" pydantic-settings = "^2.1.0" email-validator = "^2.1.0.post1" pytest-asyncio = "^0.23.3" +redis = "^5.0.1" [tool.poetry.group.dev.dependencies] diff --git a/tests/conftest.py b/tests/conftest.py index 18a4d88..bfc702e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,16 +53,10 @@ def app(event_loop) -> Generator[FastAPI, None, None]: yield app -@pytest_asyncio.fixture(scope="function") +@pytest_asyncio.fixture() async def client(app) -> AsyncGenerator[AsyncClient, None]: async with AsyncClient( app=app, base_url="http://localhost:8000/api/v1/menus", ) as async_client: yield async_client - - -@pytest_asyncio.fixture(scope="function") -async def asession(event_loop) -> AsyncGenerator[AsyncSession, None]: - async with async_session_maker() as session: - yield session diff --git a/tests/test_api.py b/tests/test_api.py index a627f10..ece7945 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,9 +7,10 @@ from httpx import AsyncClient, Response class TestBaseCrud: class Menu: @staticmethod - async def read_all(ac: AsyncClient) -> Tuple[int, dict]: + async def read_all(cli: AsyncClient) -> Tuple[int, dict]: """чтение всех меню""" - response: Response = await ac.get("/") + + response: Response = await cli.get("/") return response.status_code, response.json() @staticmethod @@ -144,264 +145,269 @@ class TestBaseCrud: ) return response.status_code - @pytest.mark.asyncio - async def test_menu_crud(self, client: AsyncClient) -> None: - """Тестирование функций меню""" - code, rspn = await self.Menu.read_all(client) - assert code == 200 - data = {"title": "Menu", "description": None} - code, rspn = await self.Menu.write(client, data) - assert code == 201 - assert rspn["title"] == "Menu" - assert rspn["description"] is None +@pytest.mark.asyncio +async def test_menu_crud(client) -> None: + """Тестирование функций меню""" + code, rspn = await TestBaseCrud.Menu.read_all(client) + assert code == 200 - code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) - assert code == 200 - assert menu["title"] == rspn["title"] - - upd_data = { - "id": rspn.get("id"), - "title": "upd Menu", - "description": "", - } - code, upd_rspn = await self.Menu.update(client, upd_data) - assert code == 200 - assert upd_rspn["title"] == "upd Menu" - - code = await self.Menu.delete(client, rspn) - assert code == 200 - - code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) - assert code == 404 - - @pytest.mark.asyncio - async def test_submenus(self, client) -> None: - # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} - code, rspn = await self.Menu.write(client, menu) - assert code == 201 - menu.update(rspn) - - # Проверяем наличие подменю - code, rspn = await self.Submenu.read_all(client, menu) - assert code == 200 - assert rspn == [] - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - assert code == 201 - submenu.update(rspn) - - # Проверяем меню на наличие подменю - code, rspn = await self.Menu.get(client, menu) - assert code == 200 - assert rspn["submenus_count"] == 1 - - # Обновляем подменю и проверяем - submenu["title"] = "updated_submenu" - code, rspn = await self.Submenu.update(client, menu, submenu) - assert code == 200 - assert submenu["title"] == rspn["title"] - submenu.update(rspn) - - # Удаляем подменю - code = await self.Submenu.delete(client, menu, submenu) - assert code == 200 - - # Проверяем меню - code, rspn = await self.Menu.get(client, menu) - assert code == 200 - assert rspn["submenus_count"] == 0 - - # Проверяем удаленное подменю - code, rspn = await self.Submenu.get(client, menu, submenu) - assert code == 404 - - # удаляем сопутствующее - await self.Menu.delete(client, menu) - - @pytest.mark.asyncio - async def test_dishes(self, client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await self.Menu.write(client, menu) - assert code == 201 - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await self.Submenu.write(client, menu, submenu) - assert code == 201 - submenu.update(rspn) - - # Проверяем все блюда в подменю - code, rspn = await self.Dish.read_all(client, menu, submenu) - assert code == 200 - assert rspn == [] - - # Добавляем блюдо - dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], - } - code, rspn = await self.Dish.write(client, menu, submenu, dish) - assert code == 201 - dish.update(rspn) - - # Получаем блюдо - code, rspn = await self.Dish.get(client, menu, submenu, dish) - assert code == 200 - assert rspn["title"] == dish["title"] - - # Проверяем меню на количество блюд - code, rspn = await self.Menu.get(client, menu) - assert code == 200 - assert rspn["dishes_count"] == 1 - - # Проверяем подменю на наличие блюд - code, rspn = await self.Submenu.get(client, menu, submenu) - assert code == 200 - assert rspn["dishes_count"] == 1 - - # Обновляем блюдо и проверяем - dish["title"] = "updated_dish" - code, rspn = await self.Dish.update(client, menu, submenu, dish) - assert code == 200 - assert dish["title"] == rspn["title"] - dish.update(rspn) - - # Удаляем подменю - code = await self.Dish.delete(client, menu, submenu, dish) - assert code == 200 - - # Проверяем меню - code, rspn = await self.Menu.get(client, menu) - assert code == 200 - assert rspn["dishes_count"] == 0 - - # Проверяем подменю на наличие блюд - code, rspn = await self.Submenu.get(client, menu, submenu) - assert code == 200 - assert rspn["dishes_count"] == 0 - - # Проверяем удаленное блюдо - code, rspn = await self.Dish.get(client, menu, submenu, dish) - assert code == 404 - - # удаляем сопутствующее - await self.Submenu.delete(client, menu, submenu) - await self.Menu.delete(client, menu) + data = {"title": "Menu", "description": None} + code, rspn = await TestBaseCrud.Menu.write(client, data) + assert code == 201 + assert rspn["title"] == "Menu" + assert rspn["description"] is None + data = {"title": "Menu1", "description": "11"} + code, rspn = await TestBaseCrud.Menu.write(client, data) + code, rspn = await TestBaseCrud.Menu.read_all(client) -class TestСontinuity: - @pytest.mark.asyncio - async def test_postman_continuity(self, client): - # Создаем меню - menu = { - "title": "Menu", - "description": "main menu", - } - code, rspn = await TestBaseCrud.Menu.write(client, menu) - assert code == 201 - assert "id" in rspn.keys() - menu.update(rspn) - - # Создаем подменю - submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], - } - code, rspn = await TestBaseCrud.Submenu.write(client, menu, submenu) - assert code == 201 - assert "id" in rspn.keys() - submenu.update(rspn) - - # Добавляем блюдо1 - dish = { - "title": "dish1", - "description": "some dish1", - "price": "13.50", - "parent_submenu": submenu["id"], - } - code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) - assert code == 201 - assert "id" in rspn.keys() - dish.update(rspn) - - # Добавляем блюдо2 - dish = { - "title": "dish2", - "description": "some dish2", - "price": "12.50", - "parent_submenu": submenu["id"], - } - code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) - assert code == 201 - assert "id" in rspn.keys() - dish.update(rspn) - - # Просматриваем конкретное меню - code, rspn = await TestBaseCrud.Menu.get(client, menu) - assert code == 200 - assert "id" in rspn.keys() - assert menu["id"] == rspn["id"] - assert "submenus_count" in rspn.keys() - assert rspn["submenus_count"] == 1 - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 2 - - # Просматриваем конкретное подменю - code, rspn = await TestBaseCrud.Submenu.get(client, menu, submenu) - assert code == 200 - assert "id" in rspn.keys() - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 2 - - # Удаляем подменю - code = await TestBaseCrud.Submenu.delete(client, menu, submenu) - assert code == 200 - - # Просматриваем список подменю - code, rspn = await TestBaseCrud.Submenu.read_all(client, menu) - assert code == 200 - assert rspn == [] - - # Просматриваем список блюд - code, rspn = await TestBaseCrud.Dish.read_all(client, menu, submenu) - assert code == 200 - assert rspn == [] - - # Просматриваем конкретное меню - code, rspn = await TestBaseCrud.Menu.get(client, menu) - assert code == 200 - assert "id" in rspn.keys() - assert menu["id"] == rspn["id"] - assert "submenus_count" in rspn.keys() - assert rspn["submenus_count"] == 0 - assert "dishes_count" in rspn.keys() - assert rspn["dishes_count"] == 0 - - # Удаляем меню - code = await TestBaseCrud.Menu.delete(client, menu) - assert code == 200 - - # Просматриваем все меню - code, rspn = await TestBaseCrud.Menu.read_all(client) - assert code == 200 - assert rspn == [] +# code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) +# assert code == 200 +# assert menu["title"] == rspn["title"] +# +# upd_data = { +# "id": rspn.get("id"), +# "title": "upd Menu", +# "description": "", +# } +# code, upd_rspn = await self.Menu.update(client, upd_data) +# assert code == 200 +# assert upd_rspn["title"] == "upd Menu" +# +# code = await self.Menu.delete(client, rspn) +# assert code == 200 +# +# code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) +# assert code == 404 +# +# @pytest.mark.asyncio +# async def test_submenus(self, client) -> None: +# # Создаем меню и проверяем ответ +# menu = {"title": "Menu", "description": "main menu"} +# code, rspn = await self.Menu.write(client, menu) +# assert code == 201 +# menu.update(rspn) +# +# # Проверяем наличие подменю +# code, rspn = await self.Submenu.read_all(client, menu) +# assert code == 200 +# assert rspn == [] +# +# # Создаем и проверяем подменю +# submenu = { +# "title": "Submenu", +# "description": "submenu", +# "parent_menu": menu["id"], +# } +# code, rspn = await self.Submenu.write(client, menu, submenu) +# assert code == 201 +# submenu.update(rspn) +# +# # Проверяем меню на наличие подменю +# code, rspn = await self.Menu.get(client, menu) +# assert code == 200 +# assert rspn["submenus_count"] == 1 +# +# # Обновляем подменю и проверяем +# submenu["title"] = "updated_submenu" +# code, rspn = await self.Submenu.update(client, menu, submenu) +# assert code == 200 +# assert submenu["title"] == rspn["title"] +# submenu.update(rspn) +# +# # Удаляем подменю +# code = await self.Submenu.delete(client, menu, submenu) +# assert code == 200 +# +# # Проверяем меню +# code, rspn = await self.Menu.get(client, menu) +# assert code == 200 +# assert rspn["submenus_count"] == 0 +# +# # Проверяем удаленное подменю +# code, rspn = await self.Submenu.get(client, menu, submenu) +# assert code == 404 +# +# # удаляем сопутствующее +# await self.Menu.delete(client, menu) +# +# @pytest.mark.asyncio +# async def test_dishes(self, client: AsyncClient) -> None: +# # Создаем меню и проверяем ответ +# menu = { +# "title": "Menu", +# "description": "main menu", +# } +# code, rspn = await self.Menu.write(client, menu) +# assert code == 201 +# menu.update(rspn) +# +# # Создаем и проверяем подменю +# submenu = { +# "title": "Submenu", +# "description": "submenu", +# "parent_menu": menu["id"], +# } +# code, rspn = await self.Submenu.write(client, menu, submenu) +# assert code == 201 +# submenu.update(rspn) +# +# # Проверяем все блюда в подменю +# code, rspn = await self.Dish.read_all(client, menu, submenu) +# assert code == 200 +# assert rspn == [] +# +# # Добавляем блюдо +# dish = { +# "title": "dish", +# "description": "some dish", +# "price": "12.5", +# "parent_submenu": submenu["id"], +# } +# code, rspn = await self.Dish.write(client, menu, submenu, dish) +# assert code == 201 +# dish.update(rspn) +# +# # Получаем блюдо +# code, rspn = await self.Dish.get(client, menu, submenu, dish) +# assert code == 200 +# assert rspn["title"] == dish["title"] +# +# # Проверяем меню на количество блюд +# code, rspn = await self.Menu.get(client, menu) +# assert code == 200 +# assert rspn["dishes_count"] == 1 +# +# # Проверяем подменю на наличие блюд +# code, rspn = await self.Submenu.get(client, menu, submenu) +# assert code == 200 +# assert rspn["dishes_count"] == 1 +# +# # Обновляем блюдо и проверяем +# dish["title"] = "updated_dish" +# code, rspn = await self.Dish.update(client, menu, submenu, dish) +# assert code == 200 +# assert dish["title"] == rspn["title"] +# dish.update(rspn) +# +# # Удаляем подменю +# code = await self.Dish.delete(client, menu, submenu, dish) +# assert code == 200 +# +# # Проверяем меню +# code, rspn = await self.Menu.get(client, menu) +# assert code == 200 +# assert rspn["dishes_count"] == 0 +# +# # Проверяем подменю на наличие блюд +# code, rspn = await self.Submenu.get(client, menu, submenu) +# assert code == 200 +# assert rspn["dishes_count"] == 0 +# +# # Проверяем удаленное блюдо +# code, rspn = await self.Dish.get(client, menu, submenu, dish) +# assert code == 404 +# +# # удаляем сопутствующее +# await self.Submenu.delete(client, menu, submenu) +# await self.Menu.delete(client, menu) +# +# +# class TestСontinuity: +# @pytest.mark.asyncio +# async def test_postman_continuity(self, client): +# # Создаем меню +# menu = { +# "title": "Menu", +# "description": "main menu", +# } +# code, rspn = await TestBaseCrud.Menu.write(client, menu) +# assert code == 201 +# assert "id" in rspn.keys() +# menu.update(rspn) +# +# # Создаем подменю +# submenu = { +# "title": "Submenu", +# "description": "submenu", +# "parent_menu": menu["id"], +# } +# code, rspn = await TestBaseCrud.Submenu.write(client, menu, submenu) +# assert code == 201 +# assert "id" in rspn.keys() +# submenu.update(rspn) +# +# # Добавляем блюдо1 +# dish = { +# "title": "dish1", +# "description": "some dish1", +# "price": "13.50", +# "parent_submenu": submenu["id"], +# } +# code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) +# assert code == 201 +# assert "id" in rspn.keys() +# dish.update(rspn) +# +# # Добавляем блюдо2 +# dish = { +# "title": "dish2", +# "description": "some dish2", +# "price": "12.50", +# "parent_submenu": submenu["id"], +# } +# code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) +# assert code == 201 +# assert "id" in rspn.keys() +# dish.update(rspn) +# +# # Просматриваем конкретное меню +# code, rspn = await TestBaseCrud.Menu.get(client, menu) +# assert code == 200 +# assert "id" in rspn.keys() +# assert menu["id"] == rspn["id"] +# assert "submenus_count" in rspn.keys() +# assert rspn["submenus_count"] == 1 +# assert "dishes_count" in rspn.keys() +# assert rspn["dishes_count"] == 2 +# +# # Просматриваем конкретное подменю +# code, rspn = await TestBaseCrud.Submenu.get(client, menu, submenu) +# assert code == 200 +# assert "id" in rspn.keys() +# assert "dishes_count" in rspn.keys() +# assert rspn["dishes_count"] == 2 +# +# # Удаляем подменю +# code = await TestBaseCrud.Submenu.delete(client, menu, submenu) +# assert code == 200 +# +# # Просматриваем список подменю +# code, rspn = await TestBaseCrud.Submenu.read_all(client, menu) +# assert code == 200 +# assert rspn == [] +# +# # Просматриваем список блюд +# code, rspn = await TestBaseCrud.Dish.read_all(client, menu, submenu) +# assert code == 200 +# assert rspn == [] +# +# # Просматриваем конкретное меню +# code, rspn = await TestBaseCrud.Menu.get(client, menu) +# assert code == 200 +# assert "id" in rspn.keys() +# assert menu["id"] == rspn["id"] +# assert "submenus_count" in rspn.keys() +# assert rspn["submenus_count"] == 0 +# assert "dishes_count" in rspn.keys() +# assert rspn["dishes_count"] == 0 +# +# # Удаляем меню +# code = await TestBaseCrud.Menu.delete(client, menu) +# assert code == 200 +# +# # Просматриваем все меню +# code, rspn = await TestBaseCrud.Menu.read_all(client) +# assert code == 200 +# assert rspn == [] 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) From b223053cf65172036e895ac88fed240c3520c7b1 Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Fri, 2 Feb 2024 13:01:37 +0000 Subject: [PATCH 02/17] sync --- dump.rdb | Bin 0 -> 3057 bytes fastfood/app.py | 1 - fastfood/cruds/menu.py | 76 +++++++++++++++++---------------------- fastfood/routers/menu.py | 32 ++++++++--------- fastfood/service/menu.py | 31 ++++++++-------- tests/test_api.py | 45 +++++++++++------------ 6 files changed, 88 insertions(+), 97 deletions(-) create mode 100644 dump.rdb diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..6be976d5c449b12f7fb88ddc3260cdf0b4c9dc0a GIT binary patch literal 3057 zcmd_sZ)_7~0LSsWUfXr+ws!oPL>Rjg2Lp4Zf7cZx^bZgR0)erh3D`b&_jJ$2wpV&r zWG0J+1j*dFDLW#(87(7|Y$0JUKp_ZY6<-jdkq{CfBgYFAUtyxJDiN=x&Bj=aQDY3g zdvb4|&-4A=_Vw%9+HE$wJztrGEGJNVVN&O@I;^gI)zc3&Cko@fyp9pM7@X)nbrI$b zDFL!n48|tToHS#31K=YR4Fouj{f7Oj(>?z2IL>{9V;J^|!|iGT!0V-0*5jsJZnlL2 zEfGMu-Cl>SwKd}O0DCTg1=6~-_DD*Dg|SA-6aj)5;d#~?<5?ILWJ$Xj##8cuynMgh zEt{I9IUxarxVRvKI0HkZey#{*X`Zkr3Ze{xu`Vm0j9D}FcuiYo8P*H;8 zSc=?5aC}_wQJ8@|Yv3XrWaS>IBI6~i2;v>s z*3)k|+>sx?wLUd@Y-B@|Kp&L19FnD~iWrE4PMDOX#ftuc@2WPLiotj~T_a0%p>~+$ zK$LqAZqB$eKi8mYq!H*8E$Ku~j9TpbJ8((k>odIsgswP~=(Hv}6G>j=$1BLD%&Eqp zxoN}5vB}i>Tf_N|!-jrORz4_imn9<$1t!TQd{6Z97Rex_XtEDWSGL|szQGCN_%JRR{gYb2f`mx^q(v*Gx}bu2Fn?x#<~w6EnvblKk$*Q1 zliAY*s+20zCX89qYWcQW%K{O|$RQcI_+CprSqu6qbug9?yX2k}ZJCSH*2n2@DoSSP zh|@*4xIGl(^w=nuodFa8cJ<`7*?`^2x|}XYE}#qa7R;nyU^8`r;F>JfW*)ibPvmx< zb6v(tXpGV`XukJ-T|%?}c{IPwMB~8pF$E`R zxl@MYd76%WeFz;R8>j{ zW%;Ts!CsU=K2#<2Pm|z#fFt6-Rs!i!Wu{V7jn?=H9MxKkT64dQ=8zTmydHATJOV8U znwK8GGr*7f1`gdkd8y(0)eDxv>V zRzo3MdWFV`++jobo({F5wa80~6|2xzw7Lhqrpz6~+I9ByHfBGwRSU2%%~myOzkeuQoHPo+WqjS zYWMk6?E;qDW#iLocXXO@OR;ttZ`loK@wmJYMkp6>s^dtz?NqD7NmGnNU1)VVy|9)3 uf6Qd>U}EU>#OedvHrX~l8We~B#hIKf#Z&)R|Dlnw|)mVk~O*j literal 0 HcmV?d00001 diff --git a/fastfood/app.py b/fastfood/app.py index 25e3d66..e4037b8 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -1,4 +1,3 @@ -import aioredis from fastapi import FastAPI, Request from starlette.responses import JSONResponse diff --git a/fastfood/cruds/menu.py b/fastfood/cruds/menu.py index d1e31ea..ea6b9bd 100644 --- a/fastfood/cruds/menu.py +++ b/fastfood/cruds/menu.py @@ -26,55 +26,45 @@ class MenuCrud: await self.db.refresh(new_menu) return new_menu - @staticmethod - async def get_menu_item( - menu_id: UUID, session: AsyncSession = Depends(get_async_session) - ): - async with session: - m = aliased(models.Menu) - s = aliased(models.SubMenu) - d = aliased(models.Dish) + async def get_menu_item(self, menu_id: UUID): + 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) + query = ( + select( + m, + func.count(distinct(s.id)).label("submenus_count"), + func.count(distinct(d.id)).label("dishes_count"), ) - menu = await session.execute(query) - menu = menu.scalars().one_or_none() - if menu is None: - return None + .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 - @staticmethod async def update_menu_item( + self, menu_id: UUID, menu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), ): - 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 + query = ( + update(models.Menu) + .where(models.Menu.id == menu_id) + .values(**menu.model_dump()) + ) + await self.db.execute(query) + await self.db.commit() + qr = select(models.Menu).where(models.Menu.id == menu_id) + updated_menu = await self.db.execute(qr) + return updated_menu - @staticmethod - async def delete_menu_item( - menu_id: UUID, session: AsyncSession = Depends(get_async_session) - ): - async with session: - query = delete(models.Menu).where(models.Menu.id == menu_id) - await session.execute(query) - await session.commit() + async def delete_menu_item(self, menu_id: UUID): + query = delete(models.Menu).where(models.Menu.id == menu_id) + await self.db.execute(query) + await self.db.commit() diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index fb5b2b7..71d10ae 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -20,8 +20,7 @@ async def get_menus( menu: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await menu.read_menus() - return result + return await menu.read_menus() @router.post("/", status_code=201, response_model=schemas.Menu) @@ -30,16 +29,16 @@ async def add_menu( responce: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): - rspn = await responce.create_menu(menu) - return rspn + return await responce.create_menu(menu) @router.get("/{menu_id}", response_model=schemas.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") @@ -50,19 +49,20 @@ async def get_menu( async def update_menu( menu_id: UUID, menu: schemas.MenuBase, - session: AsyncSession = Depends(get_async_session), + 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() -@router.delete("/{menu_id}") -async def delete_menu( - menu_id: UUID, - session: AsyncSession = Depends(get_async_session), -): - await crud.delete_menu_item(menu_id=menu_id, session=session) +# +# @router.delete("/{menu_id}") +# async def delete_menu( +# menu_id: UUID, +# session: AsyncSession = Depends(get_async_session), +# ): +# await crud.delete_menu_item(menu_id=menu_id, session=session) diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py index 2faf96c..871166f 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -1,4 +1,5 @@ import pickle +from uuid import UUID import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends @@ -36,21 +37,21 @@ class MenuService: await self.cache_client.clear_after_change(str(data.id), self.background_tasks) return data - # async def read_menu(self, menu_id: int | str): - # cached = await self.cache_client.get(f'{menu_id}') - # if cached is not None: - # return cached - # - # data = await self.menu_crud.read_menu(menu_id) - # await self.cache_client.set(f'{menu_id}', data, self.background_tasks) - # return data - # - # async def update_menu(self, menu_id: int | str, menu_data): - # data = await self.menu_crud.update_menu(menu_id, menu_data) - # await self.cache_client.set(f'{menu_id}', data, self.background_tasks) - # await self.cache_client.clear_after_change(menu_id, self.background_tasks) - # return data - # + async def read_menu(self, menu_id: UUID): + cached = await self.cache_client.get(str(menu_id)) + if cached is not None: + return cached + + data = await self.menu_crud.get_menu_item(menu_id) + await self.cache_client.set(str(menu_id), data, self.background_tasks) + return data + + async def update_menu(self, menu_id: UUID, menu_data): + data = await self.menu_crud.update_menu_item(menu_id, menu_data) + await self.cache_client.set(str(menu_id), data, self.background_tasks) + await self.cache_client.clear_after_change(str(menu_id), self.background_tasks) + return data + # async def del_menu(self, menu_id: int | str): # data = await self.menu_crud.del_menu(menu_id) # await self.cache_client.delete(f'{menu_id}', self.background_tasks) diff --git a/tests/test_api.py b/tests/test_api.py index ece7945..6aedb6b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,10 +7,10 @@ from httpx import AsyncClient, Response class TestBaseCrud: class Menu: @staticmethod - async def read_all(cli: AsyncClient) -> Tuple[int, dict]: + async def read_all(ac: AsyncClient) -> Tuple[int, dict]: """чтение всех меню""" - response: Response = await cli.get("/") + response: Response = await ac.get("/") return response.status_code, response.json() @staticmethod @@ -149,32 +149,33 @@ class TestBaseCrud: @pytest.mark.asyncio async def test_menu_crud(client) -> None: """Тестирование функций меню""" - code, rspn = await TestBaseCrud.Menu.read_all(client) + code, menus = await TestBaseCrud.Menu.read_all(client) assert code == 200 + assert menus == [] data = {"title": "Menu", "description": None} - code, rspn = await TestBaseCrud.Menu.write(client, data) + code, menu = await TestBaseCrud.Menu.write(client, data) assert code == 201 - assert rspn["title"] == "Menu" - assert rspn["description"] is None - data = {"title": "Menu1", "description": "11"} - code, rspn = await TestBaseCrud.Menu.write(client, data) - code, rspn = await TestBaseCrud.Menu.read_all(client) + assert menu["title"] == "Menu" + assert menu["description"] is None + code, menus = await TestBaseCrud.Menu.read_all(client) + assert len(menus) == 1 + + code, menu = await TestBaseCrud.Menu.get(client, {"id": menu.get("id")}) + assert code == 200 + assert menu["title"] == data["title"] + + upd_menu = { + "id": menu.get("id"), + "title": "upd Menu", + "description": "", + } + code, menu = await TestBaseCrud.Menu.update(client, upd_menu) + assert code == 200 + print(menu) + # assert menu["title"] == "upd Menu" -# code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) -# assert code == 200 -# assert menu["title"] == rspn["title"] -# -# upd_data = { -# "id": rspn.get("id"), -# "title": "upd Menu", -# "description": "", -# } -# code, upd_rspn = await self.Menu.update(client, upd_data) -# assert code == 200 -# assert upd_rspn["title"] == "upd Menu" -# # code = await self.Menu.delete(client, rspn) # assert code == 200 # From 0ba422397a62a4a58c4f77caa4c0907cec1306dc Mon Sep 17 00:00:00 2001 From: pi3c Date: Fri, 2 Feb 2024 23:38:12 +0300 Subject: [PATCH 03/17] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82.=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81,=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=B8=D0=BB=20=D0=BA=20?= =?UTF-8?q?=D0=BA=D1=8D=D1=88=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dump.rdb | Bin 3057 -> 0 bytes example.env | 1 + fastfood/app.py | 3 +- fastfood/config.py | 1 + fastfood/cruds/dish.py | 64 -- fastfood/cruds/redis_cache.py | 59 -- fastfood/cruds/submenu.py | 80 --- fastfood/dbase.py | 15 +- fastfood/{cruds => repository}/__init__.py | 10 +- fastfood/repository/dish.py | 72 ++ fastfood/{cruds => repository}/menu.py | 3 +- fastfood/repository/redis.py | 24 + fastfood/repository/submenu.py | 76 +++ fastfood/routers/dish.py | 56 +- fastfood/routers/menu.py | 31 +- fastfood/routers/submenu.py | 57 +- fastfood/service/dish.py | 58 ++ fastfood/service/menu.py | 57 +- fastfood/service/submenu.py | 48 ++ manage.py | 2 +- tests/conftest.py | 12 +- tests/repository.py | 150 +++++ tests/test_api.py | 736 ++++++++++----------- tests/test_postman.py | 235 +++++++ 24 files changed, 1127 insertions(+), 723 deletions(-) delete mode 100644 dump.rdb delete mode 100644 fastfood/cruds/dish.py delete mode 100644 fastfood/cruds/redis_cache.py delete mode 100644 fastfood/cruds/submenu.py rename fastfood/{cruds => repository}/__init__.py (59%) create mode 100644 fastfood/repository/dish.py rename fastfood/{cruds => repository}/menu.py (97%) create mode 100644 fastfood/repository/redis.py create mode 100644 fastfood/repository/submenu.py create mode 100644 fastfood/service/dish.py create mode 100644 fastfood/service/submenu.py create mode 100644 tests/repository.py create mode 100644 tests/test_postman.py diff --git a/dump.rdb b/dump.rdb deleted file mode 100644 index 6be976d5c449b12f7fb88ddc3260cdf0b4c9dc0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3057 zcmd_sZ)_7~0LSsWUfXr+ws!oPL>Rjg2Lp4Zf7cZx^bZgR0)erh3D`b&_jJ$2wpV&r zWG0J+1j*dFDLW#(87(7|Y$0JUKp_ZY6<-jdkq{CfBgYFAUtyxJDiN=x&Bj=aQDY3g zdvb4|&-4A=_Vw%9+HE$wJztrGEGJNVVN&O@I;^gI)zc3&Cko@fyp9pM7@X)nbrI$b zDFL!n48|tToHS#31K=YR4Fouj{f7Oj(>?z2IL>{9V;J^|!|iGT!0V-0*5jsJZnlL2 zEfGMu-Cl>SwKd}O0DCTg1=6~-_DD*Dg|SA-6aj)5;d#~?<5?ILWJ$Xj##8cuynMgh zEt{I9IUxarxVRvKI0HkZey#{*X`Zkr3Ze{xu`Vm0j9D}FcuiYo8P*H;8 zSc=?5aC}_wQJ8@|Yv3XrWaS>IBI6~i2;v>s z*3)k|+>sx?wLUd@Y-B@|Kp&L19FnD~iWrE4PMDOX#ftuc@2WPLiotj~T_a0%p>~+$ zK$LqAZqB$eKi8mYq!H*8E$Ku~j9TpbJ8((k>odIsgswP~=(Hv}6G>j=$1BLD%&Eqp zxoN}5vB}i>Tf_N|!-jrORz4_imn9<$1t!TQd{6Z97Rex_XtEDWSGL|szQGCN_%JRR{gYb2f`mx^q(v*Gx}bu2Fn?x#<~w6EnvblKk$*Q1 zliAY*s+20zCX89qYWcQW%K{O|$RQcI_+CprSqu6qbug9?yX2k}ZJCSH*2n2@DoSSP zh|@*4xIGl(^w=nuodFa8cJ<`7*?`^2x|}XYE}#qa7R;nyU^8`r;F>JfW*)ibPvmx< zb6v(tXpGV`XukJ-T|%?}c{IPwMB~8pF$E`R zxl@MYd76%WeFz;R8>j{ zW%;Ts!CsU=K2#<2Pm|z#fFt6-Rs!i!Wu{V7jn?=H9MxKkT64dQ=8zTmydHATJOV8U znwK8GGr*7f1`gdkd8y(0)eDxv>V zRzo3MdWFV`++jobo({F5wa80~6|2xzw7Lhqrpz6~+I9ByHfBGwRSU2%%~myOzkeuQoHPo+WqjS zYWMk6?E;qDW#iLocXXO@OR;ttZ`loK@wmJYMkp6>s^dtz?NqD7NmGnNU1)VVy|9)3 uf6Qd>U}EU>#OedvHrX~l8We~B#hIKf#Z&)R|Dlnw|)mVk~O*j 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 e4037b8..092928b 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -1,5 +1,4 @@ -from fastapi import FastAPI, Request -from starlette.responses import JSONResponse +from fastapi import FastAPI from fastfood.routers.dish import router as dish_router from fastfood.routers.menu import router as menu_router diff --git a/fastfood/config.py b/fastfood/config.py index 90394d3..58e5577 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): POSTGRES_PASSWORD: str = "" POSTGRES_USER: str = "" POSTGRES_DB_TEST: str = "" + REDIS_DB: str = "" @property def DATABASE_URL_asyncpg(self): 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/redis_cache.py b/fastfood/cruds/redis_cache.py deleted file mode 100644 index 55b01e2..0000000 --- a/fastfood/cruds/redis_cache.py +++ /dev/null @@ -1,59 +0,0 @@ -import pickle -from typing import Any - -import redis.asyncio as redis -from fastapi import BackgroundTasks, Depends - - -def get_async_redis_pool(): - redis_url = "redis://localhost" - return redis.from_url(redis_url, decode_responses=False) - - -async def get_async_redis_client( - redis_pool: redis.Redis = Depends(get_async_redis_pool), -): - return redis_pool - - -class AsyncRedisCache: - def __init__(self, redis_pool: redis.Redis = Depends(get_async_redis_pool)) -> None: - self.redis_pool = redis_pool - self.ttl = 1800 - - async def get(self, key: str) -> Any | None: - data = await self.redis_pool.get(key) - if data is not None: - return pickle.loads(data) - return None - - async def set( - self, key: str, value: Any, background_tasks: BackgroundTasks - ) -> None: - data = pickle.dumps(value) - background_tasks.add_task(self._set_cache, key, data) - - async def _set_cache(self, key: str, data: Any) -> None: - await self.redis_pool.setex(key, self.ttl, data) - - async def delete(self, key: str, background_tasks: BackgroundTasks) -> None: - background_tasks.add_task(self._delete_cache, key) - - async def _delete_cache(self, key: str) -> None: - await self.redis_pool.delete(key) - - async def clear_cache( - self, pattern: str, background_tasks: BackgroundTasks - ) -> None: - keys = [key async for key in self.redis_pool.scan_iter(pattern)] - if keys: - background_tasks.add_task(self._clear_keys, keys) - - async def _clear_keys(self, keys: list[str]) -> None: - await self.redis_pool.delete(*keys) - - async def clear_after_change( - self, menu_id: int | str, background_tasks: BackgroundTasks - ) -> None: - await self.clear_cache(f"{str(menu_id)}*", background_tasks) - await self.clear_cache("all*", background_tasks) 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..5d3e5b9 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 +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_DB, decode_responses=False) + + +async def get_async_redis_client( + redis_pool: redis.Redis = Depends(get_redis_pool), +): + return redis_pool 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..19c96a1 --- /dev/null +++ b/fastfood/repository/dish.py @@ -0,0 +1,72 @@ +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from fastfood import models +from fastfood.dbase import get_async_session +from fastfood.schemas import DishBase + + +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): + query = select(models.Dish).where( + models.Dish.parent_submenu == submenu_id, + ) + dishes = await self.db.execute(query) + return dishes.scalars().all() + + async def create_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_data: DishBase, + ): + new_dish = models.Dish(**dish_data.model_dump()) + new_dish.parent_submenu = submenu_id + self.db.add(new_dish) + await self.db.flush() + await self.db.commit() + return new_dish + + async def get_dish_item( + self, + menu_id: UUID, + submenu_id: UUID, + dish_id: UUID, + ): + query = select(models.Dish).where(models.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: DishBase, + ): + query = ( + update(models.Dish) + .where(models.Dish.id == dish_id) + .values(**dish_data.model_dump()) + ) + await self.db.execute(query) + await self.db.commit() + qr = select(models.Dish).where(models.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, + ): + query = delete(models.Dish).where(models.Dish.id == dish_id) + await self.db.execute(query) + await self.db.commit() diff --git a/fastfood/cruds/menu.py b/fastfood/repository/menu.py similarity index 97% rename from fastfood/cruds/menu.py rename to fastfood/repository/menu.py index ea6b9bd..818947a 100644 --- a/fastfood/cruds/menu.py +++ b/fastfood/repository/menu.py @@ -1,4 +1,3 @@ -from typing import Annotated from uuid import UUID from fastapi import Depends @@ -10,7 +9,7 @@ from fastfood import models, schemas from fastfood.dbase import get_async_session -class MenuCrud: +class MenuRepository: def __init__(self, session: AsyncSession = Depends(get_async_session)): self.db = session diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py new file mode 100644 index 0000000..1e1a08b --- /dev/null +++ b/fastfood/repository/redis.py @@ -0,0 +1,24 @@ +from typing import Any + +import redis.asyncio as redis +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_redis_pool + + +class RedisRepository: + def __init__( + self, + redis_pool: redis.Redis = Depends(get_redis_pool), + ) -> None: + self.redis_pool = redis_pool + self.ttl = 1800 + + async def get(self, key: str) -> Any | None: + + return None + + async def set( + self, key: str, value: Any, background_tasks: BackgroundTasks + ) -> None: + pass diff --git a/fastfood/repository/submenu.py b/fastfood/repository/submenu.py new file mode 100644 index 0000000..083757e --- /dev/null +++ b/fastfood/repository/submenu.py @@ -0,0 +1,76 @@ +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 models, schemas +from fastfood.dbase import get_async_session + + +class SubMenuRepository: + def __init__(self, session: AsyncSession = Depends(get_async_session)): + self.db = session + + async def get_submenus(self, menu_id: UUID): + query = select(models.SubMenu).where( + models.SubMenu.parent_menu == menu_id, + ) + submenus = await self.db.execute(query) + return submenus + + async def create_submenu_item( + self, + menu_id: UUID, + submenu: schemas.MenuBase, + ): + new_submenu = models.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) + return new_submenu + + async def get_submenu_item( + self, + menu_id: UUID, + submenu_id: UUID, + ): + s = aliased(models.SubMenu) + d = aliased(models.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: schemas.MenuBase, + ): + query = ( + update(models.SubMenu) + .where(models.SubMenu.id == submenu_id) + .values(**submenu_data.model_dump()) + ) + await self.db.execute(query) + await self.db.commit() + qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id) + updated_submenu = await self.db.execute(qr) + return updated_submenu + + async def delete_submenu_item(self, menu_id: UUID, submenu_id: UUID): + query = delete(models.SubMenu).where( + models.SubMenu.id == submenu_id, + ) + await self.db.execute(query) + await self.db.commit() diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index 9007cef..05b9dd4 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -1,12 +1,9 @@ -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.service.dish import DishService from fastfood.utils import price_converter router = APIRouter( @@ -17,9 +14,12 @@ router = APIRouter( @router.get("/") async def get_dishes( - menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session) + menu_id: UUID, + submenu_id: UUID, + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.get_dishes(submenu_id=submenu_id, session=session) + result = await dish.read_dishes(menu_id, submenu_id) return result @@ -27,13 +27,14 @@ async def get_dishes( async def create_dish( menu_id: UUID, submenu_id: UUID, - dish: schemas.DishBase, - session: AsyncSession = Depends(get_async_session), + dish_data: schemas.DishBase, + dish: DishService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await crud.create_dish_item( - submenu_id=submenu_id, - dish=dish, - session=session, + result = await dish.create_dish( + menu_id, + submenu_id, + dish_data, ) return price_converter(result) @@ -43,11 +44,13 @@ 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") @@ -59,13 +62,15 @@ async def update_dish( menu_id: UUID, submenu_id: UUID, dish_id: UUID, - dish: schemas.DishBase, - session: AsyncSession = Depends(get_async_session), + dish_data: schemas.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) @@ -75,6 +80,7 @@ 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 71d10ae..bb7045d 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -2,11 +2,8 @@ from typing import List, Optional from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession -from fastfood import schemas -from fastfood.cruds import crud -from fastfood.dbase import get_async_session +from fastfood.schemas import Menu, MenuBase, MenuRead from fastfood.service.menu import MenuService router = APIRouter( @@ -15,7 +12,7 @@ router = APIRouter( ) -@router.get("/", response_model=Optional[List[schemas.Menu]]) +@router.get("/", response_model=Optional[List[Menu]]) async def get_menus( menu: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), @@ -23,16 +20,16 @@ async def get_menus( return await menu.read_menus() -@router.post("/", status_code=201, response_model=schemas.Menu) +@router.post("/", status_code=201, response_model=Menu) async def add_menu( - menu: schemas.MenuBase, + menu: MenuBase, responce: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): return await responce.create_menu(menu) -@router.get("/{menu_id}", response_model=schemas.MenuRead) +@router.get("/{menu_id}", response_model=MenuRead) async def get_menu( menu_id: UUID, responce: MenuService = Depends(), @@ -45,10 +42,10 @@ async def get_menu( 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, + menu: MenuBase, responce: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): @@ -59,10 +56,10 @@ async def update_menu( return result.scalars().one() -# -# @router.delete("/{menu_id}") -# async def delete_menu( -# menu_id: UUID, -# session: AsyncSession = Depends(get_async_session), -# ): -# await crud.delete_menu_item(menu_id=menu_id, session=session) +@router.delete("/{menu_id}") +async def delete_menu( + menu_id: UUID, + menu: MenuService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), +): + await menu.del_menu(menu_id) diff --git a/fastfood/routers/submenu.py b/fastfood/routers/submenu.py index b322532..2e1578d 100644 --- a/fastfood/routers/submenu.py +++ b/fastfood/routers/submenu.py @@ -1,11 +1,10 @@ +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, SubMenuRead +from fastfood.service.submenu import SubmenuService router = APIRouter( prefix="/api/v1/menus/{menu_id}/submenus", @@ -13,38 +12,40 @@ router = APIRouter( ) -@router.get("/") +@router.get("/", response_model=Optional[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) + result = await submenu.read_submenus(menu_id=menu_id) return result.scalars().all() -@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") @@ -53,24 +54,28 @@ async def get_submenu( @router.patch( "/{submenu_id}", - response_model=schemas.MenuBase, + response_model=MenuBase, ) 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() @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/service/dish.py b/fastfood/service/dish.py new file mode 100644 index 0000000..57962be --- /dev/null +++ b/fastfood/service/dish.py @@ -0,0 +1,58 @@ +from uuid import UUID + +import redis.asyncio as redis +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 +from fastfood.schemas import DishBase + + +class DishService: + def __init__( + self, + dish_repo: DishRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ): + self.dish_repo = dish_repo + self.cache_client = RedisRepository(redis_client) + self.background_tasks = background_tasks + + async def read_dishes(self, menu_id: UUID, submenu_id: UUID): + data = await self.dish_repo.get_dishes(menu_id, submenu_id) + return data + + async def create_dish( + self, + menu_id: UUID, + submenu_id: UUID, + dish_data: DishBase, + ): + data = await self.dish_repo.create_dish_item( + menu_id, + submenu_id, + dish_data, + ) + return data + + async def read_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): + data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) + return data + + async def update_dish( + self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase + ): + data = await self.dish_repo.update_dish_item( + menu_id, submenu_id, dish_id, dish_data + ) + return data + + async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): + data = await self.dish_repo.delete_dish_item( + menu_id, + submenu_id, + dish_id, + ) + return data diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py index 871166f..f6be1b8 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -1,66 +1,41 @@ -import pickle from uuid import UUID -import redis.asyncio as redis # type: ignore +import redis.asyncio as redis from fastapi import BackgroundTasks, Depends -from fastfood.cruds.menu import MenuCrud -from fastfood.cruds.redis_cache import AsyncRedisCache, get_async_redis_client -from fastfood.schemas import MenuRead +from fastfood.dbase import get_async_redis_client +from fastfood.repository.menu import MenuRepository +from fastfood.repository.redis import RedisRepository +from fastfood.schemas import MenuBase class MenuService: def __init__( self, - menu_crud: MenuCrud = Depends(), + menu_repo: MenuRepository = Depends(), redis_client: redis.Redis = Depends(get_async_redis_client), background_tasks: BackgroundTasks = None, ): - self.menu_crud = menu_crud - self.cache_client = AsyncRedisCache(redis_client) + self.menu_repo = menu_repo + self.cache_client = RedisRepository(redis_client) self.background_tasks = background_tasks async def read_menus(self): - cached = await self.cache_client.get("all") - - if cached: - return cached - - data = await self.menu_crud.get_menus() - print("not cached", data) - await self.cache_client.set("all", data, self.background_tasks) + data = await self.menu_repo.get_menus() return data - async def create_menu(self, menu_data): - data = await self.menu_crud.create_menu_item(menu_data) - await self.cache_client.set(str(data.id), data, self.background_tasks) - await self.cache_client.clear_after_change(str(data.id), self.background_tasks) + async def create_menu(self, menu_data: MenuBase): + data = await self.menu_repo.create_menu_item(menu_data) return data async def read_menu(self, menu_id: UUID): - cached = await self.cache_client.get(str(menu_id)) - if cached is not None: - return cached - - data = await self.menu_crud.get_menu_item(menu_id) - await self.cache_client.set(str(menu_id), data, self.background_tasks) + data = await self.menu_repo.get_menu_item(menu_id) return data async def update_menu(self, menu_id: UUID, menu_data): - data = await self.menu_crud.update_menu_item(menu_id, menu_data) - await self.cache_client.set(str(menu_id), data, self.background_tasks) - await self.cache_client.clear_after_change(str(menu_id), self.background_tasks) + data = await self.menu_repo.update_menu_item(menu_id, menu_data) return data - # async def del_menu(self, menu_id: int | str): - # data = await self.menu_crud.del_menu(menu_id) - # await self.cache_client.delete(f'{menu_id}', self.background_tasks) - # await self.cache_client.clear_after_change(menu_id, self.background_tasks) - # return data - # - # async def orm_read_menu(self, menu_id: int | str): - # return await self.menu_crud.orm_read_menu(menu_id) - # - # async def read_full_menus(self): - # menu_data = await self.menu_crud.get_full_menus() - # return FullMenuListResponse(menus=menu_data) + async def del_menu(self, menu_id: UUID): + data = await self.menu_repo.delete_menu_item(menu_id) + return data diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py new file mode 100644 index 0000000..c415469 --- /dev/null +++ b/fastfood/service/submenu.py @@ -0,0 +1,48 @@ +from uuid import UUID + +import redis.asyncio as redis +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_async_redis_client +from fastfood.repository.redis import RedisRepository +from fastfood.repository.submenu import SubMenuRepository +from fastfood.schemas import MenuBase + + +class SubmenuService: + def __init__( + self, + submenu_repo: SubMenuRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ): + self.submenu_repo = submenu_repo + self.cache_client = RedisRepository(redis_client) + self.background_tasks = background_tasks + + async def read_submenus(self, menu_id: UUID): + data = await self.submenu_repo.get_submenus(menu_id=menu_id) + return data + + async def create_submenu(self, menu_id: UUID, submenu_data: MenuBase): + data = await self.submenu_repo.create_submenu_item( + menu_id, + submenu_data, + ) + return data + + async def read_menu(self, menu_id: UUID, submenu_id: UUID): + data = await self.submenu_repo.get_submenu_item(menu_id, submenu_id) + return data + + async def update_submenu( + self, menu_id: UUID, submenu_id: UUID, submenu_data: MenuBase + ): + data = await self.submenu_repo.update_submenu_item( + menu_id, submenu_id, submenu_data + ) + return data + + async def del_menu(self, menu_id: UUID, submenu_id: UUID): + data = await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) + return data diff --git a/manage.py b/manage.py index 7a44d5f..b4778d1 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(): diff --git a/tests/conftest.py b/tests/conftest.py index bfc702e..4ade2b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,11 @@ import asyncio -from typing import AsyncGenerator, Generator +from typing import AsyncGenerator, Dict, Generator import pytest import pytest_asyncio from fastapi import FastAPI from httpx import AsyncClient -from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker, - create_async_engine) +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from fastfood.app import create_app from fastfood.config import settings @@ -31,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) @@ -60,3 +59,8 @@ async def client(app) -> AsyncGenerator[AsyncClient, None]: base_url="http://localhost:8000/api/v1/menus", ) as async_client: yield async_client + + +@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..8c1c182 --- /dev/null +++ b/tests/repository.py @@ -0,0 +1,150 @@ +from typing import Tuple + +from httpx import AsyncClient, Response + + +class Repository: + 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 diff --git a/tests/test_api.py b/tests/test_api.py index 6aedb6b..a13c20e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,414 +1,360 @@ -from typing import Tuple - import pytest -from httpx import AsyncClient, Response +from httpx import AsyncClient - -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 +from .repository import Repository as Repo @pytest.mark.asyncio -async def test_menu_crud(client) -> None: +async def test_menu_crud_empty(client: AsyncClient) -> None: """Тестирование функций меню""" - code, menus = await TestBaseCrud.Menu.read_all(client) + code, rspn = await Repo.Menu.read_all(client) assert code == 200 - assert menus == [] + assert rspn == [] + +@pytest.mark.asyncio +async def test_menu_crud_add(client: AsyncClient) -> None: + """Тестирование функций меню""" data = {"title": "Menu", "description": None} - code, menu = await TestBaseCrud.Menu.write(client, data) + code, rspn = await Repo.Menu.write(client, data) assert code == 201 - assert menu["title"] == "Menu" - assert menu["description"] is None - code, menus = await TestBaseCrud.Menu.read_all(client) - assert len(menus) == 1 + assert rspn["title"] == "Menu" + assert rspn["description"] is None + await Repo.Menu.delete(client, rspn) - code, menu = await TestBaseCrud.Menu.get(client, {"id": menu.get("id")}) + +@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"] == data["title"] + assert menu["title"] == rspn["title"] + await Repo.Menu.delete(client, menu) - upd_menu = { - "id": menu.get("id"), + +@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, menu = await TestBaseCrud.Menu.update(client, upd_menu) + code, upd_rspn = await Repo.Menu.update(client, upd_data) assert code == 200 - print(menu) - # assert menu["title"] == "upd Menu" + assert upd_rspn["title"] == "upd Menu" + await Repo.Menu.delete(client, upd_rspn) -# code = await self.Menu.delete(client, rspn) -# assert code == 200 -# -# code, menu = await self.Menu.get(client, {"id": rspn.get("id")}) -# assert code == 404 -# -# @pytest.mark.asyncio -# async def test_submenus(self, client) -> None: -# # Создаем меню и проверяем ответ -# menu = {"title": "Menu", "description": "main menu"} -# code, rspn = await self.Menu.write(client, menu) -# assert code == 201 -# menu.update(rspn) -# -# # Проверяем наличие подменю -# code, rspn = await self.Submenu.read_all(client, menu) -# assert code == 200 -# assert rspn == [] -# -# # Создаем и проверяем подменю -# submenu = { -# "title": "Submenu", -# "description": "submenu", -# "parent_menu": menu["id"], -# } -# code, rspn = await self.Submenu.write(client, menu, submenu) -# assert code == 201 -# submenu.update(rspn) -# -# # Проверяем меню на наличие подменю -# code, rspn = await self.Menu.get(client, menu) -# assert code == 200 -# assert rspn["submenus_count"] == 1 -# -# # Обновляем подменю и проверяем -# submenu["title"] = "updated_submenu" -# code, rspn = await self.Submenu.update(client, menu, submenu) -# assert code == 200 -# assert submenu["title"] == rspn["title"] -# submenu.update(rspn) -# -# # Удаляем подменю -# code = await self.Submenu.delete(client, menu, submenu) -# assert code == 200 -# -# # Проверяем меню -# code, rspn = await self.Menu.get(client, menu) -# assert code == 200 -# assert rspn["submenus_count"] == 0 -# -# # Проверяем удаленное подменю -# code, rspn = await self.Submenu.get(client, menu, submenu) -# assert code == 404 -# -# # удаляем сопутствующее -# await self.Menu.delete(client, menu) -# -# @pytest.mark.asyncio -# async def test_dishes(self, client: AsyncClient) -> None: -# # Создаем меню и проверяем ответ -# menu = { -# "title": "Menu", -# "description": "main menu", -# } -# code, rspn = await self.Menu.write(client, menu) -# assert code == 201 -# menu.update(rspn) -# -# # Создаем и проверяем подменю -# submenu = { -# "title": "Submenu", -# "description": "submenu", -# "parent_menu": menu["id"], -# } -# code, rspn = await self.Submenu.write(client, menu, submenu) -# assert code == 201 -# submenu.update(rspn) -# -# # Проверяем все блюда в подменю -# code, rspn = await self.Dish.read_all(client, menu, submenu) -# assert code == 200 -# assert rspn == [] -# -# # Добавляем блюдо -# dish = { -# "title": "dish", -# "description": "some dish", -# "price": "12.5", -# "parent_submenu": submenu["id"], -# } -# code, rspn = await self.Dish.write(client, menu, submenu, dish) -# assert code == 201 -# dish.update(rspn) -# -# # Получаем блюдо -# code, rspn = await self.Dish.get(client, menu, submenu, dish) -# assert code == 200 -# assert rspn["title"] == dish["title"] -# -# # Проверяем меню на количество блюд -# code, rspn = await self.Menu.get(client, menu) -# assert code == 200 -# assert rspn["dishes_count"] == 1 -# -# # Проверяем подменю на наличие блюд -# code, rspn = await self.Submenu.get(client, menu, submenu) -# assert code == 200 -# assert rspn["dishes_count"] == 1 -# -# # Обновляем блюдо и проверяем -# dish["title"] = "updated_dish" -# code, rspn = await self.Dish.update(client, menu, submenu, dish) -# assert code == 200 -# assert dish["title"] == rspn["title"] -# dish.update(rspn) -# -# # Удаляем подменю -# code = await self.Dish.delete(client, menu, submenu, dish) -# assert code == 200 -# -# # Проверяем меню -# code, rspn = await self.Menu.get(client, menu) -# assert code == 200 -# assert rspn["dishes_count"] == 0 -# -# # Проверяем подменю на наличие блюд -# code, rspn = await self.Submenu.get(client, menu, submenu) -# assert code == 200 -# assert rspn["dishes_count"] == 0 -# -# # Проверяем удаленное блюдо -# code, rspn = await self.Dish.get(client, menu, submenu, dish) -# assert code == 404 -# -# # удаляем сопутствующее -# await self.Submenu.delete(client, menu, submenu) -# await self.Menu.delete(client, menu) -# -# -# class TestСontinuity: -# @pytest.mark.asyncio -# async def test_postman_continuity(self, client): -# # Создаем меню -# menu = { -# "title": "Menu", -# "description": "main menu", -# } -# code, rspn = await TestBaseCrud.Menu.write(client, menu) -# assert code == 201 -# assert "id" in rspn.keys() -# menu.update(rspn) -# -# # Создаем подменю -# submenu = { -# "title": "Submenu", -# "description": "submenu", -# "parent_menu": menu["id"], -# } -# code, rspn = await TestBaseCrud.Submenu.write(client, menu, submenu) -# assert code == 201 -# assert "id" in rspn.keys() -# submenu.update(rspn) -# -# # Добавляем блюдо1 -# dish = { -# "title": "dish1", -# "description": "some dish1", -# "price": "13.50", -# "parent_submenu": submenu["id"], -# } -# code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) -# assert code == 201 -# assert "id" in rspn.keys() -# dish.update(rspn) -# -# # Добавляем блюдо2 -# dish = { -# "title": "dish2", -# "description": "some dish2", -# "price": "12.50", -# "parent_submenu": submenu["id"], -# } -# code, rspn = await TestBaseCrud.Dish.write(client, menu, submenu, dish) -# assert code == 201 -# assert "id" in rspn.keys() -# dish.update(rspn) -# -# # Просматриваем конкретное меню -# code, rspn = await TestBaseCrud.Menu.get(client, menu) -# assert code == 200 -# assert "id" in rspn.keys() -# assert menu["id"] == rspn["id"] -# assert "submenus_count" in rspn.keys() -# assert rspn["submenus_count"] == 1 -# assert "dishes_count" in rspn.keys() -# assert rspn["dishes_count"] == 2 -# -# # Просматриваем конкретное подменю -# code, rspn = await TestBaseCrud.Submenu.get(client, menu, submenu) -# assert code == 200 -# assert "id" in rspn.keys() -# assert "dishes_count" in rspn.keys() -# assert rspn["dishes_count"] == 2 -# -# # Удаляем подменю -# code = await TestBaseCrud.Submenu.delete(client, menu, submenu) -# assert code == 200 -# -# # Просматриваем список подменю -# code, rspn = await TestBaseCrud.Submenu.read_all(client, menu) -# assert code == 200 -# assert rspn == [] -# -# # Просматриваем список блюд -# code, rspn = await TestBaseCrud.Dish.read_all(client, menu, submenu) -# assert code == 200 -# assert rspn == [] -# -# # Просматриваем конкретное меню -# code, rspn = await TestBaseCrud.Menu.get(client, menu) -# assert code == 200 -# assert "id" in rspn.keys() -# assert menu["id"] == rspn["id"] -# assert "submenus_count" in rspn.keys() -# assert rspn["submenus_count"] == 0 -# assert "dishes_count" in rspn.keys() -# assert rspn["dishes_count"] == 0 -# -# # Удаляем меню -# code = await TestBaseCrud.Menu.delete(client, menu) -# assert code == 200 -# -# # Просматриваем все меню -# code, rspn = await TestBaseCrud.Menu.read_all(client) -# assert code == 200 -# assert rspn == [] +@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_postman.py b/tests/test_postman.py new file mode 100644 index 0000000..f42380e --- /dev/null +++ b/tests/test_postman.py @@ -0,0 +1,235 @@ +from typing import Dict + +import pytest +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@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 == [] From f667026d62ef80f6ea29db2d7e355f5839daba59 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sat, 3 Feb 2024 01:08:04 +0300 Subject: [PATCH 04/17] =?UTF-8?q?add=20.pre-commit-config.yaml=20=D0=9F?= =?UTF-8?q?=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D0=BB=D0=B5=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - .pre-commit-config.yaml | 50 ++ README.md | 20 +- compose_app.yml | 23 +- compose_test.yml | 23 +- fastfood/app.py | 24 +- fastfood/config.py | 26 +- fastfood/dbase.py | 2 +- fastfood/models.py | 38 +- fastfood/repository/menu.py | 4 +- fastfood/repository/redis.py | 2 +- fastfood/repository/submenu.py | 2 +- fastfood/routers/dish.py | 16 +- fastfood/routers/menu.py | 18 +- fastfood/routers/submenu.py | 18 +- fastfood/schemas.py | 3 +- fastfood/service/dish.py | 2 +- fastfood/service/menu.py | 2 +- fastfood/service/submenu.py | 2 +- fastfood/utils.py | 2 +- manage.py | 10 +- poetry.lock | 642 ++++++++++++++---- .../menu app.postman_collection.json | 2 +- .../menu app.postman_environment.json | 2 +- pyproject.toml | 5 +- tests/conftest.py | 14 +- tests/repository.py | 30 +- tests/test_api.py | 142 ++-- tests/test_postman.py | 234 ++++--- 29 files changed, 893 insertions(+), 466 deletions(-) create mode 100644 .pre-commit-config.yaml 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/README.md b/README.md index 88ce8f2..a9daf59 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 @@ -106,7 +106,7 @@ Fastapi веб приложение реализующее api для общеп По завершении работы остановите контейнеры > `$ docker-compose -f compose_app.yml down` -После успешного запуска образов документация по API будет доступна по адресу http://localhost:8000 +После успешного запуска образов документация по API будет доступна по адресу http://localhost:8000 - Запуск тестов @@ -117,7 +117,7 @@ Fastapi веб приложение реализующее api для общеп ### Linux -Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд. +Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд. Установите систему управления зависимостями > `$ pip[x] install poetry` @@ -136,7 +136,7 @@ Fastapi веб приложение реализующее api для общеп ## Запуск Запуск проекта возможен в 2х режимах: - Запуск в режиме "prod" с ключем --run-server - Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются. + Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются. - Запуск в режиме "dev" c ключем --run-test-server В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей. @@ -165,5 +165,3 @@ Fastapi веб приложение реализующее api для общеп ## Лицензия Распространяется под [MIT лицензией](https://mit-license.org/). - - diff --git a/compose_app.yml b/compose_app.yml index 2028dc8..a167cdb 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -2,44 +2,43 @@ version: "3.8" services: 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 - + 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..b7beb91 100644 --- a/compose_test.yml +++ b/compose_test.yml @@ -2,44 +2,43 @@ version: "3.8" services: 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 - + restart: always command: /bin/bash -c 'poetry run pytest -vv' - diff --git a/fastfood/app.py b/fastfood/app.py index 092928b..6d6a016 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -50,14 +50,14 @@ description = """ tags_metadata = [ { - "name": "menu", - "description": "Операции с меню.", + 'name': 'menu', + 'description': 'Операции с меню.', }, { - "name": "submenu", - "description": "Подменю и работа с ним", + 'name': 'submenu', + 'description': 'Подменю и работа с ним', }, - {"name": "dish", "description": "Блюда и работа с ними"}, + {'name': 'dish', 'description': 'Блюда и работа с ними'}, ] @@ -66,17 +66,17 @@ def create_app(redis=None) -> FastAPI: Фабрика FastAPI. """ app = FastAPI( - title="Fastfood-API", + title='Fastfood-API', description=description, - version="0.0.1", + version='0.0.1', 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 58e5577..524da54 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -2,13 +2,13 @@ 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 = "" - REDIS_DB: str = "" + POSTGRES_DB: str = '' + POSTGRES_PASSWORD: str = '' + POSTGRES_USER: str = '' + POSTGRES_DB_TEST: str = '' + REDIS_DB: str = '' @property def DATABASE_URL_asyncpg(self): @@ -16,9 +16,9 @@ class Settings(BaseSettings): Возвращает строку подключения к БД необходимую для SQLAlchemy """ 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 @@ -27,12 +27,12 @@ class Settings(BaseSettings): Возвращает строку подключения к БД необходимую для SQLAlchemy """ 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") + model_config = SettingsConfigDict(env_file='.env') settings = Settings() diff --git a/fastfood/dbase.py b/fastfood/dbase.py index 5d3e5b9..f7b38d3 100644 --- a/fastfood/dbase.py +++ b/fastfood/dbase.py @@ -1,6 +1,6 @@ from typing import AsyncGenerator -import redis.asyncio as redis +import redis.asyncio as redis # type: ignore from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 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/repository/menu.py b/fastfood/repository/menu.py index 818947a..51ac715 100644 --- a/fastfood/repository/menu.py +++ b/fastfood/repository/menu.py @@ -33,8 +33,8 @@ class MenuRepository: query = ( select( m, - func.count(distinct(s.id)).label("submenus_count"), - func.count(distinct(d.id)).label("dishes_count"), + 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) diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index 1e1a08b..037be4d 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -1,6 +1,6 @@ from typing import Any -import redis.asyncio as redis +import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_redis_pool diff --git a/fastfood/repository/submenu.py b/fastfood/repository/submenu.py index 083757e..721c25b 100644 --- a/fastfood/repository/submenu.py +++ b/fastfood/repository/submenu.py @@ -40,7 +40,7 @@ class SubMenuRepository: s = aliased(models.SubMenu) d = aliased(models.Dish) query = ( - select(s, func.count(distinct(d.id)).label("dishes_count")) + 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) diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index 05b9dd4..0c8fd33 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -7,12 +7,12 @@ from fastfood.service.dish import DishService from fastfood.utils import price_converter 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('/') async def get_dishes( menu_id: UUID, submenu_id: UUID, @@ -23,7 +23,7 @@ async def get_dishes( return result -@router.post("/", status_code=201) +@router.post('/', status_code=201) async def create_dish( menu_id: UUID, submenu_id: UUID, @@ -39,7 +39,7 @@ async def create_dish( return price_converter(result) -@router.get("/{dish_id}") +@router.get('/{dish_id}') async def get_dish( menu_id: UUID, submenu_id: UUID, @@ -53,11 +53,11 @@ async def get_dish( dish_id, ) if not result: - raise HTTPException(status_code=404, detail="dish not found") + raise HTTPException(status_code=404, detail='dish not found') return price_converter(result) -@router.patch("/{dish_id}") +@router.patch('/{dish_id}') async def update_dish( menu_id: UUID, submenu_id: UUID, @@ -75,7 +75,7 @@ async def update_dish( return price_converter(result) -@router.delete("/{dish_id}") +@router.delete('/{dish_id}') async def delete_dish( menu_id: UUID, submenu_id: UUID, diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index bb7045d..7f96f5b 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException @@ -7,12 +7,12 @@ from fastfood.schemas import Menu, 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[Menu]]) +@router.get('/', response_model=Optional[list[Menu]]) async def get_menus( menu: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), @@ -20,7 +20,7 @@ async def get_menus( return await menu.read_menus() -@router.post("/", status_code=201, response_model=Menu) +@router.post('/', status_code=201, response_model=Menu) async def add_menu( menu: MenuBase, responce: MenuService = Depends(), @@ -29,7 +29,7 @@ async def add_menu( return await responce.create_menu(menu) -@router.get("/{menu_id}", response_model=MenuRead) +@router.get('/{menu_id}', response_model=MenuRead) async def get_menu( menu_id: UUID, responce: MenuService = Depends(), @@ -38,11 +38,11 @@ async def get_menu( 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=MenuRead) +@router.patch('/{menu_id}', response_model=MenuRead) async def update_menu( menu_id: UUID, menu: MenuBase, @@ -56,7 +56,7 @@ async def update_menu( return result.scalars().one() -@router.delete("/{menu_id}") +@router.delete('/{menu_id}') async def delete_menu( menu_id: UUID, menu: MenuService = Depends(), diff --git a/fastfood/routers/submenu.py b/fastfood/routers/submenu.py index 2e1578d..8b4bca8 100644 --- a/fastfood/routers/submenu.py +++ b/fastfood/routers/submenu.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException @@ -7,12 +7,12 @@ 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("/", response_model=Optional[List[SubMenuRead]]) +@router.get('/', response_model=Optional[list[SubMenuRead]]) async def get_submenus( menu_id: UUID, submenu: SubmenuService = Depends(), @@ -22,7 +22,7 @@ async def get_submenus( return result.scalars().all() -@router.post("/", status_code=201, response_model=SubMenuRead) +@router.post('/', status_code=201, response_model=SubMenuRead) async def create_submenu_item( menu_id: UUID, submenu_data: MenuBase, @@ -36,7 +36,7 @@ async def create_submenu_item( return result -@router.get("/{submenu_id}", response_model=SubMenuRead) +@router.get('/{submenu_id}', response_model=SubMenuRead) async def get_submenu( menu_id: UUID, submenu_id: UUID, @@ -48,12 +48,12 @@ async def get_submenu( submenu_id=submenu_id, ) 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}", + '/{submenu_id}', response_model=MenuBase, ) async def update_submenu( @@ -71,7 +71,7 @@ async def update_submenu( return result.scalars().one() -@router.delete("/{submenu_id}") +@router.delete('/{submenu_id}') async def delete_submenu( menu_id: UUID, submenu_id: UUID, diff --git a/fastfood/schemas.py b/fastfood/schemas.py index 8fab6a1..c14ff13 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 diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index 57962be..5dad1c4 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -1,6 +1,6 @@ from uuid import UUID -import redis.asyncio as redis +import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py index f6be1b8..c3eb13f 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -1,6 +1,6 @@ from uuid import UUID -import redis.asyncio as redis +import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py index c415469..167e22f 100644 --- a/fastfood/service/submenu.py +++ b/fastfood/service/submenu.py @@ -1,6 +1,6 @@ from uuid import UUID -import redis.asyncio as redis +import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client diff --git a/fastfood/utils.py b/fastfood/utils.py index aa1205f..7ef5890 100644 --- a/fastfood/utils.py +++ b/fastfood/utils.py @@ -1,3 +1,3 @@ def price_converter(dish: dict) -> dict: - dish.price = str(dish.price) + dish['price'] = str(dish['price']) return dish diff --git a/manage.py b/manage.py index b4778d1..f204f65 100644 --- a/manage.py +++ b/manage.py @@ -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/poetry.lock b/poetry.lock index 0bae181..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,27 +918,86 @@ 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 = "5.0.1" +version = "4.6.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, - {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, + {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"}, + {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"}, ] [package.dependencies] @@ -688,6 +1007,22 @@ async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2 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" @@ -814,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" @@ -844,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 = "8da16a83882a9b35a5a05441a33e2296b04a5f664dbb090fec0f384c709fb7ef" +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 38fa2dd..a9dc80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,16 @@ asyncpg = "^0.29.0" pydantic-settings = "^2.1.0" email-validator = "^2.1.0.post1" pytest-asyncio = "^0.23.3" -redis = "^5.0.1" +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 4ade2b0..c0c253c 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, Generator 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="session", 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,7 +45,7 @@ async def get_test_session() -> AsyncGenerator[AsyncSession, None]: yield session -@pytest.fixture(scope="session") +@pytest.fixture(scope='session') def app(event_loop) -> Generator[FastAPI, None, None]: app: FastAPI = create_app() app.dependency_overrides[get_async_session] = get_test_session @@ -56,11 +56,11 @@ def app(event_loop) -> Generator[FastAPI, None, None]: 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.fixture(scope="session") -def session_data() -> Dict: +@pytest.fixture(scope='session') +def session_data() -> dict: return {} diff --git a/tests/repository.py b/tests/repository.py index 8c1c182..b1c9ce1 100644 --- a/tests/repository.py +++ b/tests/repository.py @@ -1,31 +1,29 @@ -from typing import Tuple - from httpx import AsyncClient, Response class Repository: class Menu: @staticmethod - async def read_all(ac: AsyncClient) -> Tuple[int, dict]: + async def read_all(ac: AsyncClient) -> tuple[int, dict]: """чтение всех меню""" - response: Response = await ac.get("/") + response: Response = await ac.get('/') return response.status_code, response.json() @staticmethod - async def get(ac: AsyncClient, data: dict) -> Tuple[int, dict]: + 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]: + async def write(ac: AsyncClient, data: dict) -> tuple[int, dict]: """создания меню""" - response: Response = await ac.post("/", json=data) + response: Response = await ac.post('/', json=data) return response.status_code, response.json() @staticmethod - async def update(ac: AsyncClient, data: dict) -> Tuple[int, dict]: + async def update(ac: AsyncClient, data: dict) -> tuple[int, dict]: """Обновление меню по id""" response: Response = await ac.patch( f"/{data.get('id')}", @@ -41,7 +39,7 @@ class Repository: class Submenu: @staticmethod - async def read_all(ac: AsyncClient, menu: dict) -> Tuple[int, dict]: + 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() @@ -51,7 +49,7 @@ class Repository: ac: AsyncClient, menu: dict, submenu: dict, - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """Получение меню по id""" response: Response = await ac.get( f"/{menu.get('id')}/submenus/{submenu.get('id')}", @@ -63,7 +61,7 @@ class Repository: ac: AsyncClient, menu: dict, submenu: dict, - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """создания меню""" response: Response = await ac.post( f"/{menu.get('id')}/submenus/", @@ -74,7 +72,7 @@ class Repository: @staticmethod async def update( ac: AsyncClient, menu: dict, submenu: dict - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """Обновление меню по id""" response: Response = await ac.patch( f"/{menu.get('id')}/submenus/{submenu.get('id')}", @@ -94,7 +92,7 @@ class Repository: @staticmethod async def read_all( ac: AsyncClient, menu: dict, submenu: dict - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """чтение всех блюд""" response: Response = await ac.get( f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", @@ -104,7 +102,7 @@ class Repository: @staticmethod async def get( ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """Получение блюда по id""" response: Response = await ac.get( f"/{menu.get('id')}/submenus/{submenu.get('id')}" @@ -115,7 +113,7 @@ class Repository: @staticmethod async def write( ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """создания блюда""" response: Response = await ac.post( f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", @@ -126,7 +124,7 @@ class Repository: @staticmethod async def update( ac: AsyncClient, menu: dict, submenu: dict, dish: dict - ) -> Tuple[int, dict]: + ) -> tuple[int, dict]: """Обновление блюда по id""" response: Response = await ac.patch( f"/{menu.get('id')}/submenus/{submenu.get('id')}" diff --git a/tests/test_api.py b/tests/test_api.py index a13c20e..d57f408 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,52 +15,52 @@ async def test_menu_crud_empty(client: AsyncClient) -> None: @pytest.mark.asyncio async def test_menu_crud_add(client: AsyncClient) -> None: """Тестирование функций меню""" - data = {"title": "Menu", "description": 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 + 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} + data = {'title': 'Menu', 'description': None} code, rspn = await Repo.Menu.write(client, data) - code, menu = await Repo.Menu.get(client, {"id": rspn.get("id")}) + code, menu = await Repo.Menu.get(client, {'id': rspn.get('id')}) assert code == 200 - assert menu["title"] == rspn["title"] + 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} + data = {'title': 'Menu', 'description': None} code, rspn = await Repo.Menu.write(client, data) upd_data = { - "id": rspn.get("id"), - "title": "upd Menu", - "description": "", + '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" + 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} + 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")}) + code, rspn = await Repo.Menu.get(client, {'id': rspn.get('id')}) assert code == 404 @@ -71,7 +71,7 @@ async def test_menu_crud_get_all(client: AsyncClient) -> None: assert code == 200 assert rspn == [] - data = {"title": "Menu", "description": None} + data = {'title': 'Menu', 'description': None} code, rspn = await Repo.Menu.write(client, data) code, upd_rspn = await Repo.Menu.read_all(client) @@ -83,7 +83,7 @@ async def test_menu_crud_get_all(client: AsyncClient) -> None: @pytest.mark.asyncio async def test_submenus_get_all(client) -> None: # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} + menu = {'title': 'Menu', 'description': 'main menu'} code, rspn = await Repo.Menu.write(client, menu) assert code == 201 menu.update(rspn) @@ -95,9 +95,9 @@ async def test_submenus_get_all(client) -> None: # Создаем и проверяем подменю submenu = { - "title": "Submenu", - "description": "submenu", - "parent_menu": menu["id"], + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) @@ -115,15 +115,15 @@ async def test_submenus_get_all(client) -> None: @pytest.mark.asyncio async def test_submenus_add(client) -> None: # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} + 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"], + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], } code, rspn = await Repo.Submenu.write(client, menu, submenu) assert code == 201 @@ -137,24 +137,24 @@ async def test_submenus_add(client) -> None: @pytest.mark.asyncio async def test_submenus_update(client) -> None: # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} + 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"], + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) # Обновляем подменю и проверяем - submenu["title"] = "updated_submenu" + submenu['title'] = 'updated_submenu' code, rspn = await Repo.Submenu.update(client, menu, submenu) assert code == 200 - assert submenu["title"] == rspn["title"] + assert submenu['title'] == rspn['title'] submenu.update(rspn) # удаляем сопутствующее @@ -165,15 +165,15 @@ async def test_submenus_update(client) -> None: @pytest.mark.asyncio async def test_submenus_delete(client) -> None: # Создаем меню и проверяем ответ - menu = {"title": "Menu", "description": "main menu"} + 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"], + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) @@ -194,17 +194,17 @@ async def test_submenus_delete(client) -> None: async def test_dishes_get_all(client: AsyncClient) -> None: # Создаем меню и проверяем ответ menu = { - "title": "Menu", - "description": "main 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"], + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) @@ -216,10 +216,10 @@ async def test_dishes_get_all(client: AsyncClient) -> None: # Добавляем блюдо dish = { - "title": "dish", - "description": "some dish", - "price": "12.5", - "parent_submenu": submenu["id"], + '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 @@ -239,27 +239,27 @@ async def test_dishes_get_all(client: AsyncClient) -> None: async def test_dishes_add(client: AsyncClient) -> None: # Создаем меню и проверяем ответ menu = { - "title": "Menu", - "description": "main 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"], + '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"], + '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 @@ -268,7 +268,7 @@ async def test_dishes_add(client: AsyncClient) -> None: # Получаем блюдо code, rspn = await Repo.Dish.get(client, menu, submenu, dish) assert code == 200 - assert rspn["title"] == dish["title"] + assert rspn['title'] == dish['title'] # удаляем сопутствующее await Repo.Dish.delete(client, menu, submenu, dish) @@ -280,36 +280,36 @@ async def test_dishes_add(client: AsyncClient) -> None: async def test_dishes_update(client: AsyncClient) -> None: # Создаем меню и проверяем ответ menu = { - "title": "Menu", - "description": "main 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"], + '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"], + '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" + dish['title'] = 'updated_dish' code, rspn = await Repo.Dish.update(client, menu, submenu, dish) assert code == 200 - assert dish["title"] == rspn["title"] + assert dish['title'] == rspn['title'] dish.update(rspn) # удаляем сопутствующее @@ -322,27 +322,27 @@ async def test_dishes_update(client: AsyncClient) -> None: async def test_dishes_delete(client: AsyncClient) -> None: # Создаем меню и проверяем ответ menu = { - "title": "Menu", - "description": "main 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"], + '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"], + '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) diff --git a/tests/test_postman.py b/tests/test_postman.py index f42380e..c507bb1 100644 --- a/tests/test_postman.py +++ b/tests/test_postman.py @@ -1,5 +1,3 @@ -from typing import Dict - import pytest from httpx import AsyncClient @@ -7,160 +5,160 @@ from .repository import Repository as Repo @pytest.mark.asyncio -async def test_01(client: AsyncClient, session_data: Dict): +async def test_01(client: AsyncClient, session_data: dict): """Проверяет создание меню""" - menu = {"title": "Menu", "description": "some_menu_desc"} + 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") + 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") + 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"} +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"), + '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") + 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") + 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): +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"), + '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"), + '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"} + 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") + 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") + 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): +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"), + '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"), + '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"} + 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") + 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") + 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): +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"), + '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 + 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): +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"), + '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"), + '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 + 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): +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"), + '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"), + '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) @@ -168,11 +166,11 @@ async def test_07_del_submenu(client: AsyncClient, session_data: Dict): @pytest.mark.asyncio -async def test_07_check_submenus(client: AsyncClient, session_data: Dict): +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"), + '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) @@ -181,16 +179,16 @@ async def test_07_check_submenus(client: AsyncClient, session_data: Dict): @pytest.mark.asyncio -async def test_08_check_dishes(client: AsyncClient, session_data: Dict): +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"), + '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"), + '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) @@ -199,28 +197,28 @@ async def test_08_check_dishes(client: AsyncClient, session_data: Dict): @pytest.mark.asyncio -async def test_09_check_menu(client: AsyncClient, session_data: Dict): +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"), + '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 + 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): +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"), + '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) @@ -228,7 +226,7 @@ async def test_10_del_menu(client: AsyncClient, session_data: Dict): @pytest.mark.asyncio -async def test_11_check_menus(client: AsyncClient, session_data: Dict): +async def test_11_check_menus(client: AsyncClient, session_data: dict): code, rspn = await Repo.Menu.read_all(client) assert code == 200 From 45dd8dc73e75fa94efcc21585d325d15a16ff300 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sat, 3 Feb 2024 02:58:06 +0300 Subject: [PATCH 05/17] =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20?= =?UTF-8?q?=D0=B8=D0=B7=20openapi.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/app.py | 55 +++++++------------------------------------------ openapi.json | 1 + 2 files changed, 9 insertions(+), 47 deletions(-) create mode 100644 openapi.json diff --git a/fastfood/app.py b/fastfood/app.py index 6d6a016..202ef5f 100644 --- a/fastfood/app.py +++ b/fastfood/app.py @@ -1,53 +1,11 @@ +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', @@ -65,10 +23,13 @@ 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', 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"}}}} From 2afba14e448436845ef978d6cb882cdc014933ca Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 00:14:13 +0300 Subject: [PATCH 06/17] =?UTF-8?q?fix:=20=D0=9F=D0=BE=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20pydantic=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=81=D1=82=D1=8B=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/repository/dish.py | 8 ++++---- fastfood/routers/dish.py | 22 ++++++++++------------ fastfood/routers/menu.py | 3 +-- fastfood/schemas.py | 6 +++++- fastfood/service/dish.py | 30 ++++++++++++++++++++++-------- fastfood/utils.py | 3 --- 6 files changed, 42 insertions(+), 30 deletions(-) delete mode 100644 fastfood/utils.py diff --git a/fastfood/repository/dish.py b/fastfood/repository/dish.py index 19c96a1..52090b4 100644 --- a/fastfood/repository/dish.py +++ b/fastfood/repository/dish.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from fastfood import models from fastfood.dbase import get_async_session -from fastfood.schemas import DishBase +from fastfood.schemas import Dish_db class DishRepository: @@ -24,13 +24,13 @@ class DishRepository: self, menu_id: UUID, submenu_id: UUID, - dish_data: DishBase, + dish_data: Dish_db, ): new_dish = models.Dish(**dish_data.model_dump()) new_dish.parent_submenu = submenu_id self.db.add(new_dish) - await self.db.flush() await self.db.commit() + await self.db.refresh(new_dish) return new_dish async def get_dish_item( @@ -48,7 +48,7 @@ class DishRepository: menu_id: UUID, submenu_id: UUID, dish_id: UUID, - dish_data: DishBase, + dish_data: Dish_db, ): query = ( update(models.Dish) diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index 0c8fd33..b7fd910 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -2,9 +2,8 @@ from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastfood import schemas +from fastfood.schemas import Dish, DishBase from fastfood.service.dish import DishService -from fastfood.utils import price_converter router = APIRouter( prefix='/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes', @@ -12,7 +11,7 @@ router = APIRouter( ) -@router.get('/') +@router.get('/', response_model=list[Dish]) async def get_dishes( menu_id: UUID, submenu_id: UUID, @@ -23,23 +22,22 @@ async def get_dishes( 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_data: schemas.DishBase, + dish_data: DishBase, dish: DishService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): - result = await dish.create_dish( + return await dish.create_dish( menu_id, submenu_id, dish_data, ) - return price_converter(result) -@router.get('/{dish_id}') +@router.get('/{dish_id}', response_model=Dish) async def get_dish( menu_id: UUID, submenu_id: UUID, @@ -54,15 +52,15 @@ async def get_dish( ) if not result: raise HTTPException(status_code=404, detail='dish not found') - return price_converter(result) + 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_data: schemas.DishBase, + dish_data: DishBase, dish: DishService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): @@ -72,7 +70,7 @@ async def update_dish( dish_id, dish_data, ) - return price_converter(result) + return result @router.delete('/{dish_id}') diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index 7f96f5b..b727d27 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -1,4 +1,3 @@ -from typing import Optional from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException @@ -12,7 +11,7 @@ router = APIRouter( ) -@router.get('/', response_model=Optional[list[Menu]]) +@router.get('/', response_model=list[Menu]) async def get_menus( menu: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), diff --git a/fastfood/schemas.py b/fastfood/schemas.py index c14ff13..cf14ecb 100644 --- a/fastfood/schemas.py +++ b/fastfood/schemas.py @@ -25,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 index 5dad1c4..d0c834b 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -6,7 +6,7 @@ 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 -from fastfood.schemas import DishBase +from fastfood.schemas import Dish_db, DishBase class DishService: @@ -22,6 +22,11 @@ class DishService: async def read_dishes(self, menu_id: UUID, submenu_id: UUID): 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) return data async def create_dish( @@ -30,24 +35,33 @@ class DishService: submenu_id: UUID, dish_data: DishBase, ): + dish = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.create_dish_item( menu_id, submenu_id, - dish_data, + dish, ) - return data + response = data.__dict__ + response['price'] = str(response['price']) + return response async def read_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) - return data + if data is None: + return + response = data.__dict__ + response['price'] = str(response['price']) + + return response async def update_dish( self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase ): - data = await self.dish_repo.update_dish_item( - menu_id, submenu_id, dish_id, dish_data - ) - return data + dish = Dish_db(**dish_data.model_dump()) + data = await self.dish_repo.update_dish_item(menu_id, submenu_id, dish_id, dish) + response = data.__dict__ + response['price'] = str(response['price']) + return response async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): data = await self.dish_repo.delete_dish_item( diff --git a/fastfood/utils.py b/fastfood/utils.py deleted file mode 100644 index 7ef5890..0000000 --- a/fastfood/utils.py +++ /dev/null @@ -1,3 +0,0 @@ -def price_converter(dish: dict) -> dict: - dish['price'] = str(dish['price']) - return dish From f807bdd2751172229194e84720f9dcd779f20b61 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 00:24:09 +0300 Subject: [PATCH 07/17] =?UTF-8?q?fix:=20=D0=9F=D0=BE=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20pydantic=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=81=D1=82=D1=8B=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index c0c253c..4dd381e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ def app(event_loop) -> Generator[FastAPI, None, None]: yield app -@pytest_asyncio.fixture() +@pytest_asyncio.fixture(scope='session') async def client(app) -> AsyncGenerator[AsyncClient, None]: async with AsyncClient( app=app, From 628babc2956b761479797cbddf7cd19a197960d7 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 02:28:12 +0300 Subject: [PATCH 08/17] repo.dish typehint --- fastfood/repository/dish.py | 33 +++++++++++++++------------------ fastfood/service/dish.py | 4 +++- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/fastfood/repository/dish.py b/fastfood/repository/dish.py index 52090b4..7935d49 100644 --- a/fastfood/repository/dish.py +++ b/fastfood/repository/dish.py @@ -4,8 +4,8 @@ from fastapi import Depends from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession -from fastfood import models from fastfood.dbase import get_async_session +from fastfood.models import Dish from fastfood.schemas import Dish_db @@ -13,20 +13,20 @@ 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): - query = select(models.Dish).where( - models.Dish.parent_submenu == submenu_id, + 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 dishes.scalars().all() + return [x for x in dishes.scalars().all()] async def create_dish_item( self, menu_id: UUID, submenu_id: UUID, dish_data: Dish_db, - ): - new_dish = models.Dish(**dish_data.model_dump()) + ) -> Dish: + new_dish = Dish(**dish_data.model_dump()) new_dish.parent_submenu = submenu_id self.db.add(new_dish) await self.db.commit() @@ -38,8 +38,8 @@ class DishRepository: menu_id: UUID, submenu_id: UUID, dish_id: UUID, - ): - query = select(models.Dish).where(models.Dish.id == dish_id) + ) -> Dish | None: + query = select(Dish).where(Dish.id == dish_id) submenu = await self.db.execute(query) return submenu.scalars().one_or_none() @@ -49,15 +49,11 @@ class DishRepository: submenu_id: UUID, dish_id: UUID, dish_data: Dish_db, - ): - query = ( - update(models.Dish) - .where(models.Dish.id == dish_id) - .values(**dish_data.model_dump()) - ) + ) -> 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(models.Dish).where(models.Dish.id == dish_id) + qr = select(Dish).where(Dish.id == dish_id) updated_submenu = await self.db.execute(qr) return updated_submenu.scalars().one() @@ -66,7 +62,8 @@ class DishRepository: menu_id: UUID, submenu_id: UUID, dish_id: UUID, - ): - query = delete(models.Dish).where(models.Dish.id == dish_id) + ) -> int: + query = delete(Dish).where(Dish.id == dish_id) await self.db.execute(query) await self.db.commit() + return 200 diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index d0c834b..fc7b648 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -22,12 +22,14 @@ class DishService: async def read_dishes(self, menu_id: UUID, submenu_id: UUID): data = await self.dish_repo.get_dishes(menu_id, submenu_id) + if data: + print(type(data[0])) response = [] for row in data: dish = row.__dict__ dish['price'] = str(dish['price']) response.append(dish) - return data + return response async def create_dish( self, From 015a0bcc87f79fc11db9ee1cf725155814130c7d Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 02:41:12 +0300 Subject: [PATCH 09/17] service.dish typehint --- fastfood/service/dish.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index fc7b648..ef7766b 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -15,15 +15,13 @@ class DishService: 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_client = RedisRepository(redis_client) self.background_tasks = background_tasks - async def read_dishes(self, menu_id: UUID, submenu_id: UUID): + async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[dict]: data = await self.dish_repo.get_dishes(menu_id, submenu_id) - if data: - print(type(data[0])) response = [] for row in data: dish = row.__dict__ @@ -36,7 +34,7 @@ class DishService: menu_id: UUID, submenu_id: UUID, dish_data: DishBase, - ): + ) -> dict: dish = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.create_dish_item( menu_id, @@ -47,28 +45,27 @@ class DishService: response['price'] = str(response['price']) return response - async def read_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): + async def read_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> dict: data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) if data is None: - return + return {} response = data.__dict__ response['price'] = str(response['price']) - return response async def update_dish( self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase - ): + ) -> dict: dish = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.update_dish_item(menu_id, submenu_id, dish_id, dish) response = data.__dict__ response['price'] = str(response['price']) return response - async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID): - data = await self.dish_repo.delete_dish_item( + 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, ) - return data + return response From 181c6f10af267653d4de8ca6882b7e6b64c2d689 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 02:49:06 +0300 Subject: [PATCH 10/17] service.dish typehint --- fastfood/routers/dish.py | 2 +- fastfood/service/dish.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index b7fd910..b23ac1b 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -17,7 +17,7 @@ async def get_dishes( submenu_id: UUID, dish: DishService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), -): +) -> list[Dish]: result = await dish.read_dishes(menu_id, submenu_id) return result diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index ef7766b..675f805 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -6,7 +6,7 @@ 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 -from fastfood.schemas import Dish_db, DishBase +from fastfood.schemas import Dish, Dish_db, DishBase class DishService: @@ -20,13 +20,13 @@ class DishService: self.cache_client = RedisRepository(redis_client) self.background_tasks = background_tasks - async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[dict]: + async def read_dishes(self, menu_id: UUID, submenu_id: UUID) -> list[Dish]: 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) + response.append(Dish(**dish)) return response async def create_dish( @@ -34,7 +34,7 @@ class DishService: menu_id: UUID, submenu_id: UUID, dish_data: DishBase, - ) -> dict: + ) -> Dish: dish = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.create_dish_item( menu_id, @@ -43,24 +43,26 @@ class DishService: ) response = data.__dict__ response['price'] = str(response['price']) - return response + return Dish(**response) - async def read_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> dict: + async def read_dish( + self, menu_id: UUID, submenu_id: UUID, dish_id: UUID + ) -> Dish | None: data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) if data is None: - return {} + return None response = data.__dict__ response['price'] = str(response['price']) - return response + return Dish(**response) async def update_dish( self, menu_id: UUID, submenu_id: UUID, dish_id, dish_data: DishBase - ) -> dict: + ) -> Dish: dish = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.update_dish_item(menu_id, submenu_id, dish_id, dish) response = data.__dict__ response['price'] = str(response['price']) - return response + return Dish(**response) async def del_dish(self, menu_id: UUID, submenu_id: UUID, dish_id: UUID) -> int: response = await self.dish_repo.delete_dish_item( From 35659529b48c85ab59cd085b5100decc3e7b559d Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 17:13:37 +0300 Subject: [PATCH 11/17] service/repo.submenu typehint --- fastfood/repository/submenu.py | 48 +++++++++++++++++++--------------- fastfood/routers/submenu.py | 9 +++---- fastfood/service/submenu.py | 20 +++++++++----- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/fastfood/repository/submenu.py b/fastfood/repository/submenu.py index 721c25b..523ad89 100644 --- a/fastfood/repository/submenu.py +++ b/fastfood/repository/submenu.py @@ -5,40 +5,45 @@ from sqlalchemy import delete, distinct, func, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased -from fastfood import models, schemas 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): - query = select(models.SubMenu).where( - models.SubMenu.parent_menu == menu_id, + 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 submenus + return [x for x in submenus.scalars().all()] async def create_submenu_item( self, menu_id: UUID, - submenu: schemas.MenuBase, - ): - new_submenu = models.SubMenu(**submenu.model_dump()) + 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) - return 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, - ): - s = aliased(models.SubMenu) - d = aliased(models.Dish) + ) -> 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) @@ -55,22 +60,23 @@ class SubMenuRepository: self, menu_id: UUID, submenu_id: UUID, - submenu_data: schemas.MenuBase, - ): + submenu_data: MenuBase, + ) -> SubMenu: query = ( - update(models.SubMenu) - .where(models.SubMenu.id == submenu_id) + update(SubMenu) + .where(SubMenu.id == submenu_id) .values(**submenu_data.model_dump()) ) await self.db.execute(query) await self.db.commit() - qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id) + qr = select(SubMenu).where(SubMenu.id == submenu_id) updated_submenu = await self.db.execute(qr) - return updated_submenu + return updated_submenu.scalar_one() - async def delete_submenu_item(self, menu_id: UUID, submenu_id: UUID): - query = delete(models.SubMenu).where( - models.SubMenu.id == submenu_id, + 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/submenu.py b/fastfood/routers/submenu.py index 8b4bca8..6206e78 100644 --- a/fastfood/routers/submenu.py +++ b/fastfood/routers/submenu.py @@ -1,4 +1,3 @@ -from typing import Optional from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException @@ -12,14 +11,14 @@ router = APIRouter( ) -@router.get('/', response_model=Optional[list[SubMenuRead]]) +@router.get('/', response_model=list[SubMenuRead]) async def get_submenus( menu_id: UUID, submenu: SubmenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), ): result = await submenu.read_submenus(menu_id=menu_id) - return result.scalars().all() + return result @router.post('/', status_code=201, response_model=SubMenuRead) @@ -54,7 +53,7 @@ async def get_submenu( @router.patch( '/{submenu_id}', - response_model=MenuBase, + response_model=SubMenuRead, ) async def update_submenu( menu_id: UUID, @@ -68,7 +67,7 @@ async def update_submenu( submenu_id=submenu_id, submenu_data=submenu_data, ) - return result.scalars().one() + return result @router.delete('/{submenu_id}') diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py index 167e22f..b32c748 100644 --- a/fastfood/service/submenu.py +++ b/fastfood/service/submenu.py @@ -6,7 +6,7 @@ from fastapi import BackgroundTasks, Depends from fastfood.dbase import get_async_redis_client from fastfood.repository.redis import RedisRepository from fastfood.repository.submenu import SubMenuRepository -from fastfood.schemas import MenuBase +from fastfood.schemas import MenuBase, SubMenuRead class SubmenuService: @@ -15,14 +15,23 @@ class SubmenuService: 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_client = RedisRepository(redis_client) self.background_tasks = background_tasks - async def read_submenus(self, menu_id: UUID): + async def read_submenus(self, menu_id: UUID) -> list[SubMenuRead]: data = await self.submenu_repo.get_submenus(menu_id=menu_id) - return data + 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) + return submenus async def create_submenu(self, menu_id: UUID, submenu_data: MenuBase): data = await self.submenu_repo.create_submenu_item( @@ -44,5 +53,4 @@ class SubmenuService: return data async def del_menu(self, menu_id: UUID, submenu_id: UUID): - data = await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) - return data + return await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) From 2754b82b5d18395cef734a4e5a82f80c28e41d36 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 17:49:55 +0300 Subject: [PATCH 12/17] service/repo.submenu typehint --- fastfood/service/submenu.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/fastfood/service/submenu.py b/fastfood/service/submenu.py index b32c748..ad08b47 100644 --- a/fastfood/service/submenu.py +++ b/fastfood/service/submenu.py @@ -33,24 +33,40 @@ class SubmenuService: submenus.append(submenu) return submenus - async def create_submenu(self, menu_id: UUID, submenu_data: MenuBase): + async def create_submenu( + self, menu_id: UUID, submenu_data: MenuBase + ) -> SubMenuRead: data = await self.submenu_repo.create_submenu_item( menu_id, submenu_data, ) - return data + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + menu['dishes_count'] = len(menu.pop('dishes')) + menu = SubMenuRead(**menu) + return menu - async def read_menu(self, menu_id: UUID, submenu_id: UUID): + async def read_menu(self, menu_id: UUID, submenu_id: UUID) -> SubMenuRead | None: data = await self.submenu_repo.get_submenu_item(menu_id, submenu_id) - return data + if data is None: + return None + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + menu['dishes_count'] = len(menu.pop('dishes')) + menu = SubMenuRead(**menu) + 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 ) - return data + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + menu['dishes_count'] = len(menu.pop('dishes')) + menu = SubMenuRead(**menu) + return menu - async def del_menu(self, menu_id: UUID, submenu_id: UUID): + async def del_menu(self, menu_id: UUID, submenu_id: UUID) -> int: return await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) From 5173fcd36c774ab1ef4c4afe926c2e8d60ce27e0 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 17:59:20 +0300 Subject: [PATCH 13/17] repo.menu typehint --- fastfood/repository/menu.py | 38 ++++++++++++++++++------------------- fastfood/routers/menu.py | 2 +- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/fastfood/repository/menu.py b/fastfood/repository/menu.py index 51ac715..b7dad66 100644 --- a/fastfood/repository/menu.py +++ b/fastfood/repository/menu.py @@ -5,30 +5,31 @@ from sqlalchemy import delete, distinct, func, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased -from fastfood import models, schemas +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): - query = select(models.Menu) + async def get_menus(self) -> list[Menu]: + query = select(Menu) menus = await self.db.execute(query) - return menus.scalars().all() + return [x for x in menus.scalars().all()] - async def create_menu_item(self, menu: schemas.MenuBase): - new_menu = models.Menu(**menu.model_dump()) + 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): - m = aliased(models.Menu) - s = aliased(models.SubMenu) - d = aliased(models.Dish) + async def get_menu_item(self, menu_id: UUID) -> Menu | None: + m = aliased(Menu) + s = aliased(SubMenu) + d = aliased(Dish) query = ( select( @@ -51,19 +52,16 @@ class MenuRepository: self, menu_id: UUID, menu: schemas.MenuBase, - ): - query = ( - update(models.Menu) - .where(models.Menu.id == menu_id) - .values(**menu.model_dump()) - ) + ) -> Menu: + query = update(Menu).where(Menu.id == menu_id).values(**menu.model_dump()) await self.db.execute(query) await self.db.commit() - qr = select(models.Menu).where(models.Menu.id == menu_id) + qr = select(Menu).where(Menu.id == menu_id) updated_menu = await self.db.execute(qr) - return updated_menu + return updated_menu.scalar_one() - async def delete_menu_item(self, menu_id: UUID): - query = delete(models.Menu).where(models.Menu.id == menu_id) + async def delete_menu_item(self, menu_id: UUID) -> int: + query = delete(Menu).where(Menu.id == menu_id) await self.db.execute(query) await self.db.commit() + return 200 diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index b727d27..cb1baf8 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -52,7 +52,7 @@ async def update_menu( menu_id=menu_id, menu_data=menu, ) - return result.scalars().one() + return result @router.delete('/{menu_id}') From 09d0627d704ddf6c7478af05ab03c0905b52258b Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 4 Feb 2024 18:26:17 +0300 Subject: [PATCH 14/17] service.menu typehint --- fastfood/routers/menu.py | 6 ++-- fastfood/service/menu.py | 63 +++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index cb1baf8..5c638e7 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -2,7 +2,7 @@ from uuid import UUID from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException -from fastfood.schemas import Menu, MenuBase, MenuRead +from fastfood.schemas import MenuBase, MenuRead from fastfood.service.menu import MenuService router = APIRouter( @@ -11,7 +11,7 @@ router = APIRouter( ) -@router.get('/', response_model=list[Menu]) +@router.get('/', response_model=list[MenuRead]) async def get_menus( menu: MenuService = Depends(), background_tasks: BackgroundTasks = BackgroundTasks(), @@ -19,7 +19,7 @@ async def get_menus( return await menu.read_menus() -@router.post('/', status_code=201, response_model=Menu) +@router.post('/', status_code=201, response_model=MenuRead) async def add_menu( menu: MenuBase, responce: MenuService = Depends(), diff --git a/fastfood/service/menu.py b/fastfood/service/menu.py index c3eb13f..e743afa 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -6,7 +6,7 @@ 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 -from fastfood.schemas import MenuBase +from fastfood.schemas import MenuBase, MenuRead class MenuService: @@ -15,27 +15,68 @@ class MenuService: 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_client = RedisRepository(redis_client) self.background_tasks = background_tasks - async def read_menus(self): + async def read_menus(self) -> list[MenuRead]: data = await self.menu_repo.get_menus() - return data + 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) - async def create_menu(self, menu_data: MenuBase): + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + menu = MenuRead(**menu) + menus.append(menu) + return menus + + async def create_menu(self, menu_data: MenuBase) -> MenuRead: data = await self.menu_repo.create_menu_item(menu_data) - return data + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 - async def read_menu(self, menu_id: UUID): + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + + return MenuRead(**menu) + + async def read_menu(self, menu_id: UUID) -> MenuRead | None: data = await self.menu_repo.get_menu_item(menu_id) - return data + if data is None: + return None + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 - async def update_menu(self, menu_id: UUID, menu_data): + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + + return MenuRead(**menu) + + async def update_menu(self, menu_id: UUID, menu_data) -> MenuRead: data = await self.menu_repo.update_menu_item(menu_id, menu_data) - return data + menu = data.__dict__ + menu = {k: v for k, v in menu.items() if not k.startswith('_')} + dishes_conter = 0 - async def del_menu(self, menu_id: UUID): + for sub in data.submenus: + dishes_conter += len(sub.dishes) + menu['submenus_count'] = len(menu.pop('submenus')) + menu['dishes_count'] = dishes_conter + + return MenuRead(**menu) + + async def del_menu(self, menu_id: UUID) -> int: data = await self.menu_repo.delete_menu_item(menu_id) return data From 291c61f8736ca39b7beb6afb310d0455e4eb6774 Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Sun, 4 Feb 2024 18:40:58 +0000 Subject: [PATCH 15/17] =?UTF-8?q?reverse=5Furl=20=D0=B2=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/repository.py | 76 ++++++++++++++++++++++++++++++++++----------- tests/urls.py | 18 +++++++++++ 2 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 tests/urls.py diff --git a/tests/repository.py b/tests/repository.py index b1c9ce1..f4aca24 100644 --- a/tests/repository.py +++ b/tests/repository.py @@ -1,5 +1,7 @@ from httpx import AsyncClient, Response +from .urls import reverse_url + class Repository: class Menu: @@ -7,26 +9,28 @@ class Repository: async def read_all(ac: AsyncClient) -> tuple[int, dict]: """чтение всех меню""" - response: Response = await ac.get('/') + 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(f"/{data.get('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('/', json=data) + 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( - f"/{data.get('id')}", + reverse_url('menu', menu_id=data.get('id')), json=data, ) return response.status_code, response.json() @@ -34,14 +38,18 @@ class Repository: @staticmethod async def delete(ac: AsyncClient, data: dict) -> int: """Удаление меню по id""" - response: Response = await ac.delete(f"/{data.get('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(f"/{menu.get('id')}/submenus/") + response: Response = await ac.get( + reverse_url('submenus', menu_id=menu.get('id')), + ) return response.status_code, response.json() @staticmethod @@ -52,7 +60,11 @@ class Repository: ) -> tuple[int, dict]: """Получение меню по id""" response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}", + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), ) return response.status_code, response.json() @@ -64,7 +76,7 @@ class Repository: ) -> tuple[int, dict]: """создания меню""" response: Response = await ac.post( - f"/{menu.get('id')}/submenus/", + reverse_url('submenu', menu_id=menu.get('id')), json=submenu, ) return response.status_code, response.json() @@ -75,7 +87,11 @@ class Repository: ) -> tuple[int, dict]: """Обновление меню по id""" response: Response = await ac.patch( - f"/{menu.get('id')}/submenus/{submenu.get('id')}", + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), json=submenu, ) return response.status_code, response.json() @@ -84,7 +100,11 @@ class Repository: async def delete(ac: AsyncClient, menu: dict, submenu: dict) -> int: """Удаление меню по id""" response: Response = await ac.delete( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" + reverse_url( + 'submenu', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), ) return response.status_code @@ -95,7 +115,11 @@ class Repository: ) -> tuple[int, dict]: """чтение всех блюд""" response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", + reverse_url( + 'dishes', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), ) return response.status_code, response.json() @@ -105,8 +129,12 @@ class Repository: ) -> tuple[int, dict]: """Получение блюда по id""" response: Response = await ac.get( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}", + reverse_url( + 'dish', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + dish_id=dish.get('id'), + ), ) return response.status_code, response.json() @@ -116,7 +144,11 @@ class Repository: ) -> tuple[int, dict]: """создания блюда""" response: Response = await ac.post( - f"/{menu.get('id')}/submenus/{submenu.get('id')}/dishes/", + reverse_url( + 'dishes', + menu_id=menu.get('id'), + submenu_id=submenu.get('id'), + ), json=dish, ) return response.status_code, response.json() @@ -127,8 +159,12 @@ class Repository: ) -> tuple[int, dict]: """Обновление блюда по id""" response: Response = await ac.patch( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}", + 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() @@ -142,7 +178,11 @@ class Repository: ) -> int: """Удаление блюда по id""" response: Response = await ac.delete( - f"/{menu.get('id')}/submenus/{submenu.get('id')}" - f"/dishes/{dish.get('id')}" + 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/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 From 43eca19d91902e1fd0b026a00dd8af59265b4737 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 5 Feb 2024 03:40:12 +0300 Subject: [PATCH 16/17] =?UTF-8?q?REDIS=20=D0=BA=D1=8D=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=BD=D0=B0=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=BB=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/repository/menu.py | 3 +- fastfood/repository/redis.py | 55 ++++++++++++++++++--- fastfood/service/dish.py | 96 ++++++++++++++++++++++++++++++------ fastfood/service/menu.py | 49 ++++++++++++++---- fastfood/service/submenu.py | 84 ++++++++++++++++++++++++------- 5 files changed, 234 insertions(+), 53 deletions(-) diff --git a/fastfood/repository/menu.py b/fastfood/repository/menu.py index b7dad66..a7937a8 100644 --- a/fastfood/repository/menu.py +++ b/fastfood/repository/menu.py @@ -60,8 +60,7 @@ class MenuRepository: updated_menu = await self.db.execute(qr) return updated_menu.scalar_one() - async def delete_menu_item(self, menu_id: UUID) -> int: + 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() - return 200 diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index 037be4d..a560efd 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -1,3 +1,4 @@ +import pickle from typing import Any import redis.asyncio as redis # type: ignore @@ -6,19 +7,59 @@ 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, - redis_pool: redis.Redis = Depends(get_redis_pool), + pool: redis.Redis = Depends(get_redis_pool), ) -> None: - self.redis_pool = redis_pool + 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, background_tasks: BackgroundTasks - ) -> None: - pass + 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/service/dish.py b/fastfood/service/dish.py index 675f805..5b921ed 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -5,7 +5,7 @@ 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 +from fastfood.repository.redis import RedisRepository, get_key from fastfood.schemas import Dish, Dish_db, DishBase @@ -17,16 +17,32 @@ class DishService: background_tasks: BackgroundTasks = None, ) -> None: self.dish_repo = dish_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + 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( @@ -35,34 +51,79 @@ class DishService: submenu_id: UUID, dish_data: DishBase, ) -> Dish: - dish = Dish_db(**dish_data.model_dump()) + dish_db = Dish_db(**dish_data.model_dump()) data = await self.dish_repo.create_dish_item( menu_id, submenu_id, - dish, + dish_db, ) - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + 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 - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + 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 = Dish_db(**dish_data.model_dump()) - data = await self.dish_repo.update_dish_item(menu_id, submenu_id, dish_id, dish) - response = data.__dict__ - response['price'] = str(response['price']) - return Dish(**response) + 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( @@ -70,4 +131,7 @@ class DishService: 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 index e743afa..48147e6 100644 --- a/fastfood/service/menu.py +++ b/fastfood/service/menu.py @@ -5,7 +5,7 @@ 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 +from fastfood.repository.redis import RedisRepository, get_key from fastfood.schemas import MenuBase, MenuRead @@ -17,10 +17,15 @@ class MenuService: background_tasks: BackgroundTasks = None, ) -> None: self.menu_repo = menu_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + 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: @@ -34,6 +39,8 @@ class MenuService: 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: @@ -46,10 +53,23 @@ class MenuService: dishes_conter += len(sub.dishes) menu['submenus_count'] = len(menu.pop('submenus')) menu['dishes_count'] = dishes_conter - - return MenuRead(**menu) + 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 @@ -61,8 +81,11 @@ class MenuService: dishes_conter += len(sub.dishes) menu['submenus_count'] = len(menu.pop('submenus')) menu['dishes_count'] = dishes_conter - - return MenuRead(**menu) + 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) @@ -74,9 +97,15 @@ class MenuService: 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 - return MenuRead(**menu) - - async def del_menu(self, menu_id: UUID) -> int: + 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 index ad08b47..db23343 100644 --- a/fastfood/service/submenu.py +++ b/fastfood/service/submenu.py @@ -4,7 +4,7 @@ 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 +from fastfood.repository.redis import RedisRepository, get_key from fastfood.repository.submenu import SubMenuRepository from fastfood.schemas import MenuBase, SubMenuRead @@ -18,10 +18,17 @@ class SubmenuService: ) -> None: self.submenu_repo = submenu_repo - self.cache_client = RedisRepository(redis_client) - self.background_tasks = background_tasks + 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: @@ -31,6 +38,10 @@ class SubmenuService: 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( @@ -40,20 +51,40 @@ class SubmenuService: menu_id, submenu_data, ) - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) - return menu + 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 - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) + 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( @@ -62,11 +93,28 @@ class SubmenuService: data = await self.submenu_repo.update_submenu_item( menu_id, submenu_id, submenu_data ) - menu = data.__dict__ - menu = {k: v for k, v in menu.items() if not k.startswith('_')} - menu['dishes_count'] = len(menu.pop('dishes')) - menu = SubMenuRead(**menu) - return menu + 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: - return await self.submenu_repo.delete_submenu_item(menu_id, submenu_id) + 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 From a5eebd15ba57de92e4c87c94b0fc9089e9e2d62b Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 5 Feb 2024 19:13:40 +0300 Subject: [PATCH 17/17] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++++- compose_app.yml | 16 ++++++++++++++++ compose_test.yml | 19 +++++++++++++++++++ fastfood/config.py | 30 +++++++++++++++++++++++++++++- fastfood/dbase.py | 2 +- tests/conftest.py | 15 +++------------ tests/test_postman.py | 5 +++++ 7 files changed, 78 insertions(+), 15 deletions(-) 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/compose_app.yml b/compose_app.yml index a167cdb..a8ad9a8 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -1,5 +1,19 @@ 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 @@ -38,6 +52,8 @@ services: depends_on: db: condition: service_healthy + redis: + condition: service_healthy restart: always diff --git a/compose_test.yml b/compose_test.yml index b7beb91..43cea1b 100644 --- a/compose_test.yml +++ b/compose_test.yml @@ -1,5 +1,19 @@ 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 @@ -38,6 +52,11 @@ services: depends_on: db: condition: service_healthy + redis: + condition: service_healthy + + volumes: + - .:/usr/src/fastfood restart: always diff --git a/fastfood/config.py b/fastfood/config.py index 524da54..e180dc7 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -1,3 +1,5 @@ +import os + from pydantic_settings import BaseSettings, SettingsConfigDict @@ -11,10 +13,20 @@ class Settings(BaseSettings): 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}' @@ -26,12 +38,28 @@ 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}' ) + @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') diff --git a/fastfood/dbase.py b/fastfood/dbase.py index f7b38d3..73bc929 100644 --- a/fastfood/dbase.py +++ b/fastfood/dbase.py @@ -20,7 +20,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: def get_redis_pool(): - return redis.from_url(settings.REDIS_DB, decode_responses=False) + return redis.from_url(settings.REDIS_URL, decode_responses=False) async def get_async_redis_client( diff --git a/tests/conftest.py b/tests/conftest.py index 4dd381e..30f1296 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import asyncio -from typing import AsyncGenerator, Generator +from typing import AsyncGenerator import pytest import pytest_asyncio @@ -45,22 +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='session') -async def client(app) -> AsyncGenerator[AsyncClient, None]: async with AsyncClient( app=app, base_url='http://localhost:8000/api/v1/menus', ) as async_client: yield async_client - - -@pytest.fixture(scope='session') -def session_data() -> dict: - return {} diff --git a/tests/test_postman.py b/tests/test_postman.py index c507bb1..da61dbf 100644 --- a/tests/test_postman.py +++ b/tests/test_postman.py @@ -4,6 +4,11 @@ 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): """Проверяет создание меню"""