Update aiogram 3 template

This commit is contained in:
dj
2026-05-29 09:44:39 +03:00
parent e144cb1424
commit 2eced5dc4f
43 changed files with 1877 additions and 1252 deletions
+5 -2
View File
@@ -1,2 +1,5 @@
from .db_settings import Settingsx, ModelBase as ModelSettings
from .db_users import Usersx, ModelBase as ModelUsers
from .db_settings import SettingsModel, SettingsRepository
from .db_users import UserModel, UsersRepository
ModelSettings = SettingsModel
ModelUsers = UserModel
-494
View File
@@ -1,494 +0,0 @@
# - *- coding: utf- 8 - *-
import sqlite3
from typing import Any, Generic, TypeVar, Optional
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefinedType
from tgbot.data.config import PATH_DATABASE
from tgbot.database.adb_migration import CONST_MIGRATE_TABLES, CONST_MIGRATE_COLUMNS
from tgbot.utils.const_functions import ded
# noinspection PyProtectedMember
################################################################################
############################### SUPPORT FUNCTIONS ##############################
# Converting the resulting list into a dictionary
def dict_factory(cursor, row) -> dict:
save_dict = {}
for idx, col in enumerate(cursor.description):
save_dict[col[0]] = row[idx]
return save_dict
# Formatting a query without arguments
def update_format(sql, parameters: dict) -> tuple[str, list]:
values = ", ".join([
f"{item} = ?" for item in parameters
])
sql += f" {values}"
return sql, list(parameters.values())
# Formatting a query with arguments
def update_format_where(sql, parameters: dict) -> tuple[str, list]:
sql += " WHERE "
sql += " AND ".join([
f"{item} = ?" for item in parameters
])
return sql, list(parameters.values())
################################################################################
################# BASIC CLASS FOR WORKING WITH DATABASE TABLES #################
ModelTranslator = TypeVar("ModelTranslator", bound="ModelBase")
class Databasex(Generic[ModelTranslator]):
def __init__(self):
self.storage_name = "storage"
self.column_one = False
self.table_model = None
# Delete entry
def delete(self, **kwargs):
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"DELETE FROM {self.storage_name}"
sql, parameters = update_format_where(sql, kwargs)
con.execute(sql, parameters)
# Clear all entries
def clear(self):
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"DELETE FROM {self.storage_name}"
con.execute(sql)
# Get entry
def get(self, **kwargs) -> Optional[ModelTranslator]:
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"SELECT * FROM {self.storage_name}"
sql, parameters = update_format_where(sql, kwargs)
response = con.execute(sql, parameters).fetchone()
if response is not None:
response = self.table_model(**response)
return response
# Get entries
def gets(self, **kwargs) -> list[Optional[ModelTranslator]]:
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"SELECT * FROM {self.storage_name}"
sql, parameters = update_format_where(sql, kwargs)
response = con.execute(sql, parameters).fetchall()
return [self.table_model(**cache_object) for cache_object in response]
# Get all entries
def get_all(self) -> list[Optional[ModelTranslator]]:
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"SELECT * FROM {self.storage_name}"
response = con.execute(sql).fetchall()
return [self.table_model(**cache_object) for cache_object in response]
# Edit entry
def update(self, **kwargs):
...
# Export column names, default data, and type hints
def fields(self) -> dict[str, list[dict[str, Any]]]:
for field_name, field_data in self.table_model.model_fields.items():
field_name: str
field_data: FieldInfo
column_data = [
{
'column': field_name,
'type': field_data.annotation,
'value': field_data.default,
} for field_name, field_data in self.table_model.model_fields.items()
]
return {'column_data': column_data, 'column_one': self.column_one}
################################################################################
################################################################################
# Export all tables from the Database to a list
def database_export_fields_db() -> list:
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
database_tables = con.execute("SELECT * FROM sqlite_master where type='table'").fetchall()
database_tables_db = [table['name'] for table in database_tables]
# If there are service tables, remove them from the list of available tables.
if "sqlite_sequence" in database_tables_db:
database_tables_db.remove("sqlite_sequence")
if "sqlite_master" in database_tables_db:
database_tables_db.remove("sqlite_master")
return database_tables_db
# Export all tables, columns, and typehints from structures
def database_export_fields_structure() -> dict:
# Import all table models
import tgbot.database as import_modeles
# Storing all structure files (db_X.py) in a single list
all_tables = [
getattr(import_modeles, name)
for name in dir(import_modeles)
if (
isinstance(getattr(import_modeles, name), type) and
(name.istitle() and name.endswith("x"))
)
]
save_tables = {} # Storage of all sorted and converted tables/columns
# Iterating through tables and columns
for table in all_tables:
table_fields = table().fields()
column_data = table_fields['column_data']
column_one = table_fields['column_one']
cache_columns = []
# If the table name is not in the dictionary, add it.
if table().storage_name not in save_tables:
save_tables[table().storage_name] = {
'column_one': column_one,
'column_datas': [],
}
# Column sorting
for table_field in column_data:
column_name = table_field['column']
column_type = table_field['type'].__name__
column_value = table_field['value']
# Replacing python date types with SQL
column_type = column_type.replace(
"str", "TEXT",
).replace(
"int", "INTEGER",
).replace(
"float", "REAL",
)
# If there is no default value, replace it with - None
if isinstance(column_value, PydanticUndefinedType):
column_value = None
# Default values based on type hints
if column_type == "TEXT":
if "status" in column_name.lower():
column_value = "False"
else:
column_value = "None"
elif column_type == "INTEGER":
column_value = 0
elif column_type == "REAL":
column_value = 0.0
else:
column_value = table_field['value']
save_tables[table().storage_name]['column_datas'].append(
{
'column': column_name,
'type': column_type,
'value': column_value,
}
)
return save_tables
################################################################################
################################################################################
# Database migration
def database_migration():
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
# If there are values for tables or columns, perform migration.
if len(CONST_MIGRATE_TABLES) > 0 or len(CONST_MIGRATE_COLUMNS) > 0:
print(f"|| Migrating | Data was found")
# Export existing tables in the Database
database_tables_db = database_export_fields_db()
# Renaming tables - iterating through tables
for table_name_later, table_name_new in CONST_MIGRATE_TABLES.items():
if table_name_later in database_tables_db:
print(f"|| Migrating | Table '{table_name_later}' renamed to '{table_name_new}' (✓)")
con.execute(f"ALTER TABLE {table_name_later} RENAME TO {table_name_new}")
else:
print(f"|| Migrating | Table '{table_name_later}' for renamed not found (X)")
# Renaming columns - iterating through tables with columns inside
for table_name in CONST_MIGRATE_COLUMNS:
if table_name in database_tables_db:
# Unloading existing columns from the current table
columns_export = [
column['name'] for column in con.execute(f"PRAGMA table_info({table_name})").fetchall()
]
# Browsing columns to rename them
for column_name_later, column_name_new in CONST_MIGRATE_COLUMNS[table_name].items():
if column_name_later in columns_export:
print(
f"|| Migrating | Column '{column_name_later}' renamed to '{column_name_new}' from table '{table_name}' (✓)"
)
con.execute(
f"ALTER TABLE {table_name} RENAME COLUMN {column_name_later} TO {column_name_new}"
)
else:
print(
f"|| Migrating | Column '{column_name_later}' from table '{table_name}' not found (X)"
)
else:
print(f"|| Migrating | Table '{table_name}' for renamed column(s) not found (X)")
else:
print(f"|| Migrating | Data not found")
print(f"||")
# Checking the relevance of columns (adding new ones, deleting old ones)
def database_check_columns():
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
symbol_log = True
# Exporting tables and columns from structures
fields_structure = database_export_fields_structure()
for number, field in enumerate(fields_structure.items()):
table_name: str = field[0] # Table name
table_data: dict = field[1] # Table data
column_datas = table_data['column_datas'] # Column name, type, and value
columns_cache_add = [] # Columns that will be added to the database
columns_cache_edit_columns = [] # Columns whose date types will be changed in the database
columns_cache_edit_data = None # Which table columns will change date types in the database?
# Unloading existing columns from the current table
column_datas_types_db = [column for column in con.execute(f"PRAGMA table_info({table_name})").fetchall()]
# Unloading existing column names from the current table
columns_export_db = [column['name'] for column in column_datas_types_db]
# Iterating through columns from a structure
for column_structure in column_datas:
# If a column from the structure is present in the database table
if column_structure['column'] in columns_export_db:
columns_export_db.remove(column_structure['column'])
# Date type of the current column in the Structure
column_type_structure = column_structure['type']
# Date type of the current column in the database
column_type_database = [
column for column in con.execute(f"PRAGMA table_info({table_name})").fetchall() if
column_structure['column'] == column['name']
]
# If the date column type (DB) is in the list, extract it.
if len(column_type_database) > 0:
column_type_database = column_type_database[0]['type']
# If the date types differ and have not been saved before, add them to the dictionary.
if column_type_structure != column_type_database:
if columns_cache_edit_data is None:
columns_cache_edit_data = column_datas
if column_structure['column'] not in columns_cache_edit_columns:
columns_cache_edit_columns.append(
f"{column_structure['column']}: {column_type_database} -> {column_type_structure} "
)
else:
# If a column from the structure is not present in the database, save it for addition.
columns_cache_add.append(column_structure)
# The remaining columns that are present in the database table but are missing from the structure
columns_cache_delete = columns_export_db
# Adding new columns
for column in columns_cache_add:
print(f"|| Modification | Column '{column['column']}' added to table '{table_name}'")
con.execute(
f"ALTER TABLE {table_name} ADD COLUMN {column['column']} {column['type']} DEFAULT '{column['value']}'"
)
# Deleting existing obsolete columns
for column in columns_cache_delete:
print(f"|| Modification | Column '{column}' deleted from table '{table_name}'")
con.execute(f"ALTER TABLE {table_name} DROP COLUMN {column}")
# Changing the date types of columns in a table
sql_edit_columns_create_table = []
sql_edit_columns_insert_values = []
columns_cache_edit_columns = ", ".join(columns_cache_edit_columns)
if columns_cache_edit_data is not None:
for column in columns_cache_edit_data:
if column['type'] == "TEXT":
sql_edit_columns_create_table.append(
f"{column['column']} {column['type']} DEFAULT '{column['value']}'"
)
else:
sql_edit_columns_create_table.append(
f"{column['column']} {column['type']} DEFAULT {column['value']}"
)
sql_edit_columns_insert_values.append(column['column'])
sql_edit_columns_create_table = ", ".join(sql_edit_columns_create_table)
sql_edit_columns_create_table = f"CREATE TABLE {table_name}_new ({sql_edit_columns_create_table})"
sql_edit_columns_insert_values = ", ".join(sql_edit_columns_insert_values)
sql_edit_columns_insert_values = f"INSERT INTO {table_name}_new ({sql_edit_columns_insert_values}) SELECT {sql_edit_columns_insert_values} FROM {table_name}"
con.execute(sql_edit_columns_create_table) # Creating a new table
con.execute(sql_edit_columns_insert_values) # Transferring data from old columns to new ones
con.execute(f"DROP TABLE {table_name};")
con.execute(f"ALTER TABLE {table_name}_new RENAME TO {table_name};")
print(
f"|| Modification | Columns '{columns_cache_edit_columns}' was edit data type for table '{table_name}'"
)
# Creating/deleting tables, columns, and records
def database_initialization():
print("======== DATABASE PROCESSING ========")
database_migration() # Migration check
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
# List of exported tables and columns from structures
database_fields = database_export_fields_structure()
# List of exported tables from the structure
database_tables_import = [table for table in database_fields.keys()]
# List of tables available in the Database
database_tables_db = database_export_fields_db()
# List of tables to be deleted that are not present in imports but are present in the database
database_tables_delete = [
table for table in database_tables_db if table not in database_tables_import
]
# Browse the list of imported tables, columns, and default records to create them.
for number, db_field in enumerate(database_fields.items()):
table_name: str = db_field[0] # Table name
table_data: dict = db_field[1] # Table data
column_one = table_data['column_one'] # Will the table have one record or more
column_datas = table_data['column_datas'] # Column name, data type, and value
column_len = len(column_datas) # Length (number) of all columns in the table
# Checking for the required number of records in a table
if len(con.execute(f"PRAGMA table_info({table_name})").fetchall()) == column_len:
print(f"|| Creating | Table '{table_name}' was found ({number + 1}/{len(database_fields)})")
else:
print(f"|| Creating | Table '{table_name}' not found ({number + 1}/{len(database_fields)})")
# Column names with type hints (user_id INTEGER, user_name TEXT)
columns_cache_create = []
# Column names without type hints (user_id, user_name)
columns_cache_insert = []
# Default data for automatic entry into columns (settings, payment systems)
columns_values = []
############################################################
##################### CREATING TABLES ######################
for column in column_datas:
column_name = column['column'] # Table name
column_value = column['value'] # Default value in column
column_type = column['type'] # Entry data type (INTEGER, TEXT, REAL, ...)
# Incrementable entry check
if column_name == "increment":
column_execute_create = "increment INTEGER PRIMARY KEY AUTOINCREMENT"
else:
column_execute_create = f"{column_name} {column_type}"
columns_cache_insert.append(column_name)
columns_cache_create.append(column_execute_create)
# If default data is available, add it.
if column_one and column_value is not None:
columns_values.append(column_value)
# Collecting a request with all added columns
column_execute_create = ",\n".join(columns_cache_create)
column_execute_create = ded(f"""
CREATE TABLE IF NOT EXISTS {table_name}(
{column_execute_create}
)
""")
# Request to create a table and columns
con.execute(column_execute_create)
############################################################
####################### ADDING ENTRY #######################
# If there is a record to add and the table supports only one record
if column_one and len(columns_values) > 0:
column_check_have = con.execute(f"SELECT * FROM {table_name}").fetchall()
# If there is no entry in the table, add it.
if len(column_check_have) == 0:
# Request collection
column_execute_insert = ",\n".join(columns_cache_insert)
column_execute_insert = ded(f"""
INSERT INTO {table_name}(
{column_execute_insert}
)
VALUES ({"?," * (len(columns_cache_insert) - 1) + "?"})
""")
# Request to add entries to the table
con.execute(column_execute_insert, columns_values)
print(f"|| Creating | Table '{table_name}' was created ({number + 1}/{len(database_fields)})")
# If there are tables to be deleted, delete them.
if len(database_tables_delete) > 0:
for table_name in database_tables_delete:
print(f"|| Database | Table '{table_name}' was deleted")
con.execute(f"DROP TABLE {table_name}")
print("||")
database_check_columns() # Checking the relevance of columns
print("=====================================")
print()
-36
View File
@@ -1,36 +0,0 @@
# Example with migration - Tables
#
# CONST_MIGRATE_TABLES = {
# 'old_table_name1': 'new_table_name1',
# 'old_table_name2': 'new_table_name2',
# }
# Example with migration - Columns
# CONST_MIGRATE_COLUMNS = {
# 'name_of_the_table_in_which_the_column_is_located1': {
# 'old_column_name11': 'new_column_name11',
# 'old_column_name12': 'new_column_name12',
# },
# 'name_of_the_table_in_which_the_column_is_located2': {
# 'old_column_name21': 'new_column_name21',
# 'old_column_name22': 'new_column_name22',
# },
# }
################################################################################
############################### MIGRATION TABLES ###############################
CONST_MIGRATE_TABLES = {
# 'storage_refillme': 'storage_refill',
# 'storage_purchasesme': 'storage_purchases',
}
################################################################################
############################### MIGRATION COLUMNS ##############################
CONST_MIGRATE_COLUMNS = {
# 'storage_refill': {
# 'refill_methodme': 'refill_method',
# 'refill_amountme': 'refill_amount',
# },
}
+51
View File
@@ -0,0 +1,51 @@
# - *- coding: utf- 8 - *-
from collections.abc import AsyncIterator
from contextlib import AbstractAsyncContextManager, 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 по умолчанию не включает foreign keys, поэтому включаем явно
@event.listens_for(engine.sync_engine, "connect")
def _enable_sqlite_foreign_keys(dbapi_connection, connection_record) -> None:
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
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
# Отдельная обертка, чтобы IDE нормально видела асинхронный контекст
def session_scope() -> AbstractAsyncContextManager[AsyncSession]:
return _session_scope()
# Закрываем пул соединений при остановке приложения
async def close_database() -> None:
await engine.dispose()
+48 -30
View File
@@ -1,43 +1,61 @@
# - *- coding: utf- 8 - *-
import sqlite3
from sqlalchemy import Boolean, Integer
from sqlalchemy import update as sqlalchemy_update
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.orm import Mapped, mapped_column
from pydantic import BaseModel
from tgbot.data.config import PATH_DATABASE
from tgbot.database.adb_helper import Databasex, dict_factory, update_format
from tgbot.database.core import Base, session_scope
from tgbot.database.repository import BaseRepository
# Table model
class ModelBase(BaseModel):
status_work: str # Status bot work
# Модель настроек бота
class SettingsModel(Base):
__tablename__ = "storage_settings"
class Config:
json_schema_extra = {
'storage_name': 'storage_settings'
}
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
status_work: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
# Settings
class Settingsx(Databasex[ModelBase]):
ModelBase = SettingsModel
BaseModel = SettingsModel
# Репозиторий настроек бота
class SettingsRepository(BaseRepository[SettingsModel]):
def __init__(self):
super().__init__()
self.table_model = ModelBase
self.storage_name = self.table_model.model_json_schema()['storage_name']
self.column_one = True
self.table_model = SettingsModel
self.storage_name = SettingsModel.__tablename__
# Get entry
def get(self) -> ModelBase:
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"SELECT * FROM {self.storage_name}"
# Создание строки настроек если их еще нет
async def ensure_default(self) -> None:
statement = insert(SettingsModel).values(id=1, status_work=False)
statement = statement.on_conflict_do_nothing(index_elements=[SettingsModel.id])
return ModelBase(**con.execute(sql).fetchone())
async with session_scope() as session:
await session.execute(statement)
# Edit entry
def update(self, **kwargs):
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"UPDATE {self.storage_name} SET"
sql, parameters = update_format(sql, kwargs)
# Настройки должны быть всегда, поэтому при пустой таблице создаем дефолт
async def get(self) -> SettingsModel:
settings = await super().get(id=1)
con.execute(sql, parameters)
if settings is None:
await self.ensure_default()
settings = await super().get(id=1)
if settings is None:
raise RuntimeError("Настройки бота по умолчанию не сохранились")
return settings
# Обновление единственной строки настроек
async def update(self, **kwargs) -> None:
if not kwargs:
return
async with session_scope() as session:
await session.execute(
sqlalchemy_update(SettingsModel)
.where(SettingsModel.id == 1)
.values(**kwargs)
)
+87 -59
View File
@@ -1,78 +1,106 @@
# - *- coding: utf- 8 - *-
import sqlite3
from sqlalchemy import BigInteger, Integer, String, or_
from sqlalchemy import update as sqlalchemy_update
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.orm import Mapped, mapped_column
from pydantic import BaseModel
from tgbot.data.config import PATH_DATABASE
from tgbot.database.adb_helper import Databasex, dict_factory, update_format
from tgbot.utils.const_functions import get_unix, ded
from tgbot.database.core import Base, session_scope
from tgbot.database.repository import BaseRepository
from tgbot.utils.const_functions import get_unix
# Table model
class ModelBase(BaseModel):
increment: int # increment
user_id: int # User id
user_login: str # User username
user_name: str # User name
user_surname: str # User surname
user_fullname: str # User fullname (name + surname)
user_unix: int # Date of user registration in the bot (in UNIX time)
# Модель пользователя Telegram
class UserModel(Base):
__tablename__ = "storage_users"
class Config:
json_schema_extra = {
'storage_name': '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_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
# Users
class Usersx(Databasex[ModelBase]):
ModelBase = UserModel
BaseModel = UserModel
# Репозиторий пользователей
class UsersRepository(BaseRepository[UserModel]):
def __init__(self):
super().__init__()
self.table_model = ModelBase
self.storage_name = self.table_model.model_json_schema()['storage_name']
self.column_one = False
self.table_model = UserModel
self.storage_name = UserModel.__tablename__
# Add entry
def add(
# Для совместимости add ведет себя как upsert
async def add(
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str,
user_fullname: str,
):
user_unix = get_unix()
) -> UserModel:
return await self.upsert(
user_id=user_id,
user_login=user_login,
user_name=user_name,
user_surname=user_surname,
user_fullname=user_fullname,
)
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
# Создание пользователя или обновление его данных по user_id
async def upsert(
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str,
user_fullname: str,
) -> UserModel:
statement = insert(UserModel).values(
user_id=user_id,
user_login=user_login,
user_name=user_name,
user_surname=user_surname,
user_fullname=user_fullname,
user_unix=get_unix(),
)
statement = statement.on_conflict_do_update(
index_elements=[UserModel.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_(
UserModel.user_login != statement.excluded.user_login,
UserModel.user_name != statement.excluded.user_name,
UserModel.user_surname != statement.excluded.user_surname,
UserModel.user_fullname != statement.excluded.user_fullname,
),
)
con.execute(
ded(f"""
INSERT INTO {self.storage_name} (
user_id,
user_login,
user_name,
user_surname,
user_fullname,
user_unix
) VALUES (?, ?, ?, ?, ?, ?)
"""),
[
user_id,
user_login,
user_name,
user_surname,
user_fullname,
user_unix,
],
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
# Обновление пользователя по Telegram ID
async def update(self, user_id: int, **kwargs) -> None:
if not kwargs:
return
async with session_scope() as session:
await session.execute(
sqlalchemy_update(UserModel)
.where(UserModel.user_id == user_id)
.values(**kwargs)
)
# Edit entry
def update(self, user_id: int, **kwargs):
with sqlite3.connect(PATH_DATABASE) as con:
con.row_factory = dict_factory
sql = f"UPDATE {self.storage_name} SET"
sql, parameters = update_format(sql, kwargs)
parameters.append(user_id)
con.execute(sql + "WHERE user_id = ?", parameters)
+59
View File
@@ -0,0 +1,59 @@
# - *- coding: utf- 8 - *-
import asyncio
from contextlib import AbstractAsyncContextManager
from pathlib import Path
from typing import Optional
from alembic import command
from alembic.config import Config
from sqlalchemy.ext.asyncio import AsyncConnection, 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()
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, command.upgrade, config, "head")
return
config = get_alembic_config(str(engine.url))
async with engine_context(engine) as connection:
await connection.run_sync(_upgrade_with_connection, config)
# Отдельная обертка нужна, чтобы IDE корректно видела async context manager
def engine_context(engine: AsyncEngine) -> AbstractAsyncContextManager[AsyncConnection]:
return engine.begin()
# Alembic умеет работать с синхронным соединением внутри async-engine
def _upgrade_with_connection(connection, config: Config) -> None:
config.attributes["connection"] = connection
command.upgrade(config, "head")
# Ручной запуск миграций из консоли
async def _main() -> None:
await run_migrations()
print("Миграции базы данных применены")
if __name__ == "__main__":
asyncio.run(_main())
+106
View File
@@ -0,0 +1,106 @@
# - *- coding: utf- 8 - *-
from typing import Generic, List, Optional, Type, TypeVar
from sqlalchemy import delete as sqlalchemy_delete
from sqlalchemy import select
from sqlalchemy import update as sqlalchemy_update
from sqlalchemy.exc import SQLAlchemyError
from tgbot.database.core import Base, database_path, session_scope
from tgbot.utils.misc.bot_logging import bot_logger
ModelTranslator = TypeVar("ModelTranslator", bound=Base)
# Базовый репозиторий с общими методами работы с БД
class BaseRepository(Generic[ModelTranslator]):
def __init__(self):
self.storage_name = "storage"
self.table_model: Optional[Type[ModelTranslator]] = None
# Без модели репозиторий работать не должен
def _model(self) -> Type[ModelTranslator]:
if self.table_model is None:
raise RuntimeError("Модель базы данных не настроена")
return self.table_model
# Удаление только по явному фильтру, без случайной чистки всей таблицы
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 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[ModelTranslator]:
model = self._model()
statement = select(model).filter_by(**kwargs)
async with session_scope() as session:
response = await session.execute(statement)
return response.scalars().first()
# Возвращение всех записей по фильтру
async def gets(self, **kwargs) -> List[ModelTranslator]:
model = self._model()
statement = select(model).filter_by(**kwargs)
async with session_scope() as session:
response = await session.execute(statement)
return list(response.scalars().all())
# Возвращение всей таблицы
async def get_all(self) -> List[ModelTranslator]:
model = self._model()
async with session_scope() as session:
response = await session.execute(select(model))
return list(response.scalars().all())
# Обновляем только записи, которые попали под фильтр
async def update(self, filters: dict, **kwargs) -> None:
if not filters:
raise ValueError("Для обновления нужен хотя бы один фильтр")
if not kwargs:
return
model = self._model()
async with session_scope() as session:
await session.execute(
sqlalchemy_update(model)
.filter_by(**filters)
.values(**kwargs)
)
# Готовим подключение к БД. Сами миграции запускаются отдельно через migrate.py.
async def prepare_database() -> None:
database_path.parent.mkdir(parents=True, exist_ok=True)
# Импортируем модели, чтобы репозитории работали с уже загруженными таблицами
import tgbot.database # noqa: F401
from tgbot.database.db_settings import SettingsRepository
try:
await SettingsRepository().ensure_default()
except SQLAlchemyError as error:
raise RuntimeError("База данных не готова. Запусти миграции командой: python migrate.py") from error
bot_logger.info("База данных готова")