Compare commits

...

6 Commits

21 changed files with 4683 additions and 81 deletions

View File

@ -0,0 +1,70 @@
# fastfood
Fastapi веб приложение реализующее api для общепита.
## Описание
Данный проект, это результат выполнения практического домашнего задания интенсива от YLAB Development. Проект реализован на фреймворке fastapi, с использованием sqlalchemy. В качестве базы данных используется postgresql.
### Техническое задание
## Возможности
В проекте реализованы 3 сущности: Menu, SubMenu и Dish. Для каждого них реализованы 4 метода http запросов: GET, POST, PATCH и DELETE c помощью которых можно управлять данными.
Для Menu доступен метод GET возвращающий все его SubMenu. Аналогично для SubMenu реализован метод для возврата всех Dish.
## Зависимости
- postgresql Для работы сервиса необходима установленная СУБД. Должна быть создана база данных и пользователь с правами на нее.
- poetry - Система управления зависимостями в Python.
Остальное добавится автоматически на этапе установки.
## Установка
Установите и настройте postgresql согласно офф. документации. Создайте пользователя и бд.
Установите систему управления зависимостями
> `$ pip install poetry`
Клонируйте репозиторий
> `$ git clone https://git.pi3c.ru/pi3c/fastfood.git`
Перейдите в каталог
> `$ cd fastfood`
> `$ poetry install --no-root`
Создастся виртуальное окружение и установятся зависимости
## Запуск
Запуск проекта возможен в 2х режимах:
- Запуск в режиме "prod" с ключем --run-server
Подразумевает наличие уже созданных таблиц в базе данных(например с помощью Alembic). Манипуляций со структурой БД не происходит. Данные не удаляются.
- Запуск в режиме "dev" c ключем --run-test-server
В этом случае при каждом запуске проекта все таблицы с данными удаляются из БД и создаются снова согласно описанных моделей.
Для запуска проекта сначала активируем виртуальное окружение
> `$ poetry shell`
и запускаем проект в соответстующем режиме
>`$ python manage.py --ключ`
вместо этого, так же допускается и другой вариант запуска одной командой без предварительной активации окружения
>`$ poetry run python manage.py --ключ`
## TODO
- Добавить миграции
- Провести рефакторинг, много дублирующего кода
- Много чего другого :)
## Авторы
- Сергей Ванюшкин <pi3c@yandex.ru>
## Лицензия
Распространяется под [MIT лицензией](https://www.opensource.org/licenses/mit-license.php).
Подробнее на русском в файле LICENSE.md

5
example.env Normal file
View File

@ -0,0 +1,5 @@
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=postgres
DB_NAME=postgres

View File

@ -1,20 +1,87 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastfood.routes import router from fastfood.routers.dish import router as dish_router
from fastfood.routers.menu import router as menu_router
from fastfood.routers.submenu import router as submenu_router
description = """
# 🔥🔥🔥Fastfood-API поможет тебе подкрепиться 🔥🔥🔥
### У нас есть Menu. Ты можеш выбрать блюда из кухни, которая тебе нравится
## Menu
Ты можешь **add menu**.
Ты можешь **read menu**.
Ты можешь **patch menu**.
Ты можешь **delete menu**.
### У нас есть в SubMenu, где ты сможешь найти
десерты/напитки/супчики/прочие вкусности
# SubMenu
Ты можешь **add submenu into menu**.
Ты можешь **read submenu**.
Ты можешь **patch submenu**.
Ты можешь **delete menu**.
### У нас есть в Dish, где ты сможешь найти блюдо по вкусу
# Dish
Ты можешь **add dish into submenu**.
Ты можешь **read dish**.
Ты можешь **patch dish**.
Ты можешь **delete dish**.
## Приятного аппетита
"""
async def generate_test_data(): tags_metadata = [
""" {
Создание БД и наполнение ее данными "name": "menu",
""" "description": "Операции с меню.",
pass },
{
"name": "submenu",
"description": "Подменю и работа с ним",
},
{"name": "dish", "description": "Блюда и работа с ними"},
]
def create_app(): def create_app():
""" """
Создание экземпляра приложения FastAPI и врзврат его Фабрика FastAPI.
""" """
app = FastAPI() app = FastAPI(
app.include_router(router) title="Fastfood-API",
description=description,
version="0.0.1",
contact={
"name": "Sergey Vanyushkin",
"url": "http://pi3c.ru",
"email": "pi3c@yandex.ru",
},
license_info={
"name": "MIT license",
"url": "https://mit-license.org/",
},
openapi_tags=tags_metadata,
)
app.include_router(menu_router)
app.include_router(submenu_router)
app.include_router(dish_router)
return app return app

View File

@ -6,7 +6,7 @@ class Settings(BaseSettings):
DB_PORT: int = 5432 DB_PORT: int = 5432
DB_USER: str = "postrges" DB_USER: str = "postrges"
DB_PASS: str = "postgres" DB_PASS: str = "postgres"
DB_NAME: str = "demo_db" DB_NAME: str = "postgres"
@property @property
def DATABASE_URL_asyncpg(self): def DATABASE_URL_asyncpg(self):

View File

@ -0,0 +1,19 @@
from fastfood import models
from fastfood.dbase import async_engine
from .dish import DishCrud
from .menu import MenuCrud
from .submenu import SubMenuCrud
async def create_db_and_tables():
async with async_engine.begin() as conn:
await conn.run_sync(models.Base.metadata.drop_all)
await conn.run_sync(models.Base.metadata.create_all)
class Crud(MenuCrud, SubMenuCrud, DishCrud):
pass
crud = Crud()

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

@ -0,0 +1,64 @@
from uuid import UUID
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import models, schemas
class DishCrud:
@staticmethod
async def get_dishes(submenu_id: UUID, session: AsyncSession):
async with session:
query = select(models.Dish).where(models.Dish.parent_submenu == submenu_id)
dishes = await session.execute(query)
return dishes.scalars().all()
@staticmethod
async def create_dish_item(
submenu_id: UUID,
dish: schemas.DishBase,
session: AsyncSession,
):
async with session:
new_dish = models.Dish(**dish.model_dump())
new_dish.parent_submenu = submenu_id
session.add(new_dish)
await session.flush()
await session.commit()
return new_dish
@staticmethod
async def get_dish_item(
dish_id: UUID,
session: AsyncSession,
):
async with session:
query = select(models.Dish).where(models.Dish.id == dish_id)
submenu = await session.execute(query)
return submenu.scalars().one_or_none()
@staticmethod
async def update_dish_item(
dish_id: UUID,
dish: schemas.DishBase,
session: AsyncSession,
):
async with session:
query = (
update(models.Dish)
.where(models.Dish.id == dish_id)
.values(**dish.model_dump())
)
await session.execute(query)
await session.commit()
qr = select(models.Dish).where(models.Dish.id == dish_id)
updated_submenu = await session.execute(qr)
return updated_submenu.scalars().one()
@staticmethod
async def delete_dish_item(dish_id: UUID, session: AsyncSession):
async with session:
query = delete(models.Dish).where(models.Dish.id == dish_id)
await session.execute(query)
await session.commit()

View File

@ -1,18 +1,12 @@
from uuid import UUID from uuid import UUID
from sqlalchemy import delete, select, update
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import models, schemas from fastfood import models, schemas
from fastfood.dbase import async_engine
async def create_db_and_tables(): class MenuCrud:
async with async_engine.begin() as conn:
await conn.run_sync(models.Base.metadata.drop_all)
await conn.run_sync(models.Base.metadata.create_all)
class Crud:
@staticmethod @staticmethod
async def get_menus(session: AsyncSession): async def get_menus(session: AsyncSession):
async with session: async with session:
@ -34,15 +28,37 @@ class Crud:
async with session: async with session:
query = select(models.Menu).where(models.Menu.id == menu_id) query = select(models.Menu).where(models.Menu.id == menu_id)
menu = await session.execute(query) menu = await session.execute(query)
return menu.scalars().one_or_none() menu = menu.scalars().one_or_none()
if menu is None:
return None
submenu_query = select(
func.count(models.SubMenu.id).label("counter")
).filter(models.SubMenu.parent_menu == menu_id)
counter = await session.execute(submenu_query)
dish_query = (
select(func.count(models.Dish.id))
.join(models.SubMenu)
.filter(models.Dish.parent_submenu == models.SubMenu.id)
.filter(models.SubMenu.parent_menu == menu_id)
)
dishes = await session.execute(dish_query)
menu.submenus_count = counter.scalars().one_or_none()
menu.dishes_count = dishes.scalars().one_or_none()
return menu
@staticmethod @staticmethod
async def update_menu_item(menu_id: UUID, async def update_menu_item(
menu: schemas.MenuBase, menu_id: UUID,
session: AsyncSession, menu: schemas.MenuBase,
): session: AsyncSession,
):
async with session: async with session:
query = update(models.Menu).where(models.Menu.id == menu_id).values(**menu.model_dump()) query = (
update(models.Menu)
.where(models.Menu.id == menu_id)
.values(**menu.model_dump())
)
await session.execute(query) await session.execute(query)
await session.commit() await session.commit()
qr = select(models.Menu).where(models.Menu.id == menu_id) qr = select(models.Menu).where(models.Menu.id == menu_id)

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

@ -0,0 +1,77 @@
from uuid import UUID
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import models, schemas
class SubMenuCrud:
@staticmethod
async def get_submenus(menu_id: UUID, session: AsyncSession):
async with session:
query = select(models.SubMenu).where(models.SubMenu.parent_menu == menu_id)
submenus = await session.execute(query)
return submenus.scalars().all()
@staticmethod
async def create_submenu_item(
menu_id: UUID,
submenu: schemas.MenuBase,
session: AsyncSession,
):
async with session:
new_submenu = models.SubMenu(**submenu.model_dump())
new_submenu.parent_menu = menu_id
session.add(new_submenu)
await session.flush()
await session.commit()
return new_submenu
@staticmethod
async def get_submenu_item(
menu_id: UUID,
submenu_id: UUID,
session: AsyncSession,
):
async with session:
query = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
submenu = await session.execute(query)
submenu = submenu.scalars().one_or_none()
if submenu is None:
return None
dish_query = (
select(func.count(models.Dish.id))
.join(models.SubMenu)
.filter(models.Dish.parent_submenu == models.SubMenu.id)
)
dishes = await session.execute(dish_query)
submenu.dishes_count = dishes.scalars().one_or_none()
return submenu
@staticmethod
async def update_submenu_item(
submenu_id: UUID,
submenu: schemas.MenuBase,
session: AsyncSession,
):
async with session:
query = (
update(models.SubMenu)
.where(models.SubMenu.id == submenu_id)
.values(**submenu.model_dump())
)
await session.execute(query)
await session.commit()
qr = select(models.SubMenu).where(models.SubMenu.id == submenu_id)
updated_submenu = await session.execute(qr)
return updated_submenu.scalars().one()
@staticmethod
async def delete_submenu_item(submenu_id: UUID, session: AsyncSession):
async with session:
query = delete(models.SubMenu).where(models.SubMenu.id == submenu_id)
await session.execute(query)
await session.commit()

View File

@ -1,14 +1,10 @@
from typing import AsyncGenerator from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker,
AsyncSession, create_async_engine)
async_sessionmaker,
create_async_engine,
)
from fastfood.config import settings from fastfood.config import settings
async_engine = create_async_engine(settings.DATABASE_URL_asyncpg) async_engine = create_async_engine(settings.DATABASE_URL_asyncpg)
async_session_maker = async_sessionmaker( async_session_maker = async_sessionmaker(
async_engine, async_engine,

View File

@ -1,5 +1,4 @@
import uuid import uuid
from decimal import Decimal
from typing import Annotated, List, Optional from typing import Annotated, List, Optional
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
@ -7,7 +6,7 @@ from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
uuidpk = Annotated[ uuidpk = Annotated[
int, uuid.UUID,
mapped_column( mapped_column(
UUID(as_uuid=True), UUID(as_uuid=True),
primary_key=True, primary_key=True,
@ -18,33 +17,40 @@ str_25 = Annotated[str, 25]
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass id: Mapped[uuidpk]
title: Mapped[str_25]
description: Mapped[Optional[str]]
class Menu(Base): class Menu(Base):
__tablename__ = "menu" __tablename__ = "menu"
id: Mapped[uuidpk] submenus: Mapped[List["SubMenu"]] = relationship(
title: Mapped[str_25] "SubMenu",
description: Mapped[Optional[str]] backref="menu",
submenus: Mapped[List["SubMenu"]] = relationship() lazy="dynamic",
cascade="all, delete",
)
class SubMenu(Base): class SubMenu(Base):
__tablename__ = "submenu" __tablename__ = "submenu"
id: Mapped[uuidpk] parent_menu: Mapped[uuid.UUID] = mapped_column(
title: Mapped[str_25] ForeignKey("menu.id", ondelete="CASCADE")
description: Mapped[Optional[str]] )
parent_menu: Mapped[UUID] = mapped_column(ForeignKey("menu.id")) dishes: Mapped[List["Dish"]] = relationship(
dishes: Mapped[List["Dish"]] = relationship() "Dish",
backref="submenu",
lazy="dynamic",
cascade="all, delete",
)
class Dish(Base): class Dish(Base):
__tablename__ = "dish" __tablename__ = "dish"
id: Mapped[uuidpk] price: Mapped[float]
title: Mapped[str_25] parent_submenu: Mapped[uuid.UUID] = mapped_column(
description: Mapped[Optional[str]] ForeignKey("submenu.id", ondelete="CASCADE")
price: Mapped[Decimal] )
parent_submenu: Mapped[UUID] = mapped_column(ForeignKey("submenu.id"))

View File

80
fastfood/routers/dish.py Normal file
View File

@ -0,0 +1,80 @@
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import schemas
from fastfood.cruds import crud
from fastfood.dbase import get_async_session
from fastfood.utils import price_converter
router = APIRouter(
prefix="/api/v1/menus/{menu_id}/submenus/{submenu_id}/dishes",
tags=["dish"],
)
@router.get("/")
async def get_dishes(
menu_id: UUID, submenu_id: UUID, session: AsyncSession = Depends(get_async_session)
):
result = await crud.get_dishes(submenu_id=submenu_id, session=session)
return result
@router.post("/", status_code=201)
async def create_dish(
menu_id: UUID,
submenu_id: UUID,
dish: schemas.DishBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.create_dish_item(
submenu_id=submenu_id,
dish=dish,
session=session,
)
return price_converter(result)
@router.get("/{dish_id}")
async def get_dish(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.get_dish_item(
dish_id=dish_id,
session=session,
)
if not result:
raise HTTPException(status_code=404, detail="dish not found")
return price_converter(result)
@router.patch("/{dish_id}")
async def update_dish(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
dish: schemas.DishBase,
session: AsyncSession = Depends(get_async_session),
):
result = await crud.update_dish_item(
dish_id=dish_id,
dish=dish,
session=session,
)
return price_converter(result)
@router.delete("/{dish_id}")
async def delete_dish(
menu_id: UUID,
submenu_id: UUID,
dish_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
await crud.delete_dish_item(dish_id=dish_id, session=session)

View File

@ -1,23 +1,26 @@
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import insert, select
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from fastfood import models, schemas from fastfood import schemas
from fastfood.crud import Crud as crud from fastfood.cruds import crud
from fastfood.dbase import get_async_session from fastfood.dbase import get_async_session
router = APIRouter() router = APIRouter(
prefix="/api/v1/menus",
tags=["menu"],
)
@router.get("/api/v1/menus", response_model=Optional[List[schemas.Menu]]) @router.get("/", response_model=Optional[List[schemas.Menu]])
async def get_menus(session: AsyncSession = Depends(get_async_session)): async def get_menus(session: AsyncSession = Depends(get_async_session)):
result = await crud.get_menus(session=session) result = await crud.get_menus(session=session)
return result return result
@router.post("/api/v1/menus", status_code=201, response_model=schemas.Menu) @router.post("/", status_code=201, response_model=schemas.Menu)
async def add_menu( async def add_menu(
menu: schemas.MenuBase, menu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
@ -29,27 +32,34 @@ async def add_menu(
return result return result
@router.get("/api/v1/menus/{menu_id}", response_model=schemas.Menu) @router.get("/{menu_id}", response_model=schemas.MenuRead)
async def get_menu( async def get_menu(
menu_id: UUID, menu_id: UUID,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
result = await crud.get_menu_item(menu_id=menu_id, session=session) result = await crud.get_menu_item(menu_id=menu_id, session=session)
if not result: if not result:
raise HTTPException(status_code=404, detail="menu not found") raise HTTPException(status_code=404, detail="menu not found")
return result return result
@router.patch("/api/v1/menus/{menu_id}", response_model=schemas.MenuBase) @router.patch("/{menu_id}", response_model=schemas.MenuBase)
async def update_menu( async def update_menu(
menu_id: UUID, menu_id: UUID,
menu: schemas.MenuBase, menu: schemas.MenuBase,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
result = await crud.update_menu_item(menu_id=menu_id, menu=menu, session=session) result = await crud.update_menu_item(
menu_id=menu_id,
menu=menu,
session=session,
)
return result return result
@router.delete("/api/v1/menus/{menu_id}") @router.delete("/{menu_id}")
async def delete_menu(menu_id: UUID, session: AsyncSession = Depends(get_async_session)): async def delete_menu(
menu_id: UUID,
session: AsyncSession = Depends(get_async_session),
):
await crud.delete_menu_item(menu_id=menu_id, session=session) await crud.delete_menu_item(menu_id=menu_id, session=session)

View File

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

View File

@ -1,5 +1,4 @@
from decimal import Decimal from typing import Optional
from typing import List, Optional
from uuid import UUID from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
@ -8,16 +7,27 @@ from pydantic import BaseModel
class MenuBase(BaseModel): class MenuBase(BaseModel):
title: str title: str
description: Optional[str] description: Optional[str]
class Config: class Config:
from_attributes = True from_attributes = True
class Menu(MenuBase): class Menu(MenuBase):
id: UUID id: UUID
title: str
description: Optional[str]
# submenus: Optional[List[SubMenu]]
class Config:
from_attributes = True class MenuRead(Menu):
submenus_count: int
dishes_count: int
class SubMenuRead(Menu):
dishes_count: int
class DishBase(MenuBase):
price: float
class Dish(DishBase, Menu):
pass

3
fastfood/utils.py Normal file
View File

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

View File

@ -3,12 +3,12 @@ import sys
import uvicorn import uvicorn
from fastfood.crud import create_db_and_tables from fastfood.cruds import create_db_and_tables
def run_app(): def run_app():
""" """
Запуск Запуск FastAPI
""" """
uvicorn.run( uvicorn.run(
app="fastfood.app:create_app", app="fastfood.app:create_app",
@ -17,13 +17,15 @@ def run_app():
) )
if __name__ == "__main__": async def recreate():
if "filldb" in sys.argv: """Удаление и создание таблиц в базе данных для тестирования"""
"""Наполнение БД демонстрационными данными""" await create_db_and_tables()
pass
if __name__ == "__main__":
if "--run-server" in sys.argv: if "--run-server" in sys.argv:
async def create(): run_app()
await create_db_and_tables()
asyncio.run(create()) if "--run-test-server" in sys.argv:
asyncio.run(recreate())
run_app() run_app()

118
poetry.lock generated
View File

@ -126,6 +126,41 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "dnspython"
version = "2.5.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.8"
files = [
{file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"},
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"},
]
[package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=5.0.3)", "mypy (>=1.0.1)", "pylint (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "sphinx (>=7.0.0)", "twine (>=4.0.0)", "wheel (>=0.41.0)"]
dnssec = ["cryptography (>=41)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.25.1)"]
doq = ["aioquic (>=0.9.20)"]
idna = ["idna (>=2.1)"]
trio = ["trio (>=0.14)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "email-validator"
version = "2.1.0.post1"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
files = [
{file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"},
{file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.0" version = "1.2.0"
@ -289,6 +324,87 @@ files = [
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"] testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "psycopg2-binary"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.5.3" version = "2.5.3"
@ -635,4 +751,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "7d4c2713da8d969278204677c9f1b4790b62a0114fe51a0d973313bf079b871c" content-hash = "84fe024aa665ddad3077ca7e73054d1a5cb019a9a3e78af917922433ff4b3d8c"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
{
"id": "7e7cd612-7f40-491b-8bd6-ba2322a3d0d7",
"name": "menu app",
"values": [
{
"key": "LOCAL_URL",
"value": "127.0.0.1:8000",
"type": "default",
"enabled": true
},
{
"key": "api_test_menu_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "api_test_submenu_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "api_test_dish_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_menu_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_menu_title",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_menu_description",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_submenu_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_submenu_title",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_submenu_description",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_dish_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_dish_title",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_dish_description",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "target_dish_price",
"value": "",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2023-01-12T16:22:10.333Z",
"_postman_exported_using": "Postman/10.6.7"
}

View File

@ -12,6 +12,8 @@ fastapi = "^0.109.0"
uvicorn = "^0.26.0" uvicorn = "^0.26.0"
asyncpg = "^0.29.0" asyncpg = "^0.29.0"
pydantic-settings = "^2.1.0" pydantic-settings = "^2.1.0"
psycopg2-binary = "^2.9.9"
email-validator = "^2.1.0.post1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]