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 = ...