├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ └── 001_create_account_table.py ├── bot ├── __init__.py ├── __main__.py ├── common.py ├── config_reader.py ├── db │ ├── __init__.py │ ├── base.py │ └── models.py ├── handlers │ ├── __init__.py │ ├── callbacks.py │ └── commands.py ├── keyboards.py ├── middlewares │ ├── __init__.py │ └── db.py └── ui_commands.py ├── docker-compose.example.yml ├── env_dist ├── images └── screenshot.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea/ 3 | /docker-compose.yml 4 | /.env 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Separate "build" image 2 | FROM python:3.11-slim-bullseye as compile-image 3 | RUN python -m venv /opt/venv 4 | ENV PATH="/opt/venv/bin:$PATH" 5 | COPY requirements.txt . 6 | RUN pip install --no-cache-dir --upgrade pip \ 7 | && pip install --no-cache-dir -r requirements.txt 8 | 9 | # "Run" image 10 | FROM python:3.11-slim-bullseye 11 | COPY --from=compile-image /opt/venv /opt/venv 12 | ENV PATH="/opt/venv/bin:$PATH" 13 | WORKDIR /app 14 | COPY bot /app/bot 15 | CMD ["python", "-m", "bot"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Aleksandr (also known as MasterGroosha on GitHub) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aiogram-and-sqlalchemy-demo 2 | 3 | 4 | A simple demo of using aiogram 3.x + async sqlalchemy 2.0+, made as a pop-it game where you need to click 5 | green circles and avoid red ones. 6 | 7 | ![screenshot](images/screenshot.png) 8 | 9 | Used tech: 10 | * [aiogram 3.x](https://github.com/aiogram/aiogram) 11 | * [SQLAlchemy 2.0+](https://www.sqlalchemy.org/) 12 | * PostgreSQL as database 13 | * psycopg3 as database driver for SQLAlchemy 14 | * Docker with docker-compose for deployment 15 | 16 | Don't forget to create "postgres_data" (required) and "pgadmin_data" (if using PG Admin) directories 17 | before you run `docker-compose up -d` 18 | 19 | Also copy `env_dist` file to `.env` and fill it with your data 20 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # set to 'true' to search source files recursively 55 | # in each "version_locations" directory 56 | # new in Alembic version 1.10 57 | # recursive_version_locations = false 58 | 59 | # the output encoding used when revision files 60 | # are written from script.py.mako 61 | # output_encoding = utf-8 62 | 63 | sqlalchemy.url = driver://user:pass@localhost/dbname 64 | 65 | 66 | [post_write_hooks] 67 | # post_write_hooks defines scripts or Python functions that are run 68 | # on newly generated revision scripts. See the documentation for further 69 | # detail and examples 70 | 71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 72 | # hooks = black 73 | # black.type = console_scripts 74 | # black.entrypoint = black 75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 76 | 77 | # Logging configuration 78 | [loggers] 79 | keys = root,sqlalchemy,alembic 80 | 81 | [handlers] 82 | keys = console 83 | 84 | [formatters] 85 | keys = generic 86 | 87 | [logger_root] 88 | level = WARN 89 | handlers = console 90 | qualname = 91 | 92 | [logger_sqlalchemy] 93 | level = WARN 94 | handlers = 95 | qualname = sqlalchemy.engine 96 | 97 | [logger_alembic] 98 | level = INFO 99 | handlers = 100 | qualname = alembic 101 | 102 | [handler_console] 103 | class = StreamHandler 104 | args = (sys.stderr,) 105 | level = NOTSET 106 | formatter = generic 107 | 108 | [formatter_generic] 109 | format = %(levelname)-5.5s [%(name)s] %(message)s 110 | datefmt = %H:%M:%S 111 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from alembic.script import ScriptDirectory 6 | from sqlalchemy import pool 7 | from sqlalchemy.engine import Connection 8 | from sqlalchemy.ext.asyncio import async_engine_from_config 9 | 10 | from bot.config_reader import config as bot_config 11 | from bot.db import Base 12 | 13 | # this is the Alembic Config object, which provides 14 | # access to the values within the .ini file in use. 15 | config = context.config 16 | 17 | # Interpret the config file for Python logging. 18 | # This line sets up loggers basically. 19 | if config.config_file_name is not None: 20 | fileConfig(config.config_file_name) 21 | 22 | # add your model's MetaData object here 23 | # for 'autogenerate' support 24 | # from myapp import mymodel 25 | # target_metadata = mymodel.Base.metadata 26 | target_metadata = Base.metadata 27 | config.set_main_option( 28 | 'sqlalchemy.url', 29 | bot_config.db_url 30 | ) 31 | 32 | # other values from the config, defined by the needs of env.py, 33 | # can be acquired: 34 | # my_important_option = config.get_main_option("my_important_option") 35 | # ... etc. 36 | 37 | 38 | def run_migrations_offline() -> None: 39 | """Run migrations in 'offline' mode. 40 | 41 | This configures the context with just a URL 42 | and not an Engine, though an Engine is acceptable 43 | here as well. By skipping the Engine creation 44 | we don't even need a DBAPI to be available. 45 | 46 | Calls to context.execute() here emit the given string to the 47 | script output. 48 | 49 | """ 50 | url = config.get_main_option("sqlalchemy.url") 51 | context.configure( 52 | url=url, 53 | target_metadata=target_metadata, 54 | literal_binds=True, 55 | dialect_opts={"paramstyle": "named"}, 56 | process_revision_directives=process_revision_directives, 57 | ) 58 | 59 | with context.begin_transaction(): 60 | context.run_migrations() 61 | 62 | 63 | def do_run_migrations(connection: Connection) -> None: 64 | context.configure(connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives,) 65 | 66 | with context.begin_transaction(): 67 | context.run_migrations() 68 | 69 | 70 | async def run_async_migrations() -> None: 71 | """In this scenario we need to create an Engine 72 | and associate a connection with the context. 73 | 74 | """ 75 | 76 | connectable = async_engine_from_config( 77 | config.get_section(config.config_ini_section, {}), 78 | prefix="sqlalchemy.", 79 | poolclass=pool.NullPool, 80 | ) 81 | 82 | async with connectable.connect() as connection: 83 | await connection.run_sync(do_run_migrations) 84 | 85 | await connectable.dispose() 86 | 87 | 88 | def run_migrations_online() -> None: 89 | """Run migrations in 'online' mode.""" 90 | 91 | asyncio.run(run_async_migrations()) 92 | 93 | 94 | def process_revision_directives(context, revision, directives): 95 | # extract Migration 96 | migration_script = directives[0] 97 | # extract current head revision 98 | head_revision = ScriptDirectory.from_config(context.config).get_current_head() 99 | 100 | if head_revision is None: 101 | # edge case with first migration 102 | new_rev_id = 1 103 | else: 104 | # default branch with incrementation 105 | last_rev_id = int(head_revision) 106 | new_rev_id = last_rev_id + 1 107 | # fill zeros up to 3 digits: 1 -> 001 108 | migration_script.rev_id = '{0:03}'.format(new_rev_id) 109 | 110 | 111 | if context.is_offline_mode(): 112 | run_migrations_offline() 113 | else: 114 | run_migrations_online() 115 | -------------------------------------------------------------------------------- /alembic/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() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/001_create_account_table.py: -------------------------------------------------------------------------------- 1 | """create account table 2 | 3 | Revision ID: 001 4 | Revises: 5 | Create Date: 2023-03-20 10:03:56.034181 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '001' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('playerscore', 22 | sa.Column('user_id', sa.BigInteger(), autoincrement=False, nullable=False), 23 | sa.Column('score', sa.Integer(), nullable=True), 24 | sa.PrimaryKeyConstraint('user_id'), 25 | sa.UniqueConstraint('user_id') 26 | ) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade() -> None: 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_table('playerscore') 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasterGroosha/aiogram-and-sqlalchemy-demo/3fcd41f05420a0c5aab2e62aaa00dd3c5b4489cb/bot/__init__.py -------------------------------------------------------------------------------- /bot/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import Bot, Dispatcher 4 | from aiogram.utils.callback_answer import CallbackAnswerMiddleware 5 | from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker 6 | 7 | from bot.config_reader import config 8 | from bot.handlers import commands, callbacks 9 | from bot.middlewares import DbSessionMiddleware 10 | from bot.ui_commands import set_ui_commands 11 | 12 | 13 | async def main(): 14 | engine = create_async_engine(url=config.db_url, echo=True) 15 | sessionmaker = async_sessionmaker(engine, expire_on_commit=False) 16 | 17 | bot = Bot(config.bot_token.get_secret_value(), parse_mode="HTML") 18 | 19 | # Setup dispatcher and bind routers to it 20 | dp = Dispatcher() 21 | dp.update.middleware(DbSessionMiddleware(session_pool=sessionmaker)) 22 | # Automatically reply to all callbacks 23 | dp.callback_query.middleware(CallbackAnswerMiddleware()) 24 | 25 | # Register handlers 26 | dp.include_router(commands.router) 27 | dp.include_router(callbacks.router) 28 | 29 | # Set bot commands in UI 30 | await set_ui_commands(bot) 31 | 32 | # Run bot 33 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 34 | 35 | 36 | if __name__ == "__main__": 37 | asyncio.run(main()) 38 | -------------------------------------------------------------------------------- /bot/common.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | 3 | 4 | class BallsCallbackFactory(CallbackData, prefix="ball"): 5 | color: str 6 | 7 | -------------------------------------------------------------------------------- /bot/config_reader.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings, SecretStr, PostgresDsn 2 | 3 | 4 | class Settings(BaseSettings): 5 | bot_token: SecretStr 6 | db_url: PostgresDsn 7 | 8 | class Config: 9 | env_file = '.env' 10 | env_file_encoding = 'utf-8' 11 | 12 | 13 | config = Settings() 14 | -------------------------------------------------------------------------------- /bot/db/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from .models import PlayerScore 3 | 4 | __all__ = [ 5 | "Base", 6 | "PlayerScore" 7 | ] 8 | -------------------------------------------------------------------------------- /bot/db/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /bot/db/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, BigInteger 2 | 3 | from bot.db.base import Base 4 | 5 | 6 | class PlayerScore(Base): 7 | __tablename__ = "playerscore" 8 | 9 | user_id = Column(BigInteger, primary_key=True, unique=True, autoincrement=False) 10 | score = Column(Integer, default=0) 11 | -------------------------------------------------------------------------------- /bot/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasterGroosha/aiogram-and-sqlalchemy-demo/3fcd41f05420a0c5aab2e62aaa00dd3c5b4489cb/bot/handlers/__init__.py -------------------------------------------------------------------------------- /bot/handlers/callbacks.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | 3 | from aiogram import Router, F 4 | from aiogram.exceptions import TelegramBadRequest 5 | from aiogram.types import CallbackQuery 6 | from sqlalchemy import select 7 | from sqlalchemy.ext.asyncio import AsyncSession 8 | 9 | from bot.common import BallsCallbackFactory 10 | from bot.db.models import PlayerScore 11 | from bot.keyboards import generate_balls 12 | 13 | router = Router(name="callbacks-router") 14 | 15 | 16 | @router.callback_query(BallsCallbackFactory.filter(F.color == "red")) 17 | async def cb_miss(callback: CallbackQuery, session: AsyncSession): 18 | """ 19 | Invoked on red ball tap 20 | :param callback: CallbackQuery from Telegram 21 | :param session: DB connection session 22 | """ 23 | 24 | await session.merge(PlayerScore(user_id=callback.from_user.id, score=0)) 25 | await session.commit() 26 | 27 | with suppress(TelegramBadRequest): 28 | await callback.message.edit_text("Your score: 0", reply_markup=generate_balls()) 29 | 30 | 31 | @router.callback_query(BallsCallbackFactory.filter(F.color == "green")) 32 | async def cb_hit(callback: CallbackQuery, session: AsyncSession): 33 | """ 34 | Invoked on green ball tap 35 | :param callback:CallbackQuery from Telegram 36 | :param session: DB connection session 37 | """ 38 | db_query = await session.execute(select(PlayerScore).filter_by(user_id=callback.from_user.id)) 39 | player: PlayerScore = db_query.scalar() 40 | # Note: we're incrementing client-side, not server-side 41 | player.score += 1 42 | await session.commit() 43 | 44 | # Since we have "expire_on_commit=False", we can use player instance here 45 | with suppress(TelegramBadRequest): 46 | await callback.message.edit_text(f"Your score: {player.score}", reply_markup=generate_balls()) 47 | -------------------------------------------------------------------------------- /bot/handlers/commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, html 2 | from aiogram.filters import CommandStart, Command 3 | from aiogram.types import Message 4 | from sqlalchemy import select 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from bot.db.models import PlayerScore 8 | from bot.keyboards import generate_balls 9 | 10 | router = Router(name="commands-router") 11 | 12 | 13 | @router.message(CommandStart()) 14 | async def cmd_start(message: Message): 15 | """ 16 | Handles /start command 17 | :param message: Telegram message with "/start" text 18 | """ 19 | await message.answer( 20 | "Hi there! This is a simple clicker bot. Tap on green ball, but don't tap on red ones!\n" 21 | "If you tap a red ball, you'll have to start over.\n\n" 22 | "Enough talk. Just tap /play and have fun!" 23 | ) 24 | 25 | 26 | @router.message(Command("play")) 27 | async def cmd_play(message: Message, session: AsyncSession): 28 | """ 29 | Handles /play command 30 | :param message: Telegram message with "/play" text 31 | :param session: DB connection session 32 | """ 33 | await session.merge(PlayerScore(user_id=message.from_user.id, score=0)) 34 | await session.commit() 35 | 36 | await message.answer("Your score: 0", reply_markup=generate_balls()) 37 | 38 | 39 | @router.message(Command("top")) 40 | async def cmd_top(message: Message, session: AsyncSession): 41 | """ 42 | Handles /top command. Show top 5 players 43 | :param message: Telegram message with "/top" text 44 | :param session: DB connection session 45 | """ 46 | sql = select(PlayerScore).order_by(PlayerScore.score.desc()).limit(5) 47 | text_template = "Top 5 players:\n\n{scores}" 48 | top_players_request = await session.execute(sql) 49 | players = top_players_request.scalars() 50 | 51 | score_entries = [f"{index+1}. ID{item.user_id}: {html.bold(item.score)}" for index, item in enumerate(players)] 52 | score_entries_text = "\n".join(score_entries)\ 53 | .replace(f"{message.from_user.id}", f"{message.from_user.id} (it's you!)") 54 | await message.answer(text_template.format(scores=score_entries_text), parse_mode="HTML") 55 | -------------------------------------------------------------------------------- /bot/keyboards.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | from aiogram.types import InlineKeyboardMarkup 4 | from aiogram.utils.keyboard import InlineKeyboardBuilder 5 | 6 | from bot.common import BallsCallbackFactory 7 | 8 | 9 | def generate_balls() -> InlineKeyboardMarkup: 10 | """ 11 | Generates a new 3x3 play field with 8 red balls and 1 green ball 12 | :return: Inline keyboard 13 | """ 14 | balls_mask = [False] * 9 15 | balls_mask[randint(0, 8)] = True 16 | balls = ["🔴", "🟢"] 17 | data = ["red", "green"] 18 | builder = InlineKeyboardBuilder() 19 | for item in balls_mask: 20 | builder.button( 21 | text=balls[item], 22 | callback_data=BallsCallbackFactory(color=data[item]).pack() 23 | ) 24 | return builder.adjust(3).as_markup() 25 | -------------------------------------------------------------------------------- /bot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import DbSessionMiddleware 2 | 3 | __all__ = [ 4 | "DbSessionMiddleware" 5 | ] -------------------------------------------------------------------------------- /bot/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Awaitable, Dict, Any 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import TelegramObject 5 | from sqlalchemy.ext.asyncio import async_sessionmaker 6 | 7 | 8 | class DbSessionMiddleware(BaseMiddleware): 9 | def __init__(self, session_pool: async_sessionmaker): 10 | super().__init__() 11 | self.session_pool = session_pool 12 | 13 | async def __call__( 14 | self, 15 | handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 16 | event: TelegramObject, 17 | data: Dict[str, Any], 18 | ) -> Any: 19 | async with self.session_pool() as session: 20 | data["session"] = session 21 | return await handler(event, data) 22 | -------------------------------------------------------------------------------- /bot/ui_commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot 2 | from aiogram.types import BotCommandScopeAllPrivateChats, BotCommand 3 | 4 | 5 | async def set_ui_commands(bot: Bot): 6 | """ 7 | Sets bot commands in UI 8 | :param bot: Bot instance 9 | """ 10 | commands = [ 11 | BotCommand(command="play", description="Start new game"), 12 | BotCommand(command="top", description="View top players") 13 | ] 14 | await bot.set_my_commands( 15 | commands=commands, 16 | scope=BotCommandScopeAllPrivateChats() 17 | ) 18 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | bot: 4 | image: groosha/aiogram-and-sqlalchemy-demo:latest 5 | restart: always 6 | stop_signal: SIGINT 7 | environment: 8 | - BOT_TOKEN 9 | - DB_HOST 10 | - DB_USER 11 | - DB_PASS 12 | - DB_NAME 13 | depends_on: 14 | - db 15 | db: 16 | image: postgres:12-alpine 17 | restart: always 18 | environment: 19 | POSTGRES_USER: ${DB_USER} 20 | POSTGRES_PASSWORD: ${DB_PASS} 21 | POSTGRES_DB: ${DB_NAME} 22 | volumes: 23 | - "/path/to/your/postgres/directory:/var/lib/postgresql/data" 24 | # This is optional, not necessary 25 | pgadmin: 26 | image: dpage/pgadmin4 27 | restart: always 28 | environment: 29 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_USER} 30 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} 31 | ports: 32 | - "127.0.0.1:5050:80" 33 | volumes: 34 | # Don't forget to set owner:group for this dir as 5050:5050 35 | # (sudo chown -R 5050:5050 ) 36 | - "/path/to/your/pgadmin/directory:/var/lib/pgadmin" 37 | 38 | -------------------------------------------------------------------------------- /env_dist: -------------------------------------------------------------------------------- 1 | # rename this file to .env (with the leading dot) and place it next to docker-compose.yml 2 | BOT_TOKEN=123456789:AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQq 3 | DB_URL=postgresql+psycopg://user:password@server/db 4 | PGADMIN_USER=admin@admin.com 5 | PGADMIN_PASSWORD=password 6 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasterGroosha/aiogram-and-sqlalchemy-demo/3fcd41f05420a0c5aab2e62aaa00dd3c5b4489cb/images/screenshot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.1.0 2 | aiogram==3.0.0b7 3 | aiohttp==3.8.4 4 | aiosignal==1.3.1 5 | alembic==1.10.2 6 | async-timeout==4.0.2 7 | attrs==22.2.0 8 | certifi==2022.12.7 9 | charset-normalizer==3.1.0 10 | frozenlist==1.3.3 11 | greenlet==2.0.2 12 | idna==3.4 13 | magic-filter==1.0.9 14 | Mako==1.2.4 15 | MarkupSafe==2.1.2 16 | multidict==6.0.4 17 | psycopg==3.1.8 18 | psycopg-binary==3.1.8 19 | psycopg-pool==3.1.6 20 | pydantic==1.10.6 21 | python-dotenv==1.0.0 22 | SQLAlchemy==2.0.7 23 | typing_extensions==4.5.0 24 | yarl==1.8.2 25 | --------------------------------------------------------------------------------