diff --git a/scripts/pybabel.py b/scripts/pybabel.py
index 4a43684..112fac6 100644
--- a/scripts/pybabel.py
+++ b/scripts/pybabel.py
@@ -10,14 +10,25 @@ import argparse
import subprocess
from pathlib import Path
+# Constant paths for project.
LOCALES_DIR = Path("src/locales")
LOCALES_DOMAIN = "messages"
LOCALES_POT = LOCALES_DIR / f"{LOCALES_DOMAIN}.pot"
LOCALES_WIDTH = 80
-def run_cmd(cmds: list[list[str]]) -> None:
+def run_commands(cmds: list[list[str]]) -> None:
+ """
+ Run terminal commands from Python.
+ Used for pybabel commands.
+
+ Args:
+ cmds (list[list[str]]): list of commands split by space.
+ Example: [["echo", "Hello World"], ["uv", "run", "main.py"]]
+ """
try:
+ # Try to run command and print output.
+ # Print only stderr output (pybabel use stderr).
for cmd in cmds:
# DEBUG: User input is completely safe,
# as it only accepts specific values.
@@ -34,6 +45,10 @@ def run_cmd(cmds: list[list[str]]) -> None:
def main() -> None:
+ """
+ Main logic of script.
+ Parse args, try to run commands.
+ """
parser = argparse.ArgumentParser(
description="Wrapper for pybabel operations."
)
@@ -65,20 +80,26 @@ def main() -> None:
cmd.append("pybabel")
cmd.append(args.operation)
+ # Extract operation require only extract command.
if args.operation == "extract":
cmd.append(".")
cmd.append("-o")
cmd.append(str(LOCALES_POT))
- run_cmd([cmd])
+ run_commands([cmd])
return
+ # Other operations (init, update, compile)
+ # may require multiple commands.
+
+ # Set base flags for these operations.
cmd.append("-D")
cmd.append(LOCALES_DOMAIN)
cmd.append("-d")
cmd.append(str(LOCALES_DIR))
+ # Specific flags
if args.operation in ("update", "init"):
cmd.append("-i")
cmd.append(str(LOCALES_POT))
@@ -87,6 +108,7 @@ def main() -> None:
cmd.append("-w")
cmd.append(str(LOCALES_WIDTH))
+ # Add all langs or specific lang.
if args.language == "all":
langs = [
str(path_name.name)
@@ -98,7 +120,8 @@ def main() -> None:
cmds = [cmd + ["-l"] + [lang] for lang in langs]
- run_cmd(cmds)
+ # Run commands
+ run_commands(cmds)
if __name__ == "__main__":
diff --git a/src/bot/__init__.py b/src/bot/__init__.py
index e56b04b..20fbcba 100644
--- a/src/bot/__init__.py
+++ b/src/bot/__init__.py
@@ -1,5 +1,6 @@
__all__ = ["start_bot"]
+
from aiogram import Bot, Dispatcher
from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession
@@ -13,6 +14,14 @@ async def start_bot(
redis_client: Redis,
database_session: AsyncSession,
) -> None:
+ """
+ Start Telegram bot.
+
+ Args:
+ bot_token (str): Telegram API bot token.
+ redis_client (Redis): async configured client for redis.
+ database_session (AsyncSession): async database session.
+ """
bot = Bot(bot_token)
dispatcher = Dispatcher()
@@ -20,6 +29,7 @@ async def start_bot(
connect_handlers(dispatcher)
connect_middlewares(dispatcher)
+ # Drop telegram bot updates, add bot menu.
await bot.delete_webhook(drop_pending_updates=True)
await registry.set_menu(bot)
diff --git a/src/bot/handlers/__init__.py b/src/bot/handlers/__init__.py
index 6e753ae..2f8e8ea 100644
--- a/src/bot/handlers/__init__.py
+++ b/src/bot/handlers/__init__.py
@@ -7,10 +7,17 @@ from aiogram import Router
from .utils.registry import RouterRegistry
+# Main registry router for bot handlers.
registry: RouterRegistry = RouterRegistry()
def connect_handlers(dispatcher: Router) -> None:
+ """
+ Load callbacks and commands modules to register bot handlers.
+
+ Args:
+ dispatcher (Router): Aiogram Dispatcher.
+ """
importlib.import_module("bot.handlers.callbacks")
importlib.import_module("bot.handlers.commands")
diff --git a/src/bot/handlers/callbacks/__init__.py b/src/bot/handlers/callbacks/__init__.py
index a9a2c5b..fa969e2 100644
--- a/src/bot/handlers/callbacks/__init__.py
+++ b/src/bot/handlers/callbacks/__init__.py
@@ -1 +1,4 @@
__all__ = []
+
+
+# TODO: Add automatic import all modules from this folder.
diff --git a/src/bot/handlers/commands/__init__.py b/src/bot/handlers/commands/__init__.py
index 897cbe5..5e9d7fa 100644
--- a/src/bot/handlers/commands/__init__.py
+++ b/src/bot/handlers/commands/__init__.py
@@ -1,6 +1,8 @@
__all__ = []
+# TODO: Add automatic import all modules from this folder.
+
from aiogram.types import Message
from bot.handlers import registry
diff --git a/src/bot/handlers/utils/filters/__init__.py b/src/bot/handlers/utils/filters/__init__.py
index 4c326f2..e59a335 100644
--- a/src/bot/handlers/utils/filters/__init__.py
+++ b/src/bot/handlers/utils/filters/__init__.py
@@ -10,10 +10,26 @@ from bot.handlers.utils.types import ChatType
class ChatTypeFilter(BaseFilter):
+ """
+ Chat type filter for handlers.
+ Only for callbacks and messages.
+
+ Attrs:
+ chat_types (Union[ChatType, list[ChatType]]):
+ Telegram Chat Type from enum ChatType.
+ """
+
def __init__(
self,
chat_types: Union[ChatType, list[ChatType]],
) -> None:
+ """
+ ChatTypeFilter initialization.
+
+ Args:
+ chat_types (Union[ChatType, list[ChatType]]):
+ telegram chat type from enum ChatType.
+ """
if isinstance(chat_types, ChatType):
self.chat_type = [chat_types.value]
else:
@@ -23,6 +39,17 @@ class ChatTypeFilter(BaseFilter):
self,
event: Union[Message, CallbackQuery],
) -> bool:
+ """
+ Try checking the event's chat type.
+ Or return False if the event doesn't have the message attr.
+
+ Args:
+ event (Union[Message, CallbackQuery]):
+ a callback or a message event.
+
+ Returns:
+ bool: whether the event's chat type is in the chat_types.
+ """
current_chat_type: str
if isinstance(event, Message):
diff --git a/src/bot/handlers/utils/keyboards/__init__.py b/src/bot/handlers/utils/keyboards/__init__.py
index a9a2c5b..f25d66d 100644
--- a/src/bot/handlers/utils/keyboards/__init__.py
+++ b/src/bot/handlers/utils/keyboards/__init__.py
@@ -1 +1,4 @@
__all__ = []
+
+
+# TODO: Add reply keyboard markups for bot here.
diff --git a/src/bot/handlers/utils/registry/__init__.py b/src/bot/handlers/utils/registry/__init__.py
index af8a346..43c52a8 100644
--- a/src/bot/handlers/utils/registry/__init__.py
+++ b/src/bot/handlers/utils/registry/__init__.py
@@ -19,12 +19,30 @@ from bot.handlers.utils.types import (
class RouterRegistry:
+ """
+ Router Registry for Aiogram.
+ Stores the configured router and information about handlers in it.
+
+ Attrs:
+ _router (Router): Aiogram Router.
+ _handlers (list[HandlerMeta]): list of handlers meta info.
+ """
+
def __init__(self) -> None:
+ """
+ RouterRegistry initialization.
+ """
self._router = Router()
self._handlers: list[HandlerMeta] = list()
@property
def router(self) -> Router:
+ """
+ Getter for the _router attr.
+
+ Returns:
+ Router: _description_
+ """
return self._router
def register(
@@ -36,6 +54,42 @@ class RouterRegistry:
command: str,
is_callback: bool = False,
) -> Callable[[Any], HandlerType]:
+ """
+ Handler Registration Decorator for handler func.
+
+ Example:
+ ```
+ from aiogram.types import Message
+
+ from bot.handlers import registry
+ from bot.handlers.utils.types import ChatType
+
+
+ @registry.register(
+ command="start",
+ chat_types=ChatType.PRIVATE,
+ description="Test Start Function Description",
+ )
+ async def cmd_start(message: Message) -> None:
+ await message.answer(
+ "Test Start Function Answer Text"
+ )
+ ```
+
+ Args:
+ description (str):
+ handler description for Telegram bot menu.
+ chat_types (Union[ChatType, list[ChatType]]):
+ list of Telegram chat types.
+ command (str):
+ specifying handler command trigger (data for callback).
+ filters (Union[BaseFilter, list[BaseFilter]], optional):
+ List of a handler's filters.
+ Defaults to [].
+ is_callback (bool, optional):
+ If the func is for callback, then True.
+ Defaults to False.
+ """
if isinstance(chat_types, ChatType):
chat_types = [chat_types]
@@ -70,6 +124,12 @@ class RouterRegistry:
return decorator
async def set_menu(self, bot: Bot) -> None:
+ """
+ Set the Telegram Bot menu.
+
+ Args:
+ bot (Bot): Aiogram Bot.
+ """
await bot.set_my_commands(
[
BotCommand(
diff --git a/src/bot/handlers/utils/states/__init__.py b/src/bot/handlers/utils/states/__init__.py
index a9a2c5b..9414a38 100644
--- a/src/bot/handlers/utils/states/__init__.py
+++ b/src/bot/handlers/utils/states/__init__.py
@@ -1 +1,4 @@
__all__ = []
+
+
+# TODO: Add states for FSM Aiogram here.
diff --git a/src/bot/handlers/utils/types/chat.py b/src/bot/handlers/utils/types/chat.py
index b24f393..d517f88 100644
--- a/src/bot/handlers/utils/types/chat.py
+++ b/src/bot/handlers/utils/types/chat.py
@@ -2,6 +2,11 @@ from enum import Enum
class ChatType(Enum):
+ """
+ All Telegram Chat Types.
+ Private, group, supergroup, channel.
+ """
+
PRIVATE = "private"
GROUP = "group"
SUPERGROUP = "supergroup"
diff --git a/src/bot/handlers/utils/types/handler.py b/src/bot/handlers/utils/types/handler.py
index 67e6fa8..34a2c1c 100644
--- a/src/bot/handlers/utils/types/handler.py
+++ b/src/bot/handlers/utils/types/handler.py
@@ -10,6 +10,23 @@ HandlerType = TypeVar(
@dataclass
class HandlerMeta:
+ """
+ Special class for storing useful information about the handler.
+
+ Attrs:
+ func (Callable[[Any], Awaitable[Any]]):
+ aiogram handler func.
+ description (str):
+ handler description for Telegram bot menu.
+ chat_types (list[ChatType]):
+ list of Telegram chat types.
+ command (str):
+ specifying handler command trigger (data for callback).
+ is_callback (bool):
+ If the func is for callback, then True.
+ Defaults to False.
+ """
+
func: Callable[[Any], Awaitable[Any]]
description: str
chat_types: list[ChatType]
diff --git a/src/bot/middlewares/__init__.py b/src/bot/middlewares/__init__.py
index a139ade..c9ca463 100644
--- a/src/bot/middlewares/__init__.py
+++ b/src/bot/middlewares/__init__.py
@@ -4,4 +4,6 @@ __all__ = ["connect_middlewares"]
from aiogram import Router
-def connect_middlewares(dispatcher: Router): ...
+def connect_middlewares(dispatcher: Router):
+ # TODO: Add database and localization middleware and connect them.
+ ...
diff --git a/src/bot/services/__init__.py b/src/bot/services/__init__.py
index a9a2c5b..7dfd4a8 100644
--- a/src/bot/services/__init__.py
+++ b/src/bot/services/__init__.py
@@ -1 +1,5 @@
__all__ = []
+
+
+# TODO: Add bot business logic
+# (working with other service components).
diff --git a/src/config/utils.py b/src/config/utils.py
index 884006d..c944472 100644
--- a/src/config/utils.py
+++ b/src/config/utils.py
@@ -8,7 +8,42 @@ from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
+# Constant paths for project.
+LOG_DIR = Path("logs")
+LOG_DIR.mkdir(parents=True, exist_ok=True)
+
+# Log format for loguru logger.
+LOG_FORMAT = (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level:<8} | "
+ "{process}.{thread.name} | "
+ "{module}.{function}:{line} – "
+ "{message}"
+)
+
+
class Settings(BaseSettings):
+ """
+ Class to specify env settings for the project.
+
+ The .env file is located in the project root.
+
+ Env Vars:
+ bot_token (str):
+ Telegram API bot token.
+ listen_logging (bool):
+ intercepting logs from logging.
+ Defaults to False.
+ file_log_level (Literal):
+ logging level for the .log file.
+ "DEBUG", "INFO", "WARNING" or "ERROR".
+ Defaults to "DEBUG".
+ console_log_level (Literal):
+ logging level for the console.
+ "DEBUG", "INFO", "WARNING" or "ERROR".
+ Defaults to "INFO".
+ """
+
bot_token: str = Field(frozen=True)
listen_logging: bool = Field(
@@ -43,20 +78,18 @@ class Settings(BaseSettings):
)
-LOG_DIR = Path("logs")
-LOG_DIR.mkdir(parents=True, exist_ok=True)
-
-LOG_FORMAT = (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level:<8} | "
- "{process}.{thread.name} | "
- "{module}.{function}:{line} – "
- "{message}"
-)
-
-
class InterceptHandler(logging.Handler):
+ """
+ Class for intercepting logs from logging, including Aiogram.
+ """
+
def emit(self, record: logging.LogRecord) -> None:
+ """
+ Try to intercepting logs from logging.
+
+ Args:
+ record (logging.LogRecord): record from logging.
+ """
try:
level = loguru_logger.level(record.levelname).name
except ValueError:
@@ -82,6 +115,14 @@ def configure_logger(
listen_logging: bool,
console_log_level: str,
) -> None:
+ """
+ Configure loguru logger.
+
+ Args:
+ file_log_level (str): logging level for the .log file.
+ listen_logging (bool): intercepting logs from logging.
+ console_log_level (str): logging level for the console.
+ """
if listen_logging:
logging.root.handlers.clear()
logging.root.setLevel(logging.DEBUG)
diff --git a/src/database/models.py b/src/database/models.py
index e69de29..c855580 100644
--- a/src/database/models.py
+++ b/src/database/models.py
@@ -0,0 +1 @@
+# TODO: Add database models.
diff --git a/src/database/session.py b/src/database/session.py
index 7431e3b..abdcd2d 100644
--- a/src/database/session.py
+++ b/src/database/session.py
@@ -1,3 +1,4 @@
from sqlalchemy.ext.asyncio import AsyncSession
+# TODO: Add database session.
session: AsyncSession = ...
diff --git a/src/main.py b/src/main.py
index 5c25333..824ed22 100644
--- a/src/main.py
+++ b/src/main.py
@@ -7,6 +7,10 @@ from redis_client import client
def main() -> None:
+ """
+ Launch of all service components.
+ Configure logger and start bot.
+ """
configure_logger(
file_log_level=settings.file_log_level,
console_log_level=settings.console_log_level,
diff --git a/src/redis_client/__init__.py b/src/redis_client/__init__.py
index 4c9e398..c020937 100644
--- a/src/redis_client/__init__.py
+++ b/src/redis_client/__init__.py
@@ -3,4 +3,5 @@ __all__ = ["client"]
from redis.asyncio.client import Redis
+# TODO: Add redis client.
client: Redis = ...