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