From 3b1a1614cf113f50bb135d68e2d7ccc7137a4f7e Mon Sep 17 00:00:00 2001 From: pi3c Date: Wed, 7 Feb 2024 12:37:43 +0300 Subject: [PATCH 01/16] fix: .env for local run --- .env | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 92c135e..0000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -DB_HOST=db -DB_PORT=5432 -POSTGRES_USER=testuser -POSTGRES_PASSWORD=test -POSTGRES_DB=fastfood_db -POSTGRES_DB_TEST=testdb -REDIS_DB=redis://localhost \ No newline at end of file From e4656825cb40bd29947c57b2ec36a30ef262f87c Mon Sep 17 00:00:00 2001 From: pi3c Date: Fri, 9 Feb 2024 02:57:34 +0300 Subject: [PATCH 02/16] =?UTF-8?q?upd:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=80=D0=BE=D1=83=D1=82=20summary=20=D1=81=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=BE=D0=BC=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D1=81=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/Menu.xlsx | Bin 0 -> 12978 bytes fastfood/app.py | 2 + fastfood/repository/redis.py | 1 + fastfood/repository/summary.py | 19 + fastfood/routers/summary.py | 17 + fastfood/schemas.py | 8 + fastfood/service/summary.py | 54 ++ openapi.json | 1204 +++++++++++++++++++++++++++++++- 8 files changed, 1304 insertions(+), 1 deletion(-) create mode 100644 admin/Menu.xlsx create mode 100644 fastfood/repository/summary.py create mode 100644 fastfood/routers/summary.py create mode 100644 fastfood/service/summary.py diff --git a/admin/Menu.xlsx b/admin/Menu.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a76c3264630d2c253cac4f84eedc30c7f7ea79cc GIT binary patch literal 12978 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 FastAPI: @@ -15,6 +16,7 @@ def create_app() -> FastAPI: app.include_router(menu_router) app.include_router(submenu_router) app.include_router(dish_router) + app.include_router(summary_router) def custom_openapi(): with open('openapi.json') as openapi: diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index a560efd..a32bdd4 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -63,3 +63,4 @@ class RedisRepository: 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) + await self.clear_cache('summary', bg_task) diff --git a/fastfood/repository/summary.py b/fastfood/repository/summary.py new file mode 100644 index 0000000..a2fef0a --- /dev/null +++ b/fastfood/repository/summary.py @@ -0,0 +1,19 @@ +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from fastfood.dbase import get_async_session +from fastfood.models import Menu, SubMenu + + +class SummaryRepository: + def __init__(self, session: AsyncSession = Depends(get_async_session)) -> None: + self.db = session + + async def get_data(self): + query = select(Menu).options( + selectinload(Menu.submenus).selectinload(SubMenu.dishes) + ) + data = await self.db.execute(query) + return [x for x in data.scalars().all()] diff --git a/fastfood/routers/summary.py b/fastfood/routers/summary.py new file mode 100644 index 0000000..ee36330 --- /dev/null +++ b/fastfood/routers/summary.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, BackgroundTasks, Depends + +from fastfood.schemas import MenuSummary +from fastfood.service.summary import SummaryService + +router = APIRouter( + prefix='/api/v1/summary', + tags=['summary'], +) + + +@router.get('/', response_model=list[MenuSummary]) +async def get_summary( + sum: SummaryService = Depends(), + background_tasks: BackgroundTasks = BackgroundTasks(), +) -> list[MenuSummary]: + return await sum.read_data() diff --git a/fastfood/schemas.py b/fastfood/schemas.py index cf14ecb..d068a35 100644 --- a/fastfood/schemas.py +++ b/fastfood/schemas.py @@ -34,3 +34,11 @@ class Dish(DishBase, Menu): class Dish_db(MenuBase): price: float + + +class SubMenuSummary(Menu): + dishes: list[Dish_db] + + +class MenuSummary(Menu): + submenus: list[SubMenuSummary] diff --git a/fastfood/service/summary.py b/fastfood/service/summary.py new file mode 100644 index 0000000..2c1a412 --- /dev/null +++ b/fastfood/service/summary.py @@ -0,0 +1,54 @@ +import redis.asyncio as redis # type: ignore +from fastapi import BackgroundTasks, Depends + +from fastfood.dbase import get_async_redis_client +from fastfood.repository.redis import RedisRepository, get_key +from fastfood.repository.summary import SummaryRepository +from fastfood.schemas import DishBase, MenuSummary, SubMenuSummary + + +class SummaryService: + def __init__( + self, + sum_repo: SummaryRepository = Depends(), + redis_client: redis.Redis = Depends(get_async_redis_client), + background_tasks: BackgroundTasks = None, + ) -> None: + self.sum_repo = sum_repo + self.cache = RedisRepository(redis_client) + self.key = get_key + self.bg_tasks = background_tasks + + async def read_data(self): + def dump_to_schema(schema, obj): + obj = obj.__dict__ + obj = {k: v for k, v in obj.items() if not k.startswith('_')} + if 'price' in obj.keys(): + obj['price'] = str(obj['price']) + return schema(**obj) + + cached_data = await self.cache.get(self.key('summary')) + if cached_data is not None: + return cached_data + + result = [] + data = await self.sum_repo.get_data() + for menu in data: + menus_res = dump_to_schema(MenuSummary, menu) + menus_res.submenus = [] + + for sub in menu.submenus: + sub_res = dump_to_schema(SubMenuSummary, sub) + sub_res.dishes = [] + + for dish in sub.dishes: + dish_res = dump_to_schema(DishBase, dish) + sub_res.dishes.append(dish_res) + + menus_res.submenus.append(sub_res) + + result.append(menus_res) + + await self.cache.set(self.key('summary'), data, self.bg_tasks) + + return result diff --git a/openapi.json b/openapi.json index 25d7b97..7e2baf3 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1,1203 @@ -{"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", "contact": {"name": "Sergey Vanyushkin", "url": "http://pi3c.ru/", "email": "pi3c@yandex.ru"}, "license": {"name": "MIT license", "url": "https://mit-license.org/"}, "version": "0.0.3"}, "paths": {"/api/v1/menus/": {"get": {"tags": ["menu"], "summary": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432\u0441\u0435 \u043c\u0435\u043d\u044e.", "operationId": "get_menus_api_v1_menus__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"items": {"$ref": "#/components/schemas/MenuRead"}, "type": "array", "title": "Response Get Menus Api V1 Menus Get"}}}}}}, "post": {"tags": ["menu"], "summary": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043c\u0435\u043d\u044e", "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/MenuRead"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/api/v1/menus/{menu_id}": {"get": {"tags": ["menu"], "summary": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID", "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"}}}}, "404": {"description": "Menu not found", "content": {"application/json": {"example": {"detail": "sting"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["menu"], "summary": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID", "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"}}}}, "404": {"description": "Menu not found", "content": {"application/json": {"example": {"detail": "string"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["menu"], "summary": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID", "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": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u043c\u0435\u043d\u044e \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u044e \u043f\u043e UUID \u043c\u0435\u043d\u044e", "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": {"type": "array", "items": {"$ref": "#/components/schemas/SubMenuRead"}, "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": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e \u043f\u043e UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0433\u043e \u043c\u0435\u043d\u044e", "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": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID \u0438 UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u043c\u0435\u043d\u044e", "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"}}}}, "404": {"description": "Submenu not found", "content": {"application/json": {"example": {"detail": "string"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["submenu"], "summary": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID \u0438 UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u043c\u0435\u043d\u044e", "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/SubMenuRead"}}}}, "404": {"description": "Submenu not found", "content": {"application/json": {"example": {"detail": "string"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["submenu"], "summary": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043e\u0434\u043c\u0435\u043d\u044e \u043f\u043e \u0435\u0433\u043e UUID", "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": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0431\u043b\u044e\u0434", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0435\u0445 \u0431\u043b\u044e\u0434\u0430 \u043f\u043e UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043c\u0435\u043d\u044e \u0438 \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "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": {"type": "array", "items": {"$ref": "#/components/schemas/Dish"}, "title": "Response Get Dishes Api V1 Menus Menu Id Submenus Submenu Id Dishes Get"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "post": {"tags": ["dish"], "summary": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0431\u043b\u044e\u0434\u043e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0431\u043b\u044e\u0434\u043e \u043f\u043e UUID\u0435\u0433\u043e \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043c\u0435\u043d\u044e \u0438 \u043f\u043e\u0434\u043c\u0435\u043d\u044e", "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": {"$ref": "#/components/schemas/Dish"}}}}, "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": "\u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e \u043f\u043e \u0435\u0433\u043e UUID \u0438 UUID \u0435\u0433\u043e \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043c\u0435\u043d\u044e", "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": {"$ref": "#/components/schemas/Dish"}}}}, "404": {"description": "Dish not found", "content": {"application/json": {"example": {"detail": "string"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "patch": {"tags": ["dish"], "summary": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e \u043f\u043e \u0435\u0433\u043e UUID \u0438 UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043c\u0435\u043d\u044e", "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": {"$ref": "#/components/schemas/Dish"}}}}, "404": {"description": "Dish not found", "content": {"application/json": {"example": {"detail": "string"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}, "delete": {"tags": ["dish"], "summary": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e", "description": "\u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0431\u043b\u044e\u0434\u043e \u043f\u043e \u0435\u0433\u043e UUID \u0438 UUID \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043c\u0435\u043d\u044e", "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": {"Dish": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "id": {"type": "string", "format": "uuid", "title": "Id"}, "price": {"type": "string", "title": "Price"}}, "type": "object", "required": ["title", "description", "id", "price"], "title": "Dish"}, "DishBase": {"properties": {"title": {"type": "string", "title": "Title"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Description"}, "price": {"type": "string", "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"}, "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"}}}, "tags": [{"name": "menu", "description": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0441 \u043c\u0435\u043d\u044e."}, {"name": "submenu", "description": "\u041f\u043e\u0434\u043c\u0435\u043d\u044e \u0438 \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u043d\u0438\u043c"}, {"name": "dish", "description": "\u0411\u043b\u044e\u0434\u0430 \u0438 \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 \u043d\u0438\u043c\u0438"}]} +{ + "openapi": "3.1.0", + "info": { + "title": "Fastfood-API", + "description": "\n# 🔥🔥🔥Fastfood-API поможет тебе подкрепиться 🔥🔥🔥\n\n### У нас есть Menu. Ты можеш выбрать блюда из кухни, которая тебе нравится\n\n## Menu\n\nТы можешь **add menu**.\n\nТы можешь **read menu**.\n\nТы можешь **patch menu**.\n\nТы можешь **delete menu**.\n\n### У нас есть в SubMenu, где ты сможешь найти\nдесерты/напитки/супчики/прочие вкусности\n\n# SubMenu\n\nТы можешь **add submenu into menu**.\n\nТы можешь **read submenu**.\n\nТы можешь **patch submenu**.\n\nТы можешь **delete menu**.\n\n### У нас есть в Dish, где ты сможешь найти блюдо по вкусу\n\n# Dish\n\nТы можешь **add dish into submenu**.\n\nТы можешь **read dish**.\n\nТы можешь **patch dish**.\n\nТы можешь **delete dish**.\n\n# Summary\n\nМожеш посмотреть все меню целиком\n\n## Приятного аппетита\n", + "contact": { + "name": "Sergey Vanyushkin", + "url": "http://pi3c.ru/", + "email": "pi3c@yandex.ru" + }, + "license": { + "name": "MIT license", + "url": "https://mit-license.org/" + }, + "version": "0.0.3" + }, + "paths": { + "/api/v1/menus/": { + "get": { + "tags": [ + "menu" + ], + "summary": "Получить список меню", + "description": "Этот метод позволяет получить все меню.", + "operationId": "get_menus_api_v1_menus__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MenuRead" + }, + "type": "array", + "title": "Response Get Menus Api V1 Menus Get" + } + } + } + } + } + }, + "post": { + "tags": [ + "menu" + ], + "summary": "Создать меню", + "description": "Этот метод позволяет создать меню", + "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/MenuRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/menus/{menu_id}": { + "get": { + "tags": [ + "menu" + ], + "summary": "Получить меню", + "description": "Этот метод позволяет получить меню по его UUID", + "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" + } + } + } + }, + "404": { + "description": "Menu not found", + "content": { + "application/json": { + "example": { + "detail": "sting" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "menu" + ], + "summary": "Обновить меню", + "description": "Этот метод позволяет изменить меню по его UUID", + "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" + } + } + } + }, + "404": { + "description": "Menu not found", + "content": { + "application/json": { + "example": { + "detail": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "menu" + ], + "summary": "Удалить меню", + "description": "Этот метод позволяет удалить меню по его UUID", + "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": "Получить список подменю", + "description": "Этот метод позволяет получить список подменю основного меню по UUID меню", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubMenuRead" + }, + "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": "Создать подменю", + "description": "Этот метод позволяет создать подменю по UUID родителского меню", + "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": "Получить подменю", + "description": "Этот метод позволяет получить подменю по его UUID и UUID родительского меню", + "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" + } + } + } + }, + "404": { + "description": "Submenu not found", + "content": { + "application/json": { + "example": { + "detail": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "submenu" + ], + "summary": "Обновить подменю", + "description": "Этот метод позволяет обновить подменю по его UUID и UUID родительского меню", + "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/SubMenuRead" + } + } + } + }, + "404": { + "description": "Submenu not found", + "content": { + "application/json": { + "example": { + "detail": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "submenu" + ], + "summary": "Удалить подменю", + "description": "Этот метод позволяет удалить подменю по его UUID", + "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": "Получить список блюд", + "description": "Этот метод позволяет получить список всех блюда по UUID родительских меню и подменю", + "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": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Dish" + }, + "title": "Response Get Dishes Api V1 Menus Menu Id Submenus Submenu Id Dishes Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "dish" + ], + "summary": "Создать блюдо", + "description": "Этот метод позволяет создать блюдо по UUIDего родительских меню и подменю", + "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": { + "$ref": "#/components/schemas/Dish" + } + } + } + }, + "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": "Получить блюдо", + "description": "Этот метод позволяет получить блюдо по его UUID и UUID его родительских меню", + "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": { + "$ref": "#/components/schemas/Dish" + } + } + } + }, + "404": { + "description": "Dish not found", + "content": { + "application/json": { + "example": { + "detail": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": [ + "dish" + ], + "summary": "Обновить блюдо", + "description": "Этот метод позволяет обновить блюдо по его UUID и UUID родительских меню", + "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": { + "$ref": "#/components/schemas/Dish" + } + } + } + }, + "404": { + "description": "Dish not found", + "content": { + "application/json": { + "example": { + "detail": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "dish" + ], + "summary": "Удалить блюдо", + "description": "Этот метод позволяет удалить блюдо по его UUID и UUID родительских меню", + "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" + } + } + } + } + } + } + }, + "/api/v1/summary/": { + "get": { + "tags": [ + "summary" + ], + "summary": "Получить все меню", + "description": "Этот метод позволяет получить полную структуру меню состоящую из меню, подменю и блюд", + "operationId": "get_summary_api_v1_summary__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MenuSummary" + }, + "type": "array", + "title": "Response Get Summary Api V1 Summary Get" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Dish": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "price": { + "type": "string", + "title": "Price" + } + }, + "type": "object", + "required": [ + "title", + "description", + "id", + "price" + ], + "title": "Dish" + }, + "DishBase": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "price": { + "type": "string", + "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" + }, + "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" + }, + "MenuSummary": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "submenus": { + "items": { + "$ref": "#/components/schemas/SubMenuSummary" + }, + "type": "array", + "title": "Submenus" + } + }, + "type": "object", + "required": [ + "title", + "description", + "id", + "submenus" + ], + "title": "MenuSummary" + }, + "SubMenuSummary": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "dishes": { + "items": { + "$ref": "#/components/schemas/DishBase" + }, + "type": "array", + "title": "Dishes" + } + }, + "type": "object", + "required": [ + "title", + "description", + "id", + "dishes" + ], + "title": "SubMenuSummary" + } + } + }, + "tags": [ + { + "name": "menu", + "description": "Операции с меню." + }, + { + "name": "submenu", + "description": "Подменю и работа с ним" + }, + { + "name": "dish", + "description": "Блюда и работа с ними" + }, + { + "name": "summary", + "description": "Вывод всей структуры меню" + } + ] +} From d9633dcfbd502c2d3c0b931e44ea3686dbb1581b Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 11 Feb 2024 03:14:17 +0300 Subject: [PATCH 03/16] =?UTF-8?q?upd:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20bg=5Ftask=20xlsx>>DBase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/Menu.xlsx | Bin 12978 -> 10342 bytes bg_tasks/__init__.py | 0 bg_tasks/bg_task.py | 33 +++ bg_tasks/updater.py | 142 +++++++++ creds.json | 13 + fastfood/repository/redis.py | 2 +- manage.py | 30 +- poetry.lock | 554 ++++++++++++++++++++++++++++++++++- pyproject.toml | 3 + 9 files changed, 771 insertions(+), 6 deletions(-) create mode 100644 bg_tasks/__init__.py create mode 100644 bg_tasks/bg_task.py create mode 100644 bg_tasks/updater.py create mode 100644 creds.json diff --git a/admin/Menu.xlsx b/admin/Menu.xlsx index a76c3264630d2c253cac4f84eedc30c7f7ea79cc..ec412aeca1bdc7ef8f6669f46bba94796b91d531 100644 GIT binary patch literal 10342 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 literal 12978 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 None: + result = loop.run_until_complete(main()) + return result diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py new file mode 100644 index 0000000..d1cf1e1 --- /dev/null +++ b/bg_tasks/updater.py @@ -0,0 +1,142 @@ +import os +import pickle +from uuid import UUID + +import openpyxl +import redis.asyncio as redis # type: ignore +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from fastfood.config import settings +from fastfood.models import Dish, Menu, SubMenu + +file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') + +redis = redis.Redis(host='127.0.0.1', port=6379, db=0) + +async_engine = create_async_engine(settings.DATABASE_URL_asyncpg) +async_session_maker = async_sessionmaker( + async_engine, + class_=AsyncSession, + expire_on_commit=False, +) + + +async def refresh_cache(disconts: dict): + await redis.flushall() + + for key in disconts.keys(): + await redis.set(f'DISCONT:{str(key)}', disconts[key]) + + await redis.set('XLSX_MOD_TIME', pickle.dumps(os.path.getmtime(file))) + + +async def is_changed_xls() -> bool: + """Проверяет, изменен ли файл с последнего запуска таска.""" + if not os.path.exists(file): + return False + + mod_time = os.path.getmtime(file) + cached_time = await redis.get('XLSX_MOD_TIME') + + if cached_time is not None: + cached_time = pickle.loads(cached_time) + + if mod_time == cached_time: + return False + + return True + + +async def xlsx_to_dict() -> dict: + """Парсит Menu.xlsx в словарь""" + print('run dumping xlsx_to_dict') + wb = openpyxl.load_workbook(file).worksheets[0] + + data = {} + + menu = None + submenu = None + dish = None + + for row in wb.iter_rows(values_only=True): + if row[0] is not None: + menu = row[0] + data[menu] = { + 'id': None, + 'title': row[1], + 'description': row[2], + 'submenus': dict(), + } + elif row[1] is not None: + submenu = row[1] + data[menu]['submenus'][submenu] = { + 'id': None, + 'title': row[2], + 'description': row[3], + 'dishes': dict(), + } + elif row[2] is not None: + dish = row[2] + data[menu]['submenus'][submenu]['dishes'][dish] = { + 'id': None, + 'title': row[3], + 'description': row[4], + 'price': row[5], + 'discont': row[6], + } + return data + + +async def refresh_all_data(data: dict) -> dict[UUID, int | float]: + """Чистит кэш удаляем старые данные и пушим новые""" + + disconts = {} + + async with async_session_maker() as session: + await session.execute(delete(Menu)) + await session.commit() + + for menu_key in data.keys(): + menu = Menu( + title=data[menu_key].get('title'), + description=data[menu_key].get('description'), + ) + session.add(menu) + await session.flush() + + submenus = data[menu_key]['submenus'] + for sub_key in submenus.keys(): + submenu = SubMenu( + title=submenus[sub_key]['title'], + description=submenus[sub_key]['description'], + parent_menu=menu.id, + ) + session.add(submenu) + await session.flush() + + dishes = data[menu_key]['submenus'][sub_key]['dishes'] + print(dishes) + for dish_key in dishes.keys(): + dish = Dish( + title=dishes[dish_key]['title'], + description=dishes[dish_key]['description'], + price=dishes[dish_key]['price'], + parent_submenu=submenu.id, + ) + session.add(dish) + await session.flush() + if dishes[dish_key]['discont'] is not None: + disconts[dish.id] = dishes[dish_key]['discont'] + + await session.commit() + return disconts + + +async def main(): + changed = await is_changed_xls() + if changed: + print('Файл изменен, обновляю базу') + menu_data = await xlsx_to_dict() + discont_data = await refresh_all_data(menu_data) + await refresh_cache(discont_data) diff --git a/creds.json b/creds.json new file mode 100644 index 0000000..ad13043 --- /dev/null +++ b/creds.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "psyched-ceiling-413920", + "private_key_id": "d19492eea6a030092cf8ad767b62d7909734ccb1", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPP2UrfDE+UOlD\nA761Jemy1IKKdIanRKXSziGvDYJ7zbY5gPFxY8Vo+9fsh1oumvROXqEH4+1LiR0J\nnUiy33R1KDOHETeP4H/FJ3u8+gHoL8wsA7SN5pIX9AR8AyBZCBjgSSp+KJchfrp/\nWN2qnFAbgp248QPvmG7/wLzBNnVsAQhQULLKqSc46hbiZ8Jt7t0ajJgOFRJSp2wP\nT5VE5k737w6b4OH8mUnhw7VK04Wk6DBmQhN1jrnxmMxmdG2hSM2zR824RIMBBs/O\n1dF+5Vkav0tgja/tVqm41Aaa2vgPRACP6bpF13YS+8C1lzw6s+7M/VdE5TH5NXRU\nvChuRknBAgMBAAECggEABmuckna0krVsawaXhLaQ30DsLf5w9hdLTvDy6CCuO9Aw\nPKb//9UNNmjMKD4rlQNY1YFS6jbxZNZRrIC7aftwQOGE2mKuIMBl6+tinuy0tLr/\nl3baS+22VZyyG36ILNrqZJ8epGm08CEsNVYRKKwS0x3aXZKFnnlnqaeYn2CUzdqa\na9iNZqrdXdRt4O7KVP7IfdNi11WuOL4epmHwBBYmCxiN0Z2KAIYvS6AcflYWtYTZ\npsBFjCQexqS37PdUyyQX9E/gKwqNZmahYwIC3vsCMCLdQQ93iODYni7LKsG0vvls\nwz03TtlMmZpMJJQGkALeqlv7jeyj+oRuqg6gjs2moQKBgQDxuDt3u1rDWhTJ50bD\nAp5T1LaiV0/+lu29ElTmYpa0RF1tlHvrndFm/MrdUjpzP4/VISmRkP3bmAgwPP6p\nYeALqQXCCGJtl44LG6D9VIOCOZxntytjLHogY8S3BLpwzKC+VMFsd56ay6wCl03S\nJEnvG10FQX8sFd+6j5qMy73OoQKBgQDbfc7hV4/r7PMaUVWFRqWjLry3dtTErxnM\nTdX30BDtuqMrm+hx0zC85ePcsbx+Zhwneyaxw2ICN5F954mJurBqs9cVaxitNSv1\nX5XjAoZqf3TevufkmSBXog6t/p4FHqAHftHYzwQvQXIINFrmT15PJkbx0lMYEYzw\nPyB7doBHIQKBgGiJi7ZpYYRw1eLH0fOOk1if+uhUqHTrYx/M6MjGRHTryBgXCkzI\n8QIAO9/hqwOirpq2/9pDgXZR1uC90EkC2jlQvPvAUokg7T5ikYpd3Y4ZSkoUjoAS\ngTK20yFvuw4DgVUvJIO7a+14PgjU1MQYC52MEPuv6sbvItX1Oxq/FnRhAoGAHWYK\ncbBSvJzuKtY+CC3gPa0i5cfq07VIVU8Pm7OosM7Q0CR/y88ntgVsscC0qJFwr/EU\ny7aJyBY9TInYqDPzMTeJVXsUwQ5gJut4ngFWk6kitDsJwFqqNFKmeLOj4repY5ee\n79U6kEHJzkOE8VgsH5nW4sjzDEQ9hmhOJ3tFz0ECgYEA8N+7yq1tK/99S8ThYW1J\n9mvUXRhAcFamBYp+8bIBdnQlrM9bGd9j8gYzQj+RBcvfCpVHFM20z8CC8oN0bitk\nh5MEjLBkw1vaywFlA/hcnA8A3g+5/IgHl03Y1tPWnyAtB77vE2M2ThklZ5l4E8eT\nP1vYw9RUSAPjtd43XDTqPNQ=\n-----END PRIVATE KEY-----\n", + "client_email": "tester@psyched-ceiling-413920.iam.gserviceaccount.com", + "client_id": "100697987276606879445", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/tester%40psyched-ceiling-413920.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index a32bdd4..80f0e35 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -22,7 +22,7 @@ def get_key(level: str, **kwargs) -> str: case 'dish': return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" - return 'abracadabra' + return 'summary' class RedisRepository: diff --git a/manage.py b/manage.py index f204f65..a05d492 100644 --- a/manage.py +++ b/manage.py @@ -1,12 +1,28 @@ import asyncio +import multiprocessing import sys +from subprocess import Popen import uvicorn from fastfood.repository import create_db_and_tables +loop = asyncio.get_event_loop() -def run_app(): + +def start_celery_worker(): + Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'worker', '--loglevel=info']) + + +def start_celery_beat(): + Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'beat', '--loglevel=info']) + + +celery_worker_process = multiprocessing.Process(target=start_celery_worker) +celery_beat_process = multiprocessing.Process(target=start_celery_beat) + + +async def run_app(): """ Запуск FastAPI """ @@ -27,8 +43,14 @@ async def recreate(): if __name__ == '__main__': if '--run-server' in sys.argv: - run_app() + pass if '--run-test-server' in sys.argv: - asyncio.run(recreate()) - run_app() + celery_worker_process.start() + celery_beat_process.start() + + loop.run_until_complete(recreate()) + loop.run_until_complete(run_app()) + + celery_beat_process.kill() + celery_worker_process.kill() diff --git a/poetry.lock b/poetry.lock index dc0ad01..d132770 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,19 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "amqp" +version = "5.2.0" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +files = [ + {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, + {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + [[package]] name = "annotated-types" version = "0.6.0" @@ -101,6 +115,83 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] +[[package]] +name = "billiard" +version = "4.2.0" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +files = [ + {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, + {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, +] + +[[package]] +name = "cachetools" +version = "5.3.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, +] + +[[package]] +name = "celery" +version = "5.3.6" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +files = [ + {file = "celery-5.3.6-py3-none-any.whl", hash = "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"}, + {file = "celery-5.3.6.tar.gz", hash = "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9"}, +] + +[package.dependencies] +billiard = ">=4.2.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.4,<6.0" +python-dateutil = ">=2.8.2" +tzdata = ">=2022.7" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==41.0.5)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.10.0)", "elasticsearch (<=8.11.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.7)"] +pymemcache = ["python-memcached (==1.59)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery (==0.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.5)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.22.0)"] + [[package]] name = "certifi" version = "2024.2.2" @@ -187,6 +278,105 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -201,6 +391,55 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-didyoumean" +version = "0.3.0" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + [[package]] name = "colorama" version = "0.4.6" @@ -379,6 +618,17 @@ files = [ dnspython = ">=2.0.0" idna = ">=2.0.0" +[[package]] +name = "et-xmlfile" +version = "1.1.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"}, + {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -428,6 +678,47 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 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 = "google-auth" +version = "2.27.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google-auth-2.27.0.tar.gz", hash = "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821"}, + {file = "google_auth-2.27.0-py2.py3-none-any.whl", hash = "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "google-auth-oauthlib-1.2.0.tar.gz", hash = "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8"}, + {file = "google_auth_oauthlib-1.2.0-py2.py3-none-any.whl", hash = "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf"}, +] + +[package.dependencies] +google-auth = ">=2.15.0" +requests-oauthlib = ">=0.7.0" + +[package.extras] +tool = ["click (>=6.0.0)"] + [[package]] name = "greenlet" version = "3.0.3" @@ -499,6 +790,22 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "gspread" +version = "6.0.1" +description = "Google Spreadsheets Python API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gspread-6.0.1-py3-none-any.whl", hash = "sha256:6c3af32b753fe75d9dd513ea9e088e9e043e09b9e3bf04d61d77213f37e67b79"}, + {file = "gspread-6.0.1.tar.gz", hash = "sha256:8c8bf83be676a019d3a483455d8b17b442f2acfc620172f245422ca4fc960dd0"}, +] + +[package.dependencies] +google-auth = ">=1.12.0" +google-auth-oauthlib = ">=0.4.1" +StrEnum = "0.4.15" + [[package]] name = "h11" version = "0.14.0" @@ -591,6 +898,38 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "kombu" +version = "5.3.5" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "kombu-5.3.5-py3-none-any.whl", hash = "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488"}, + {file = "kombu-5.3.5.tar.gz", hash = "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +vine = "*" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "mypy" version = "1.8.0" @@ -663,6 +1002,36 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "openpyxl" +version = "3.1.2" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.6" +files = [ + {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"}, + {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"}, +] + +[package.dependencies] +et-xmlfile = "*" + [[package]] name = "packaging" version = "23.2" @@ -722,6 +1091,45 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "pyasn1" +version = "0.5.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, + {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.3.0" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, + {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.6.0" + [[package]] name = "pycparser" version = "2.21" @@ -916,6 +1324,20 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1007,6 +1429,59 @@ 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 = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "setuptools" version = "69.0.3" @@ -1023,6 +1498,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments 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 = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -1138,6 +1624,22 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "strenum" +version = "0.4.15" +description = "An Enum that inherits from str." +optional = false +python-versions = "*" +files = [ + {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, + {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, +] + +[package.extras] +docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] +release = ["twine"] +test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] + [[package]] name = "tomli" version = "2.0.1" @@ -1189,6 +1691,34 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "tzdata" +version = "2023.4" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, +] + +[[package]] +name = "urllib3" +version = "2.2.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.26.0" @@ -1208,6 +1738,17 @@ 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 = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + [[package]] name = "virtualenv" version = "20.25.0" @@ -1228,7 +1769,18 @@ platformdirs = ">=3.9.1,<5" 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)"] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "106e42984de924817e2dc083ad78699b3411f9aa60de5bb5c1a95ca94a21fda1" +content-hash = "5ad429e191b066c84074106341a72bb5e17ba85c5733acaae3d2ffd2db40e338" diff --git a/pyproject.toml b/pyproject.toml index a9dc80a..6fe9861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,9 @@ pytest-asyncio = "^0.23.3" redis = "^4.6.0" types-redis = "^4.6.0.3" mypy = "^1.4.1" +celery = "^5.3.6" +openpyxl = "^3.1.2" +gspread = "^6.0.1" [tool.poetry.group.dev.dependencies] From ffb5b855c448dd6dd72c6926456fae186e4e427b Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 11 Feb 2024 20:10:25 +0300 Subject: [PATCH 04/16] =?UTF-8?q?upd:=20=D0=9F=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BA=D0=B8=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B5=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/Menu.xlsx | Bin 10342 -> 10342 bytes bg_tasks/bg_task.py | 7 +------ bg_tasks/updater.py | 2 +- fastfood/service/dish.py | 33 +++++++++++++++++++++------------ fastfood/service/summary.py | 34 +++++++++++++++++++++++++++++----- manage.py | 8 ++++---- 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/admin/Menu.xlsx b/admin/Menu.xlsx index ec412aeca1bdc7ef8f6669f46bba94796b91d531..ac0262075865bd6f07388245aa3a22805f905de4 100644 GIT binary patch delta 5170 zcmc(jdpOi-8^=Y3hEztAb=nd$AAybi@E5bBRzum228HG$Jq>%_Q zOLCf#T8A`}!#K@O$SLbQ#xPF9J9>BDYv0}Lb?sGe?eF?rzvsU1=kWbK_wTu$KkoO~ z_gE>cvDjL$h=hcMh%GT+N^Om(E%C`WYmSMov_WEaVk>Ri`fVTL9C4?0LO;%0S`Bb8 zzqSV)%D0Vqc4Su)TvW^A2{d6_XWC?Lk?UiOF*8125CrlnsV+~MXrACD>m`kym-BjY z$|6TG7Ith6?Bo>+LoqjginDQDn?<#|jb?3=vesQWTY2WL9BF!@{WTv^uXoqTrgz>n z)f`N5@?x#u`7{;})5-Hx&6;W?pRxjGju<~bq1b&^@}}=FY+UrnA@#H)J6)aAFPgN9 zUk;;)N;2$BcKonR-xyU0k>N2StTq(4Uv3HnALqY`Z0acTZZQkYsX)F^Aq^IXwqPG! zw2{Kb`o!yswX|l`a&~d{i`PXpzM%$hYLJN5%x2U~WbQGYEgRmg+Uor*t!?)rppd(c z6JFj>@?1$TmLUe2-Q0Zc-2A=v(nMQqipeNk~SF>0menJ4timqzd26X&H!d!hL@ z8+_S#B_F&uJ2*t_N~MV^=c#Po58oTcFE5!-g0RNo5@s5&7f4F+L^o zhi6mV%*GvJNWE|46lCOqd*(%FM)C!>rqgW#L{!rE@6L|g;&Qe`u6fj+$0{E%CKGF; z&N9AoYuYSo&lKnOoZ&tKNY%^*eT5$icopC89p%TLdNv(f5HDr zmJqZz>7+UOPA`Jft~&c(Q?PC&5nqGPN-YStoGeVn9}^Mx^`^F!Lpi(Y!G}YcvgM>G zP`(^G)%0Q*81cMyb4=R~$ypl7G`>=f+c)HK>6_RBj>DmmaVEW`_D;T4K*B>m)6mEB zQB;|XOEU?6yYJZ-u`cPkhdPElexJ1*sK4OtzF5ynQMz7%FyG~vyUc>(4IT65>xz|l zGd`{RW!MC7?Z%KpNj4-%WuCL431I#NFj>xni~!WK2En>vt8)38=qTRMoTqBG_t9z8 z(f!Kn`0e2>?3nq17^kKAma`CKS4mp3szR|qe13!mmr@Y?Xlw|{Zwx<#%R(HULtJc$T{|QTH5!y;D5xi` z$0M9=%WA`4ar1|I_<?%2;SwRp`d0W#`la;W1oBck( zc9?tTZHcO9ed+`W^o+3@Lg;fP730=d ze37|Iz6>{${X+!VT%e(|w4=J&iZIw0YoVUu;8a;6TU4!)E1xP& zQgeQE;ng+`d)=^Xp<5tyFACk$x)~bnlFP}OAQPKB*`+Ltt%=#Ep^M+dYwfWqHJdF4 z+nvq!O=%o={XUC2~-cB!lPjq9bwDb;O`)q=OFYu zqAmxu{VD}a$4cP%^b9q^A3Oce;-OVpi2QLa*~ z=~yis459;$f?#Su=sse>B&z2s#gC3f;9x2`;1~#|7KH96>T*%r2^2CNYl4G8b--i@ zh5U3fLKXr+}y@Ewp*Ixks8v3FU;r;VP_5Lg;%>SQ5zelXXx;{U^7tmNuLU{kI z*8R784Y6S#1@Ljx{%^B@kNWCQmbVNu{5Qh;=Qr!0F@*X5XM}^$jat_FIX=bE#S zJ)D=($>RpI+DPN|1vyF89+%IUx=!~BJRpN14h^`>!)M$G4r4pI;ogFWc7DvcbbCLW z1w|iDz6Wi-2yr=nhk3Ux(v;2j-+-WwH@o#&*aQA!?~~i8vV~v{6Kh1U(#C5O>~Ffm85CEK`K?#uXyxF>iqCfZ3V0$o;- z2R7m-b9Y8u7p?d9l{0G?tWY$foes|)ia9~n7&=(6&?&mLsE{^`AE$S|Xz4~Lyxg8+ z`yz$7C(0D$)L=8-4!J$nj4Y-`EsOgb#tb~7zu~SFwXEphf)OO^@$_!ex9c?;Z+hrO+wW`ET z8m9&kw6RxCD65U^&hz*{9Y^9qT@a=zWdm5bOpo}Sp0&3!_a{e$SRlk^-pLBGW>s*( zO&1h`E?}F&@t1$`$dI0r-~8i&_kH}Xg$e+KHWObn!xWyg69Iv4iGmj6U;mAn1L!y3 zMxtN?Ol+^S2XU*^7NAVIEq9_BU*XrF(iSOIcW<@DEg5h3!p(L5TgTbOeG(+X+h})| z{OR{1qE5Bq{DG%pI~1?(mo`8BUd@R1Msq~rv~1L#r1bH|GqSb=v}OUbcjVOcgO}gC zBzVVoM7yuL(e8Sw-`s`Q!$f;bj~3_oN~w*)eUE9E!q8W8ea~Qb+dgeLn%#GJZDKq_ zGjos6L7aXQzx;K%@7fe7)e~b!e4+8WIRf#Sg0Bi#T{Qj%F>=F)(|a!Nc<<(yw7lK8 zv&baN)F|~*-w!3mBkYqi;!A~a-&m|NyLjdMi^ceDA&5ISHZDENsf}r09%ZtOy(n;S zRx+t(gZ+ZD8)^On@8N{Oa0$Lhvx)0Xq)Z~Ad!CG?OQ2a~pe1i`r+|ZF_4^5HbZKJ)!E86?&;wd*HU-yg z>324Q$8E`RV#u9W81tA3yXQRvTwppZgs~%dxmKwI)d%oDq}NotZMDYk&Vs_SM&X4e zy6K(Q8W7jpb1s!2(_5}-(Bt^qY>eguO>#P}Jx5#}pXV9`v=yslbd`M$T>d$C8Vd_-oI2Y-&-F6yl%+O@05kg7c-yKR%@QD_HKK5M*02}@ z9-!W8m>IQsSEDP~)GnCvC@J^r@CtACvJt#3D{LKa&UsS&@kHmpgc@bGud6d&Qiut` zRI|9lL09aM`8yg?z5O3Xg+JcE!!K;6=E%55>`4_Xv~KLyb9;`>yjfd^yTZR#k?~TY zra3{4@9`|$|Kb#LyCAO|n8+}0j+JSEm%R=^T}&$I3#buO=&hZz-rmulYnfJ2{}amG z{7zZ8<6T&cnpsbVaXa@TRpTBkM&+q|vSf(Y?Ht{(Znn?ZbB^RD=XBLsifaoDyUQ#| z)!wZjhl0I}^f5+O@?uo&Q={E5c;1_vAvl<;b>A}%GRQEQIM@jMixbswT+jh<3V!Z5 zQ53F(;=OemQ?|TQV@pDrhu@(x>Za9qIXbybd*+)Kc;>p@=xKl3n_a!b)I~F>TR)|c z;UP2cpYKXg51Kv#Bi*bFl=0pcspSn(7arM7V~?_!H~2lHZ~LT}!T!{pnVqU8`K|!mNy#Qaj!(Z#(q5g=wIRLz zP%mNrA~ZY~f1LH85x3f{8MO@;Ow?=Fnp4nH6>Ko9{GQcefKQ7Ho_)aAE-k1zW;y0u zsx~!mS#{gkh@*9+;AWidFf6^>w#8vfEIxLzmD%k(fuFOsgllDfW09>5_=6= zo{N^3uG1-a5?4Q@vSGpO^FT_4R8I;|Wby!S07#Kt^0#mo{S<=@i!LuhZZd@S>UCiz zJ#t&w5!a7Lq9xe-B$q{uy>A&7m;WgMd!P8SNU`_*IoTlga;#xzFT;e}$f3&sIZDy% z@edVpS&r)0qJE4fXDx>Qi=ngnwRMTnPgv((>GiQhZ4lx@Z7&sUKiG%{ImlEKQg!mDC>rnE323igSmqb9WLs_pSz}*CyQT;oiWOD`@ zj)8ARKyE=r3Ov(G0W+2Cj&J5TUFJ z2{4Bc&)2uck#iVm2Mk;l0ZD|iDkZ?52r^^(cW`8&ih(|jfq#d9Btco#65u{U{Fr_% zjy%XfhhpGI5RgbxfUi3Bx5~$o$p2xn{=WwQfDin$@?_wz+&*!%LR#4Jxj3sR+ZV;u zaVwa37)BlZd@5hQ{bao(Gqj|&6xY>AZSs8&b?UUIzn!#j27F~ZLUfCqWAf3A}q5VbkxCr|wPl1n1pX%NAgTkB%qeH60%9wdbC>v-&aJ?OxYs#|x?Q z1%avR$(@r14Bw_&pX$s~#3Z}h;&3Htp}T}pbHY)WXV;t2-jr_M}(dNpe>@~ z1f5aUI?ypjD1Q2B%yeudPB&GZd0JN7lPqjrs9%&@pwpx$>w*C(D8 z&FL+*M|w0Hw@=G}K-$A1Ahe{Jh$sZ~-!>kxQxUb)rh_VJf9cjKX)T#pC8H(Nq;z!6 rJCK>fQlE*qLmBlYSn9P9G0MoL2|p_9eVM1MC%H85e>%k7aD#sXT@bg$ diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index 1ad4aba..c73b51e 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -22,12 +22,7 @@ celery_app.conf.beat_schedule = { } -celery_task_app = Celery( - 'tasks', broker='amqp://guest:guest@localhost', backend='rpc://' -) - - -@celery_task_app.task +@celery_app.task def periodic_task() -> None: result = loop.run_until_complete(main()) return result diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py index d1cf1e1..339b434 100644 --- a/bg_tasks/updater.py +++ b/bg_tasks/updater.py @@ -26,7 +26,7 @@ async def refresh_cache(disconts: dict): await redis.flushall() for key in disconts.keys(): - await redis.set(f'DISCONT:{str(key)}', disconts[key]) + await redis.set(f'DISCONT:{str(key)}', pickle.dumps(disconts[key])) await redis.set('XLSX_MOD_TIME', pickle.dumps(os.path.getmtime(file))) diff --git a/fastfood/service/dish.py b/fastfood/service/dish.py index 689f4f0..981cf09 100644 --- a/fastfood/service/dish.py +++ b/fastfood/service/dish.py @@ -3,6 +3,7 @@ from uuid import UUID import redis.asyncio as redis # type: ignore from fastapi import BackgroundTasks, Depends +from fastfood import models from fastfood.dbase import get_async_redis_client from fastfood.repository.dish import DishRepository from fastfood.repository.redis import RedisRepository, get_key @@ -21,6 +22,19 @@ class DishService: self.bg_tasks = background_tasks self.key = get_key + async def _get_discont(self, dish) -> dict: + discont = await self.cache.get(f"DISCONT:{str(dish.get('id'))}") + if discont is not None: + discont = float(discont) + dish['price'] = round(dish['price'] - (dish['price'] * discont / 100), 2) + return dish + + async def _convert_dish_to_dict(self, row: models.Dish) -> Dish: + dish = row.__dict__ + dish = await self._get_discont(dish) + dish['price'] = str(dish['price']) + return Dish(**dish) + 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)) @@ -31,9 +45,9 @@ class DishService: 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)) + dish = await self._convert_dish_to_dict(row) + response.append(dish) + await self.cache.set( self.key( 'dishes', @@ -57,9 +71,7 @@ class DishService: submenu_id, dish_db, ) - dish = data.__dict__ - dish['price'] = str(dish['price']) - dish = Dish(**dish) + dish = await self._convert_dish_to_dict(data) await self.cache.set( self.key('dish', menu_id=str(menu_id), submenu_id=str(submenu_id)), dish, @@ -86,9 +98,8 @@ class DishService: data = await self.dish_repo.get_dish_item(menu_id, submenu_id, dish_id) if data is None: return None - dish = data.__dict__ - dish['price'] = str(dish['price']) - dish = Dish(**dish) + dish = await self._convert_dish_to_dict(data) + await self.cache.set( self.key( 'dish', @@ -112,9 +123,7 @@ class DishService: if data is None: return None - dish = data.__dict__ - dish['price'] = str(dish['price']) - dish = Dish(**dish) + dish = await self._convert_dish_to_dict(data) await self.cache.set( self.key( diff --git a/fastfood/service/summary.py b/fastfood/service/summary.py index 2c1a412..735c53f 100644 --- a/fastfood/service/summary.py +++ b/fastfood/service/summary.py @@ -20,29 +20,53 @@ class SummaryService: self.bg_tasks = background_tasks async def read_data(self): - def dump_to_schema(schema, obj): + + result = [] + + async def dump_to_schema( + schema, obj + ) -> MenuSummary | SubMenuSummary | DishBase: + """Функция преобразует объект SQLAlchemy к Pydantic модели + + Входящие параметры + schema: Pydantic модель + obj: ORM объект + + Возвращаемые данные + schema: MenuSummary | SubMenuSummary | DishBase + """ obj = obj.__dict__ obj = {k: v for k, v in obj.items() if not k.startswith('_')} + if 'price' in obj.keys(): + discont = await self.cache.get(f"DISCONT:{str(obj.get('id'))}") + + if discont is not None: + discont = float(discont) + obj['price'] = round( + obj['price'] - (obj['price'] * discont / 100), 2 + ) obj['price'] = str(obj['price']) + return schema(**obj) cached_data = await self.cache.get(self.key('summary')) + if cached_data is not None: return cached_data - result = [] data = await self.sum_repo.get_data() + for menu in data: - menus_res = dump_to_schema(MenuSummary, menu) + menus_res = await dump_to_schema(MenuSummary, menu) menus_res.submenus = [] for sub in menu.submenus: - sub_res = dump_to_schema(SubMenuSummary, sub) + sub_res = await dump_to_schema(SubMenuSummary, sub) sub_res.dishes = [] for dish in sub.dishes: - dish_res = dump_to_schema(DishBase, dish) + dish_res = await dump_to_schema(DishBase, dish) sub_res.dishes.append(dish_res) menus_res.submenus.append(sub_res) diff --git a/manage.py b/manage.py index a05d492..cb5762c 100644 --- a/manage.py +++ b/manage.py @@ -10,11 +10,11 @@ from fastfood.repository import create_db_and_tables loop = asyncio.get_event_loop() -def start_celery_worker(): +def start_celery_worker() -> None: Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'worker', '--loglevel=info']) -def start_celery_beat(): +def start_celery_beat() -> None: Popen(['celery', '-A', 'bg_tasks.bg_task.celery_app', 'beat', '--loglevel=info']) @@ -22,7 +22,7 @@ celery_worker_process = multiprocessing.Process(target=start_celery_worker) celery_beat_process = multiprocessing.Process(target=start_celery_beat) -async def run_app(): +async def run_app() -> None: """ Запуск FastAPI """ @@ -36,7 +36,7 @@ async def run_app(): ) -async def recreate(): +async def recreate() -> None: """Удаление и создание таблиц в базе данных для тестирования""" await create_db_and_tables() From 550a058b6fc5ac370ea85baae98fcebbaa361299 Mon Sep 17 00:00:00 2001 From: pi3c Date: Sun, 11 Feb 2024 23:17:57 +0300 Subject: [PATCH 05/16] =?UTF-8?q?upd=20=D0=A0=D0=B0=D0=B7=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=20=D1=82=D0=B5=D1=81=D1=82=D1=8B,=20=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8C=D1=88=D0=B8=D0=B2=20=D0=BF=D0=BE=D1=80=D1=82=D1=8F?= =?UTF-8?q?=D0=BD=D0=BA=D1=83=20upd=20=D0=A2=D0=B5=D1=81=D1=82=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20summary=20=D1=80=D0=BE=D1=83=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/routers/dish.py | 4 +- fastfood/routers/menu.py | 4 +- fastfood/routers/submenu.py | 4 +- tests/repository.py | 8 + tests/test_api.py | 360 ------------------------------------ tests/test_dish.py | 174 +++++++++++++++++ tests/test_menu.py | 80 ++++++++ tests/test_submenu.py | 113 +++++++++++ tests/test_summary.py | 117 ++++++++++++ 9 files changed, 498 insertions(+), 366 deletions(-) delete mode 100644 tests/test_api.py create mode 100644 tests/test_dish.py create mode 100644 tests/test_menu.py create mode 100644 tests/test_submenu.py create mode 100644 tests/test_summary.py diff --git a/fastfood/routers/dish.py b/fastfood/routers/dish.py index 22b443f..e8d0594 100644 --- a/fastfood/routers/dish.py +++ b/fastfood/routers/dish.py @@ -63,7 +63,7 @@ async def get_dish( if not result: raise HTTPException( status_code=404, - detail=f'Блюдо c UUID={dish_id} не существует, доступ невозможен', + detail='dish not found', ) return result @@ -89,7 +89,7 @@ async def update_dish( if not result: raise HTTPException( status_code=404, - detail=f'Блюдо c UUID={dish_id} не существует, обновление невозможно', + detail='dish not found', ) return result diff --git a/fastfood/routers/menu.py b/fastfood/routers/menu.py index e1a82be..963f2a0 100644 --- a/fastfood/routers/menu.py +++ b/fastfood/routers/menu.py @@ -50,7 +50,7 @@ async def get_menu( if not result: raise HTTPException( status_code=404, - detail=f'Меню c UUID={menu_id} не существует, доступ невозможен', + detail='menu not found', ) return result @@ -72,7 +72,7 @@ async def update_menu( if not result: raise HTTPException( status_code=404, - detail=f'Меню c UUID={menu_id} не существует, Обновление невозможно', + detail='menu not found', ) return result diff --git a/fastfood/routers/submenu.py b/fastfood/routers/submenu.py index 67649dc..b52e523 100644 --- a/fastfood/routers/submenu.py +++ b/fastfood/routers/submenu.py @@ -59,7 +59,7 @@ async def get_submenu( if not result: raise HTTPException( status_code=404, - detail=f'Подменю c UUID={submenu_id} не существует, доступ невозможен', + detail='submenu not found', ) return result @@ -83,7 +83,7 @@ async def update_submenu( if not result: raise HTTPException( status_code=404, - detail=f'Gjlvеню c UUID={submenu_id} не существует, обновление невозможно', + detail='submenu not found', ) return result diff --git a/tests/repository.py b/tests/repository.py index e563f2a..5d8b929 100644 --- a/tests/repository.py +++ b/tests/repository.py @@ -186,3 +186,11 @@ class Repository: ), ) return response.status_code + + class Summary: + @staticmethod + async def read_summary(ac: AsyncClient) -> tuple[int, dict]: + """чтение summary""" + + response: Response = await ac.get(reverse('get_summary')) + return response.status_code, response.json() diff --git a/tests/test_api.py b/tests/test_api.py deleted file mode 100644 index d57f408..0000000 --- a/tests/test_api.py +++ /dev/null @@ -1,360 +0,0 @@ -import pytest -from httpx import AsyncClient - -from .repository import Repository as Repo - - -@pytest.mark.asyncio -async def test_menu_crud_empty(client: AsyncClient) -> None: - """Тестирование функций меню""" - code, rspn = await Repo.Menu.read_all(client) - assert code == 200 - assert rspn == [] - - -@pytest.mark.asyncio -async def test_menu_crud_add(client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {'title': 'Menu', 'description': None} - code, rspn = await Repo.Menu.write(client, data) - assert code == 201 - assert rspn['title'] == 'Menu' - assert rspn['description'] is None - await Repo.Menu.delete(client, rspn) - - -@pytest.mark.asyncio -async def test_menu_crud_get(client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {'title': 'Menu', 'description': None} - code, rspn = await Repo.Menu.write(client, data) - code, menu = await Repo.Menu.get(client, {'id': rspn.get('id')}) - assert code == 200 - assert menu['title'] == rspn['title'] - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_menu_crud_update(client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {'title': 'Menu', 'description': None} - code, rspn = await Repo.Menu.write(client, data) - - upd_data = { - 'id': rspn.get('id'), - 'title': 'upd Menu', - 'description': '', - } - code, upd_rspn = await Repo.Menu.update(client, upd_data) - assert code == 200 - assert upd_rspn['title'] == 'upd Menu' - await Repo.Menu.delete(client, upd_rspn) - - -@pytest.mark.asyncio -async def test_menu_crud_delete(client: AsyncClient) -> None: - """Тестирование функций меню""" - data = {'title': 'Menu', 'description': None} - code, rspn = await Repo.Menu.write(client, data) - - code = await Repo.Menu.delete(client, rspn) - assert code == 200 - - code, rspn = await Repo.Menu.get(client, {'id': rspn.get('id')}) - assert code == 404 - - -@pytest.mark.asyncio -async def test_menu_crud_get_all(client: AsyncClient) -> None: - """Тестирование функций меню""" - code, rspn = await Repo.Menu.read_all(client) - assert code == 200 - assert rspn == [] - - data = {'title': 'Menu', 'description': None} - code, rspn = await Repo.Menu.write(client, data) - - code, upd_rspn = await Repo.Menu.read_all(client) - assert code == 200 - assert upd_rspn == [rspn] - await Repo.Menu.delete(client, rspn) - - -@pytest.mark.asyncio -async def test_submenus_get_all(client) -> None: - # Создаем меню и проверяем ответ - menu = {'title': 'Menu', 'description': 'main menu'} - code, rspn = await Repo.Menu.write(client, menu) - assert code == 201 - menu.update(rspn) - - # Проверяем наличие подменю - code, rspn = await Repo.Submenu.read_all(client, menu) - assert code == 200 - assert rspn == [] - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Проверяем наличие подменю - code, upd_rspn = await Repo.Submenu.read_all(client, menu) - assert code == 200 - assert upd_rspn == [rspn] - - # удаляем сопутствующее - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_submenus_add(client) -> None: - # Создаем меню и проверяем ответ - menu = {'title': 'Menu', 'description': 'main menu'} - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - assert code == 201 - submenu.update(rspn) - - # удаляем сопутствующее - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_submenus_update(client) -> None: - # Создаем меню и проверяем ответ - menu = {'title': 'Menu', 'description': 'main menu'} - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Обновляем подменю и проверяем - submenu['title'] = 'updated_submenu' - code, rspn = await Repo.Submenu.update(client, menu, submenu) - assert code == 200 - assert submenu['title'] == rspn['title'] - submenu.update(rspn) - - # удаляем сопутствующее - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_submenus_delete(client) -> None: - # Создаем меню и проверяем ответ - menu = {'title': 'Menu', 'description': 'main menu'} - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Удаляем подменю - code = await Repo.Submenu.delete(client, menu, submenu) - assert code == 200 - - # Проверяем удаленное подменю - code, rspn = await Repo.Submenu.get(client, menu, submenu) - assert code == 404 - - # удаляем сопутствующее - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_dishes_get_all(client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - 'title': 'Menu', - 'description': 'main menu', - } - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Проверяем все блюда в подменю - code, rspn = await Repo.Dish.read_all(client, menu, submenu) - assert code == 200 - assert rspn == [] - - # Добавляем блюдо - dish = { - 'title': 'dish', - 'description': 'some dish', - 'price': '12.5', - 'parent_submenu': submenu['id'], - } - code, rspn = await Repo.Dish.write(client, menu, submenu, dish) - assert code == 201 - dish.update(rspn) - - code, upd_rspn = await Repo.Dish.read_all(client, menu, submenu) - - assert code == 200 - - # удаляем сопутствующее - await Repo.Dish.delete(client, menu, submenu, dish) - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_dishes_add(client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - 'title': 'Menu', - 'description': 'main menu', - } - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - 'title': 'dish', - 'description': 'some dish', - 'price': '12.5', - 'parent_submenu': submenu['id'], - } - code, rspn = await Repo.Dish.write(client, menu, submenu, dish) - assert code == 201 - dish.update(rspn) - - # Получаем блюдо - code, rspn = await Repo.Dish.get(client, menu, submenu, dish) - assert code == 200 - assert rspn['title'] == dish['title'] - - # удаляем сопутствующее - await Repo.Dish.delete(client, menu, submenu, dish) - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_dishes_update(client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - 'title': 'Menu', - 'description': 'main menu', - } - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - 'title': 'dish', - 'description': 'some dish', - 'price': '12.5', - 'parent_submenu': submenu['id'], - } - code, rspn = await Repo.Dish.write(client, menu, submenu, dish) - dish.update(rspn) - - # Обновляем блюдо и проверяем - dish['title'] = 'updated_dish' - code, rspn = await Repo.Dish.update(client, menu, submenu, dish) - assert code == 200 - assert dish['title'] == rspn['title'] - dish.update(rspn) - - # удаляем сопутствующее - await Repo.Dish.delete(client, menu, submenu, dish) - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) - - -@pytest.mark.asyncio -async def test_dishes_delete(client: AsyncClient) -> None: - # Создаем меню и проверяем ответ - menu = { - 'title': 'Menu', - 'description': 'main menu', - } - code, rspn = await Repo.Menu.write(client, menu) - menu.update(rspn) - - # Создаем и проверяем подменю - submenu = { - 'title': 'Submenu', - 'description': 'submenu', - 'parent_menu': menu['id'], - } - code, rspn = await Repo.Submenu.write(client, menu, submenu) - submenu.update(rspn) - - # Добавляем блюдо - dish = { - 'title': 'dish', - 'description': 'some dish', - 'price': '12.5', - 'parent_submenu': submenu['id'], - } - code, rspn = await Repo.Dish.write(client, menu, submenu, dish) - dish.update(rspn) - - # Удаляем подменю - code = await Repo.Dish.delete(client, menu, submenu, dish) - assert code == 200 - - # Проверяем удаленное блюдо - code, rspn = await Repo.Dish.get(client, menu, submenu, dish) - assert code == 404 - - # удаляем сопутствующее - await Repo.Submenu.delete(client, menu, submenu) - await Repo.Menu.delete(client, menu) diff --git a/tests/test_dish.py b/tests/test_dish.py new file mode 100644 index 0000000..c1bfabe --- /dev/null +++ b/tests/test_dish.py @@ -0,0 +1,174 @@ +import pytest +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@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_menu.py b/tests/test_menu.py new file mode 100644 index 0000000..4668944 --- /dev/null +++ b/tests/test_menu.py @@ -0,0 +1,80 @@ +import pytest +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@pytest.mark.asyncio +async def test_menu_crud_empty(client: AsyncClient) -> None: + """Тестирование функций меню""" + code, rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert rspn == [] + + +@pytest.mark.asyncio +async def test_menu_crud_add(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + assert code == 201 + assert rspn['title'] == 'Menu' + assert rspn['description'] is None + await Repo.Menu.delete(client, rspn) + + +@pytest.mark.asyncio +async def test_menu_crud_get(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + code, menu = await Repo.Menu.get(client, {'id': rspn.get('id')}) + assert code == 200 + assert menu['title'] == rspn['title'] + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_menu_crud_update(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + upd_data = { + 'id': rspn.get('id'), + 'title': 'upd Menu', + 'description': '', + } + code, upd_rspn = await Repo.Menu.update(client, upd_data) + assert code == 200 + assert upd_rspn['title'] == 'upd Menu' + await Repo.Menu.delete(client, upd_rspn) + + +@pytest.mark.asyncio +async def test_menu_crud_delete(client: AsyncClient) -> None: + """Тестирование функций меню""" + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + code = await Repo.Menu.delete(client, rspn) + assert code == 200 + + code, rspn = await Repo.Menu.get(client, {'id': rspn.get('id')}) + assert code == 404 + + +@pytest.mark.asyncio +async def test_menu_crud_get_all(client: AsyncClient) -> None: + """Тестирование функций меню""" + code, rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert rspn == [] + + data = {'title': 'Menu', 'description': None} + code, rspn = await Repo.Menu.write(client, data) + + code, upd_rspn = await Repo.Menu.read_all(client) + assert code == 200 + assert upd_rspn == [rspn] + await Repo.Menu.delete(client, rspn) diff --git a/tests/test_submenu.py b/tests/test_submenu.py new file mode 100644 index 0000000..d3c8d56 --- /dev/null +++ b/tests/test_submenu.py @@ -0,0 +1,113 @@ +import pytest + +from .repository import Repository as Repo + + +@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) diff --git a/tests/test_summary.py b/tests/test_summary.py new file mode 100644 index 0000000..f37ddd6 --- /dev/null +++ b/tests/test_summary.py @@ -0,0 +1,117 @@ +import pytest +from httpx import AsyncClient + +from .repository import Repository as Repo + + +@pytest.mark.asyncio +async def test_summary_with_menu(client: AsyncClient) -> None: + # Проверяем пустое summary + code, rspn = await Repo.Summary.read_summary(client) + assert code == 200 + assert rspn == [] + + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + # Удалим ненужные ключи, тк в модели они не используются + del menu['submenus_count'] + del menu['dishes_count'] + menu.__setattr__('submenus', list()) + + # Проверяем summary c меню + code, rspn = await Repo.Summary.read_summary(client) + assert code == 200 + assert rspn == [menu] + + # удаляем сопутствующее + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_summary_with_submenus(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + del menu['submenus_count'] + del menu['dishes_count'] + menu.__setattr__('submenus', list()) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + submenu.__setattr__('dishes', list()) + del submenu['dishes_count'] + del submenu['parent_menu'] + + menu.__setattr__('submenus', [submenu]) + + # Получаем блюдо + code, rspn = await Repo.Summary.read_summary(client) + assert code == 200 + assert rspn == [menu] + + await Repo.Menu.delete(client, menu) + + +@pytest.mark.asyncio +async def test_summary_with_dishes(client: AsyncClient) -> None: + # Создаем меню и проверяем ответ + menu = { + 'title': 'Menu', + 'description': 'main menu', + } + code, rspn = await Repo.Menu.write(client, menu) + menu.update(rspn) + + del menu['submenus_count'] + del menu['dishes_count'] + menu.__setattr__('submenus', list()) + + # Создаем и проверяем подменю + submenu = { + 'title': 'Submenu', + 'description': 'submenu', + 'parent_menu': menu['id'], + } + code, rspn = await Repo.Submenu.write(client, menu, submenu) + submenu.update(rspn) + submenu.__setattr__('dishes', list()) + del submenu['dishes_count'] + del submenu['parent_menu'] + + # Добавляем блюдо + 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) + del dish['parent_submenu'] + del dish['id'] + + submenu.__setattr__('dishes', dish) + menu.__setattr__('submenus', submenu) + + code, rspn = await Repo.Summary.read_summary(client) + assert code == 200 + assert rspn == [menu] + + await Repo.Menu.delete(client, menu) From fc9577c538d99ff19feafc65a1116fcf14fa6ef1 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 00:39:51 +0300 Subject: [PATCH 06/16] =?UTF-8?q?upd:=20=D0=9A=D0=BE=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D0=B9=D0=BD=D0=B5=D1=80=D1=8B=20=D0=B4=D0=BB=D1=8F=20celery=20?= =?UTF-8?q?&=20rabbitmq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++++- bg_tasks/updater.py | 18 ++++++++++++------ compose_app.yml | 31 +++++++++++++++++++++++++++++++ example.env | 15 +++++++++++---- fastfood/config.py | 24 +++++++++++++++--------- manage.py | 4 ++++ 6 files changed, 78 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 09858f3..fcddb6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,11 @@ RUN mkdir -p /usr/src/fastfood WORKDIR /usr/src/fastfood -COPY . . +COPY ./example.env . + +COPY ./poetry.lock . + +COPY ./pyproject.toml . RUN touch /usr/src/RUN_IN_DOCKER diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py index 339b434..d1a13f7 100644 --- a/bg_tasks/updater.py +++ b/bg_tasks/updater.py @@ -12,7 +12,9 @@ from fastfood.models import Dish, Menu, SubMenu file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') -redis = redis.Redis(host='127.0.0.1', port=6379, db=0) +redis = redis.Redis( + host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB +) async_engine = create_async_engine(settings.DATABASE_URL_asyncpg) async_session_maker = async_sessionmaker( @@ -22,7 +24,10 @@ async_session_maker = async_sessionmaker( ) -async def refresh_cache(disconts: dict): +async def refresh_cache(disconts: dict) -> None: + """Очищает кэш при обновлении БД и ставит отметку времени обновления + и сохраняет данные скидок на товар + """ await redis.flushall() for key in disconts.keys(): @@ -50,7 +55,6 @@ async def is_changed_xls() -> bool: async def xlsx_to_dict() -> dict: """Парсит Menu.xlsx в словарь""" - print('run dumping xlsx_to_dict') wb = openpyxl.load_workbook(file).worksheets[0] data = {} @@ -89,7 +93,9 @@ async def xlsx_to_dict() -> dict: async def refresh_all_data(data: dict) -> dict[UUID, int | float]: - """Чистит кэш удаляем старые данные и пушим новые""" + """Удаляет старые данные и сохраняет новые. + Создает и возвращает список со скидками с привязкой по UUID товара + """ disconts = {} @@ -133,10 +139,10 @@ async def refresh_all_data(data: dict) -> dict[UUID, int | float]: return disconts -async def main(): +async def main() -> None: + """Главная функция фоновой задачи""" changed = await is_changed_xls() if changed: - print('Файл изменен, обновляю базу') menu_data = await xlsx_to_dict() discont_data = await refresh_all_data(menu_data) await refresh_cache(discont_data) diff --git a/compose_app.yml b/compose_app.yml index a8ad9a8..46203c4 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -57,4 +57,35 @@ services: restart: always + volumes: + - .:/usr/src/fastfood + command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server' + + celery: + container_name: celery + + build: + context: . + + env_file: + - .env + + depends_on: + - rabbitmq + - db + - app + - redis + + volumes: + - .:/usr/src/fastfood + + command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-selery' + + rabbitmq: + container_name: rabbit + + image: "rabbitmq:management" + + ports: + - 5672:5672 diff --git a/example.env b/example.env index 73ef17e..51d748a 100644 --- a/example.env +++ b/example.env @@ -1,7 +1,14 @@ -DB_HOST=db -DB_PORT=5432 +# PosgreSQL адрес сервера +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 +# Пользователь БД Postgres POSTGRES_USER=testuser POSTGRES_PASSWORD=test +# БД рабочая и тестовая POSTGRES_DB=fastfood_db -POSTGRES_DB_TEST=testdb -REDIS_DB=redis://localhost +POSTGRES_DB_TEST=fastfood_db_test + +# Redis +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_DB=0 diff --git a/fastfood/config.py b/fastfood/config.py index e180dc7..7420230 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -4,13 +4,17 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - DB_HOST: str = '' - DB_PORT: int = 5432 + # Конфиг PostgreSql + POSTGRES_HOST: str = '' + POSTGRES_PORT: int = 5432 POSTGRES_DB: str = '' POSTGRES_PASSWORD: str = '' POSTGRES_USER: str = '' POSTGRES_DB_TEST: str = '' - REDIS_DB: str = '' + # Конфиг Redis + REDIS_HOST: str = '' + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 @property def DATABASE_URL_asyncpg(self) -> str: @@ -18,19 +22,18 @@ class Settings(BaseSettings): Возвращает строку подключения к БД необходимую для 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}' + f'@db:5432/{self.POSTGRES_DB}' ) return ( 'postgresql+asyncpg://' f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' - f'@{self.DB_HOST}:{self.DB_PORT}/{self.POSTGRES_DB}' + f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}' ) @property @@ -43,22 +46,25 @@ class Settings(BaseSettings): return ( 'postgresql+asyncpg://' f'{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}' - f'@db:{self.DB_PORT}/{self.POSTGRES_DB_TEST}' + f'@db:5432/{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}' + f'@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB_TEST}' ) @property def REDIS_URL(self): + """ + Возвращает строку подключения к REDIS + """ file_path = '/usr/src/RUN_IN_DOCKER' if os.path.exists(file_path): return 'redis://redis:6379/0' - return self.REDIS_DB + return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}' model_config = SettingsConfigDict(env_file='.env') diff --git a/manage.py b/manage.py index cb5762c..2ef59b1 100644 --- a/manage.py +++ b/manage.py @@ -42,6 +42,10 @@ async def recreate() -> None: if __name__ == '__main__': + if '--run-celery' in sys.argv: + celery_worker_process.start() + celery_beat_process.start() + if '--run-server' in sys.argv: pass From 68db31a033469c1c135e8d13362bc015786292d1 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 00:54:53 +0300 Subject: [PATCH 07/16] =?UTF-8?q?fix:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=83=D1=80=D0=BB=D0=B0=20=D0=BA=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bg_tasks/bg_task.py | 3 ++- fastfood/config.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index c73b51e..ddf7360 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -2,6 +2,7 @@ import asyncio from celery import Celery +from ..fastfood.config import settings from .updater import main loop = asyncio.get_event_loop() @@ -9,7 +10,7 @@ loop = asyncio.get_event_loop() celery_app = Celery( 'tasks', - broker='amqp://guest:guest@localhost', + broker=settings.REBBITMQ_URL, backend='rpc://', include=['bg_tasks.bg_task'], ) diff --git a/fastfood/config.py b/fastfood/config.py index 7420230..4bd39c9 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -66,6 +66,17 @@ class Settings(BaseSettings): return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}' + @property + def REBBITMQ_URL(self): + """ + Возвращает строку подключения к REBBITMQ + """ + file_path = '/usr/src/RUN_IN_DOCKER' + if os.path.exists(file_path): + return 'amqp://guest:guest@localhost' + + # return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" + model_config = SettingsConfigDict(env_file='.env') From 02134d247afb1e4ebcb069c544f7cc54f072598f Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 01:06:45 +0300 Subject: [PATCH 08/16] fix --- compose_app.yml | 2 +- manage.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/compose_app.yml b/compose_app.yml index 46203c4..aa17a83 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -60,7 +60,7 @@ services: volumes: - .:/usr/src/fastfood - command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-test-server' + command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-docker-server' celery: container_name: celery diff --git a/manage.py b/manage.py index 2ef59b1..1c275c7 100644 --- a/manage.py +++ b/manage.py @@ -46,10 +46,11 @@ if __name__ == '__main__': celery_worker_process.start() celery_beat_process.start() - if '--run-server' in sys.argv: - pass + if '--run-docker-server' in sys.argv: + loop.run_until_complete(recreate()) + loop.run_until_complete(run_app()) - if '--run-test-server' in sys.argv: + if '--run-local-server' in sys.argv: celery_worker_process.start() celery_beat_process.start() From e6576e9e58e8c4dde92d72ef921d56f0c90a2803 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 01:11:00 +0300 Subject: [PATCH 09/16] fix --- bg_tasks/bg_task.py | 3 +-- fastfood/config.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index ddf7360..1959e26 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -2,7 +2,6 @@ import asyncio from celery import Celery -from ..fastfood.config import settings from .updater import main loop = asyncio.get_event_loop() @@ -10,7 +9,7 @@ loop = asyncio.get_event_loop() celery_app = Celery( 'tasks', - broker=settings.REBBITMQ_URL, + broker='amqp://guest:guest@rabbitmq', backend='rpc://', include=['bg_tasks.bg_task'], ) diff --git a/fastfood/config.py b/fastfood/config.py index 4bd39c9..b5450d8 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -73,7 +73,7 @@ class Settings(BaseSettings): """ file_path = '/usr/src/RUN_IN_DOCKER' if os.path.exists(file_path): - return 'amqp://guest:guest@localhost' + return 'amqp://guest:guest@rabbitmq' # return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" From 47cb0e08c7f727685fc099d3aa4e27afdfe7bd93 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 01:29:06 +0300 Subject: [PATCH 10/16] fix --- compose_app.yml | 27 ++++++++++++++++++++++++--- manage.py | 4 ---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/compose_app.yml b/compose_app.yml index aa17a83..e274a4b 100644 --- a/compose_app.yml +++ b/compose_app.yml @@ -62,8 +62,8 @@ services: command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-docker-server' - celery: - container_name: celery + celery_worker: + container_name: celeryworker build: context: . @@ -80,7 +80,28 @@ services: volumes: - .:/usr/src/fastfood - command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-selery' + command: ["celery", "-A", "bg_tasks.bg_task:celery_app", "worker", "--loglevel=info", "--concurrency", "1", "-P", "solo"] + + celery_beat: + container_name: celerybeat + + build: + context: . + + env_file: + - .env + + depends_on: + - rabbitmq + - db + - app + - redis + + volumes: + - .:/usr/src/fastfood + + command: ["celery", "-A", "bg_tasks.bg_task:celery_app", "beat", "--loglevel=info"] + rabbitmq: container_name: rabbit diff --git a/manage.py b/manage.py index 1c275c7..b502fd2 100644 --- a/manage.py +++ b/manage.py @@ -42,10 +42,6 @@ async def recreate() -> None: if __name__ == '__main__': - if '--run-celery' in sys.argv: - celery_worker_process.start() - celery_beat_process.start() - if '--run-docker-server' in sys.argv: loop.run_until_complete(recreate()) loop.run_until_complete(run_app()) From e6d1070d9a0fbba7e8a3cd4cb0d1ed458e631d9e Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 01:42:53 +0300 Subject: [PATCH 11/16] fix --- bg_tasks/updater.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py index d1a13f7..68c2f76 100644 --- a/bg_tasks/updater.py +++ b/bg_tasks/updater.py @@ -12,9 +12,7 @@ from fastfood.models import Dish, Menu, SubMenu file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') -redis = redis.Redis( - host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB -) +redis = redis.Redis.from_url(url=settings.REDIS_URL) async_engine = create_async_engine(settings.DATABASE_URL_asyncpg) async_session_maker = async_sessionmaker( From f28637f5dd5a7ba79b54a6ae68c011b1bde6b173 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 02:42:46 +0300 Subject: [PATCH 12/16] fix --- fastfood/config.py | 2 +- tests/test_summary.py | 28 ++++++++++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/fastfood/config.py b/fastfood/config.py index b5450d8..62a8891 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -75,7 +75,7 @@ class Settings(BaseSettings): if os.path.exists(file_path): return 'amqp://guest:guest@rabbitmq' - # return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" + return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}' model_config = SettingsConfigDict(env_file='.env') diff --git a/tests/test_summary.py b/tests/test_summary.py index f37ddd6..35363c4 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -12,17 +12,13 @@ async def test_summary_with_menu(client: AsyncClient) -> None: assert rspn == [] # Создаем меню и проверяем ответ - menu = { - 'title': 'Menu', - 'description': 'main menu', - } + menu = {'title': 'Menu', 'description': 'main menu', 'submenus': []} code, rspn = await Repo.Menu.write(client, menu) menu.update(rspn) # Удалим ненужные ключи, тк в модели они не используются del menu['submenus_count'] del menu['dishes_count'] - menu.__setattr__('submenus', list()) # Проверяем summary c меню code, rspn = await Repo.Summary.read_summary(client) @@ -36,30 +32,30 @@ async def test_summary_with_menu(client: AsyncClient) -> None: @pytest.mark.asyncio async def test_summary_with_submenus(client: AsyncClient) -> None: # Создаем меню и проверяем ответ - menu = { + menu: dict[str, str | list | float] = { 'title': 'Menu', 'description': 'main menu', + 'submenus': [], } code, rspn = await Repo.Menu.write(client, menu) menu.update(rspn) del menu['submenus_count'] del menu['dishes_count'] - menu.__setattr__('submenus', list()) # Создаем и проверяем подменю - submenu = { + submenu: dict[str, str | list | float] = { 'title': 'Submenu', 'description': 'submenu', 'parent_menu': menu['id'], + 'dishes': list(), } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) - submenu.__setattr__('dishes', list()) del submenu['dishes_count'] del submenu['parent_menu'] - menu.__setattr__('submenus', [submenu]) + menu['submenus'] = [submenu] # Получаем блюдо code, rspn = await Repo.Summary.read_summary(client) @@ -72,26 +68,26 @@ async def test_summary_with_submenus(client: AsyncClient) -> None: @pytest.mark.asyncio async def test_summary_with_dishes(client: AsyncClient) -> None: # Создаем меню и проверяем ответ - menu = { + menu: dict[str, str | list | float] = { 'title': 'Menu', 'description': 'main menu', + 'submenus': [], } code, rspn = await Repo.Menu.write(client, menu) menu.update(rspn) del menu['submenus_count'] del menu['dishes_count'] - menu.__setattr__('submenus', list()) # Создаем и проверяем подменю - submenu = { + submenu: dict[str, str | list | float] = { 'title': 'Submenu', 'description': 'submenu', 'parent_menu': menu['id'], + 'dishes': [], } code, rspn = await Repo.Submenu.write(client, menu, submenu) submenu.update(rspn) - submenu.__setattr__('dishes', list()) del submenu['dishes_count'] del submenu['parent_menu'] @@ -107,8 +103,8 @@ async def test_summary_with_dishes(client: AsyncClient) -> None: del dish['parent_submenu'] del dish['id'] - submenu.__setattr__('dishes', dish) - menu.__setattr__('submenus', submenu) + submenu['dishes'] = [dish] + menu['submenus'] = [submenu] code, rspn = await Repo.Summary.read_summary(client) assert code == 200 From 5e213e759dfcaa4189b3b7994bf1e564aabc721a Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 03:03:24 +0300 Subject: [PATCH 13/16] fix --- bg_tasks/bg_task.py | 4 +++- fastfood/config.py | 2 +- manage.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index 1959e26..3703ea6 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -2,6 +2,8 @@ import asyncio from celery import Celery +from fastfood.config import settings + from .updater import main loop = asyncio.get_event_loop() @@ -9,7 +11,7 @@ loop = asyncio.get_event_loop() celery_app = Celery( 'tasks', - broker='amqp://guest:guest@rabbitmq', + broker=settings.REBBITMQ_URL, backend='rpc://', include=['bg_tasks.bg_task'], ) diff --git a/fastfood/config.py b/fastfood/config.py index 62a8891..8409f07 100644 --- a/fastfood/config.py +++ b/fastfood/config.py @@ -75,7 +75,7 @@ class Settings(BaseSettings): if os.path.exists(file_path): return 'amqp://guest:guest@rabbitmq' - return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}' + return 'amqp://guest:guest@127.0.0.1' model_config = SettingsConfigDict(env_file='.env') diff --git a/manage.py b/manage.py index b502fd2..639f437 100644 --- a/manage.py +++ b/manage.py @@ -43,10 +43,12 @@ async def recreate() -> None: if __name__ == '__main__': if '--run-docker-server' in sys.argv: + """Запуск FastAPI в докере. Celery запускается в отдельном контейнере""" loop.run_until_complete(recreate()) loop.run_until_complete(run_app()) if '--run-local-server' in sys.argv: + """Локальный запуск FastAPI с запуском Celery в отдельных процессах""" celery_worker_process.start() celery_beat_process.start() From b2a284d7915c49f3aa26ddb273d49d7c3454ce71 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 22:22:59 +0300 Subject: [PATCH 14/16] =?UTF-8?q?upd=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BD=D0=B5=20=D0=B4=D1=80?= =?UTF-8?q?=D0=BE=D0=BF=D0=B0=D0=B5=D1=82=20=D0=B1=D0=B0=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/.~lock.Menu.xlsx# | 1 + admin/Menu.xlsx | Bin 10342 -> 10099 bytes bg_tasks/bg_task.py | 2 +- bg_tasks/parser.py | 94 ++++++++++ bg_tasks/updater.py | 325 +++++++++++++++++++++++++---------- fastfood/repository/redis.py | 10 +- 6 files changed, 338 insertions(+), 94 deletions(-) create mode 100644 admin/.~lock.Menu.xlsx# create mode 100644 bg_tasks/parser.py diff --git a/admin/.~lock.Menu.xlsx# b/admin/.~lock.Menu.xlsx# new file mode 100644 index 0000000..4cab078 --- /dev/null +++ b/admin/.~lock.Menu.xlsx# @@ -0,0 +1 @@ +,pi3c,pi3code,12.02.2024 22:20,file:///home/pi3c/.config/libreoffice/4; diff --git a/admin/Menu.xlsx b/admin/Menu.xlsx index ac0262075865bd6f07388245aa3a22805f905de4..216bc3d21bb3bfc5776d63b55e18cbb62ce240e1 100644 GIT binary patch delta 6515 zcmc&(2T+sE)&@ZVX$sMZC`F}95s+SlPe6(oI!F_xNtI4$FC9SzF(92tF-TVegc3kX zXrT!NgwP=%oe+xjAN}XQ^W8iD+?nsro%!B*-r2Kf&px|nXSST(c?e0H9->J>2_mDW zrY4I_)%rqzi=5&#MT(QFocSfd0w``%zpwn7KzRqD1^Y~K2@rp%QIl8}EgAuC8{QrB zm#X*9_%3TX2O4{(!_*TXCFRcf_4dpit9(Y+yy*iyYK>^)*SvP$qcqK$NK+X`om?16 zmR6=u)J0oX+&08Uw@%(>Y5q9hkd&+HRqgTm=J7H%oFXxr^qu))p5xK5n@vxqzChXL z%P23x#TJ{boPjfydrSf@u#1!uV_b2U5_^-aLU=u8GTNN&cH6xw^7KBQM~5T5TJMxc zeidMEl+Wf5b&HcCoO$Uv-z?(U4ey9@cN!O>`~qT1V6G`wrR{#pntaLM?s z_(1VoL{8X)4v+26O+N-DHiKA#O)?yUL@R0x7bV{C?;<*wM#{av6tE@mUPNrEQq=P} z+L)RwSKgz`Ebiddg29|$!z99YF$?{ z1!M6wrPi}h{VXq&kGP2}l!E#V=du$yN+#jCyGiY;I_Yeb{D@9t+DFDtOod-WnZw5) zLLV$yap~y{kR_=3NLj>pJt+D>|Hv-hiI-5~b#(8GA>eL_^{4;FY%Fr-*LvZYO_ z8#a7|;2D4|c5GSiaEi$&wgYhYI0=RQ?aYt|Jr`|)*rojtIA6LV54g`ys+ zkU9_r{KrMb;8gyYj{0{*ABIoy}$v(?%C&Zelzt-DOl?GkCs{4_}U z1-_|w3(}AG7+VFu5)H|IP9GO3!7khP)1=adn3NaY;Sucm;gf7wGH`hdwNasl2%R?XytAiRUB=Eubps_!A$YK(v|5BqixmV z{+8j^0p)-`YJpm!#T5z1g9tS|9u9IybO3$I*tZ(IeU@GR{GgTo(Z0x`6BHuUigVWq zm5XKMmwibu?DpOF@;RF$UB>O=YW~Zqgr_0k_1eQU>3)YJM{XO5TEwj8DyV#?(2Gvkzk1VQrjImxu^822{mB}fG(ARn- z`KBHSs_^h~td4xFn17#2y1-aEpBY+5NNZ_BBb{K(Ns;uUFnTzBWs@2l=;{`svDJn~!5mU#`ze)GdaIk~Qd$?#PNQZSmgmy7#T9TMQxt+bB zLvx#_dl6Gc1hORfE&s8I{S2?T?F}iBg-i~POIB%<+wk}YSkErL?AJ$HB^RxVT)Vy$ zACNb`ua3VQ&7>{-Sn;eF8D%_|f8;&e)iW}(T%sPGXQJ%-Bl7_a)^qF1Art%D6!mQ< z?ChsFNqMB9r zSyr|O850y=Myw%Ycf(=2tkNQlnKavRq1s?I!MwQICkvF$_PXa+1UnI)lU#iYlyu<0 zGCO)UGm}sl>>Mz&Lp_OiYQ2gW3Zua4eE@Vho6u4HheG;&;FkTcxw2q7s$zgAry^Cb zZ6BikN=(3}$qAcM^DTpBd*aQa45=-Yt%-h)rT86_bO51c< z*6{bOPP%@|vIDB|e<-YUT%gdN?`Irjx2e(~oWTJjUVu-)_cvoCPvVJ01CotP=d|H(%@<+1=@mj|Us|ptIZ8ADU%) zPf>sru!ej1X`*7|v6<(ZAho8$NTxH5Liy5H));jB#*xF+vcs+L_tLYn9+od2&?xF& zjLYpA!F(j!Z_jioet-Vn)mJX7-Pab_SiEn@HiVfx1eW7SQpwb%cdWZ`+%+sd%}m1> zT9%Xh=dZZT&vBl$)3D5$^mcd@fY~=ph<|zOthaUupZ$m>Ss&7sw7Db6>?Bn|Osm$k ztnqHXg*7@OX-Q%z@(eUa6!{QS{mQRqiODLU5{^}_ENA!HkgBfPcls1sI~tggpEx9* zke{woSE)ZK2YGu_l97F)|CerC7qYq zx$gPw?e=>YnvUW3gaV>GIN+-W*PR<>Mt+>JQIEONhrXz5boQ~}0uiLrBPM7(ZqM6C zel0aoQd%-W^0I}~v#)+MLg$r6bzIJu6DJ_AuTKU)6{2Cd-s1J_5{Z2DJZmdp44h5Q zUA%5MmGUHWT1Z6V*$o-h^9rh|LqmbVQ&$`yAL^P--c2i(QWcKjDC*qj_50cB%qlWl zzSa4-i?4atT1ze#Pam@!8_P9p=d|kj6wU;9n!V^06q)=k+P1(kc;*_a|L5mTB{avG zl9ZCs&+z_A;&K7%wLWSuRl0%NwH$cQ!N$Njd){Y^(m+k<{LMbADcF_*G~jWMj=HWp zYu>G%_IDRFbW?5M<^$N^dSpcLO1*}3-Fj_oRc)nXLw(4B72;y}sHSD}J$NT@l6QFa z*WQquJ&~eEPDb|X%)bn|G1;HK3#T${kAV$PH?0hXTrGb-O7mRS(;mgIY1Td%z^ksa zw@dqhd|}V}pn?rAZ2rivonb1h457p&1UaW_oZnTV1Yb3+zi;w#?16$Y1-*>&J;$AU zifFH?oY1#ip)~ZPZ9fddDZfy4Q)ri|Yq-|U-$@KobD9(&FExC9RqK|4&LAa%S~au_ z7{ByVs0GJ)4CXJE8V6Ude&b@x5ww@wpnm<%bab>m%JBJdSs_p4aa^{dsWee){PkLFoQ`-h^>p!2LdN?p?_K-%T`=Nf4N=yc9fraD4vF z+OuUcDoYl8#(fLfj1f=S@$!_#jG^q|^hA}!Y#-#__E|2I%6qaMDh0dT@f_4ta+HkG zb6yiVrGev^W8Mp zdd~)_yRc}*IyeqX^bU8$D~hUm_?u4ZCv|r>2Ue_9hLz)<4iOeh*OzenZsnHqfIS*2 zd+?5N+fAVt;ah!Vt(f`a3Ho3k;XKss?BhN#wvLdH>-0-)PSq}AD#0~j$Cl$&yM`4_fq^1zW zn&Ji5WxyuUw06g8_-F4tw%p|KtzP`_5Q*_vkB}}eQNG<)E7yj?IiuLi(gJ-g1q;$j zQWZ}oYNK4@!_X&wCs`&dRLMptWR%MABhDzt(YBplXzRkP4ZKfp8G{WXd+V?7QVSFxJqJBIurKEehQ^UQa6pl8OVBCwuB87}LsE6jp_!7$1`IU6ioFO_ zw)yypguFtg4{p!H1O32AT#aH#t&%I-khw=g+Q<%Q97lpy1l(ESoujNPFGaS z#YcDwhto};Fo~`xM+QDXGNueSOakF@<0W|KZ*VWx9?Wi+XZ(0#8`g|f8;?3Dv7Aj)dD~FiRSXxNTiNnZY&4HshwmD@`e`xvYemV& z+z~>_S85ix`szuT$;T4Ezo>J5k!DR8TVb}2P3O?lTJb0Ov!taeDQLwr0!Z1zJhq+B zZLE6SpVHeZpj=8j?KEXJp`DS7m5mLVUo>Ugqb~1#-=XgHt7>L5!m|;f3HI8$GArKB zKF|G^-e(Sxlx}RdM-Pg|+cfJ_Mc;nk{gHub@zwn$j`*wxxeQFt(XwtTD97u!bS>0# zW!-O>r#z6XvVKMDzH~0h={6v`d@e=MaEKL!T{(UyxZt*GQEhz`&pd~vOr+O4@mE>v zXWZn(1WvzUzkQcawc3v}c9-XI$=wr;nWlJVaA!**{lJF5=Bg+ivyms1IWk&6Xsgib z8j;unQX$Hq-&Og}wO~ZuY?{Rx7(Nv_yCqj`3SEqf7I0;5mU>Dm$pxy$N@oS4HPy7u zu3ny<*}F{-ua8Mp49wDWi!euQ(g-HvZl9v=-HnuM2Q1#YB_)B_Wya@J=HBXjj>jdv zjqd~sPZQ*QdW1)vBb3uX4Hy%KBOBPuwR{8{w$a?84_@;Xo|N>GUVTd-SVrO*nYm#w zh(bk&Bc5D(CkVKAjx|&X>p46F%nETd3kLJKQq(F?vD{v#5FLv(?qDge?3$Vln$FMh zCqN&@)^wITHN_;{krvM z=yw$QxX-`OCW+Q^-m@x4B&#c*LF0*ZhU2$kU9Xk#5IUfeD(rneoey0YlhoLSWMl4D zdax-!7%!5LYDSKwVuCu8INB!U+n*eW*r#-EGzLM)qhzUNtD@*o7rb9EvP9};q$ilZ zafCWEbGA*Wv_E0JW1qtCScu>@yRpUot!b)8putP9bd_w**EG1fs?9%C;cz{&&7WWX zwLe-7-E4WP5|KHTqoPhrYYA+v@)JkPB5TCXW2>=Q18j2@q)rr4SN3nA`^kb&zZ6tp zbRH)Vv6QI1t&!#T5mNU{4ST_I6&A#D4i>}m7+}&b{Wp*RzVuK=^1s*m@Q#c5lWhkj z|6|dRg41l-Y1XA9JT!;7!2LRrF*W9^K0#*25(f&&@APVE_|AJqFLC)LAPJ#F@Pkz+ z_((z^Kd?p}EoLIZeOh1uv^GC>E^IMvIw^*d!{GL$kuskoy+AH-$m(z#1L2n7Ih|O+Z-*nw5)& z=v(j#2!q~(HL$c8Ld52#atJi51r4#b;1v@FrGhoOX)*H=?j+?#Xx1DW;%UJvD-6m6 zYxL4$79uuD%H)1oly#7B3*J9m{~CZlRroylrY%S+^MK%6;BM7$J?_X?ZfBYJy|?XI z1AE+2u$;{g`u{qB|2pGeBLsd3+WegW{@&&VPsjf+^&nAnOM}JlBVC1n~DZuLJxQ@2`*d z!AsT)3To4Tox2Q7g=>8ZOI+SBe>1$N><=ds*Wod#4 zTv_30-pia`13XT$nNizgZlrCg&E()qPY4*mIkP#tzv~UGl}B_hmzX$P5GVueYXVy0 z*7u_golknA@5e`fnMhVY0eoE25dM2P=3R2cD4hLWQ-jrm{b7E1Fws6EFWFN4#ON%_ zYgdotA-}%n-n_Q@3_&^wdg-&uy^r22J6tSzT#r>tAx#eLao7cN_qG)xR!2?72|%>4 ztt7nOvUs?r0?b z_(!El_2{i9;-#*x9<52^2ID1g<<+#w?gdu`MZx@6NLz&yE3tbP9K99`N%)JX?t|gp zFapXlb!WiV#Qc!)S5(Y*v!e@K%S1-@d6oS1#+3XFi0mKF46sQqiQlgaSGZ*uPG1>} z|8c!~4fcdv@ORN$ZdURpSP?fL$1j6FUu%H=6jhUvjZwX$z6_(~p`-a__d6DcaqwI^ zlRyWP)@ed|5bNJ v7R6|N8n+If{Z~Ut*cQ)?-(&MZOT!+XB-j`zOry+7Xj&Ykm{nX2xtuIif8Jzdqi>Wi?eFcktqFo>9#7^EAl zQhJpQkKp26E`kTe|3$?46YvtpS2V>F3g)QCeI}p=*wmD36UxGbf|FXua8rKw8q`m* z8T@WnHcGijtnv2V8W9Ub_^9g9HDQ+LPq_)RAMiem7iMT;=flDe`}h}PXH&_{Nwf){ zTo#BcJN$mnG9Js2@Tjgyb)U^>Ua=rYF|F(xZCm!`h2a}j8{=QEKNHxSkjj`ZBfOp= ze4|Ou9Kb)=W`8o(u0ZTirNVTZCqod0|M-W42a{*oH0#(%x_Sc79#!9y8;=I)t;|8d*V2D1_0JIB(TN}07@D%%Y?^Qu*^z~> zd1HaxxTKVj<8~Wpp)P&C-G|H%-6Ugjs;>H|M{`4h#<18jh7xfT5cg@y`KJgob5?~B?obB>JEu* zO@b;^fdIxdjjiHHiP4iYOz&gS*XlA}o?izFwc6Z`^D3%~w2dX4Lwi1OmIwEIb1&pX z$1Q)|v+xxV$Eb*}OoJp0Dob}lJiNnBy;c3=q{ z)L2^e5&5;9t+yiKzV*j+n8(5avFFE^7Pb9ejw%7=vb3l~>K~-(pg?qz6Eb_uK|PPH zr{aC?bCvX^yaU`r#-{BjphN7L!~HpOo4V^F5d3nR2~%~6WbCFuno%y})K`uvYML0j zpv!KfFUC`|6Os!`YgDfScrXBjKg+QE(mlX>CFrm^tbbFVE(jS#v0~#+o2?N)m#I3hiH9v0vSPQ#6To z+ggBRl=#!-gKF*o(X_kd79_)fq_PCx>?d4G9Z zXwvzEaLZF>o{r3bBJ;Eq@<=O*{qk-3<$QHdGl{^Zr?;A7#0LZ1Gxszkf=f!@s%cpV z9+$X^JZ#NBVshf_#8?IrpgNgmd#>>gortH^b*TPp!L)dSJvea#zY+^`d|%dY2o^kTP%>sbXcn|Y2P)#rUVmrHGQDrO|Y zVvk^@;<&s(l6_V`kPs}{woE+*rx9_F|n7N@BvraVmiDRZH=XKS=cT}|r_Lp;|rO)h|U@bJY>s42oPw!^Q58@Eu3PKoQvcUaTR<;ZbW@Wbhcb z{UlF(F$BLcThpA}MRe1v{7R5|Ja1;OcqkTf&L37y4ZieYAP>lAUsvFz&U+pD$n z+3YzE=I87g{TlYSZY8I;N`!9m4N{Ul%algFvs7DqNJSBN`MfN`(i=U zD6+AiP8{bfgk)mwpH*8@hL-Xw(`vN3#N)@b zzG(wP0hVCPcgDqp-vx9-SsHe3pX2{pRh^T~6Id>RK&;pPvZ%%($T)x?0dOErH@ZQA zJ5>yy_i;IA676*6c2R}(CuJ3MV(F&Sl)k}s@ia4siOXzX zV!XNdi|twScL(D2vzCZYx|H_zKb-nF?&y=2=dvBSbDC2z-(*eN~p1 z=XKUE5MPs@M zAcxn7o_+tgCj<94=zAQ~`c)({E;^dMS|e9UqW)~{T{_WT+EoWAPUngn#+%XA+Y#d* zK;Vv2W!?HhRs8aWOyc>O#y06MpTCYCe5Z{E0!85e#pimU->!=f5~o1U45-P9-sieH zDYE_w$tZy|Okn4;=t_zOCjgBgI+t{fil?ikr?LB4I$j{=th;v#!YM6HtEh+84E84K z7gXYdJ0nfI=xheB;~SufcCbYR%%Wxwj>4Lle%HnngBhPouT$Jx(4p zzh!rn*ZjaJX@LO+ZYI7~4bZwWK zSinIt7Aqy0%VsSImlxZ?pibZ}0W*M(M zf43R;nF{VFnEFHmnldZ3HO6|lc>DCyFaO*Go06Sec;fwqk6>iKZ<6C2C`SI+BtN*8 z`M-bs>vsUUM>Nl|C#Te4oZS1fjrc6>m%NsR?>+6(910G|BVz7awthx+zCB&7B)ay} zDdvy`Td(vXcNy)TBQBgKHT$!6hIuh4N~g9UwXF^Ia1Q&yU`n*ycl*rjaFu^-<`vd- zL=95DT&J%MLG_Q=9ZoOE_Q~E|4;=~6@eC4aGV=rk>o#i^Dy^*xF()g}YUu8E+r_+{ zy1R}-syJhC)`gaaYSyb1(mnmdK5KnUIs0Z+H?Wuk`*xd+jTOvFL3MzC9?i5>bj3T3 zT9MO{fO;e8HwPzch3%G9y8H?p&ZH2ut(>`Y5+e|B%3jMp@(?2TUcLq{VVY;XJr(VD;s8q_5OaWbK6y$$c^2Mnq%ao zpS0ezSVl1#%)EpiLBC#YPpwF=FNWyY0qc_;j&qHv8gW)<=3P(&qZ6Vt!|W{SyU6{M z6OpfDBgU2VWvLB*hP{QD;&r)!gWoyX#sf9U^lP z*v$SPQdcKG7m(*04%UJUs#>cZcEYLj_2+@RgUu_zs(tL+ zbv}LBt+Ul!7591*k!3q);h3^5L+RBGblq@gN~xAtQnrU$;HTrr5{ew>gCvhT=>`=O z&Z4$~#sQxe^Ur-HP8@B2_D-Y$jISD{G`Nk6&L=p%WsHlDyDJ&@4qQj>QefbYd;8ZMZ*&{L*DKZQA!i6^mBG-^YWbI5M4NGA5E)qRV?*#t@@L$UOu^%RuavL z)<77LSQ^H&gDCUQTL+e9aRZ^!CizlwM^aA1F@(!hoQg{%UGyBOm%XLTb!$39zwVSS zV{yLYD5kMgrf0Isu#dD9wbMTwC(~=&miGOl6?Y$nd3lEo4_`-Z%1>f`;~=lz(PbD$3%F2;H|grtZ^lx0GlheHH;KKyAz^xSl;GE-PO7v z76#+sYgCB`Tw8-E6as##Nl^WFs9m1I1PjEdE^ousGxe1|5d@$qs+ zyJU&!5A4L@m^=@+V3ZZf|5)(5|;MH zIRJU}`iN*V`cixGvqQfD%hZVICFGQ0Uh>i~4ZA=_-}q49=cb)StJ zuXQDFbIs&Tcb4|PdhaWCtIaFZCPne+!&4x>*6k4@!ceg~d!X%37MUgb3IpW!)_9J* zdS&6gpBnkOejKuqelsH$8)1E~wr`qM9xT>OHp#XXxsPhX*!OWz=Oqs4gy;{^D$ zOGN%H$h1p-1Mlz5FPit?AkLYVF77$)<+w0f17H+D^%I8hBt{})+0FB;zS8m-2!#|} zuw@sl(*yn&bi_&Y!}GyaGDa>W7tJebWNBw1Exl7x`UK zgg0Wi7Pe;0h2sPJLzEjxp)W<{+15{0N26D zOt^^n!LK07Eu>IvaM_mpGarOV9gM+*i-I5g8lv1v3Y`rW+LlN7AhhdXye3=_esCm2 zxt$a`7hJY2kM%)#)WM`oxS09D(GcZMQs{iJ(2o2qI3m6drfR~)$q$Z)D0h)U7lO-n zx$w-{$f_4lvu}Kvai$BU99=ztq9W_BchS zR*1>tzZ1aUqU>T+{{q0js`#7Afq!Aw-=tpa!vFtd!8cgQ-v;1s)AWBU0{;84YKDCK z&jI{5!v7DlPXC4N|AG4QW=0ykxF$y0@-e0$0>Gr5^Uz(ePR!F8+DuJ*V>$|aFS zV@6k24TJN%B2}_6WHa~Jz;k`^{b)(8RdrwjjE|Q?=}_`Fv+&FQQggMHJ>Ic_mg;xkFll+IeJAUP`VErfwqB#*3$uY;|&2_9==8^sLZ7vf?W67dgk?o_s0~9V|!WrkOyS^i2pX=QZ zIj(ru&)JJoIQH+_%hYFJ+qctyO6Dp1#WvSLrgBQOyQWcMC}W!A_Sh;RnbrQZao1sH zZ82uVJwjvacohV)a5jgzIlH*?n>)K%^7%MA z{8h#80q;fF|J#Kj|4savi_1StGpXodM?ue%E`+u(ZmQpeK_Kw8xMq%9%)d;Wm8_(& oxCp3+FOoffZnPGM$H{W@SDW9RNc*RbVkqMjIC(A)u>UIkAHYpCk^lez diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index 3703ea6..2b86b54 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -19,7 +19,7 @@ celery_app = Celery( celery_app.conf.beat_schedule = { 'run-task-every-15-seconds': { 'task': 'bg_tasks.bg_task.periodic_task', - 'schedule': 15.0, + 'schedule': 30.0, }, } diff --git a/bg_tasks/parser.py b/bg_tasks/parser.py new file mode 100644 index 0000000..29d616f --- /dev/null +++ b/bg_tasks/parser.py @@ -0,0 +1,94 @@ +import os + +import gspread +import openpyxl + +file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') + + +async def gsheets_to_rows() -> list[list[str | int | float]]: + """Получение всех строк из Google Sheets""" + + def to_int(val: str) -> int | str: + try: + res = int(val) + except ValueError: + return val + return res + + def to_float(val: str) -> float | str: + val = val.replace(',', '.') + try: + res = float(val) + except ValueError: + return val + return res + + gc = gspread.service_account(filename='creds.json') + sh = gc.open('Menu') + data = sh.sheet1.get_all_values() + for row in data: + row[:3] = list(map(to_int, row[:3])) + row[-2:] = list(map(to_float, row[-2:])) + + return data + + +async def local_xlsx_to_rows() -> list[list[str | int | float]]: + """Получение всех строк из локального файла Menu""" + data = [] + wb = openpyxl.load_workbook(file).worksheets[0] + for row in wb.iter_rows(values_only=True): + data.append(list(row)) + return data + + +async def rows_to_dict(rows: list[list]) -> tuple: + """Парсит строки полученные и источников в словарь""" + + menus = {} + submenus = {} + dishes = {} + + menu_num = None + submenu_num = None + + for row in rows: + if all(row[:3]): + menu = { + row[0]: { + 'data': {'title': row[1], 'description': row[2]}, + 'id': None, + } + } + menu_num = row[0] + menus.update(menu) + + elif all(row[1:4]): + submenu = { + (menu_num, row[1]): { + 'data': {'title': row[2], 'description': row[3]}, + 'parent_num': menu_num, + 'id': None, + 'parent_menu': None, + } + } + submenu_num = row[1] + submenus.update(submenu) + + elif all(row[3:6]): + dish = { + (menu_num, submenu_num, row[2]): { + 'data': { + 'title': row[3], + 'description': row[4], + 'price': row[5], + }, + 'parent_num': (menu_num, submenu_num), + 'id': None, + 'parent_submenu': None, + 'discont': row[6], + }, + } + dishes.update(dish) + return menus, submenus, dishes diff --git a/bg_tasks/updater.py b/bg_tasks/updater.py index 68c2f76..01af762 100644 --- a/bg_tasks/updater.py +++ b/bg_tasks/updater.py @@ -1,16 +1,14 @@ import os import pickle -from uuid import UUID -import openpyxl import redis.asyncio as redis # type: ignore -from sqlalchemy import delete +from sqlalchemy import delete, update from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from fastfood.config import settings from fastfood.models import Dish, Menu, SubMenu -file = os.path.join(os.path.curdir, 'admin', 'Menu.xlsx') +from .parser import file, gsheets_to_rows, local_xlsx_to_rows, rows_to_dict redis = redis.Redis.from_url(url=settings.REDIS_URL) @@ -22,16 +20,10 @@ async_session_maker = async_sessionmaker( ) -async def refresh_cache(disconts: dict) -> None: - """Очищает кэш при обновлении БД и ставит отметку времени обновления - и сохраняет данные скидок на товар - """ - await redis.flushall() - - for key in disconts.keys(): - await redis.set(f'DISCONT:{str(key)}', pickle.dumps(disconts[key])) - - await redis.set('XLSX_MOD_TIME', pickle.dumps(os.path.getmtime(file))) +async def clear_cache(pattern: str) -> None: + keys = [key async for key in redis.scan_iter(pattern)] + if keys: + await redis.delete(*keys) async def is_changed_xls() -> bool: @@ -51,96 +43,253 @@ async def is_changed_xls() -> bool: return True -async def xlsx_to_dict() -> dict: - """Парсит Menu.xlsx в словарь""" - wb = openpyxl.load_workbook(file).worksheets[0] +async def on_menu_change( + new_menu: dict, old_menu: dict, session: AsyncSession +) -> dict | None: + if new_menu and not old_menu: + # Создаем меню + menu = Menu( + title=new_menu['data']['title'], + description=new_menu['data']['description'], + ) + session.add(menu) + await session.flush() + new_menu['id'] = str(menu.id) + elif new_menu and old_menu: + # Обновляем меню + await session.execute( + update(Menu).where(Menu.id == old_menu['id']).values(**(new_menu['data'])) + ) + new_menu['id'] = old_menu['id'] - data = {} + else: + # Удаляем меню + await session.execute(delete(Menu).where(Menu.id == old_menu['id'])) - menu = None - submenu = None - dish = None + await session.commit() + # Чистим кэш + await clear_cache('MENUS*') + await clear_cache('summary') - for row in wb.iter_rows(values_only=True): - if row[0] is not None: - menu = row[0] - data[menu] = { - 'id': None, - 'title': row[1], - 'description': row[2], - 'submenus': dict(), - } - elif row[1] is not None: - submenu = row[1] - data[menu]['submenus'][submenu] = { - 'id': None, - 'title': row[2], - 'description': row[3], - 'dishes': dict(), - } - elif row[2] is not None: - dish = row[2] - data[menu]['submenus'][submenu]['dishes'][dish] = { - 'id': None, - 'title': row[3], - 'description': row[4], - 'price': row[5], - 'discont': row[6], - } - return data + return new_menu -async def refresh_all_data(data: dict) -> dict[UUID, int | float]: - """Удаляет старые данные и сохраняет новые. - Создает и возвращает список со скидками с привязкой по UUID товара +async def menus_updater(menus: dict, session: AsyncSession) -> None: + """Проверяет пункты меню на изменения + При необходимости запускае обновление БД + через фенкцию on_menu_change """ + cached_menus = await redis.get('ALL_MENUS') - disconts = {} + if cached_menus is not None: + cached_menus = pickle.loads(cached_menus) + else: + cached_menus = {} - async with async_session_maker() as session: - await session.execute(delete(Menu)) - await session.commit() + for key in menus.keys(): + if key not in cached_menus.keys(): + # Создание меню + menu = await on_menu_change(menus[key], {}, session) + menus[key] = menu + elif key in cached_menus.keys(): + # Обновление меню + if menus[key].get('data') != cached_menus[key].get('data'): + menu = await on_menu_change(menus[key], cached_menus[key], session) + menus[key] = menu + else: + menus[key]['id'] = cached_menus[key]['id'] - for menu_key in data.keys(): - menu = Menu( - title=data[menu_key].get('title'), - description=data[menu_key].get('description'), - ) - session.add(menu) - await session.flush() + for key in {k: cached_menus[k] for k in set(cached_menus) - set(menus)}: + # Проверяем на удаленные меню + await on_menu_change({}, cached_menus.pop(key), session) - submenus = data[menu_key]['submenus'] - for sub_key in submenus.keys(): - submenu = SubMenu( - title=submenus[sub_key]['title'], - description=submenus[sub_key]['description'], - parent_menu=menu.id, + await redis.set('ALL_MENUS', pickle.dumps(menus)) + + +async def on_submenu_change( + new_sub: dict, old_sub: dict, session: AsyncSession +) -> dict: + if new_sub and not old_sub: + # Создаем меню + submenu = SubMenu( + title=new_sub['data']['title'], + description=new_sub['data']['description'], + ) + submenu.parent_menu = new_sub['parent_menu'] + + session.add(submenu) + await session.flush() + new_sub['id'] = str(submenu.id) + new_sub['parent_menu'] = str(submenu.parent_menu) + elif new_sub and old_sub: + # Обновляем меню + await session.execute( + update(SubMenu) + .where(SubMenu.id == old_sub['id']) + .values(**(new_sub['data'])) + ) + new_sub['id'] = old_sub['id'] + new_sub['parent_menu'] = old_sub['parent_menu'] + + else: + # Удаляем меню + await session.execute(delete(SubMenu).where(SubMenu.id == old_sub['id'])) + + await clear_cache('MENUS*') + await clear_cache('summary') + + await session.commit() + return new_sub + + +async def submenus_updater(submenus: dict, session: AsyncSession) -> None: + """Проверяет пункты подменю на изменения + При необходимости запускае обновление БД + """ + # Получаем Меню из кэша для получения их ID по померу в таблице + cached_menus = await redis.get('ALL_MENUS') + if cached_menus is not None: + cached_menus = pickle.loads(cached_menus) + else: + cached_menus = {} + + # Получаем подмен из кэша + cached_sub = await redis.get('ALL_SUBMENUS') + + if cached_sub is not None: + cached_sub = pickle.loads(cached_sub) + else: + cached_sub = {} + + for key in submenus.keys(): + parent = cached_menus[submenus[key]['parent_num']]['id'] + submenus[key]['parent_menu'] = parent + + if key not in cached_sub.keys(): + # Получаем и ставим UUID parent_menu + submenus[key]['parent_menu'] = parent + + submenu = await on_submenu_change(submenus[key], {}, session) + submenus[key] = submenu + elif key in cached_sub.keys(): + # Обновление меню + if submenus[key].get('data') != cached_sub[key].get('data'): + submenu = await on_submenu_change( + submenus[key], cached_sub[key], session ) - session.add(submenu) - await session.flush() + submenus[key] = submenu + else: + submenus[key]['id'] = cached_sub[key]['id'] + submenus[key]['parent_menu'] = cached_sub[key]['parent_menu'] - dishes = data[menu_key]['submenus'][sub_key]['dishes'] - print(dishes) - for dish_key in dishes.keys(): - dish = Dish( - title=dishes[dish_key]['title'], - description=dishes[dish_key]['description'], - price=dishes[dish_key]['price'], - parent_submenu=submenu.id, - ) - session.add(dish) - await session.flush() - if dishes[dish_key]['discont'] is not None: - disconts[dish.id] = dishes[dish_key]['discont'] + for key in {k: cached_sub[k] for k in set(cached_sub) - set(submenus)}: + # Проверяем на удаленные меню + await on_submenu_change({}, cached_sub.pop(key), session) - await session.commit() - return disconts + await redis.set('ALL_SUBMENUS', pickle.dumps(submenus)) + + +async def on_dish_change(new_dish: dict, old_dish, session: AsyncSession) -> dict: + if new_dish and not old_dish: + dish = Dish( + title=new_dish['data']['title'], + description=new_dish['data']['description'], + price=new_dish['data']['price'], + ) + dish.parent_submenu = new_dish['parent_submenu'] + + session.add(dish) + await session.flush() + new_dish['id'] = str(dish.id) + new_dish['parent_submenu'] = str(dish.parent_submenu) + new_dish['data']['price'] = str(dish.price) + elif new_dish and old_dish: + # Обновляем меню + await session.execute( + update(Dish).where(Dish.id == old_dish['id']).values(**(new_dish['data'])) + ) + new_dish['id'] = old_dish['id'] + new_dish['parent_submenu'] = old_dish['parent_submenu'] + new_dish['data']['price'] = old_dish['data']['price'] + + else: + # Удаляем меню + await session.execute(delete(Dish).where(Dish.id == old_dish['id'])) + + await clear_cache('MENUS*') + await clear_cache('summary') + + await session.commit() + return new_dish + + +async def dishes_updater(dishes: dict, session: AsyncSession) -> None: + """Проверяет блюда на изменения + При необходимости запускае обновление БД + """ + cached_submenus = await redis.get('ALL_SUBMENUS') + if cached_submenus is not None: + cached_submenus = pickle.loads(cached_submenus) + else: + cached_submenus = {} + + # Получаем подмен из кэша + cached_dishes = await redis.get('ALL_DISHES') + + if cached_dishes is not None: + cached_dishes = pickle.loads(cached_dishes) + else: + cached_dishes = {} + + await clear_cache('DISCONT*') + + for key in {k: cached_dishes[k] for k in set(cached_dishes) - set(dishes)}: + # Проверяем на удаленные меню + await on_submenu_change({}, cached_dishes.pop(key), session) + + for key in dishes.keys(): + parent = cached_submenus[dishes[key]['parent_num']]['id'] + dishes[key]['parent_submenu'] = parent + + if key not in cached_dishes.keys(): + # Получаем и ставим UUID parent_menu + dishes[key]['parent_submenu'] = parent + + dish = await on_dish_change(dishes[key], {}, session) + dishes[key] = dish + elif key in cached_dishes.keys(): + # Обновление меню + if dishes[key].get('data') != cached_dishes[key].get('data'): + dish = await on_dish_change(dishes[key], cached_dishes[key], session) + dishes[key] = dish + else: + dishes[key]['id'] = cached_dishes[key]['id'] + dishes[key]['parent_submenu'] = cached_dishes[key]['parent_submenu'] + + if dishes[key]['discont'] is not None: + await redis.set( + f"DISCONT:{dishes[key]['id']}", pickle.dumps(dishes[key]['discont']) + ) + + await redis.set('ALL_DISHES', pickle.dumps(dishes)) + + +async def updater(rows): + menus, submenus, dishes = await rows_to_dict(rows) + async with async_session_maker() as session: + await menus_updater(menus, session) + await submenus_updater(submenus, session) + await dishes_updater(dishes, session) async def main() -> None: """Главная функция фоновой задачи""" changed = await is_changed_xls() if changed: - menu_data = await xlsx_to_dict() - discont_data = await refresh_all_data(menu_data) - await refresh_cache(discont_data) + rows = await local_xlsx_to_rows() + await updater(rows) + + +async def main_gsheets() -> None: + rows = await gsheets_to_rows() + await updater(rows) diff --git a/fastfood/repository/redis.py b/fastfood/repository/redis.py index 80f0e35..81ff28d 100644 --- a/fastfood/repository/redis.py +++ b/fastfood/repository/redis.py @@ -12,15 +12,15 @@ def get_key(level: str, **kwargs) -> str: case 'menus': return 'MENUS' case 'menu': - return f"{kwargs.get('menu_id')}" + return f"MENUS:{kwargs.get('menu_id')}" case 'submenus': - return f"{kwargs.get('menu_id')}:SUBMENUS" + return f"MENUS:{kwargs.get('menu_id')}:SUBMENUS" case 'submenu': - return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" + return f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}" case 'dishes': - return f"{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:DISHES" + return f"MENUS:{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 f"MENUS:{kwargs.get('menu_id')}:{kwargs.get('submenu_id')}:{kwargs.get('dish_id')}" return 'summary' From 6a0776557d6d65a24d80594041aa92b0c7e63d6b Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 22:49:16 +0300 Subject: [PATCH 15/16] =?UTF-8?q?google=20=D1=81=D0=B8=D0=BD=D1=85=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/.~lock.Menu.xlsx# | 1 - bg_tasks/bg_task.py | 22 +++++++- compose_google.yml | 112 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) delete mode 100644 admin/.~lock.Menu.xlsx# create mode 100644 compose_google.yml diff --git a/admin/.~lock.Menu.xlsx# b/admin/.~lock.Menu.xlsx# deleted file mode 100644 index 4cab078..0000000 --- a/admin/.~lock.Menu.xlsx# +++ /dev/null @@ -1 +0,0 @@ -,pi3c,pi3code,12.02.2024 22:20,file:///home/pi3c/.config/libreoffice/4; diff --git a/bg_tasks/bg_task.py b/bg_tasks/bg_task.py index 2b86b54..69ae5e0 100644 --- a/bg_tasks/bg_task.py +++ b/bg_tasks/bg_task.py @@ -4,7 +4,7 @@ from celery import Celery from fastfood.config import settings -from .updater import main +from .updater import main, main_gsheets loop = asyncio.get_event_loop() @@ -23,6 +23,26 @@ celery_app.conf.beat_schedule = { }, } +celery_app_google = Celery( + 'tasks', + broker=settings.REBBITMQ_URL, + backend='rpc://', + include=['bg_tasks.bg_task'], +) + +celery_app_google.conf.beat_schedule = { + 'run-task-every-15-seconds': { + 'task': 'bg_tasks.bg_task.periodic_task_google', + 'schedule': 30.0, + }, +} + + +@celery_app_google.task +def periodic_task_google() -> None: + result = loop.run_until_complete(main_gsheets()) + return result + @celery_app.task def periodic_task() -> None: diff --git a/compose_google.yml b/compose_google.yml new file mode 100644 index 0000000..ab8b3a9 --- /dev/null +++ b/compose_google.yml @@ -0,0 +1,112 @@ +version: "3.8" +services: + redis: + container_name: redis_test + + image: redis:7.2.4-alpine3.19 + + ports: + - '6380:6379' + + healthcheck: + test: [ "CMD", "redis-cli","ping" ] + interval: 10s + timeout: 5s + retries: 5 + + db: + container_name: pgdb + + image: postgres:15.1-alpine + + env_file: + - .env + + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + ports: + - 6432:5432 + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + + app: + container_name: fastfood_app + + build: + context: . + + env_file: + - .env + + ports: + - 8000:8000 + + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + restart: always + + volumes: + - .:/usr/src/fastfood + + command: /bin/bash -c 'poetry run python /usr/src/fastfood/manage.py --run-docker-server' + + celery_worker: + container_name: celeryworker + + build: + context: . + + env_file: + - .env + + depends_on: + - rabbitmq + - db + - app + - redis + + volumes: + - .:/usr/src/fastfood + + command: ["celery", "-A", "bg_tasks.bg_task:celery_app_google", "worker", "--loglevel=info", "--concurrency", "1", "-P", "solo"] + + celery_beat: + container_name: celerybeat + + build: + context: . + + env_file: + - .env + + depends_on: + - rabbitmq + - db + - app + - redis + + volumes: + - .:/usr/src/fastfood + + command: ["celery", "-A", "bg_tasks.bg_task:celery_app_google", "beat", "--loglevel=info"] + + + rabbitmq: + container_name: rabbit + + image: "rabbitmq:management" + + ports: + - 5672:5672 From 22a876d3ceba0e7a90a3fa44f99eaa2cf68f68b0 Mon Sep 17 00:00:00 2001 From: pi3c Date: Mon, 12 Feb 2024 23:03:28 +0300 Subject: [PATCH 16/16] =?UTF-8?q?google=20sheets=20docker=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastfood/service/summary.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastfood/service/summary.py b/fastfood/service/summary.py index 735c53f..b5cddef 100644 --- a/fastfood/service/summary.py +++ b/fastfood/service/summary.py @@ -42,7 +42,10 @@ class SummaryService: discont = await self.cache.get(f"DISCONT:{str(obj.get('id'))}") if discont is not None: - discont = float(discont) + try: + discont = float(discont) + except Exception: + discont = 0.0 obj['price'] = round( obj['price'] - (obj['price'] * discont / 100), 2 )