mirror of
https://github.com/djimboy/djimbo_template_aio3.git
synced 2026-07-25 09:44:29 +00:00
Update aiogram 3 template
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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',
|
||||
# },
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
@@ -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("База данных готова")
|
||||
Reference in New Issue
Block a user