forked from FOSS/AutoShop-Djimbo
Initial local state
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
from .db_category import CategoryModel, Categoryx
|
||||
from .db_item import ItemModel, Itemx
|
||||
from .db_payments import PaymentsModel, Paymentsx
|
||||
from .db_position import PositionModel, Positionx
|
||||
from .db_purchases import PurchaseModel, Purchasesx
|
||||
from .db_refill import RefillModel, Refillx
|
||||
from .db_settings import SettingsModel, Settingsx
|
||||
from .db_users import UserModel, UsersRepository, Userx
|
||||
from .entities import Category, Item, Payments, Position, Purchase, Refill, Settings, User
|
||||
|
||||
ModelCategory = Category
|
||||
ModelItem = Item
|
||||
ModelPayments = Payments
|
||||
ModelPosition = Position
|
||||
ModelPurchases = Purchase
|
||||
ModelRefill = Refill
|
||||
ModelSettings = Settings
|
||||
ModelUser = User
|
||||
ModelUsers = User
|
||||
SettingsRepository = Settingsx
|
||||
@@ -0,0 +1,47 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from tgbot.data.config import PATH_DATABASE
|
||||
|
||||
database_path = Path(PATH_DATABASE)
|
||||
database_url = f"sqlite+aiosqlite:///{database_path.as_posix()}"
|
||||
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
# Общая база для всех SQLAlchemy-моделей
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# Включение SQLite-настроек на каждом соединении
|
||||
@event.listens_for(engine.sync_engine, "connect")
|
||||
def _configure_sqlite(dbapi_connection, connection_record) -> None:
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.execute("PRAGMA busy_timeout=5000")
|
||||
cursor.close()
|
||||
|
||||
|
||||
# Открытие сессии с автокоммитом или откатом
|
||||
@asynccontextmanager
|
||||
async def session_scope() -> AsyncIterator[AsyncSession]:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
# Закрытие пула соединений БД
|
||||
async def close_database() -> None:
|
||||
await engine.dispose()
|
||||
@@ -0,0 +1,47 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Category
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class CategoryModel(Base):
|
||||
__tablename__ = "storage_category"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
category_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
category_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Category
|
||||
BaseModel = Category
|
||||
|
||||
|
||||
class Categoryx(BaseRepository[CategoryModel, Category]):
|
||||
# Подключение модели категорий
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = CategoryModel
|
||||
self.entity_model = Category
|
||||
self.storage_name = CategoryModel.__tablename__
|
||||
|
||||
# Добавление категории товара
|
||||
async def add(self, category_id: int, category_name: str) -> Category:
|
||||
return await self._insert(
|
||||
category_id=category_id,
|
||||
category_name=category_name,
|
||||
category_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление категории по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"category_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,73 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import Item
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import clear_list, gen_id, get_unix
|
||||
|
||||
|
||||
class ItemModel(Base):
|
||||
__tablename__ = "storage_item"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
item_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
item_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
item_data: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
|
||||
ModelBase = Item
|
||||
BaseModel = Item
|
||||
|
||||
|
||||
class Itemx(BaseRepository[ItemModel, Item]):
|
||||
# Подключение модели товаров
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = ItemModel
|
||||
self.entity_model = Item
|
||||
self.storage_name = ItemModel.__tablename__
|
||||
|
||||
# Добавление товаров пачкой
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
category_id: int,
|
||||
position_id: int,
|
||||
item_datas: List[str],
|
||||
) -> Optional[List[Item]]:
|
||||
item_unix = get_unix()
|
||||
item_datas = clear_list(item_datas)
|
||||
|
||||
if len(item_datas) == 0:
|
||||
return None
|
||||
|
||||
rows = [
|
||||
ItemModel(
|
||||
user_id=user_id,
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
item_id=gen_id(17),
|
||||
item_unix=item_unix,
|
||||
item_data=item_data.strip(),
|
||||
)
|
||||
for item_data in item_datas
|
||||
]
|
||||
|
||||
async with session_scope() as session:
|
||||
session.add_all(rows)
|
||||
|
||||
return [self._to_entity(row) for row in rows]
|
||||
|
||||
# Обновление товара по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"item_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,64 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Integer, String, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Payments
|
||||
from tgbot.database.repository import BaseRepository
|
||||
|
||||
|
||||
class PaymentsModel(Base):
|
||||
__tablename__ = "storage_payments"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
cryptobot_token: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
yoomoney_token: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
stars_course: Mapped[float] = mapped_column(Float, nullable=False, default=1.5)
|
||||
status_cryptobot: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_yoomoney: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_stars: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
|
||||
|
||||
ModelBase = Payments
|
||||
BaseModel = Payments
|
||||
|
||||
|
||||
class Paymentsx(BaseRepository[PaymentsModel, Payments]):
|
||||
# Подключение модели платежных настроек
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PaymentsModel
|
||||
self.entity_model = Payments
|
||||
self.storage_name = PaymentsModel.__tablename__
|
||||
|
||||
# Создание дефолтной строки платежей
|
||||
async def ensure_default(self) -> None:
|
||||
payments = await BaseRepository.get(self, id=1)
|
||||
|
||||
if payments is None:
|
||||
await self._insert(id=1)
|
||||
|
||||
# Получение платежных настроек
|
||||
async def get(self, **kwargs) -> Payments:
|
||||
if not kwargs:
|
||||
kwargs = {"id": 1}
|
||||
|
||||
payments = await BaseRepository.get(self, **kwargs)
|
||||
|
||||
if payments is None and kwargs == {"id": 1}:
|
||||
await self.ensure_default()
|
||||
payments = await BaseRepository.get(self, id=1)
|
||||
|
||||
if payments is None:
|
||||
raise RuntimeError("Настройки платежных систем по умолчанию не сохранились")
|
||||
|
||||
return payments
|
||||
|
||||
# Обновление платежных настроек
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if where is None:
|
||||
where = {"id": 1}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,79 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, Text, func, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import Position
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class PositionModel(Base):
|
||||
__tablename__ = "storage_position"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
position_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
position_desc: Mapped[str] = mapped_column(Text, nullable=False, default="None")
|
||||
position_photo: Mapped[str] = mapped_column(Text, nullable=False, default="None")
|
||||
position_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Position
|
||||
BaseModel = Position
|
||||
|
||||
|
||||
class Positionx(BaseRepository[PositionModel, Position]):
|
||||
# Подключение модели позиций
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PositionModel
|
||||
self.entity_model = Position
|
||||
self.storage_name = PositionModel.__tablename__
|
||||
|
||||
# Добавление позиции товара
|
||||
async def add(
|
||||
self,
|
||||
category_id: int,
|
||||
position_id: int,
|
||||
position_name: str,
|
||||
position_price: float,
|
||||
position_desc: str,
|
||||
position_photo: str,
|
||||
) -> Position:
|
||||
return await self._insert(
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
position_name=position_name,
|
||||
position_price=position_price,
|
||||
position_desc=position_desc,
|
||||
position_photo=position_photo,
|
||||
position_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление позиции по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"position_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Получение остатков товаров по позициям
|
||||
@staticmethod
|
||||
async def item_counts() -> Dict[int, int]:
|
||||
from tgbot.database.db_item import ItemModel
|
||||
|
||||
item_table = ItemModel.__table__
|
||||
statement = (
|
||||
select(item_table.c.position_id, func.count(item_table.c.increment).label("item_count"))
|
||||
.group_by(item_table.c.position_id)
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
rows = await session.execute(statement)
|
||||
|
||||
return {int(position_id): int(item_count) for position_id, item_count in rows.all()}
|
||||
@@ -0,0 +1,331 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, Text, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_factory
|
||||
from tgbot.database.entities import Purchase
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import gen_id, get_unix
|
||||
|
||||
TELEGRAM_LIMIT = 4000
|
||||
|
||||
|
||||
@dataclass
|
||||
class PurchaseResult:
|
||||
receipt: int
|
||||
items: List[List[str]]
|
||||
purchase_count: int
|
||||
purchase_unix: int
|
||||
purchase_price: float
|
||||
new_balance: float
|
||||
position_name: str
|
||||
|
||||
|
||||
class PurchaseModel(Base):
|
||||
__tablename__ = "storage_purchases"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
user_balance_before: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_balance_after: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_receipt: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_data: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
purchase_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
purchase_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_price_one: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_position_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
purchase_category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_category_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
purchase_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Purchase
|
||||
BaseModel = Purchase
|
||||
|
||||
|
||||
class Purchasesx(BaseRepository[PurchaseModel, Purchase]):
|
||||
# Подключение модели покупок
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PurchaseModel
|
||||
self.entity_model = Purchase
|
||||
self.storage_name = PurchaseModel.__tablename__
|
||||
|
||||
# Добавление записи покупки
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
user_balance_before: float,
|
||||
user_balance_after: float,
|
||||
purchase_receipt: int,
|
||||
purchase_data: str,
|
||||
purchase_count: int,
|
||||
purchase_price: float,
|
||||
purchase_price_one: float,
|
||||
purchase_position_id: int,
|
||||
purchase_position_name: str,
|
||||
purchase_category_id: int,
|
||||
purchase_category_name: str,
|
||||
) -> Purchase:
|
||||
return await self._insert(
|
||||
user_id=user_id,
|
||||
user_balance_before=user_balance_before,
|
||||
user_balance_after=user_balance_after,
|
||||
purchase_receipt=purchase_receipt,
|
||||
purchase_data=purchase_data,
|
||||
purchase_count=purchase_count,
|
||||
purchase_price=purchase_price,
|
||||
purchase_price_one=purchase_price_one,
|
||||
purchase_position_id=purchase_position_id,
|
||||
purchase_position_name=purchase_position_name,
|
||||
purchase_category_id=purchase_category_id,
|
||||
purchase_category_name=purchase_category_name,
|
||||
purchase_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление покупки по чеку или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"purchase_receipt": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Атомарная покупка товаров
|
||||
@staticmethod
|
||||
async def buy(*, user_id: int, position_id: int, requested_count: int) -> Union[PurchaseResult, str]:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
await session.execute(text("BEGIN IMMEDIATE"))
|
||||
|
||||
user_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT user_balance
|
||||
FROM storage_users
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
get_user = user_result.mappings().first()
|
||||
|
||||
if get_user is None:
|
||||
await session.rollback()
|
||||
return "USER_NOT_FOUND"
|
||||
|
||||
position_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT position_price, position_name, category_id
|
||||
FROM storage_position
|
||||
WHERE position_id = :position_id
|
||||
"""
|
||||
),
|
||||
{"position_id": position_id},
|
||||
)
|
||||
get_position = position_result.mappings().first()
|
||||
|
||||
if get_position is None:
|
||||
await session.rollback()
|
||||
return "POSITION_NOT_FOUND"
|
||||
|
||||
items_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT increment, item_id, item_data
|
||||
FROM storage_item
|
||||
WHERE position_id = :position_id
|
||||
ORDER BY increment
|
||||
LIMIT :requested_count
|
||||
"""
|
||||
),
|
||||
{"position_id": position_id, "requested_count": requested_count},
|
||||
)
|
||||
items = items_result.mappings().all()
|
||||
|
||||
if len(items) < requested_count:
|
||||
await session.rollback()
|
||||
return "NOT_ENOUGH_ITEMS"
|
||||
|
||||
purchase_price = round(float(get_position["position_price"]) * requested_count, 2)
|
||||
|
||||
if float(get_user["user_balance"]) < purchase_price:
|
||||
await session.rollback()
|
||||
return "NOT_ENOUGH_BALANCE"
|
||||
|
||||
item_ids = [row["item_id"] for row in items]
|
||||
delete_params = {f"item_id_{index}": item_id for index, item_id in enumerate(item_ids)}
|
||||
delete_marks = ", ".join(f":item_id_{index}" for index in range(len(item_ids)))
|
||||
await session.execute(
|
||||
text(f"DELETE FROM storage_item WHERE item_id IN ({delete_marks})"),
|
||||
delete_params,
|
||||
)
|
||||
|
||||
new_balance = round(float(get_user["user_balance"]) - purchase_price, 2)
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE storage_users
|
||||
SET user_balance = :new_balance
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"new_balance": new_balance, "user_id": user_id},
|
||||
)
|
||||
|
||||
receipt = gen_id()
|
||||
purchase_data = "\n".join([row["item_data"] for row in items])
|
||||
purchase_unix = get_unix()
|
||||
|
||||
category_result = await session.execute(
|
||||
text("SELECT category_name FROM storage_category WHERE category_id = :category_id"),
|
||||
{"category_id": get_position["category_id"]},
|
||||
)
|
||||
category = category_result.mappings().first()
|
||||
category_name = category["category_name"] if category else "Unknown"
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO storage_purchases (user_id,
|
||||
user_balance_before,
|
||||
user_balance_after,
|
||||
purchase_receipt,
|
||||
purchase_data,
|
||||
purchase_count,
|
||||
purchase_price,
|
||||
purchase_price_one,
|
||||
purchase_position_id,
|
||||
purchase_position_name,
|
||||
purchase_category_id,
|
||||
purchase_category_name,
|
||||
purchase_unix)
|
||||
VALUES (:user_id,
|
||||
:balance_before,
|
||||
:balance_after,
|
||||
:receipt,
|
||||
:purchase_data,
|
||||
:purchase_count,
|
||||
:purchase_price,
|
||||
:purchase_price_one,
|
||||
:position_id,
|
||||
:position_name,
|
||||
:category_id,
|
||||
:category_name,
|
||||
:purchase_unix)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"balance_before": float(get_user["user_balance"]),
|
||||
"balance_after": new_balance,
|
||||
"receipt": receipt,
|
||||
"purchase_data": purchase_data,
|
||||
"purchase_count": requested_count,
|
||||
"purchase_price": purchase_price,
|
||||
"purchase_price_one": float(get_position["position_price"]),
|
||||
"position_id": position_id,
|
||||
"position_name": get_position["position_name"],
|
||||
"category_id": get_position["category_id"],
|
||||
"category_name": category_name,
|
||||
"purchase_unix": purchase_unix,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
purchase_items = [row["item_data"] for row in items]
|
||||
purchase_items_parts = chunk_items_by_len(purchase_items)
|
||||
|
||||
return PurchaseResult(
|
||||
receipt=receipt,
|
||||
items=purchase_items_parts,
|
||||
purchase_count=requested_count,
|
||||
purchase_unix=purchase_unix,
|
||||
purchase_price=purchase_price,
|
||||
position_name=get_position["position_name"],
|
||||
new_balance=new_balance,
|
||||
)
|
||||
|
||||
|
||||
# Разделение товаров на сообщения под лимит телеграма
|
||||
def chunk_items_by_len(items: List[str], limit: int = TELEGRAM_LIMIT, separator: str = "\n\n") -> List[List[str]]:
|
||||
chunks: List[List[str]] = []
|
||||
current_chunk: List[str] = []
|
||||
current_len = 0
|
||||
sep_len = len(separator)
|
||||
|
||||
for raw_item in items:
|
||||
item = str(raw_item)
|
||||
|
||||
if len(item) > limit:
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = []
|
||||
current_len = 0
|
||||
|
||||
for part in split_long_item(item, limit):
|
||||
chunks.append([part])
|
||||
|
||||
continue
|
||||
|
||||
add_len = len(item) if not current_chunk else sep_len + len(item)
|
||||
|
||||
if current_len + add_len <= limit:
|
||||
current_chunk.append(item)
|
||||
current_len += add_len
|
||||
else:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = [item]
|
||||
current_len = len(item)
|
||||
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk)
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
# Разделение длинного товара без разрыва строк
|
||||
def split_long_item(item: str, limit: int = TELEGRAM_LIMIT) -> List[str]:
|
||||
chunks: List[str] = []
|
||||
current_lines: List[str] = []
|
||||
current_len = 0
|
||||
|
||||
for line in item.splitlines():
|
||||
line_len = len(line)
|
||||
add_len = line_len if not current_lines else 1 + line_len
|
||||
|
||||
if current_lines and current_len + add_len > limit:
|
||||
chunks.append("\n".join(current_lines))
|
||||
current_lines = []
|
||||
current_len = 0
|
||||
add_len = line_len
|
||||
|
||||
if line_len > limit:
|
||||
if current_lines:
|
||||
chunks.append("\n".join(current_lines))
|
||||
current_lines = []
|
||||
current_len = 0
|
||||
|
||||
start = 0
|
||||
|
||||
while start < line_len:
|
||||
chunks.append(line[start:start + limit])
|
||||
start += limit
|
||||
|
||||
continue
|
||||
|
||||
current_lines.append(line)
|
||||
current_len += add_len
|
||||
|
||||
if current_lines:
|
||||
chunks.append("\n".join(current_lines))
|
||||
|
||||
return chunks
|
||||
@@ -0,0 +1,146 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_factory
|
||||
from tgbot.database.entities import Refill
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class RefillModel(Base):
|
||||
__tablename__ = "storage_refill"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
refill_receipt: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
refill_comment: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
refill_amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
refill_method: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
refill_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Refill
|
||||
BaseModel = Refill
|
||||
|
||||
|
||||
class Refillx(BaseRepository[RefillModel, Refill]):
|
||||
# Подключение модели пополнений
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = RefillModel
|
||||
self.entity_model = Refill
|
||||
self.storage_name = RefillModel.__tablename__
|
||||
|
||||
# Добавление записи пополнения
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
refill_receipt: int,
|
||||
refill_comment: str,
|
||||
refill_amount: float,
|
||||
refill_method: str,
|
||||
) -> Refill:
|
||||
return await self._insert(
|
||||
user_id=user_id,
|
||||
refill_comment=refill_comment,
|
||||
refill_amount=refill_amount,
|
||||
refill_receipt=refill_receipt,
|
||||
refill_method=refill_method,
|
||||
refill_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление пополнения по чеку или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"refill_receipt": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Успешное зачисление пополнения
|
||||
@staticmethod
|
||||
async def success(
|
||||
user_id: int,
|
||||
pay_receipt: int,
|
||||
pay_comment: str,
|
||||
pay_amount: float,
|
||||
pay_method: str,
|
||||
) -> str:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
await session.execute(text("BEGIN IMMEDIATE"))
|
||||
|
||||
if pay_comment != "":
|
||||
duplicate_refill = await session.execute(
|
||||
text("SELECT increment FROM storage_refill WHERE refill_comment = :comment LIMIT 1"),
|
||||
{"comment": pay_comment},
|
||||
)
|
||||
else:
|
||||
duplicate_refill = await session.execute(
|
||||
text("SELECT increment FROM storage_refill WHERE refill_receipt = :receipt LIMIT 1"),
|
||||
{"receipt": pay_receipt},
|
||||
)
|
||||
|
||||
if duplicate_refill.mappings().first() is not None:
|
||||
await session.rollback()
|
||||
return "ALREADY"
|
||||
|
||||
user_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT user_balance, user_refill
|
||||
FROM storage_users
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
get_user = user_result.mappings().first()
|
||||
|
||||
if get_user is None:
|
||||
await session.rollback()
|
||||
return "USER_NOT_FOUND"
|
||||
|
||||
new_balance = round(float(get_user["user_balance"]) + float(pay_amount), 2)
|
||||
new_refill = round(float(get_user["user_refill"]) + float(pay_amount), 2)
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO storage_refill (user_id,
|
||||
refill_receipt,
|
||||
refill_comment,
|
||||
refill_amount,
|
||||
refill_method,
|
||||
refill_unix)
|
||||
VALUES (:user_id, :receipt, :comment, :amount, :method, :unix)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"receipt": pay_receipt,
|
||||
"comment": pay_comment,
|
||||
"amount": pay_amount,
|
||||
"method": pay_method,
|
||||
"unix": get_unix(),
|
||||
},
|
||||
)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE storage_users
|
||||
SET user_balance = :balance,
|
||||
user_refill = :refill
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"balance": new_balance, "refill": new_refill, "user_id": user_id},
|
||||
)
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
return "ok"
|
||||
@@ -0,0 +1,76 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Settings
|
||||
from tgbot.database.repository import BaseRepository
|
||||
|
||||
|
||||
class SettingsModel(Base):
|
||||
__tablename__ = "storage_settings"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
status_work: Mapped[str] = mapped_column(String(16), nullable=False, default="True")
|
||||
status_refill: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_buy: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
notification_refill: Mapped[str] = mapped_column(String(16), nullable=False, default="True")
|
||||
notification_buy: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_faq: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_support: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_bot: Mapped[str] = mapped_column(String(255), nullable=False, default="None")
|
||||
misc_hosting_text: Mapped[str] = mapped_column(String(64), nullable=False, default="telegraph")
|
||||
misc_token_telegraph: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_discord_webhook_url: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_discord_webhook_name: Mapped[str] = mapped_column(String(255), nullable=False, default="None")
|
||||
misc_hide_category: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_hide_position: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_method_prod: Mapped[str] = mapped_column(String(16), nullable=False, default="skip")
|
||||
misc_profit_day: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
misc_profit_week: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
misc_profit_month: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
ModelBase = Settings
|
||||
BaseModel = Settings
|
||||
|
||||
|
||||
class Settingsx(BaseRepository[SettingsModel, Settings]):
|
||||
# Подключение модели настроек
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = SettingsModel
|
||||
self.entity_model = Settings
|
||||
self.storage_name = SettingsModel.__tablename__
|
||||
|
||||
# Создание дефолтной строки настроек
|
||||
async def ensure_default(self) -> None:
|
||||
settings = await BaseRepository.get(self, id=1)
|
||||
|
||||
if settings is None:
|
||||
await self._insert(id=1)
|
||||
|
||||
# Получение настроек бота
|
||||
async def get(self, **kwargs) -> Settings:
|
||||
if not kwargs:
|
||||
kwargs = {"id": 1}
|
||||
|
||||
settings = await BaseRepository.get(self, **kwargs)
|
||||
|
||||
if settings is None and kwargs == {"id": 1}:
|
||||
await self.ensure_default()
|
||||
settings = await BaseRepository.get(self, id=1)
|
||||
|
||||
if settings is None:
|
||||
raise RuntimeError("Настройки бота по умолчанию не сохранились")
|
||||
|
||||
return settings
|
||||
|
||||
# Обновление настроек бота
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if where is None:
|
||||
where = {"id": 1}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,128 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, or_
|
||||
from sqlalchemy import update as sqlalchemy_update
|
||||
from sqlalchemy.dialects.sqlite import insert
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import User
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class UserModel(Base):
|
||||
__tablename__ = "storage_users"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
|
||||
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_fullname: Mapped[str] = mapped_column(String(511), nullable=False, default="")
|
||||
user_balance: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_refill: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_give: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = User
|
||||
BaseModel = User
|
||||
|
||||
|
||||
class UsersRepository(BaseRepository[UserModel, User]):
|
||||
# Подключение модели пользователей
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = UserModel
|
||||
self.entity_model = User
|
||||
self.storage_name = UserModel.__tablename__
|
||||
|
||||
# Добавление или обновление пользователя
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
user_login: str,
|
||||
user_name: str,
|
||||
user_surname: str = "",
|
||||
user_fullname: str = "",
|
||||
) -> User:
|
||||
return await self.upsert(
|
||||
user_id=user_id,
|
||||
user_login=user_login,
|
||||
user_name=user_name,
|
||||
user_surname=user_surname,
|
||||
user_fullname=user_fullname or " ".join(filter(None, [user_name, user_surname])),
|
||||
)
|
||||
|
||||
# Выполнение upsert пользователя по телеграм ID
|
||||
async def upsert(
|
||||
self,
|
||||
user_id: int,
|
||||
user_login: str,
|
||||
user_name: str,
|
||||
user_surname: str = "",
|
||||
user_fullname: str = "",
|
||||
) -> User:
|
||||
user_fullname = user_fullname or " ".join(filter(None, [user_name, user_surname]))
|
||||
user_table = UserModel.__table__
|
||||
statement = insert(UserModel).values(
|
||||
user_id=user_id,
|
||||
user_login=(user_login or "").lower(),
|
||||
user_name=user_name or "",
|
||||
user_surname=user_surname or "",
|
||||
user_fullname=user_fullname or "",
|
||||
user_balance=0,
|
||||
user_refill=0,
|
||||
user_give=0,
|
||||
user_unix=get_unix(),
|
||||
)
|
||||
statement = statement.on_conflict_do_update(
|
||||
index_elements=[user_table.c.user_id],
|
||||
set_={
|
||||
"user_login": statement.excluded.user_login,
|
||||
"user_name": statement.excluded.user_name,
|
||||
"user_surname": statement.excluded.user_surname,
|
||||
"user_fullname": statement.excluded.user_fullname,
|
||||
},
|
||||
where=or_(
|
||||
user_table.c.user_login != statement.excluded.user_login,
|
||||
user_table.c.user_name != statement.excluded.user_name,
|
||||
user_table.c.user_surname != statement.excluded.user_surname,
|
||||
user_table.c.user_fullname != statement.excluded.user_fullname,
|
||||
),
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(statement)
|
||||
|
||||
user = await self.get(user_id=user_id)
|
||||
|
||||
if user is None:
|
||||
raise RuntimeError("Пользователь не сохранился")
|
||||
|
||||
return user
|
||||
|
||||
# Обновление пользователя по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"user_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Обновление пользователя по телеграм ID
|
||||
@staticmethod
|
||||
async def update_by_user_id(user_id: int, **kwargs) -> None:
|
||||
if not kwargs:
|
||||
return
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(
|
||||
sqlalchemy_update(UserModel)
|
||||
.where(UserModel.__table__.c.user_id == user_id)
|
||||
.values(**kwargs)
|
||||
)
|
||||
|
||||
|
||||
Userx = UsersRepository
|
||||
@@ -0,0 +1,110 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Category:
|
||||
increment: int
|
||||
category_id: int
|
||||
category_name: str
|
||||
category_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
increment: int
|
||||
category_id: int
|
||||
position_id: int
|
||||
position_name: str
|
||||
position_price: float
|
||||
position_desc: str
|
||||
position_photo: str
|
||||
position_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
increment: int
|
||||
user_id: int
|
||||
category_id: int
|
||||
position_id: int
|
||||
item_id: int
|
||||
item_unix: int
|
||||
item_data: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Payments:
|
||||
id: int
|
||||
cryptobot_token: str
|
||||
yoomoney_token: str
|
||||
stars_course: float
|
||||
status_cryptobot: str
|
||||
status_yoomoney: str
|
||||
status_stars: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Purchase:
|
||||
increment: int
|
||||
user_id: int
|
||||
user_balance_before: float
|
||||
user_balance_after: float
|
||||
purchase_receipt: int
|
||||
purchase_data: str
|
||||
purchase_count: int
|
||||
purchase_price: float
|
||||
purchase_price_one: float
|
||||
purchase_position_id: int
|
||||
purchase_position_name: str
|
||||
purchase_category_id: int
|
||||
purchase_category_name: str
|
||||
purchase_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Refill:
|
||||
increment: int
|
||||
user_id: int
|
||||
refill_receipt: int
|
||||
refill_comment: str
|
||||
refill_amount: float
|
||||
refill_method: str
|
||||
refill_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
id: int
|
||||
status_work: str
|
||||
status_refill: str
|
||||
status_buy: str
|
||||
notification_refill: str
|
||||
notification_buy: str
|
||||
misc_faq: str
|
||||
misc_support: str
|
||||
misc_bot: str
|
||||
misc_hosting_text: str
|
||||
misc_token_telegraph: str
|
||||
misc_discord_webhook_url: str
|
||||
misc_discord_webhook_name: str
|
||||
misc_hide_category: str
|
||||
misc_hide_position: str
|
||||
misc_method_prod: str
|
||||
misc_profit_day: int
|
||||
misc_profit_week: int
|
||||
misc_profit_month: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
increment: int
|
||||
user_id: int
|
||||
user_login: str
|
||||
user_name: str
|
||||
user_surname: str
|
||||
user_fullname: str
|
||||
user_balance: float
|
||||
user_refill: float
|
||||
user_give: float
|
||||
user_unix: int
|
||||
@@ -0,0 +1,55 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from tgbot.database.core import database_url
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
ALEMBIC_INI = PROJECT_ROOT / "alembic.ini"
|
||||
MIGRATIONS_DIR = PROJECT_ROOT / "migrations"
|
||||
|
||||
|
||||
# Сбор Alembic-конфига от корня проекта
|
||||
def get_alembic_config(url: str = database_url) -> Config:
|
||||
config = Config(str(ALEMBIC_INI))
|
||||
config.set_main_option("script_location", str(MIGRATIONS_DIR))
|
||||
config.set_main_option("sqlalchemy.url", url)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# Применение миграций до последней версии
|
||||
async def run_migrations(engine: Optional[AsyncEngine] = None) -> None:
|
||||
if engine is None:
|
||||
config = get_alembic_config()
|
||||
await asyncio.to_thread(command.upgrade, config, "head")
|
||||
return
|
||||
|
||||
config = get_alembic_config(str(engine.url))
|
||||
|
||||
connection = engine.connect()
|
||||
await connection.start()
|
||||
transaction = connection.begin()
|
||||
await transaction.start()
|
||||
|
||||
try:
|
||||
await connection.run_sync(_upgrade_with_connection, config)
|
||||
except Exception:
|
||||
await transaction.rollback()
|
||||
raise
|
||||
else:
|
||||
await transaction.commit()
|
||||
finally:
|
||||
await connection.close()
|
||||
|
||||
|
||||
# Запуск Alembic на готовом соединении
|
||||
def _upgrade_with_connection(connection: Connection, config: Config) -> None:
|
||||
config.attributes["connection"] = connection
|
||||
command.upgrade(config, "head")
|
||||
@@ -0,0 +1,162 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import fields
|
||||
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import update as sqlalchemy_update
|
||||
from tgbot.database.core import Base, database_path, session_scope
|
||||
from tgbot.database.migration_runner import run_migrations
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
|
||||
ModelTranslator = TypeVar("ModelTranslator", bound=Base)
|
||||
EntityTranslator = TypeVar("EntityTranslator")
|
||||
|
||||
|
||||
# Базовый репозиторий для SQLAlchemy-моделей
|
||||
class BaseRepository(Generic[ModelTranslator, EntityTranslator]):
|
||||
# Настройка модели репозитория
|
||||
def __init__(self):
|
||||
self.storage_name = "storage"
|
||||
self.table_model: Optional[Type[ModelTranslator]] = None
|
||||
self.entity_model: Optional[Type[EntityTranslator]] = None
|
||||
|
||||
# Получение SQLAlchemy-модели репозитория
|
||||
def _model(self) -> Type[ModelTranslator]:
|
||||
if self.table_model is None:
|
||||
raise RuntimeError("Модель базы данных не настроена")
|
||||
|
||||
return self.table_model
|
||||
|
||||
# Преобразование ORM-строки в безопасный DTO
|
||||
def _to_entity(self, instance: ModelTranslator) -> EntityTranslator:
|
||||
if self.entity_model is None:
|
||||
raise RuntimeError("DTO-модель базы данных не настроена")
|
||||
|
||||
values = {
|
||||
field.name: getattr(instance, field.name)
|
||||
for field in fields(self.entity_model)
|
||||
}
|
||||
|
||||
return self.entity_model(**values)
|
||||
|
||||
# Добавление записи через текущую модель
|
||||
async def _insert(self, **kwargs) -> EntityTranslator:
|
||||
model = self._model()
|
||||
instance = model(**kwargs)
|
||||
|
||||
async with session_scope() as session:
|
||||
session.add(instance)
|
||||
await session.flush()
|
||||
await session.refresh(instance)
|
||||
|
||||
return self._to_entity(instance)
|
||||
|
||||
# Добавление записи с возвратом DTO
|
||||
async def add(self, **kwargs) -> EntityTranslator:
|
||||
return await self._insert(**kwargs)
|
||||
|
||||
# Удаление записи по явному фильтру
|
||||
async def delete(self, **kwargs) -> None:
|
||||
if not kwargs:
|
||||
raise ValueError("Для удаления нужен хотя бы один фильтр")
|
||||
|
||||
model = self._model()
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(sqlalchemy_delete(model).filter_by(**kwargs))
|
||||
|
||||
# Очистка текущей таблицы
|
||||
async def clear(self) -> None:
|
||||
await self.delete_all_rows()
|
||||
|
||||
# Удаление всех строк текущей таблицы
|
||||
async def delete_all_rows(self) -> None:
|
||||
model = self._model()
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(sqlalchemy_delete(model))
|
||||
|
||||
# Получение первой записи по фильтру
|
||||
async def get(self, **kwargs) -> Optional[EntityTranslator]:
|
||||
model = self._model()
|
||||
statement = select(model)
|
||||
|
||||
if kwargs:
|
||||
statement = statement.filter_by(**kwargs)
|
||||
|
||||
if hasattr(model, "increment"):
|
||||
statement = statement.order_by(getattr(model, "increment"))
|
||||
|
||||
async with session_scope() as session:
|
||||
response = await session.execute(statement)
|
||||
instance = response.scalars().first()
|
||||
|
||||
if instance is None:
|
||||
return None
|
||||
|
||||
return self._to_entity(instance)
|
||||
|
||||
# Получение записи или явно падает
|
||||
async def get_required(self, **kwargs) -> EntityTranslator:
|
||||
entity = await self.get(**kwargs)
|
||||
|
||||
if entity is None:
|
||||
raise LookupError(f"Запись не найдена в {self.storage_name}: {kwargs}")
|
||||
|
||||
return entity
|
||||
|
||||
# Получение списка записей по фильтру
|
||||
async def gets(self, **kwargs) -> List[EntityTranslator]:
|
||||
model = self._model()
|
||||
statement = select(model)
|
||||
|
||||
if kwargs:
|
||||
statement = statement.filter_by(**kwargs)
|
||||
|
||||
if hasattr(model, "increment"):
|
||||
statement = statement.order_by(getattr(model, "increment"))
|
||||
|
||||
async with session_scope() as session:
|
||||
response = await session.execute(statement)
|
||||
|
||||
return [self._to_entity(instance) for instance in response.scalars().all()]
|
||||
|
||||
# Получение всех записей таблицы
|
||||
async def get_all(self) -> List[EntityTranslator]:
|
||||
return await self.gets()
|
||||
|
||||
# Обновление записи через текущую модель
|
||||
async def _update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if not kwargs:
|
||||
return 0
|
||||
|
||||
model = self._model()
|
||||
statement = sqlalchemy_update(model).values(**kwargs)
|
||||
|
||||
if where:
|
||||
statement = statement.filter_by(**where)
|
||||
|
||||
async with session_scope() as session:
|
||||
result = await session.execute(statement)
|
||||
rowcount = getattr(result, "rowcount", 0)
|
||||
|
||||
return int(rowcount or 0)
|
||||
|
||||
# Обновление записи по фильтру
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
|
||||
# Применение миграций и создание дефолтных строк
|
||||
async def prepare_database() -> None:
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await run_migrations()
|
||||
|
||||
from tgbot.database.db_payments import Paymentsx
|
||||
from tgbot.database.db_settings import Settingsx
|
||||
|
||||
await Settingsx().ensure_default()
|
||||
await Paymentsx().ensure_default()
|
||||
|
||||
bot_logger.info("База данных готова")
|
||||
Reference in New Issue
Block a user