├── README.md ├── alembic.ini ├── bot.ini.example ├── bot.py ├── requirements.txt ├── systemd └── tgbot.service └── tgbot ├── __init__.py ├── callback_data ├── __init__.py └── callback_datas.py ├── config.py ├── data ├── __init__.py └── data.py ├── handlers ├── __init__.py ├── commands │ ├── __init__.py │ └── start.py └── query_handlers │ ├── __init__.py │ └── language_handler.py ├── keyboards ├── __init__.py ├── default │ └── __init__.py └── inline │ ├── __init__.py │ └── language.py ├── locales ├── messages.pot ├── ru │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po └── uz │ └── LC_MESSAGES │ ├── messages.mo │ └── messages.po ├── middlewares ├── __init__.py ├── db.py └── language.py ├── misc ├── __init__.py └── req_func.py ├── models ├── __init__.py ├── base.py ├── migrations │ ├── README │ ├── __init__.py │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── __init__.py ├── role.py └── user.py └── service ├── __init__.py └── repo ├── __init__.py ├── base_repo.py ├── repository.py └── user_repo.py /README.md: -------------------------------------------------------------------------------- 1 | # Aiogram bot template 2 | This is a simple aiogram bot template using PostgreSQL as database 3 | ``` 4 | ⚠️ Make sure you have a Postgresql database installed 5 | ``` 6 | 1. Rename `bot.ini.example` to `bot.ini` 7 | 2. Install requirements using: 8 | ``` 9 | pip install -r requirements.txt 10 | ``` 11 | 3. Create database in psql 12 | 4. Make migrations using Alembic 13 | ``` 14 | alembic revision --autogenerate -m "init" 15 | ``` 16 | ``` 17 | alembic upgrade head 18 | ``` 19 | 5. To add new language - add language key in ```tgbot/data/data.py``` 20 | 6. Add your logic to own bot 21 | 22 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = tgbot/models/migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date within the migration file 15 | # as well as the filename. 16 | # If specified, requires the python-dateutil library that can be 17 | # installed by adding `alembic[tz]` to the pip requirements 18 | # string value is passed to dateutil.tz.gettz() 19 | # leave blank for localtime 20 | # timezone = 21 | 22 | # max length of characters to apply to the 23 | # "slug" field 24 | # truncate_slug_length = 40 25 | 26 | # set to 'true' to run the environment during 27 | # the 'revision' command, regardless of autogenerate 28 | # revision_environment = false 29 | 30 | # set to 'true' to allow .pyc and .pyo files without 31 | # a source .py file to be detected as revisions in the 32 | # versions/ directory 33 | # sourceless = false 34 | 35 | # version location specification; This defaults 36 | # to migrations/versions. When using multiple version 37 | # directories, initial revisions must be specified with --version-path. 38 | # The path separator used here should be the separator specified by "version_path_separator" below. 39 | # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions 40 | 41 | # version path separator; As mentioned above, this is the character used to split 42 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 43 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 44 | # Valid values for version_path_separator are: 45 | # 46 | # version_path_separator = : 47 | # version_path_separator = ; 48 | # version_path_separator = space 49 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 50 | 51 | # the output encoding used when revision files 52 | # are written from script.py.mako 53 | # output_encoding = utf-8 54 | 55 | sqlalchemy.url = driver://user:pass@localhost/dbname 56 | 57 | 58 | [post_write_hooks] 59 | # post_write_hooks defines scripts or Python functions that are run 60 | # on newly generated revision scripts. See the documentation for further 61 | # detail and examples 62 | 63 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 64 | # hooks = black 65 | # black.type = console_scripts 66 | # black.entrypoint = black 67 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 68 | 69 | # Logging configuration 70 | [loggers] 71 | keys = root,sqlalchemy,alembic 72 | 73 | [handlers] 74 | keys = console 75 | 76 | [formatters] 77 | keys = generic 78 | 79 | [logger_root] 80 | level = WARN 81 | handlers = console 82 | qualname = 83 | 84 | [logger_sqlalchemy] 85 | level = WARN 86 | handlers = 87 | qualname = sqlalchemy.engine 88 | 89 | [logger_alembic] 90 | level = INFO 91 | handlers = 92 | qualname = alembic 93 | 94 | [handler_console] 95 | class = StreamHandler 96 | args = (sys.stderr,) 97 | level = NOTSET 98 | formatter = generic 99 | 100 | [formatter_generic] 101 | format = %(levelname)-5.5s [%(name)s] %(message)s 102 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /bot.ini.example: -------------------------------------------------------------------------------- 1 | [tg_bot] 2 | token = 3 | admin_id = 4 | use_redis = false 5 | 6 | [db] 7 | user = postgres 8 | password = postgres 9 | database = postgres 10 | host = 127.0.0.1 11 | port = 5432 12 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, Dispatcher 5 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 6 | from aiogram.contrib.fsm_storage.redis import RedisStorage 7 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 8 | from sqlalchemy.orm import sessionmaker 9 | 10 | from tgbot.config import load_config 11 | from tgbot.handlers.commands import register_commands 12 | from tgbot.handlers.query_handlers import register_query_handlers 13 | from tgbot.middlewares import register_middlewares 14 | from tgbot.misc.req_func import make_connection_string 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | async def main(): 20 | logging.basicConfig( 21 | level=logging.INFO, 22 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 23 | ) 24 | logger.error("Starting bot") 25 | config = load_config("bot.ini") 26 | engine = create_async_engine( 27 | make_connection_string(config.db), future=True, echo=False 28 | ) 29 | 30 | session_fabric = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) 31 | if config.tg_bot.use_redis: 32 | storage = RedisStorage() 33 | else: 34 | storage = MemoryStorage() 35 | 36 | bot = Bot(token=config.tg_bot.token) 37 | dp = Dispatcher(bot, storage=storage) 38 | 39 | register_middlewares(dp, session_fabric) 40 | register_commands(dp) 41 | register_query_handlers(dp) 42 | 43 | # start 44 | try: 45 | await dp.start_polling() 46 | finally: 47 | await dp.storage.close() 48 | await dp.storage.wait_closed() 49 | await bot.session.close() 50 | 51 | 52 | if __name__ == '__main__': 53 | try: 54 | asyncio.run(main()) 55 | except (KeyboardInterrupt, SystemExit): 56 | logger.error("Bot stopped!") 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram~=2.19 2 | aioredis 3 | SQLAlchemy~=1.4.32 4 | alembic~=1.7.7 5 | asyncpg -------------------------------------------------------------------------------- /systemd/tgbot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Course Bot 3 | After=network.target 4 | 5 | [Service] 6 | User=tgbot 7 | Group=tgbot 8 | Type=simple 9 | WorkingDirectory=/opt/tgbot 10 | ExecStart=/opt/tgbot/venv/bin/python bot.py 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /tgbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/__init__.py -------------------------------------------------------------------------------- /tgbot/callback_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/callback_data/__init__.py -------------------------------------------------------------------------------- /tgbot/callback_data/callback_datas.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.callback_data import CallbackData 2 | 3 | cb_language = CallbackData("language", "language_code") 4 | -------------------------------------------------------------------------------- /tgbot/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class DbConfig: 7 | host: str 8 | password: str 9 | user: str 10 | database: str 11 | port: int 12 | 13 | 14 | @dataclass 15 | class TgBot: 16 | token: str 17 | admin_id: int 18 | use_redis: bool 19 | 20 | 21 | @dataclass 22 | class Config: 23 | tg_bot: TgBot 24 | db: DbConfig 25 | 26 | 27 | def cast_bool(value: str) -> bool: 28 | if not value: 29 | return False 30 | return value.lower() in ("true", "t", "1", "yes") 31 | 32 | 33 | def load_config(path: str): 34 | config = configparser.ConfigParser() 35 | config.read(path) 36 | tg_bot = config["tg_bot"] 37 | 38 | return Config( 39 | tg_bot=TgBot( 40 | token=tg_bot["token"], 41 | admin_id=int(tg_bot["admin_id"]), 42 | use_redis=cast_bool(tg_bot.get("use_redis")), 43 | ), 44 | db=DbConfig(**config["db"]), 45 | ) 46 | -------------------------------------------------------------------------------- /tgbot/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/data/__init__.py -------------------------------------------------------------------------------- /tgbot/data/data.py: -------------------------------------------------------------------------------- 1 | from tgbot.middlewares.language import I18nMiddleware 2 | 3 | 4 | i18n = I18nMiddleware(domain="messages", path="tgbot/locales", default="uz") 5 | 6 | languages = { 7 | "ru": "🇷🇺 Русский", 8 | "uz": "🇺🇿 O'zbek" 9 | } 10 | -------------------------------------------------------------------------------- /tgbot/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/handlers/__init__.py -------------------------------------------------------------------------------- /tgbot/handlers/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from .start import cmd_start 4 | 5 | 6 | def register_commands(dp: Dispatcher) -> None: 7 | dp.register_message_handler(cmd_start, commands=["start"], state="*") 8 | -------------------------------------------------------------------------------- /tgbot/handlers/commands/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from tgbot.keyboards.inline.language import language_markup 3 | from tgbot.service.repo.repository import SQLAlchemyRepos 4 | from tgbot.service.repo.user_repo import UserRepo 5 | from tgbot.data.data import i18n 6 | 7 | _ = i18n.gettext 8 | 9 | 10 | async def cmd_start(message: types.Message, repo: SQLAlchemyRepos) -> None: 11 | user = repo.get_repo(UserRepo) 12 | if await user.get_user(user_id=message.from_user.id) is None: 13 | await message.answer( 14 | text='Assalomu Alaykum! Kerakli tilni tanlang\n' 15 | 'Здравствуйте! Выберите необходимый язык', 16 | reply_markup=language_markup() 17 | ) 18 | else: 19 | await message.answer( 20 | text=_('Asosiy menu') 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /tgbot/handlers/query_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from .language_handler import language_handler 4 | 5 | from tgbot.callback_data.callback_datas import cb_language 6 | 7 | 8 | def register_query_handlers(dp: Dispatcher) -> None: 9 | dp.register_callback_query_handler(language_handler, cb_language.filter(), state="*") 10 | -------------------------------------------------------------------------------- /tgbot/handlers/query_handlers/language_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from aiogram import types 4 | 5 | from tgbot.service.repo.repository import SQLAlchemyRepos 6 | from tgbot.service.repo.user_repo import UserRepo 7 | from tgbot.data.data import i18n 8 | 9 | _ = i18n.gettext 10 | 11 | 12 | async def language_handler(query: types.CallbackQuery, callback_data: Dict[str, str], repo: SQLAlchemyRepos) -> None: 13 | language_code = callback_data.get('language_code') 14 | user = repo.get_repo(UserRepo) 15 | if await user.get_user(user_id=query.from_user.id) is None: 16 | await user.add_user( 17 | user_id=query.from_user.id, 18 | name=query.from_user.full_name, 19 | language=language_code 20 | ) 21 | else: 22 | await user.update_language(user_id=query.from_user.id, language=language_code) 23 | await query.message.edit_text( 24 | text=_('Asosiy menu') 25 | ) 26 | -------------------------------------------------------------------------------- /tgbot/keyboards/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/keyboards/__init__.py -------------------------------------------------------------------------------- /tgbot/keyboards/default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/keyboards/default/__init__.py -------------------------------------------------------------------------------- /tgbot/keyboards/inline/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/keyboards/inline/__init__.py -------------------------------------------------------------------------------- /tgbot/keyboards/inline/language.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | 3 | from tgbot.callback_data.callback_datas import cb_language 4 | from tgbot.data.data import languages 5 | 6 | 7 | def language_markup() -> types.InlineKeyboardMarkup: 8 | keyboard = types.InlineKeyboardMarkup(row_width=1) 9 | for language_code, language_name in languages.items(): 10 | keyboard.insert( 11 | types.InlineKeyboardButton( 12 | text=language_name, 13 | callback_data=cb_language.new(language_code=language_code) 14 | ) 15 | ) 16 | return keyboard 17 | -------------------------------------------------------------------------------- /tgbot/locales/messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2022-07-08 00:55+0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.1\n" 19 | 20 | #: handlers/commands/start.py:20 21 | msgid "Asosiy menu" 22 | msgstr "" 23 | 24 | -------------------------------------------------------------------------------- /tgbot/locales/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/locales/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tgbot/locales/ru/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Russian translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-07-08 00:55+0500\n" 11 | "PO-Revision-Date: 2022-07-08 00:55+0500\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: ru\n" 14 | "Language-Team: ru \n" 15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 16 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=utf-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Generated-By: Babel 2.9.1\n" 21 | 22 | #: handlers/commands/start.py:20 23 | msgid "Asosiy menu" 24 | msgstr "" 25 | 26 | -------------------------------------------------------------------------------- /tgbot/locales/uz/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/locales/uz/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tgbot/locales/uz/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Uzbek translations for PROJECT. 2 | # Copyright (C) 2022 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2022-07-08 00:55+0500\n" 11 | "PO-Revision-Date: 2022-07-08 00:55+0500\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: uz\n" 14 | "Language-Team: uz \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | #: handlers/commands/start.py:20 22 | msgid "Asosiy menu" 23 | msgstr "" 24 | 25 | -------------------------------------------------------------------------------- /tgbot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from sqlalchemy.orm import sessionmaker 3 | from .db import DbSessionMiddleware 4 | from .language import I18nMiddleware 5 | 6 | 7 | def register_middlewares(dp: Dispatcher, session_fabric: sessionmaker) -> None: 8 | dp.middleware.setup(DbSessionMiddleware(session_pool=session_fabric)) 9 | dp.middleware.setup(I18nMiddleware(domain="messages", path="tgbot/locales", default="uz")) 10 | -------------------------------------------------------------------------------- /tgbot/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from tgbot.service.repo.repository import SQLAlchemyRepos 5 | 6 | 7 | class DbSessionMiddleware(LifetimeControllerMiddleware): 8 | skip_patterns = ["error", "update"] 9 | 10 | def __init__(self, session_pool: sessionmaker): 11 | super().__init__() 12 | self.session_pool = session_pool 13 | 14 | async def pre_process(self, obj, data, *args): 15 | session = self.session_pool() 16 | data['session'] = session 17 | repo = SQLAlchemyRepos(session) 18 | data['repo'] = repo 19 | 20 | async def post_process(self, obj, data, *args): 21 | session = data.get("session") 22 | if session: 23 | await session.close() 24 | del data["session"] 25 | del data["repo"] 26 | -------------------------------------------------------------------------------- /tgbot/middlewares/language.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.contrib.middlewares.i18n import I18nMiddleware as BaseI18nMiddleware 3 | 4 | from tgbot.service.repo.repository import SQLAlchemyRepos 5 | from tgbot.service.repo.user_repo import UserRepo 6 | 7 | 8 | class I18nMiddleware(BaseI18nMiddleware): 9 | 10 | async def get_user_locale(self, action, data): 11 | repo: SQLAlchemyRepos = data[-1].get('repo') 12 | user = types.User.get_current() 13 | language = await repo.get_repo(UserRepo).get_language(user_id=user.id) 14 | if language: 15 | return language 16 | else: 17 | return user.locale 18 | -------------------------------------------------------------------------------- /tgbot/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/misc/__init__.py -------------------------------------------------------------------------------- /tgbot/misc/req_func.py: -------------------------------------------------------------------------------- 1 | def make_connection_string(db, async_fallback: bool = False) -> str: 2 | result = ( 3 | f"postgresql+asyncpg://{db.user}:{db.password}@{db.host}:{db.port}/{db.database}" 4 | ) 5 | if async_fallback: 6 | result += "?async_fallback=True" 7 | return result 8 | -------------------------------------------------------------------------------- /tgbot/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Base, BaseModel 2 | from .user import User 3 | 4 | __all__ = ( 5 | "Base", 6 | "BaseModel", 7 | "User" 8 | ) -------------------------------------------------------------------------------- /tgbot/models/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, func 2 | from sqlalchemy.orm import declarative_base 3 | 4 | Base = declarative_base() 5 | metadata = Base.metadata 6 | 7 | 8 | class BaseModel(Base): 9 | __abstract__ = True 10 | 11 | created_at = Column(DateTime(True), server_default=func.now()) 12 | updated_at = Column(DateTime(True), default=func.now(), onupdate=func.now(), server_default=func.now()) -------------------------------------------------------------------------------- /tgbot/models/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /tgbot/models/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/models/migrations/__init__.py -------------------------------------------------------------------------------- /tgbot/models/migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | from tgbot.models.base import metadata 8 | 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | from tgbot.config import Config, load_config 12 | from tgbot.misc.req_func import make_connection_string 13 | 14 | config = context.config 15 | bot_config: Config = load_config('bot.ini') 16 | config.set_main_option( 17 | "sqlalchemy.url", make_connection_string(bot_config.db, async_fallback=True) 18 | ) 19 | 20 | # Interpret the config file for Python logging. 21 | # This line sets up loggers basically. 22 | if config.config_file_name is not None: 23 | fileConfig(config.config_file_name) 24 | 25 | # add your model's MetaData object here 26 | # for 'autogenerate' support 27 | # from myapp import mymodel 28 | # target_metadata = mymodel.Base.metadata 29 | target_metadata = metadata 30 | 31 | # other values from the config, defined by the needs of env.py, 32 | # can be acquired: 33 | # my_important_option = config.get_main_option("my_important_option") 34 | # ... etc. 35 | 36 | 37 | def run_migrations_offline(): 38 | """Run migrations in 'offline' mode. 39 | 40 | This configures the context with just a URL 41 | and not an Engine, though an Engine is acceptable 42 | here as well. By skipping the Engine creation 43 | we don't even need a DBAPI to be available. 44 | 45 | Calls to context.execute() here emit the given string to the 46 | script output. 47 | 48 | """ 49 | url = config.get_main_option("sqlalchemy.url") 50 | context.configure( 51 | url=url, 52 | target_metadata=target_metadata, 53 | literal_binds=True, 54 | dialect_opts={"paramstyle": "named"}, 55 | ) 56 | 57 | with context.begin_transaction(): 58 | context.run_migrations() 59 | 60 | 61 | def run_migrations_online(): 62 | """Run migrations in 'online' mode. 63 | 64 | In this scenario we need to create an Engine 65 | and associate a connection with the context. 66 | 67 | """ 68 | connectable = engine_from_config( 69 | config.get_section(config.config_ini_section), 70 | prefix="sqlalchemy.", 71 | poolclass=pool.NullPool, 72 | ) 73 | 74 | with connectable.connect() as connection: 75 | context.configure( 76 | connection=connection, target_metadata=target_metadata 77 | ) 78 | 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | 82 | 83 | if context.is_offline_mode(): 84 | run_migrations_offline() 85 | else: 86 | run_migrations_online() 87 | -------------------------------------------------------------------------------- /tgbot/models/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /tgbot/models/migrations/versions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/models/migrations/versions/__init__.py -------------------------------------------------------------------------------- /tgbot/models/role.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class UserRole(Enum): 5 | ADMIN = "admin" 6 | USER = "user" 7 | -------------------------------------------------------------------------------- /tgbot/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, BigInteger, String 2 | 3 | from tgbot.models import BaseModel 4 | 5 | 6 | class User(BaseModel): 7 | __tablename__ = 'users' 8 | 9 | user_id = Column(BigInteger, nullable=False, autoincrement=False, primary_key=True) 10 | name = Column(String(length=60), nullable=False) 11 | username = Column(String(length=100), nullable=True) 12 | language = Column(String(length=10), nullable=False) 13 | 14 | def __repr__(self): 15 | return f'{self.user_id} | {self.name} | {self.username} | {self.language}' 16 | -------------------------------------------------------------------------------- /tgbot/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/service/__init__.py -------------------------------------------------------------------------------- /tgbot/service/repo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uicodee/aiogram_template/9f6b9fbe3dbc2241f33df0f5d0f0e1a4f926a822/tgbot/service/repo/__init__.py -------------------------------------------------------------------------------- /tgbot/service/repo/base_repo.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | 5 | 6 | class BaseSQLAlchemyRepo(ABC): 7 | def __init__(self, session: AsyncSession) -> None: 8 | self._session = session -------------------------------------------------------------------------------- /tgbot/service/repo/repository.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import lru_cache 3 | from typing import Type, TypeVar 4 | 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from tgbot.service.repo.base_repo import BaseSQLAlchemyRepo 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | T = TypeVar("T", bound=BaseSQLAlchemyRepo) 12 | 13 | 14 | class SQLAlchemyRepos: 15 | def __init__(self, session: AsyncSession): 16 | self._session = session 17 | 18 | @lru_cache() 19 | def get_repo(self, repo: Type[T]) -> T: 20 | return repo(self._session) 21 | 22 | async def commit(self): 23 | await self._session.commit() 24 | 25 | async def rollback(self): 26 | await self._session.rollback() -------------------------------------------------------------------------------- /tgbot/service/repo/user_repo.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import insert, select, update 2 | 3 | from tgbot.models.user import User 4 | from tgbot.service.repo.base_repo import BaseSQLAlchemyRepo 5 | 6 | 7 | class UserRepo(BaseSQLAlchemyRepo): 8 | model = User 9 | 10 | async def add_user(self, user_id: int, name: str, language: str) -> None: 11 | sql = insert(self.model).values(user_id=user_id, name=name, language=language) 12 | await self._session.execute(sql) 13 | await self._session.commit() 14 | 15 | async def get_user(self, user_id: int) -> model | None: 16 | sql = select(self.model).where(self.model.user_id == user_id) 17 | request = await self._session.execute(sql) 18 | user = request.scalar() 19 | return user 20 | 21 | async def get_language(self, user_id: int) -> model.language: 22 | sql = select(self.model.language).filter(self.model.user_id == user_id) 23 | request = await self._session.execute(sql) 24 | return request.scalar() 25 | 26 | async def get_users(self) -> list[model]: 27 | sql = select(self.model) 28 | request = await self._session.execute(sql) 29 | user = request.scalars().all() 30 | return user 31 | 32 | async def update_status(self, user_id: int, subscribed: bool) -> None: 33 | sql = update(self.model).where(self.model.user_id == user_id).values({'subscribed': subscribed}) 34 | await self._session.execute(sql) 35 | await self._session.commit() 36 | 37 | async def update_language(self, user_id: int, language: str) -> None: 38 | sql = update(self.model).where(self.model.user_id == user_id).values({'language': language}) 39 | await self._session.execute(sql) 40 | await self._session.commit() 41 | --------------------------------------------------------------------------------