From 7b28635f09e4d8c442aacee14c791c841871d32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=ADITQ?= Date: Sat, 23 Mar 2024 10:47:39 +0300 Subject: [PATCH] chore: Global project refactoring --- .dockerignore | 2 - Dockerfile | 3 -- app/bot.py | 7 ++- app/callbacks/menu.py | 34 +++++++------- app/callbacks/profile.py | 26 +++++----- app/callbacks/travels.py | 12 +++-- app/handlers/create_travel_command.py | 2 +- app/handlers/help_command.py | 2 +- app/handlers/menu_command.py | 2 +- app/handlers/profile_command.py | 13 +---- app/handlers/start_command.py | 2 +- app/handlers/travels_command.py | 2 +- app/keyboards/builders.py | 68 ++++++++++++++------------- app/messages.py | 12 +++-- app/models/user.py | 17 +++++-- app/utils/__init__.py | 0 check.sh | 2 +- docker-compose.yml | 31 ++++++------ requirements/dev.txt | 2 +- requirements/{lints.txt => lint.txt} | 0 template.env | 20 +++++++- 21 files changed, 143 insertions(+), 116 deletions(-) delete mode 100644 app/utils/__init__.py rename requirements/{lints.txt => lint.txt} (100%) diff --git a/.dockerignore b/.dockerignore index ec4cef2..78b6919 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,7 @@ # Files README.md -task.md check.sh .flake8 -template.env # Folders img/ diff --git a/Dockerfile b/Dockerfile index 9e0e5d9..30b3d00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,11 +2,8 @@ FROM python:3.12-slim WORKDIR /app -# Copy requirements file COPY requirements/prod.txt . -# Install Python dependencies RUN pip install --no-cache-dir -r prod.txt -# Copy the rest of the application files COPY . . diff --git a/app/bot.py b/app/bot.py index 370d43c..2aed010 100644 --- a/app/bot.py +++ b/app/bot.py @@ -38,10 +38,9 @@ async def main() -> None: profile_command.router, create_travel_command.router, travels_command.router, - - menu.router, # type: ignore - profile.router, # type: ignore - travels.router, # type: ignore + menu.router, + profile.router, + travels.router, ) await bot.delete_webhook(drop_pending_updates=True) diff --git a/app/callbacks/menu.py b/app/callbacks/menu.py index 5b5ad8f..ae6db89 100644 --- a/app/callbacks/menu.py +++ b/app/callbacks/menu.py @@ -1,11 +1,10 @@ -# type: ignore -__all__ = () +__all__ = ("router",) from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.filters import StateFilter from aiogram.fsm.context import FSMContext -from aiogram.types import CallbackQuery +from aiogram.types import CallbackQuery, Message from app import messages from app.config import Config @@ -20,24 +19,18 @@ router = Router(name="menu_callback") @router.callback_query( - F.data == "menu_profile", RegisteredCallback(), StateFilter(None), + F.data == "menu_profile", + RegisteredCallback(), + StateFilter(None), ) async def profile_callback(callback: CallbackQuery) -> None: - if callback.data is None or callback.message is None: + if callback.data is None or not isinstance(callback.message, Message): return user = User().get_user_by_telegram_id(callback.from_user.id) await callback.message.answer( - messages.PROFILE.format( - username=user.username, - age=user.age, - bio=user.bio if user.bio else messages.NOT_SET, - sex=user.sex.capitalize(), - country=user.country, - city=user.city, - date_joined=user.get_human_readable_datejoined(), - ), + user.get_profile_text(), reply_markup=get(), ) await callback.answer() @@ -49,13 +42,15 @@ async def profile_callback(callback: CallbackQuery) -> None: @router.callback_query( - F.data == "menu_create_travel", RegisteredCallback(), StateFilter(None), + F.data == "menu_create_travel", + RegisteredCallback(), + StateFilter(None), ) async def create_travel_callback( callback: CallbackQuery, state: FSMContext, ) -> None: - if callback.data is None or callback.message is None: + if callback.data is None or not isinstance(callback.message, Message): return await callback.message.answer( @@ -75,11 +70,16 @@ async def create_travel_callback( @router.callback_query( - F.data == "menu_travels", RegisteredCallback(), StateFilter(None), + F.data == "menu_travels", + RegisteredCallback(), + StateFilter(None), ) async def travels_callback( callback: CallbackQuery, ) -> None: + if not isinstance(callback.message, Message): + return + page = 0 user = User().get_user_by_telegram_id(callback.from_user.id) diff --git a/app/callbacks/profile.py b/app/callbacks/profile.py index 4f1d336..9ab6cc1 100644 --- a/app/callbacks/profile.py +++ b/app/callbacks/profile.py @@ -1,5 +1,4 @@ -# type: ignore -__all__ = () +__all__ = ("router",) from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest @@ -31,7 +30,11 @@ async def profile_change_callback( callback: CallbackQuery, state: FSMContext, ) -> None: - if callback.data is None or callback.message is None: + if ( + callback.data is None + or callback.message is None + or not isinstance(callback.message, Message) + ): return column = callback.data.replace("profile_change_", "") @@ -70,6 +73,13 @@ async def profile_change_callback( @router.message(UserAlteringState.value, F.text, Registered()) async def profile_change_entered(message: Message, state: FSMContext) -> None: + if ( + message.text is None + or message.from_user is None + or message.bot is None + ): + return + column = (await state.get_data()).get("column") value = message.text.strip() @@ -205,15 +215,7 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None: try: await message.bot.edit_message_text( - messages.PROFILE.format( - username=user.username, - age=user.age, - bio=user.bio if user.bio else messages.NOT_SET, - sex=user.sex.capitalize(), - country=user.country, - city=user.city, - date_joined=user.get_human_readable_datejoined(), - ), + user.get_profile_text(), message.chat.id, state_data["profile_message_id"], reply_markup=get(), diff --git a/app/callbacks/travels.py b/app/callbacks/travels.py index 77e0e2f..27ee397 100644 --- a/app/callbacks/travels.py +++ b/app/callbacks/travels.py @@ -1,10 +1,9 @@ -# type: ignore -__all__ = () +__all__ = ("router",) from aiogram import F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.filters import StateFilter -from aiogram.types import CallbackQuery +from aiogram.types import CallbackQuery, Message from app import messages from app.config import Config @@ -17,9 +16,14 @@ router = Router(name="menu_callback") @router.callback_query( - F.data.startswith("travels_page"), RegisteredCallback(), StateFilter(None), + F.data.startswith("travels_page"), + RegisteredCallback(), + StateFilter(None), ) async def travels_callback(callback: CallbackQuery) -> None: + if callback.data is None or not isinstance(callback.message, Message): + return + page = int(callback.data.replace("travels_page_", "")) user = User().get_user_by_telegram_id(callback.from_user.id) diff --git a/app/handlers/create_travel_command.py b/app/handlers/create_travel_command.py index 2b823de..362f1ed 100644 --- a/app/handlers/create_travel_command.py +++ b/app/handlers/create_travel_command.py @@ -1,4 +1,4 @@ -__all__ = () +__all__ = ("router",) from aiogram import F, Router from aiogram.filters import Command, StateFilter diff --git a/app/handlers/help_command.py b/app/handlers/help_command.py index 94d0ed7..a4ec806 100644 --- a/app/handlers/help_command.py +++ b/app/handlers/help_command.py @@ -1,4 +1,4 @@ -__all__ = () +__all__ = ("router",) from aiogram import Router from aiogram.filters import Command, StateFilter diff --git a/app/handlers/menu_command.py b/app/handlers/menu_command.py index 43b0751..a2f14b6 100644 --- a/app/handlers/menu_command.py +++ b/app/handlers/menu_command.py @@ -1,4 +1,4 @@ -__all__ = () +__all__ = ("router",) from aiogram import Router from aiogram.filters import Command, StateFilter diff --git a/app/handlers/profile_command.py b/app/handlers/profile_command.py index 4a234cd..8e29e31 100644 --- a/app/handlers/profile_command.py +++ b/app/handlers/profile_command.py @@ -1,10 +1,9 @@ -__all__ = () +__all__ = ("router",) from aiogram import Router from aiogram.filters import Command, StateFilter from aiogram.types import Message -from app import messages from app.filters.user import Registered from app.keyboards.profile import get from app.models.user import User @@ -21,14 +20,6 @@ async def command_profile_handler(message: Message) -> None: user = User().get_user_by_telegram_id(message.from_user.id) await message.answer( - messages.PROFILE.format( - username=user.username, - age=user.age, - bio=user.bio if user.bio else messages.NOT_SET, - sex=user.sex.capitalize(), - country=user.country, - city=user.city, - date_joined=user.get_human_readable_datejoined(), - ), + user.get_profile_text(), reply_markup=get(), ) diff --git a/app/handlers/start_command.py b/app/handlers/start_command.py index f21f0dd..02897e7 100644 --- a/app/handlers/start_command.py +++ b/app/handlers/start_command.py @@ -1,4 +1,4 @@ -__all__ = () +__all__ = ("router",) from aiogram import F, Router from aiogram.filters import CommandStart, StateFilter diff --git a/app/handlers/travels_command.py b/app/handlers/travels_command.py index d83abf2..97eb8c9 100644 --- a/app/handlers/travels_command.py +++ b/app/handlers/travels_command.py @@ -1,4 +1,4 @@ -__all__ = () +__all__ = ("router",) from aiogram import Router from aiogram.filters import Command, StateFilter diff --git a/app/keyboards/builders.py b/app/keyboards/builders.py index 5369e56..4e93bd0 100644 --- a/app/keyboards/builders.py +++ b/app/keyboards/builders.py @@ -1,4 +1,4 @@ -__all__ = ("sex_keyboard",) +__all__ = ("sex_keyboard", "travels_keyboard") from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder @@ -10,9 +10,9 @@ def sex_keyboard(choices: str | list): builder = ReplyKeyboardBuilder() if isinstance(choices, str): - text = [choices] + choices = [choices] - [builder.button(text=txt) for txt in text] + [builder.button(text=choice) for choice in choices] return builder.as_markup(resize_keyboard=True) @@ -36,41 +36,45 @@ def travels_keyboard(travels: list, page: int, pages: int): builder.row(*rows, width=2) - if pages > 1: - navigation_row = [] - - if page > 0: - navigation_row.append( - InlineKeyboardButton( - text="⬅️", callback_data=f"travels_page_{page - 1}", - ), - ) - else: - navigation_row.append( - InlineKeyboardButton( - text=" ", callback_data="pass", - ), - ) + navigation_row = [] + if page > 0: navigation_row.append( InlineKeyboardButton( - text=f"{page + 1}/{pages}", callback_data="pass", + text="⬅️", + callback_data=f"travels_page_{page - 1}", + ), + ) + else: + navigation_row.append( + InlineKeyboardButton( + text=" ", + callback_data="pass", ), ) - if page < pages - 1: - navigation_row.append( - InlineKeyboardButton( - text="➡️", callback_data=f"travels_page_{page + 1}", - ), - ) - else: - navigation_row.append( - InlineKeyboardButton( - text=" ", callback_data="pass", - ), - ) + navigation_row.append( + InlineKeyboardButton( + text=f"{page + 1}/{pages}", + callback_data="pass", + ), + ) - builder.row(*navigation_row) + if page < pages - 1: + navigation_row.append( + InlineKeyboardButton( + text="➡️", + callback_data=f"travels_page_{page + 1}", + ), + ) + else: + navigation_row.append( + InlineKeyboardButton( + text=" ", + callback_data="pass", + ), + ) + + builder.row(*navigation_row) return builder.as_markup() diff --git a/app/messages.py b/app/messages.py index 372102d..7f613e8 100644 --- a/app/messages.py +++ b/app/messages.py @@ -4,14 +4,16 @@ MENU = "Menu:" TRAVELS = "📃 Travels:" NO_TRAVELS = "No travels yet. You can create one with /create_travel command." -CREATE_TRAVEL = "🧳 Let's create new travel!\nEnter /cancel to cancel creating." -INPUT_TRAVEL_TITLE = "Enter travel title:\nMaximum length: 30 characters" +CREATE_TRAVEL = ( + "🧳 Let's create new travel!\nEnter /cancel to cancel creating." +) +INPUT_TRAVEL_TITLE = ( + "Enter travel title:\nMaximum length: 30 characters" +) INPUT_TRAVEL_CALLBACK = ( "All right, travel {key} is set to: {value}" ) -INPUT_TRAVEL_DESCRIPTION = ( - "Enter travel description (enter /skip if you want to skip this step):\nMaximum length: 100 characters" -) +INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\nMaximum length: 100 characters" INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later." TRAVEL_CREATED = "Travel {title} successfully created! You can now view and edit it in the travels list (/travels command)." ACTION_CANCELED = "❌ Action canceled" diff --git a/app/models/user.py b/app/models/user.py index 4da0dbe..3f34719 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -5,7 +5,7 @@ import re import sqlalchemy as sa from sqlalchemy.orm import relationship, validates -from app import session +from app import messages, session from app.models import Base from app.utils import geo from app.utils.db import utcnow @@ -97,12 +97,23 @@ class User(Base): return normalized_value - def get_user_travels(self): + def get_user_travels(self) -> list: return self.owned_travels + self.travels - def get_human_readable_datejoined(self): + def get_human_readable_datejoined(self) -> str: return self.date_joined.strftime("%Y-%m-%d %H:%M:%S") + def get_profile_text(self) -> str: + return messages.PROFILE.format( + username=self.username, + age=self.age, + bio=self.bio if self.bio else messages.NOT_SET, + sex=self.sex.capitalize(), + country=self.country, + city=self.city, + date_joined=self.get_human_readable_datejoined(), + ) + @classmethod def get_user_queryset_by_telegram_id(cls, telegram_id): return session.query(cls).filter(cls.telegram_id == telegram_id) diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/check.sh b/check.sh index fcecd2b..c68ec96 100644 --- a/check.sh +++ b/check.sh @@ -6,7 +6,7 @@ NC='\033[0m' sort-requirements requirements/dev.txt sort-requirements requirements/prod.txt sort-requirements requirements/test.txt -sort-requirements requirements/lints.txt +sort-requirements requirements/lint.txt printf "${GREEN}Requirements sorted${NC}\n" black . diff --git a/docker-compose.yml b/docker-compose.yml index 3a900d4..1499a8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: postgres: - image: postgres:latest + image: postgres:16.2-alpine container_name: postgres healthcheck: test: pg_isready -U postgres -h localhost @@ -11,21 +11,21 @@ services: timeout: 5s retries: 10 environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: wTAb5KoZ4dBtscg + POSTGRES_DB: ${POSTGRES_DB:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} ports: - - "5432:5432" + - "${POSTGRES_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data redis: - image: redis:latest + image: redis:7.2-alpine container_name: redis healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] ports: - - "6379:6379" + - "${REDIS_PORT:-6379}:6379" volumes: - redis_data:/data @@ -38,20 +38,23 @@ services: redis: condition: service_healthy environment: - BOT_TOKEN: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q - REDIS_URL: redis://redis:6379/ - SQLALCHEMY_DATABASE_URI: postgresql://postgres:wTAb5KoZ4dBtscg@postgres:5432/postgres + BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q} + REDIS_URL: redis://redis:${REDIS_PORT:-6379}/ + SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} entrypoint: ["bash", "-c"] command: ["alembic -c app/alembic.ini upgrade head && python -m app"] pgadmin: - image: dpage/pgadmin4 + image: dpage/pgadmin4:8.4 container_name: pgadmin + depends_on: + postgres: + condition: service_healthy environment: - PGADMIN_DEFAULT_EMAIL: admin@mail.com - PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@mail.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin} ports: - - "5050:80" + - "${PGADMIN_PORT:-5050}:80" restart: always volumes: - pgadmin_data:/var/lib/pgadmin diff --git a/requirements/dev.txt b/requirements/dev.txt index 474737d..9696600 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,4 +4,4 @@ sort-requirements -r prod.txt -r test.txt --r lints.txt +-r lint.txt diff --git a/requirements/lints.txt b/requirements/lint.txt similarity index 100% rename from requirements/lints.txt rename to requirements/lint.txt diff --git a/template.env b/template.env index f474e6e..788eb21 100644 --- a/template.env +++ b/template.env @@ -1,2 +1,18 @@ -BOT_TOKEN = -SQLALCHEMY_DATABASE_URI = +# For app + +BOT_TOKEN = # default: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q +SQLALCHEMY_DATABASE_URI = # no need to specify if docker is used +REDIS_URL = # no need to specify if docker is used + +# For docker(remove if you want to keep defaults) + +POSTGRES_PORT = # default: 5432 +POSTGRES_DB = # default: postgres +POSTGRES_USER = # default: postgres +POSTGRES_PASSWORD = # default: postgres + +REDIS_PORT = # default: 6379 + +PGADMIN_PORT = # default: 5050 +PGADMIN_EMAIL = # default: admin@mail.com +PGADMIN_PASSWORD # default: admin