├── .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 | 
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 |
--------------------------------------------------------------------------------