chore: Global project refactoring

This commit is contained in:
ITQ
2024-03-23 10:47:39 +03:00
parent 1802ce81b0
commit 7b28635f09
21 changed files with 143 additions and 116 deletions
-2
View File
@@ -1,9 +1,7 @@
# Files # Files
README.md README.md
task.md
check.sh check.sh
.flake8 .flake8
template.env
# Folders # Folders
img/ img/
-3
View File
@@ -2,11 +2,8 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# Copy requirements file
COPY requirements/prod.txt . COPY requirements/prod.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r prod.txt RUN pip install --no-cache-dir -r prod.txt
# Copy the rest of the application files
COPY . . COPY . .
+3 -4
View File
@@ -38,10 +38,9 @@ async def main() -> None:
profile_command.router, profile_command.router,
create_travel_command.router, create_travel_command.router,
travels_command.router, travels_command.router,
menu.router,
menu.router, # type: ignore profile.router,
profile.router, # type: ignore travels.router,
travels.router, # type: ignore
) )
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
+17 -17
View File
@@ -1,11 +1,10 @@
# type: ignore __all__ = ("router",)
__all__ = ()
from aiogram import F, Router from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery, Message
from app import messages from app import messages
from app.config import Config from app.config import Config
@@ -20,24 +19,18 @@ router = Router(name="menu_callback")
@router.callback_query( @router.callback_query(
F.data == "menu_profile", RegisteredCallback(), StateFilter(None), F.data == "menu_profile",
RegisteredCallback(),
StateFilter(None),
) )
async def profile_callback(callback: CallbackQuery) -> 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 return
user = User().get_user_by_telegram_id(callback.from_user.id) user = User().get_user_by_telegram_id(callback.from_user.id)
await callback.message.answer( await callback.message.answer(
messages.PROFILE.format( user.get_profile_text(),
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(),
),
reply_markup=get(), reply_markup=get(),
) )
await callback.answer() await callback.answer()
@@ -49,13 +42,15 @@ async def profile_callback(callback: CallbackQuery) -> None:
@router.callback_query( @router.callback_query(
F.data == "menu_create_travel", RegisteredCallback(), StateFilter(None), F.data == "menu_create_travel",
RegisteredCallback(),
StateFilter(None),
) )
async def create_travel_callback( async def create_travel_callback(
callback: CallbackQuery, callback: CallbackQuery,
state: FSMContext, state: FSMContext,
) -> None: ) -> None:
if callback.data is None or callback.message is None: if callback.data is None or not isinstance(callback.message, Message):
return return
await callback.message.answer( await callback.message.answer(
@@ -75,11 +70,16 @@ async def create_travel_callback(
@router.callback_query( @router.callback_query(
F.data == "menu_travels", RegisteredCallback(), StateFilter(None), F.data == "menu_travels",
RegisteredCallback(),
StateFilter(None),
) )
async def travels_callback( async def travels_callback(
callback: CallbackQuery, callback: CallbackQuery,
) -> None: ) -> None:
if not isinstance(callback.message, Message):
return
page = 0 page = 0
user = User().get_user_by_telegram_id(callback.from_user.id) user = User().get_user_by_telegram_id(callback.from_user.id)
+14 -12
View File
@@ -1,5 +1,4 @@
# type: ignore __all__ = ("router",)
__all__ = ()
from aiogram import F, Router from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
@@ -31,7 +30,11 @@ async def profile_change_callback(
callback: CallbackQuery, callback: CallbackQuery,
state: FSMContext, state: FSMContext,
) -> None: ) -> 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 return
column = callback.data.replace("profile_change_", "") column = callback.data.replace("profile_change_", "")
@@ -70,6 +73,13 @@ async def profile_change_callback(
@router.message(UserAlteringState.value, F.text, Registered()) @router.message(UserAlteringState.value, F.text, Registered())
async def profile_change_entered(message: Message, state: FSMContext) -> None: 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") column = (await state.get_data()).get("column")
value = message.text.strip() value = message.text.strip()
@@ -205,15 +215,7 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
try: try:
await message.bot.edit_message_text( await message.bot.edit_message_text(
messages.PROFILE.format( user.get_profile_text(),
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(),
),
message.chat.id, message.chat.id,
state_data["profile_message_id"], state_data["profile_message_id"],
reply_markup=get(), reply_markup=get(),
+8 -4
View File
@@ -1,10 +1,9 @@
# type: ignore __all__ = ("router",)
__all__ = ()
from aiogram import F, Router from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery, Message
from app import messages from app import messages
from app.config import Config from app.config import Config
@@ -17,9 +16,14 @@ router = Router(name="menu_callback")
@router.callback_query( @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: 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_", "")) page = int(callback.data.replace("travels_page_", ""))
user = User().get_user_by_telegram_id(callback.from_user.id) user = User().get_user_by_telegram_id(callback.from_user.id)
+1 -1
View File
@@ -1,4 +1,4 @@
__all__ = () __all__ = ("router",)
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
+1 -1
View File
@@ -1,4 +1,4 @@
__all__ = () __all__ = ("router",)
from aiogram import Router from aiogram import Router
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
+1 -1
View File
@@ -1,4 +1,4 @@
__all__ = () __all__ = ("router",)
from aiogram import Router from aiogram import Router
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
+2 -11
View File
@@ -1,10 +1,9 @@
__all__ = () __all__ = ("router",)
from aiogram import Router from aiogram import Router
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
from aiogram.types import Message from aiogram.types import Message
from app import messages
from app.filters.user import Registered from app.filters.user import Registered
from app.keyboards.profile import get from app.keyboards.profile import get
from app.models.user import User 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) user = User().get_user_by_telegram_id(message.from_user.id)
await message.answer( await message.answer(
messages.PROFILE.format( user.get_profile_text(),
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(),
),
reply_markup=get(), reply_markup=get(),
) )
+1 -1
View File
@@ -1,4 +1,4 @@
__all__ = () __all__ = ("router",)
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import CommandStart, StateFilter from aiogram.filters import CommandStart, StateFilter
+1 -1
View File
@@ -1,4 +1,4 @@
__all__ = () __all__ = ("router",)
from aiogram import Router from aiogram import Router
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
+13 -9
View File
@@ -1,4 +1,4 @@
__all__ = ("sex_keyboard",) __all__ = ("sex_keyboard", "travels_keyboard")
from aiogram.types import InlineKeyboardButton from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
@@ -10,9 +10,9 @@ def sex_keyboard(choices: str | list):
builder = ReplyKeyboardBuilder() builder = ReplyKeyboardBuilder()
if isinstance(choices, str): 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) return builder.as_markup(resize_keyboard=True)
@@ -36,38 +36,42 @@ def travels_keyboard(travels: list, page: int, pages: int):
builder.row(*rows, width=2) builder.row(*rows, width=2)
if pages > 1:
navigation_row = [] navigation_row = []
if page > 0: if page > 0:
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text="⬅️", callback_data=f"travels_page_{page - 1}", text="⬅️",
callback_data=f"travels_page_{page - 1}",
), ),
) )
else: else:
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text=" ", callback_data="pass", text=" ",
callback_data="pass",
), ),
) )
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{page + 1}/{pages}", callback_data="pass", text=f"{page + 1}/{pages}",
callback_data="pass",
), ),
) )
if page < pages - 1: if page < pages - 1:
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text="➡️", callback_data=f"travels_page_{page + 1}", text="➡️",
callback_data=f"travels_page_{page + 1}",
), ),
) )
else: else:
navigation_row.append( navigation_row.append(
InlineKeyboardButton( InlineKeyboardButton(
text=" ", callback_data="pass", text=" ",
callback_data="pass",
), ),
) )
+7 -5
View File
@@ -4,14 +4,16 @@ MENU = "<b>Menu:</b>"
TRAVELS = "📃 <b>Travels:</b>" TRAVELS = "📃 <b>Travels:</b>"
NO_TRAVELS = "No travels yet. You can create one with /create_travel command." NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
CREATE_TRAVEL = "🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>" CREATE_TRAVEL = (
INPUT_TRAVEL_TITLE = "Enter travel title:\n<i>Maximum length: 30 characters</i>" "🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>"
)
INPUT_TRAVEL_TITLE = (
"Enter travel title:\n<i>Maximum length: 30 characters</i>"
)
INPUT_TRAVEL_CALLBACK = ( INPUT_TRAVEL_CALLBACK = (
"All right, travel <b>{key}</b> is set to: <b>{value}</b>" "All right, travel <b>{key}</b> is set to: <b>{value}</b>"
) )
INPUT_TRAVEL_DESCRIPTION = ( INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
"Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
)
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later." INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later."
TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)." TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)."
ACTION_CANCELED = "❌ Action canceled" ACTION_CANCELED = "❌ Action canceled"
+14 -3
View File
@@ -5,7 +5,7 @@ import re
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.orm import relationship, validates from sqlalchemy.orm import relationship, validates
from app import session from app import messages, session
from app.models import Base from app.models import Base
from app.utils import geo from app.utils import geo
from app.utils.db import utcnow from app.utils.db import utcnow
@@ -97,12 +97,23 @@ class User(Base):
return normalized_value return normalized_value
def get_user_travels(self): def get_user_travels(self) -> list:
return self.owned_travels + self.travels 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") 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 @classmethod
def get_user_queryset_by_telegram_id(cls, telegram_id): def get_user_queryset_by_telegram_id(cls, telegram_id):
return session.query(cls).filter(cls.telegram_id == telegram_id) return session.query(cls).filter(cls.telegram_id == telegram_id)
View File
+1 -1
View File
@@ -6,7 +6,7 @@ NC='\033[0m'
sort-requirements requirements/dev.txt sort-requirements requirements/dev.txt
sort-requirements requirements/prod.txt sort-requirements requirements/prod.txt
sort-requirements requirements/test.txt sort-requirements requirements/test.txt
sort-requirements requirements/lints.txt sort-requirements requirements/lint.txt
printf "${GREEN}Requirements sorted${NC}\n" printf "${GREEN}Requirements sorted${NC}\n"
black . black .
+17 -14
View File
@@ -3,7 +3,7 @@ version: "3"
services: services:
postgres: postgres:
image: postgres:latest image: postgres:16.2-alpine
container_name: postgres container_name: postgres
healthcheck: healthcheck:
test: pg_isready -U postgres -h localhost test: pg_isready -U postgres -h localhost
@@ -11,21 +11,21 @@ services:
timeout: 5s timeout: 5s
retries: 10 retries: 10
environment: environment:
POSTGRES_DB: postgres POSTGRES_DB: ${POSTGRES_DB:-postgres}
POSTGRES_USER: postgres POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: wTAb5KoZ4dBtscg POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
ports: ports:
- "5432:5432" - "${POSTGRES_PORT:-5432}:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
redis: redis:
image: redis:latest image: redis:7.2-alpine
container_name: redis container_name: redis
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"] test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
ports: ports:
- "6379:6379" - "${REDIS_PORT:-6379}:6379"
volumes: volumes:
- redis_data:/data - redis_data:/data
@@ -38,20 +38,23 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
environment: environment:
BOT_TOKEN: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q}
REDIS_URL: redis://redis:6379/ REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
SQLALCHEMY_DATABASE_URI: postgresql://postgres:wTAb5KoZ4dBtscg@postgres:5432/postgres SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
entrypoint: ["bash", "-c"] entrypoint: ["bash", "-c"]
command: ["alembic -c app/alembic.ini upgrade head && python -m app"] command: ["alembic -c app/alembic.ini upgrade head && python -m app"]
pgadmin: pgadmin:
image: dpage/pgadmin4 image: dpage/pgadmin4:8.4
container_name: pgadmin container_name: pgadmin
depends_on:
postgres:
condition: service_healthy
environment: environment:
PGADMIN_DEFAULT_EMAIL: admin@mail.com PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@mail.com}
PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin}
ports: ports:
- "5050:80" - "${PGADMIN_PORT:-5050}:80"
restart: always restart: always
volumes: volumes:
- pgadmin_data:/var/lib/pgadmin - pgadmin_data:/var/lib/pgadmin
+1 -1
View File
@@ -4,4 +4,4 @@ sort-requirements
-r prod.txt -r prod.txt
-r test.txt -r test.txt
-r lints.txt -r lint.txt
+18 -2
View File
@@ -1,2 +1,18 @@
BOT_TOKEN = <your_bot_token> # For app
SQLALCHEMY_DATABASE_URI = <database_uri>
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q
SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used
REDIS_URL = <redis_url> # no need to specify if docker is used
# For docker(remove if you want to keep defaults)
POSTGRES_PORT = <port_to_be_forwared> # default: 5432
POSTGRES_DB = <db_name> # default: postgres
POSTGRES_USER = <postgres_user> # default: postgres
POSTGRES_PASSWORD = <password> # default: postgres
REDIS_PORT = <port_to_be_forwared> # default: 6379
PGADMIN_PORT = <port_to_be_forwared> # default: 5050
PGADMIN_EMAIL = <email> # default: admin@mail.com
PGADMIN_PASSWORD <password> # default: admin