connect database

This commit is contained in:
2025-09-27 09:13:20 +03:00
parent 5982351dd2
commit 1a49545fff
16 changed files with 180 additions and 38 deletions

View File

@@ -7,19 +7,25 @@ from aiogram_dialog import setup_dialogs
from app.bot.dialogs.flows import dialogs_router from app.bot.dialogs.flows import dialogs_router
from app.bot.handlers.commands import commands_router from app.bot.handlers.commands import commands_router
from app.bot.middlewares import GetUserMiddleware from app.bot.middlewares import DbSessionMiddleware, GetUserMiddleware
from config.config import settings from config.config import settings
bot = Bot(token=settings.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN_V2)) bot = Bot(
token=settings.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN_V2),
)
dp = Dispatcher() dp = Dispatcher()
async def main(): async def main():
setup_dialogs(dp) setup_dialogs(dp)
dp.update.outer_middleware(DbSessionMiddleware())
dp.update.outer_middleware(GetUserMiddleware())
dp.include_router(commands_router) dp.include_router(commands_router)
dp.include_router(dialogs_router) dp.include_router(dialogs_router)
dp.update.outer_middleware(GetUserMiddleware())
await dp.start_polling(bot) await dp.start_polling(bot)

View File

@@ -1,23 +1,39 @@
from aiogram_dialog import Dialog, Window from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Column, Select from aiogram_dialog.widgets.kbd import Back, Cancel, Column, Select
from aiogram_dialog.widgets.text import Const, Format from aiogram_dialog.widgets.text import Const, Format
from .getters import events_getter from .getters import events_getter
from .states import EventsSG from .states import EventsSG
async def on_event_selected(
c,
widget: Select,
manager,
item_id: str,
):
manager.dialog_data["selected_event"] = item_id
await manager.next()
events_dialog = Dialog( events_dialog = Dialog(
Window( Window(
Const("События"), Const("События"),
Column( Column(
Cancel(Const("Назад")),
Select( Select(
Format("{item}"), Format("{item}"),
id="categ", id="categ",
item_id_getter=lambda x: x, item_id_getter=lambda x: x,
items="events", items="events",
) on_click=on_event_selected,
),
), ),
getter=events_getter, getter=events_getter,
state=EventsSG.events_list, state=EventsSG.events_list,
), ),
# Window(state=EventsSG.event), Window(
Format("{dialog_data[selected_event]}"),
Back(Const("Назад")),
state=EventsSG.event,
),
) )

View File

@@ -3,13 +3,15 @@ from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.kbd import Cancel, SwitchTo from aiogram_dialog.widgets.kbd import Cancel, SwitchTo
from aiogram_dialog.widgets.text import Const, Format from aiogram_dialog.widgets.text import Const, Format
from app.bot.dialogs.widgets.getters import user_getter
from .states import ProfileSG from .states import ProfileSG
profile_dialog = Dialog( profile_dialog = Dialog(
Window( Window(
Const("*Профиль*"), Const("*Профиль*"),
Format("Имя:"), Format("Имя: {user.fullname}"),
Format("Телефон:"), Format("Телефон: {user.phone}"),
SwitchTo( SwitchTo(
Const("изменить имя"), Const("изменить имя"),
id="change_name", id="change_name",
@@ -22,6 +24,7 @@ profile_dialog = Dialog(
), ),
Cancel(Const("назад")), Cancel(Const("назад")),
state=ProfileSG.profile, state=ProfileSG.profile,
getter=user_getter,
), ),
Window( Window(
Const("Введите имя"), Const("Введите имя"),

View File

@@ -4,17 +4,17 @@ from aiogram_dialog.widgets.text import Const, Format
from app.bot.dialogs.flows.events.dialogs import EventsSG from app.bot.dialogs.flows.events.dialogs import EventsSG
from app.bot.dialogs.flows.profile.dialogs import ProfileSG from app.bot.dialogs.flows.profile.dialogs import ProfileSG
from app.bot.dialogs.widgets.getters import is_admin, username_getter from app.bot.dialogs.widgets.getters import is_admin_getter, user_getter
from .states import StartSG from .states import StartSG
start_dialog = Dialog( start_dialog = Dialog(
Window( Window(
Format("Привет, {username}"), Format("Привет, {user.fullname}"),
Start(Const("события"), id="events", state=EventsSG.events_list), Start(Const("события"), id="events", state=EventsSG.events_list),
Button(Const("создать событие"), id="create_event", when="is_admin"), Button(Const("создать событие"), id="create_event", when="is_admin"),
Start(Const("профиль"), id="profile", state=ProfileSG.profile), Start(Const("профиль"), id="profile", state=ProfileSG.profile),
getter=[username_getter, is_admin], getter=[user_getter, is_admin_getter],
state=StartSG.start, state=StartSG.start,
) )
) )

View File

@@ -1,13 +1,16 @@
from aiogram.types import User from aiogram.types import User
from aiogram_dialog import DialogManager from aiogram_dialog import DialogManager
from app.infrastructure.database.models import User as UserModel
async def username_getter(
dialog_manager: DialogManager, event_from_user: User, **kwargs
) -> dict[str, str]:
return {"username": event_from_user.username}
async def is_admin( async def user_getter(
dialog_manager: DialogManager, event_from_user: User, **kwargs dialog_manager: DialogManager, event_from_user: User, user: UserModel, **kwargs
) -> dict[str, str]: ) -> dict[str, str]:
return {"is_admin": False} return {"user": user}
async def is_admin_getter(
dialog_manager: DialogManager, event_from_user: User, user: UserModel, **kwargs
) -> dict[str, str]:
return {"is_admin": user.role == "admin"}

View File

@@ -1,3 +1,4 @@
from .database import DbSessionMiddleware
from .get_user import GetUserMiddleware from .get_user import GetUserMiddleware
__all__ = ["GetUserMiddleware"] __all__ = ["DbSessionMiddleware", "GetUserMiddleware"]

View File

@@ -0,0 +1,18 @@
from typing import Any, Awaitable, Callable
from aiogram import BaseMiddleware
from aiogram.types import Update
from app.infrastructure.database import get_session
class DbSessionMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[Update, dict[str, Any]], Awaitable[Any]],
event: Update,
data: dict[str, Any],
):
async with get_session() as session:
data["session"] = session
return await handler(event, data)

View File

@@ -1,7 +1,9 @@
from typing import Any, Awaitable, Callable from typing import Any, Awaitable, Callable
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from aiogram.types import Update from aiogram.types import Update, User
from app.infrastructure.database.crud import create_user, get_user_by_tg_id
class GetUserMiddleware(BaseMiddleware): class GetUserMiddleware(BaseMiddleware):
@@ -11,5 +13,14 @@ class GetUserMiddleware(BaseMiddleware):
event: Update, event: Update,
data: dict[str, Any], data: dict[str, Any],
): ):
data.update(user={"is_admin": False}) session = data["session"]
return await handler(event, data) tg_user: User = data.get("event_from_user")
user = await get_user_by_tg_id(session, tg_user.id)
if user is None:
user = await create_user(
session,
tg_user.id,
tg_user.username,
)
data["user"] = user
return await handler(event, data)

View File

@@ -0,0 +1,4 @@
from .models import Base
from .session import async_session, engine, get_session
__all__ = ["Base", "async_session", "engine", "get_session"]

View File

@@ -0,0 +1,22 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from .models import User
async def get_user_by_tg_id(session: AsyncSession, user_tg_id: int) -> User | None:
result = await session.execute(select(User).where(User.tg_id == user_tg_id))
return result.scalar_one_or_none()
async def create_user(
session: AsyncSession,
tg_id: int,
fullname: str,
phone: str = "",
role: str = "user",
) -> User:
user = User(tg_id=tg_id, fullname=fullname, role=role, phone=phone)
session.add(user)
await session.commit()
return user

View File

@@ -1,13 +0,0 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from config.config import DATABASE_URL
engine = create_async_engine(DATABASE_URL, echo=True)
Base = declarative_base()
async_session = sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
async def get_session() -> AsyncSession:
async with async_session() as session:
yield session

View File

@@ -0,0 +1,13 @@
import asyncio
from .models import Base
from .session import engine
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
if __name__ == "__main__":
asyncio.run(init_db())

View File

@@ -6,16 +6,17 @@ from sqlalchemy import (
ForeignKey, ForeignKey,
func, func,
) )
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, declarative_base, mapped_column
from app.db.database import Base Base = declarative_base()
class User(Base): class User(Base):
__tablename__ = "user" __tablename__ = "user"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
tg_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False)
fullname: Mapped[str] = mapped_column() fullname: Mapped[str] = mapped_column()
tg_id: Mapped[int] = mapped_column(BigInteger) role: Mapped[str] = mapped_column(default="user")
phone: Mapped[str] = mapped_column(nullable=True) phone: Mapped[str] = mapped_column(nullable=True)

View File

@@ -0,0 +1,20 @@
from contextlib import asynccontextmanager
from typing import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from config.config import settings
engine = create_async_engine(settings.database_url, echo=True)
async_session = sessionmaker(
bind=engine,
expire_on_commit=False,
class_=AsyncSession,
)
@asynccontextmanager
async def get_session() -> AsyncIterator[AsyncSession]:
async with async_session() as session:
yield session

View File

@@ -9,4 +9,10 @@ dependencies = [
"dynaconf>=3.2.11", "dynaconf>=3.2.11",
"psycopg>=3.2.10", "psycopg>=3.2.10",
"sqlalchemy>=2.0.43", "sqlalchemy>=2.0.43",
"structlog>=25.4.0",
]
[dependency-groups]
dev = [
"aiosqlite>=0.21.0",
] ]

31
uv.lock generated
View File

@@ -115,6 +115,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
] ]
[[package]]
name = "aiosqlite"
version = "0.21.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@@ -578,6 +590,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" },
] ]
[[package]]
name = "structlog"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/79/b9/6e672db4fec07349e7a8a8172c1a6ae235c58679ca29c3f86a61b5e59ff3/structlog-25.4.0.tar.gz", hash = "sha256:186cd1b0a8ae762e29417095664adf1d6a31702160a46dacb7796ea82f7409e4", size = 1369138, upload-time = "2025-06-02T08:21:12.971Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/4a/97ee6973e3a73c74c8120d59829c3861ea52210667ec3e7a16045c62b64d/structlog-25.4.0-py3-none-any.whl", hash = "sha256:fe809ff5c27e557d14e613f45ca441aabda051d119ee5a0102aaba6ce40eed2c", size = 68720, upload-time = "2025-06-02T08:21:11.43Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -619,6 +640,12 @@ dependencies = [
{ name = "dynaconf" }, { name = "dynaconf" },
{ name = "psycopg" }, { name = "psycopg" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "structlog" },
]
[package.dev-dependencies]
dev = [
{ name = "aiosqlite" },
] ]
[package.metadata] [package.metadata]
@@ -629,8 +656,12 @@ requires-dist = [
{ name = "dynaconf", specifier = ">=3.2.11" }, { name = "dynaconf", specifier = ">=3.2.11" },
{ name = "psycopg", specifier = ">=3.2.10" }, { name = "psycopg", specifier = ">=3.2.10" },
{ name = "sqlalchemy", specifier = ">=2.0.43" }, { name = "sqlalchemy", specifier = ">=2.0.43" },
{ name = "structlog", specifier = ">=25.4.0" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "aiosqlite", specifier = ">=0.21.0" }]
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.20.1" version = "1.20.1"