mirror of
https://github.com/koloideal/Quizzi.git
synced 2026-06-10 18:35:28 +03:00
Initial commit
This commit is contained in:
+37
-13
@@ -1,23 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import Connection
|
||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
from trudex.infrastructure.database.models import Base
|
||||||
|
from trudex.infrastructure.utils.config import Config
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
target_metadata = None
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
db_config = Config.from_toml("config.toml").database
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
context.configure(
|
context.configure(
|
||||||
url=url,
|
url=db_config.url,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
literal_binds=True,
|
literal_binds=True,
|
||||||
dialect_opts={"paramstyle": "named"},
|
dialect_opts={"paramstyle": "named"},
|
||||||
@@ -27,7 +51,7 @@ def run_migrations_offline() -> None:
|
|||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
def do_run_migrations(connection: Connection) -> None:
|
def do_run_migrations(connection: Connection):
|
||||||
context.configure(connection=connection, target_metadata=target_metadata)
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
@@ -35,11 +59,11 @@ def do_run_migrations(connection: Connection) -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def run_async_migrations() -> None:
|
async def run_async_migrations() -> None:
|
||||||
connectable = async_engine_from_config(
|
"""In this scenario we need to create an Engine
|
||||||
config.get_section(config.config_ini_section, {}),
|
and associate a connection with the context.
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
"""
|
||||||
)
|
connectable = create_async_engine(db_config.url)
|
||||||
|
|
||||||
async with connectable.connect() as connection:
|
async with connectable.connect() as connection:
|
||||||
await connection.run_sync(do_run_migrations)
|
await connection.run_sync(do_run_migrations)
|
||||||
@@ -48,7 +72,7 @@ async def run_async_migrations() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
def run_migrations_online() -> None:
|
||||||
import asyncio
|
"""Run migrations in 'online' mode."""
|
||||||
asyncio.run(run_async_migrations())
|
asyncio.run(run_async_migrations())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
${message}
|
"""${message}
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
Revision ID: ${up_revision}
|
||||||
Revises: ${down_revision | comma,n}
|
Revises: ${down_revision | comma,n}
|
||||||
Create Date: ${create_date}
|
Create Date: ${create_date}
|
||||||
|
|
||||||
from typing import Sequence, Union
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
${imports if imports else ""}
|
${imports if imports else ""}
|
||||||
|
|
||||||
revision: str = ${repr(up_revision)}
|
revision: str = ${repr(up_revision)}
|
||||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
down_revision: str | None = ${repr(down_revision)}
|
||||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
||||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""initial
|
||||||
|
|
||||||
|
Revision ID: 409f04b7b544
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-12-31 00:38:10.367405
|
||||||
|
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = '409f04b7b544'
|
||||||
|
down_revision: str | None = None
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('first_name', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('last_name', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('group', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('is_admin', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('users')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -3,13 +3,13 @@ import logging
|
|||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
|
|
||||||
from trudex.infrastructure.utils.config import AppConfig
|
from trudex.infrastructure.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
config = AppConfig.from_toml()
|
config = Config.from_toml("config.toml")
|
||||||
|
|
||||||
logging.info("Бот запущен")
|
logging.info("Бот запущен")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker,
|
||||||
|
create_async_engine)
|
||||||
|
|
||||||
|
|
||||||
def create_engine(database_url: str) -> AsyncEngine:
|
def new_session_maker(db_url: str) -> async_sessionmaker[AsyncSession]:
|
||||||
return create_async_engine(database_url, echo=False)
|
engine = create_async_engine(
|
||||||
|
db_url,
|
||||||
|
pool_size=15,
|
||||||
def create_session_maker(engine: AsyncEngine) -> async_sessionmaker:
|
max_overflow=15,
|
||||||
return async_sessionmaker(engine, expire_on_commit=False)
|
connect_args={
|
||||||
|
"timeout": 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return async_sessionmaker(engine, class_=AsyncSession, autoflush=False, expire_on_commit=False)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from trudex.infrastructure.database.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserDAO:
|
||||||
|
def __init__(self, session: AsyncSession) -> None:
|
||||||
|
self.session: AsyncSession = session
|
||||||
|
|
||||||
|
async def get_by_id(self, user_id: int) -> User | None:
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(User).where(User.id == user_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_all(self) -> list[User]:
|
||||||
|
result = await self.session.execute(select(User))
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_by_group(self, group: int) -> list[User]:
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(User).where(User.group == group)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_admins(self) -> list[User]:
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(User).where(User.is_admin == True)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
first_name: str,
|
||||||
|
username: str | None = None,
|
||||||
|
last_name: str | None = None,
|
||||||
|
group: int | None = None,
|
||||||
|
is_admin: bool = False,
|
||||||
|
) -> User:
|
||||||
|
user = User(
|
||||||
|
id=user_id,
|
||||||
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
group=group,
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
self.session.add(user)
|
||||||
|
await self.session.flush()
|
||||||
|
return user
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
username: str | None = None,
|
||||||
|
first_name: str | None = None,
|
||||||
|
last_name: str | None = None,
|
||||||
|
group: int | None = None,
|
||||||
|
is_admin: bool | None = None,
|
||||||
|
) -> User | None:
|
||||||
|
user = await self.get_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if username is not None:
|
||||||
|
user.username = username
|
||||||
|
if first_name is not None:
|
||||||
|
user.first_name = first_name
|
||||||
|
if last_name is not None:
|
||||||
|
user.last_name = last_name
|
||||||
|
if group is not None:
|
||||||
|
user.group = group
|
||||||
|
if is_admin is not None:
|
||||||
|
user.is_admin = is_admin
|
||||||
|
|
||||||
|
await self.session.flush()
|
||||||
|
return user
|
||||||
|
|
||||||
|
async def delete(self, user_id: int) -> bool:
|
||||||
|
user = await self.get_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await self.session.delete(user)
|
||||||
|
await self.session.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def upsert(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
first_name: str,
|
||||||
|
username: str | None = None,
|
||||||
|
last_name: str | None = None,
|
||||||
|
group: int | None = None,
|
||||||
|
is_admin: bool = False,
|
||||||
|
) -> User:
|
||||||
|
user = await self.get_by_id(user_id)
|
||||||
|
if user:
|
||||||
|
if username is not None:
|
||||||
|
user.username = username
|
||||||
|
if first_name is not None:
|
||||||
|
user.first_name = first_name
|
||||||
|
if last_name is not None:
|
||||||
|
user.last_name = last_name
|
||||||
|
if group is not None:
|
||||||
|
user.group = group
|
||||||
|
if is_admin is not None:
|
||||||
|
user.is_admin = is_admin
|
||||||
|
await self.session.flush()
|
||||||
|
return user
|
||||||
|
|
||||||
|
return await self.create(
|
||||||
|
user_id=user_id,
|
||||||
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
group=group,
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
from sqlalchemy.orm import DeclarativeBase
|
from datetime import datetime
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, CheckConstraint, String, func
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||||
|
username: Mapped[str | None] = mapped_column(String(32))
|
||||||
|
first_name: Mapped[str] = mapped_column(String(64))
|
||||||
|
last_name: Mapped[str | None] = mapped_column(String(64))
|
||||||
|
group: Mapped[int | None] = mapped_column(CheckConstraint("group >= 1000 AND group <= 9999"))
|
||||||
|
is_admin: Mapped[bool] = mapped_column(default=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
from dishka import Provider, Scope
|
from collections.abc import AsyncIterable
|
||||||
|
|
||||||
|
from dishka import Provider, Scope, provide
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
|
||||||
|
from trudex.infrastructure.database.config import new_session_maker
|
||||||
|
from trudex.infrastructure.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
class DatabaseProvider(Provider):
|
class DatabaseProvider(Provider):
|
||||||
scope = Scope.APP
|
@provide(scope=Scope.APP)
|
||||||
|
def get_session_maker(self, config: Config) -> async_sessionmaker[AsyncSession]:
|
||||||
|
return new_session_maker(config.database.url)
|
||||||
|
|
||||||
|
@provide(scope=Scope.REQUEST)
|
||||||
|
async def get_session(
|
||||||
|
self, session_maker: async_sessionmaker[AsyncSession]
|
||||||
|
) -> AsyncIterable[AsyncSession]:
|
||||||
|
async with session_maker() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
class InfrastructureProvider(Provider):
|
@provide(scope=Scope.REQUEST)
|
||||||
scope = Scope.APP
|
async def get_users_dao(self, session: AsyncSession) -> UsersDAO:
|
||||||
|
return UsersDAO(session)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import tomllib
|
import tomllib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -22,12 +23,12 @@ class DatabaseConfig:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class Config:
|
||||||
bot: BotConfig
|
bot: BotConfig
|
||||||
database: DatabaseConfig
|
database: DatabaseConfig
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_toml(cls, path: str | Path = "config.toml") -> "AppConfig":
|
def from_toml(cls, path: str | Path) -> Self:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
data: dict[str, dict[str, str]] = tomllib.load(f)
|
data: dict[str, dict[str, str]] = tomllib.load(f)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user