From 658f6af46ee5e730a1f0e4a7af0099c3691df873 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 11:14:45 +0300 Subject: [PATCH 1/9] refactor: move folder `example` to the project root --- {aiogram_renderer/example => example}/main.py | 2 +- {aiogram_renderer/example => example}/routers.py | 2 +- {aiogram_renderer/example => example}/states.py | 0 {aiogram_renderer/example => example}/test.png | Bin {aiogram_renderer/example => example}/windows.py | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename {aiogram_renderer/example => example}/main.py (95%) rename {aiogram_renderer/example => example}/routers.py (98%) rename {aiogram_renderer/example => example}/states.py (100%) rename {aiogram_renderer/example => example}/test.png (100%) rename {aiogram_renderer/example => example}/windows.py (100%) diff --git a/aiogram_renderer/example/main.py b/example/main.py similarity index 95% rename from aiogram_renderer/example/main.py rename to example/main.py index d6efffc..ac57446 100644 --- a/aiogram_renderer/example/main.py +++ b/example/main.py @@ -11,7 +11,7 @@ import routers from aiogram_renderer.bot_mode import BotMode from aiogram_renderer.configure import configure_renderer -from aiogram_renderer.example.windows import main_window, alert_mode, main_window2 +from example.windows import main_window, alert_mode, main_window2 load_dotenv() diff --git a/aiogram_renderer/example/routers.py b/example/routers.py similarity index 98% rename from aiogram_renderer/example/routers.py rename to example/routers.py index 1382658..fbb56aa 100644 --- a/aiogram_renderer/example/routers.py +++ b/example/routers.py @@ -3,7 +3,7 @@ from aiogram import Router, F from aiogram.fsm.context import FSMContext from aiogram.types import Message -from aiogram_renderer.example.windows import alert_mode +from example.windows import alert_mode from aiogram_renderer.filters import IsMode from aiogram_renderer.renderer import Renderer from states import MenuStates diff --git a/aiogram_renderer/example/states.py b/example/states.py similarity index 100% rename from aiogram_renderer/example/states.py rename to example/states.py diff --git a/aiogram_renderer/example/test.png b/example/test.png similarity index 100% rename from aiogram_renderer/example/test.png rename to example/test.png diff --git a/aiogram_renderer/example/windows.py b/example/windows.py similarity index 100% rename from aiogram_renderer/example/windows.py rename to example/windows.py From 0be441a127e7c48c199fccd0d5c657ce6b0c7c62 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 11:16:35 +0300 Subject: [PATCH 2/9] chore: add .gitignore --- .gitignore | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8354d9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a templates +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +#*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +#.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores type_query-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-type_query/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder type_query settings +.spyderproject +.spyproject + +# Rope type_query settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific templates is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ From f02014e41955404c371a8b488684f9139cd8a620 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 11:24:51 +0300 Subject: [PATCH 3/9] refactor: update callback data handling and improve button data packing (using CallbackData) --- aiogram_renderer/callback_data.py | 13 +++++++++++ aiogram_renderer/filters.py | 17 +++++++++----- aiogram_renderer/handlers/inline_router.py | 22 +++++++++--------- aiogram_renderer/widgets/inline/button.py | 6 +++-- aiogram_renderer/widgets/inline/panel.py | 26 ++++++++++++---------- 5 files changed, 52 insertions(+), 32 deletions(-) create mode 100644 aiogram_renderer/callback_data.py diff --git a/aiogram_renderer/callback_data.py b/aiogram_renderer/callback_data.py new file mode 100644 index 0000000..c8168ab --- /dev/null +++ b/aiogram_renderer/callback_data.py @@ -0,0 +1,13 @@ +from aiogram.filters.callback_data import CallbackData + + +class DPanelCD(CallbackData, prefix="__dpanel__"): + page: int + panel_name: str + +class ModeCD(CallbackData, prefix="__mode__"): + name: str + +class ComeToCD(CallbackData, prefix="__cometo__"): + group: str + state: str diff --git a/aiogram_renderer/filters.py b/aiogram_renderer/filters.py index e71aed2..7f61253 100644 --- a/aiogram_renderer/filters.py +++ b/aiogram_renderer/filters.py @@ -1,5 +1,7 @@ from aiogram.filters import BaseFilter from aiogram.types import Message, CallbackQuery + +from .callback_data import ModeCD from .renderer import Renderer @@ -10,11 +12,14 @@ async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> mode = None # Для CallbackQuery проверяем правильно ли задан callback_data по системному префиксу if isinstance(event, CallbackQuery): - if event.data.startswith("__mode__:"): - # Для колбека берем название мода, указанное после "__mode__:" - mode_name = event.data.replace("__mode__:", "") + try: + callback_data = ModeCD.unpack(event.data) + except (TypeError, ValueError): + callback_data = None + + if callback_data is not None: # Проверяем нет ли у данного режима своего хендлера - mode = await renderer.bot_modes.get_mode_by_name(name=mode_name) + mode = await renderer.bot_modes.get_mode_by_name(name=callback_data.name) # Для Message, ищем его среди списков значений модов и выводим по найденному названию мода else: @@ -42,11 +47,11 @@ async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> mode = await renderer.bot_modes.get_mode_by_name(name=self.name) # Проверяем равен ли коллбек заданному режиму if isinstance(event, CallbackQuery): - if (event.data == "__mode__:" + self.name) and (mode is not None): + if event.data == ModeCD(name=self.name).pack() and mode is not None: return True # Проверяем есть ли значение Reply text в values режима elif isinstance(event, Message): - if (event.text in mode.values) and (mode is not None): + if event.text in mode.values and mode is not None: return True else: raise ValueError("Такого режима нет") diff --git a/aiogram_renderer/handlers/inline_router.py b/aiogram_renderer/handlers/inline_router.py index 92576de..8ae89f7 100644 --- a/aiogram_renderer/handlers/inline_router.py +++ b/aiogram_renderer/handlers/inline_router.py @@ -1,6 +1,8 @@ from aiogram import Router, F from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery + +from aiogram_renderer.callback_data import DPanelCD, ModeCD, ComeToCD from aiogram_renderer.filters import IsModeWithNotCustomHandler from aiogram_renderer.renderer import Renderer @@ -11,10 +13,9 @@ RESERVED_CONTAIN_CALLBACKS = ("__mode__", "__dpanel__", "__cometo__") -@router.callback_query(F.data.startswith("__cometo__")) -async def come_to_window(callback: CallbackQuery, renderer: Renderer): - open_state = callback.data.split(":")[1] + ":" + callback.data.split(":")[2] - await renderer.edit(window=open_state, chat_id=callback.message.chat.id, message_id=callback.message.message_id) +@router.callback_query(ComeToCD.filter()) +async def come_to_window(callback: CallbackQuery, callback_data: ComeToCD, renderer: Renderer): + await renderer.edit(window=f"{callback_data.group}:{callback_data.state}", chat_id=callback.message.chat.id, message_id=callback.message.message_id) @router.callback_query(F.data == "__disable__") @@ -28,22 +29,19 @@ async def delete_callback_message(callback: CallbackQuery): @router.callback_query(IsModeWithNotCustomHandler()) -async def update_mode(callback: CallbackQuery, state: FSMContext, renderer: Renderer): - mode_name = callback.data.replace("__mode__:", "") +async def update_mode(callback: CallbackQuery, callback_data: ModeCD, state: FSMContext, renderer: Renderer): # Переключаем режим - await renderer.bot_modes.update_mode(mode=mode_name) + await renderer.bot_modes.update_mode(mode=callback_data.name) # Для InilineButtonMode бот просто отредактирует окно await renderer.edit(window=await state.get_state(), chat_id=callback.message.chat.id, message_id=callback.message.message_id) -@router.callback_query(F.data.startswith("__dpanel__")) -async def switch_dynamic_panel_page(callback: CallbackQuery, state: FSMContext, renderer: Renderer): - page = int(callback.data.split(":")[1]) - panel_name = callback.data.split(":")[2] +@router.callback_query(DPanelCD.filter()) +async def switch_dynamic_panel_page(callback: CallbackQuery, callback_data: DPanelCD, state: FSMContext, renderer: Renderer): message = callback.message w_state = await state.get_state() - await renderer._switch_dynamic_panel_page(name=panel_name, page=page) + await renderer._switch_dynamic_panel_page(name=callback_data.panel_name, page=callback_data.page) await renderer.edit(window=w_state, chat_id=message.chat.id, message_id=message.message_id) diff --git a/aiogram_renderer/widgets/inline/button.py b/aiogram_renderer/widgets/inline/button.py index 7fea4d0..2a50645 100644 --- a/aiogram_renderer/widgets/inline/button.py +++ b/aiogram_renderer/widgets/inline/button.py @@ -1,6 +1,8 @@ from typing import Any from aiogram.fsm.state import State from aiogram.types import InlineKeyboardButton + +from aiogram_renderer.callback_data import ComeToCD, ModeCD from aiogram_renderer.widgets.widget import Widget @@ -36,7 +38,7 @@ class Mode(Button): def __init__(self, name: str, show_on: str = None): self.name = name - super().__init__(text=name, data=f"__mode__:{name}", show_on=show_on) + super().__init__(text=name, data=ModeCD(name=name).pack(), show_on=show_on) async def assemble(self, data: dict[str, Any], **kwargs) -> Any: """ @@ -70,4 +72,4 @@ class ComeTo(Button): __slots__ = () def __init__(self, text: str, state: State, show_on: str = None): - super().__init__(text=text, data=f"__cometo__:{state.state}", show_on=show_on) + super().__init__(text=text, data=ComeToCD(group=state.group.__name__, state=state._state).pack(), show_on=show_on) diff --git a/aiogram_renderer/widgets/inline/panel.py b/aiogram_renderer/widgets/inline/panel.py index 1a47edb..3bc4a5b 100644 --- a/aiogram_renderer/widgets/inline/panel.py +++ b/aiogram_renderer/widgets/inline/panel.py @@ -1,5 +1,7 @@ from typing import Any from aiogram.types import InlineKeyboardButton + +from aiogram_renderer.callback_data import DPanelCD from aiogram_renderer.widgets.inline.button import Button from aiogram_renderer.widgets.widget import Widget @@ -124,37 +126,37 @@ async def assemble(self, data: dict[str, Any], **kwargs) -> list[list[Any]]: if self.hide_number_pages: if page == 1: buttons.append([ - InlineKeyboardButton(text=">", callback_data=f"__dpanel__:{page + 1}:{self.name}"), + InlineKeyboardButton(text=">", callback_data=DPanelCD(page=page + 1, panel_name=self.name).pack()), ]) elif page == last_page: buttons.append([ - InlineKeyboardButton(text="<", callback_data=f"__dpanel__:{page - 1}:{self.name}"), + InlineKeyboardButton(text="<", callback_data=DPanelCD(page=page - 1, panel_name=self.name).pack()), ]) else: buttons.append([ - InlineKeyboardButton(text="<", callback_data=f"__dpanel__:{page - 1}:{self.name}"), - InlineKeyboardButton(text=">", callback_data=f"__dpanel__:{page + 1}:{self.name}"), + InlineKeyboardButton(text="<", callback_data=DPanelCD(page=page - 1, panel_name=self.name).pack()), + InlineKeyboardButton(text=">", callback_data=DPanelCD(page=page + 1, panel_name=self.name).pack()), ]) else: if page == 1: buttons.append([ InlineKeyboardButton(text="[ 1 ]", callback_data=f"__disable__"), - InlineKeyboardButton(text=">", callback_data=f"__dpanel__:{page + 1}:{self.name}"), - InlineKeyboardButton(text=str(last_page), callback_data=f"__dpanel__:{last_page}:{self.name}") + InlineKeyboardButton(text=">", callback_data=DPanelCD(page=page + 1, panel_name=self.name).pack()), + InlineKeyboardButton(text=str(last_page), callback_data=DPanelCD(page=last_page, panel_name=self.name).pack()) ]) elif page == last_page: buttons.append([ - InlineKeyboardButton(text="1", callback_data=f"__dpanel__:1:{self.name}"), - InlineKeyboardButton(text="<", callback_data=f"__dpanel__:{page - 1}:{self.name}"), + InlineKeyboardButton(text="1", callback_data=DPanelCD(page=1, panel_name=self.name).pack()), + InlineKeyboardButton(text="<", callback_data=DPanelCD(page=page - 1, panel_name=self.name).pack()), InlineKeyboardButton(text=f"[ {last_page} ]", callback_data="__disable__"), ]) else: buttons.append([ - InlineKeyboardButton(text="1", callback_data=f"__dpanel__:1:{self.name}"), - InlineKeyboardButton(text="<", callback_data=f"__dpanel__:{page - 1}:{self.name}"), + InlineKeyboardButton(text="1", callback_data=DPanelCD(page=1, panel_name=self.name).pack()), + InlineKeyboardButton(text="<", callback_data=DPanelCD(page=page - 1, panel_name=self.name).pack()), InlineKeyboardButton(text=f"[ {page} ]", callback_data="__disable__"), - InlineKeyboardButton(text=">", callback_data=f"__dpanel__:{page + 1}:{self.name}"), - InlineKeyboardButton(text=str(last_page), callback_data=f"__dpanel__:{last_page}:{self.name}") + InlineKeyboardButton(text=">", callback_data=DPanelCD(page=page + 1, panel_name=self.name).pack()), + InlineKeyboardButton(text=str(last_page), callback_data=DPanelCD(page=last_page, panel_name=self.name).pack()) ]) return buttons From 2fb36b544532e4effce8024358f582e435ed6695 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 11:35:37 +0300 Subject: [PATCH 4/9] refactor example --- example/routers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/example/routers.py b/example/routers.py index fbb56aa..8bb95e3 100644 --- a/example/routers.py +++ b/example/routers.py @@ -1,6 +1,7 @@ from asyncio import sleep from aiogram import Router, F +from aiogram.filters import CommandStart, Command, or_f from aiogram.fsm.context import FSMContext from aiogram.types import Message from example.windows import alert_mode @@ -14,7 +15,7 @@ router.callback_query.filter(F.message.chat.type == "private") -@router.message(F.text.in_({"/start", "/restart"})) +@router.message(or_f(CommandStart(), Command('restart'))) async def start(message: Message, renderer: Renderer): data = {"username": f" {message.from_user.username}" if message.from_user else "", "test_show_on": False, @@ -31,7 +32,7 @@ async def start(message: Message, renderer: Renderer): "data": ["3", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"]}} async with aioopen(file="test.png", mode="rb") as f: - message2, window = await renderer.answer( + sent_message, window = await renderer.answer( window=MenuStates.main1, chat_id=message.chat.id, data=data, @@ -39,8 +40,8 @@ async def start(message: Message, renderer: Renderer): ) for i in range(99): - await renderer.update_progress(window=MenuStates.main1, chat_id=message2.chat.id, interval=0.3, - message_id=message2.message_id, name="test_pr", percent=i, data=data) + await renderer.update_progress(window=MenuStates.main1, chat_id=sent_message.chat.id, interval=0.3, + message_id=sent_message.message_id, name="test_pr", percent=i, data=data) await sleep(0.3) From bf361838bcbfcda2e42d401c2b21928bdb01e733 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 13:53:48 +0300 Subject: [PATCH 5/9] refactor: reorganize widget imports --- aiogram_renderer/bot_mode.py | 2 +- aiogram_renderer/renderer.py | 5 +- aiogram_renderer/widgets/__init__.py | 3 + aiogram_renderer/widgets/inline/button.py | 2 +- aiogram_renderer/widgets/inline/panel.py | 4 +- aiogram_renderer/widgets/media/bytes.py | 2 +- aiogram_renderer/widgets/media/path.py | 2 +- aiogram_renderer/widgets/reply/__init__.py | 9 +- aiogram_renderer/widgets/reply/button.py | 2 +- aiogram_renderer/widgets/reply/panel.py | 4 +- aiogram_renderer/widgets/text.py | 156 --------------------- aiogram_renderer/widgets/text/__init__.py | 5 + aiogram_renderer/widgets/text/area.py | 43 ++++++ aiogram_renderer/widgets/text/progress.py | 45 ++++++ aiogram_renderer/widgets/text/text.py | 62 ++++++++ aiogram_renderer/window.py | 13 +- example/windows.py | 5 +- 17 files changed, 177 insertions(+), 187 deletions(-) delete mode 100644 aiogram_renderer/widgets/text.py create mode 100644 aiogram_renderer/widgets/text/__init__.py create mode 100644 aiogram_renderer/widgets/text/area.py create mode 100644 aiogram_renderer/widgets/text/progress.py create mode 100644 aiogram_renderer/widgets/text/text.py diff --git a/aiogram_renderer/bot_mode.py b/aiogram_renderer/bot_mode.py index eff2e2b..6e33f3b 100644 --- a/aiogram_renderer/bot_mode.py +++ b/aiogram_renderer/bot_mode.py @@ -1,6 +1,6 @@ from typing import Any from aiogram.fsm.context import FSMContext -from .widgets.media.bytes import FileBytes +from .widgets.media import FileBytes from .window import Alert diff --git a/aiogram_renderer/renderer.py b/aiogram_renderer/renderer.py index daab706..a51e869 100644 --- a/aiogram_renderer/renderer.py +++ b/aiogram_renderer/renderer.py @@ -8,9 +8,8 @@ from aiogram.types import Message, InputMediaPhoto, InputMediaVideo, InputMediaAudio, InputMediaDocument from .bot_mode import BotModes from .enums import RenderMode -from .widgets.inline.panel import DynamicPanel -from .widgets.media.bytes import FileBytes, AudioBytes, VideoBytes, PhotoBytes -from .widgets.media.path import File, Audio, Video, Photo +from .widgets.inline import DynamicPanel +from .widgets.media import FileBytes, AudioBytes, VideoBytes, PhotoBytes, File, Audio, Video, Photo from .window import Window, Alert diff --git a/aiogram_renderer/widgets/__init__.py b/aiogram_renderer/widgets/__init__.py index e69de29..dc8cace 100644 --- a/aiogram_renderer/widgets/__init__.py +++ b/aiogram_renderer/widgets/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["Widget"] + +from .widget import Widget diff --git a/aiogram_renderer/widgets/inline/button.py b/aiogram_renderer/widgets/inline/button.py index 2a50645..0d91255 100644 --- a/aiogram_renderer/widgets/inline/button.py +++ b/aiogram_renderer/widgets/inline/button.py @@ -3,7 +3,7 @@ from aiogram.types import InlineKeyboardButton from aiogram_renderer.callback_data import ComeToCD, ModeCD -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets import Widget class Button(Widget): diff --git a/aiogram_renderer/widgets/inline/panel.py b/aiogram_renderer/widgets/inline/panel.py index 3bc4a5b..2f088b9 100644 --- a/aiogram_renderer/widgets/inline/panel.py +++ b/aiogram_renderer/widgets/inline/panel.py @@ -2,8 +2,8 @@ from aiogram.types import InlineKeyboardButton from aiogram_renderer.callback_data import DPanelCD -from aiogram_renderer.widgets.inline.button import Button -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets.inline import Button +from aiogram_renderer.widgets import Widget class Panel(Widget): diff --git a/aiogram_renderer/widgets/media/bytes.py b/aiogram_renderer/widgets/media/bytes.py index 95ae211..11155b5 100644 --- a/aiogram_renderer/widgets/media/bytes.py +++ b/aiogram_renderer/widgets/media/bytes.py @@ -1,7 +1,7 @@ from typing import Any from aiogram.types import BufferedInputFile from aiogram_renderer.widgets.text import Text, Area -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets import Widget class FileBytes(Widget): diff --git a/aiogram_renderer/widgets/media/path.py b/aiogram_renderer/widgets/media/path.py index 70cea76..e314578 100644 --- a/aiogram_renderer/widgets/media/path.py +++ b/aiogram_renderer/widgets/media/path.py @@ -1,7 +1,7 @@ from typing import Any from aiogram.types import FSInputFile from aiogram_renderer.widgets.text import Text, Area -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets import Widget class File(Widget): diff --git a/aiogram_renderer/widgets/reply/__init__.py b/aiogram_renderer/widgets/reply/__init__.py index dbcaa91..187563f 100644 --- a/aiogram_renderer/widgets/reply/__init__.py +++ b/aiogram_renderer/widgets/reply/__init__.py @@ -1,11 +1,4 @@ -__all__ = [ - "ReplyButton", - "ReplyMode", - "ReplyPanel", - "ReplyRow", - "ReplyColumn" -] +__all__ = ["ReplyButton", "ReplyMode", "ReplyPanel", "ReplyRow", "ReplyColumn"] from .button import ReplyButton, ReplyMode from .panel import ReplyPanel, ReplyRow, ReplyColumn - diff --git a/aiogram_renderer/widgets/reply/button.py b/aiogram_renderer/widgets/reply/button.py index cfe8582..45aeeea 100644 --- a/aiogram_renderer/widgets/reply/button.py +++ b/aiogram_renderer/widgets/reply/button.py @@ -1,6 +1,6 @@ from typing import Any from aiogram.types import KeyboardButton -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets import Widget class ReplyButton(Widget): diff --git a/aiogram_renderer/widgets/reply/panel.py b/aiogram_renderer/widgets/reply/panel.py index b8d9052..40c069b 100644 --- a/aiogram_renderer/widgets/reply/panel.py +++ b/aiogram_renderer/widgets/reply/panel.py @@ -1,7 +1,7 @@ from typing import Any from aiogram.types import KeyboardButton -from aiogram_renderer.widgets.reply.button import ReplyButton -from aiogram_renderer.widgets.widget import Widget +from aiogram_renderer.widgets.reply import ReplyButton +from aiogram_renderer.widgets import Widget class ReplyPanel(Widget): diff --git a/aiogram_renderer/widgets/text.py b/aiogram_renderer/widgets/text.py deleted file mode 100644 index 1eda1ca..0000000 --- a/aiogram_renderer/widgets/text.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -from .widget import Widget - - -class Text(Widget): - __slots__ = ("content", "end", "end_count") - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - # Добавляем окончание, в зависимости от end_count - self.content = content - self.end = end - self.end_count = end_count - super().__init__(show_on=show_on) - - async def assemble(self, data: dict[str, Any]) -> str: - if self.show_on in data.keys(): - # Если show_on = False, не собираем текст и возвращаем пустую строку - if not data[self.show_on]: - return "" - - text = self.content - # Форматируем по data, если там заданы ключи {key} - for key, value in data.items(): - if "{" + key + "}" in text: - text = text.replace("{" + key + "}", str(value)) - - return text + "".join([self.end for _ in range(self.end_count)]) - - -class Area(Widget): - __slots__ = ('texts', 'sep', 'sep_count', 'end', 'end_count') - - def __init__(self, *texts: Text | str, sep: str = "\n", sep_count: int = 1, end: str = "\n", - end_count: int = 0, show_on: str = None): - self.texts = list(texts) - self.sep = sep - self.sep_count = sep_count - self.end = end - self.end_count = end_count - super().__init__(show_on=show_on) - - async def assemble(self, data: dict[str, Any]): - if self.show_on in data.keys(): - # Если show_on = False, не собираем Area - if not data[self.show_on]: - return "" - - # Формируем разделители, учитывая их количество и после содержимое - separators = "".join([self.sep for _ in range(self.sep_count)]) - - texts_list = [] - for text in self.texts: - # Если это виджет - if isinstance(text, Text): - # Если when в ключах data, то делаем проверку - if text.show_on in data.keys(): - # Если when = False, не собираем text - if not data[text.show_on]: - continue - asm_text = await text.assemble(data=data) - texts_list.append(asm_text + separators) - # Если это строка - else: - # Форматируем по data, если там заданы ключи {key} - for key, value in data.items(): - if "{" + key + "}" in text: - text = text.replace("{" + key + "}", str(value)) - - texts_list.append(text + separators) - - # Если все тексты скрыты выдаем пустую строку - if len(texts_list) == 0: - return "" - # В другом случае разделяем контент сепараторами и добавляем end - else: - content = "".join(texts_list)[:-self.sep_count] + "".join([self.end for _ in range(self.end_count)]) - - return content - - -class Bold(Text): - __slots__ = () - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) - - -class Italic(Text): - __slots__ = () - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) - - -class Code(Text): - __slots__ = () - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) - - -class Underline(Text): - __slots__ = () - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) - - -class Blockquote(Text): - __slots__ = () - - def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): - super().__init__(content=f"
{content}
", end=end, end_count=end_count, show_on=show_on) - - -class Progress(Widget): - __slots__ = ('name', 'load', 'no_load', 'add_percent', 'postfix', 'prefix') - - def __init__(self, name: str, load: str = "🟥", no_load: str = "⬜", - add_percent: bool = False, prefix: str = "", postfix: str = "", show_on: str = None): - """ - Текстовый виджет для отображения прогресс бара - :param name: название прогресс бара - :param load: символ для загруженной части прогресс бара - :param no_load: символ для не загруженной части прогресс бара - :param add_percent: флаг для добавления постфикса с процентами - :return: - """ - assert (len(load) == 1) and (len(no_load) == 1), ValueError("Задайте параметры load и no_load") - self.name = name - self.load = load - self.no_load = no_load - self.add_percent = add_percent - self.postfix = postfix - self.prefix = prefix - super().__init__(show_on=show_on) - - async def assemble(self, data: dict[str, Any]): - if self.show_on in data.keys(): - # Если show_on = False, не собираем Area - if not data[self.show_on]: - return "" - - percent = data[self.name] if self.name in data else 0 - assert 0.0 <= percent <= 100.0, ValueError("Процент должен быть в промежутке от 0 до 100") - # Форматируем процент убирая 0.0, 100.0 и добавляя постфикс, если он задан - percent = 0 if percent == 0.0 else 100 if percent == 100.0 else percent - percents_postfix = f" {percent}%" if self.add_percent else "" - - # Собираем линию загрузки по проценту - format_percent = 0 if percent == 0 else max(int(percent) // 10, 1) - list_load = [self.load for i_l in range(format_percent)] - list_no_load = [self.no_load for i_nl in range(10 - len(list_load))] - progress_bar = "".join(list_load) + "".join(list_no_load) - - return self.prefix + progress_bar + percents_postfix + self.postfix diff --git a/aiogram_renderer/widgets/text/__init__.py b/aiogram_renderer/widgets/text/__init__.py new file mode 100644 index 0000000..ddd3176 --- /dev/null +++ b/aiogram_renderer/widgets/text/__init__.py @@ -0,0 +1,5 @@ +__all__ = ["Text", "Bold", "Italic", "Code", "Underline", "Blockquote", "Area", "Progress"] + +from .text import Text, Bold, Italic, Code, Underline, Blockquote +from .area import Area +from .progress import Progress \ No newline at end of file diff --git a/aiogram_renderer/widgets/text/area.py b/aiogram_renderer/widgets/text/area.py new file mode 100644 index 0000000..ad3e38c --- /dev/null +++ b/aiogram_renderer/widgets/text/area.py @@ -0,0 +1,43 @@ +from typing import Any +from aiogram_renderer.widgets import Widget +from aiogram_renderer.widgets.text import Text + +class Area(Widget): + __slots__ = ('texts', 'sep', 'sep_count', 'end', 'end_count') + + def __init__(self, *texts: Text | str, sep: str = "\n", sep_count: int = 1, end: str = "\n", + end_count: int = 0, show_on: str = None): + self.texts = list(texts) + self.sep = sep + self.sep_count = sep_count + self.end = end + self.end_count = end_count + super().__init__(show_on=show_on) + + async def assemble(self, data: dict[str, Any]): + if self.show_on in data.keys(): + if not data[self.show_on]: + return "" + + separators = "".join([self.sep for _ in range(self.sep_count)]) + + texts_list = [] + for text in self.texts: + if isinstance(text, Text): + if text.show_on in data.keys(): + if not data[text.show_on]: + continue + asm_text = await text.assemble(data=data) + texts_list.append(asm_text + separators) + else: + for key, value in data.items(): + if "{" + key + "}" in text: + text = text.replace("{" + key + "}", str(value)) + texts_list.append(text + separators) + + if len(texts_list) == 0: + return "" + else: + content = "".join(texts_list)[:-self.sep_count] + "".join([self.end for _ in range(self.end_count)]) + + return content diff --git a/aiogram_renderer/widgets/text/progress.py b/aiogram_renderer/widgets/text/progress.py new file mode 100644 index 0000000..f3e4577 --- /dev/null +++ b/aiogram_renderer/widgets/text/progress.py @@ -0,0 +1,45 @@ +from typing import Any +from aiogram_renderer.widgets import Widget + +class Progress(Widget): + __slots__ = ('name', 'load', 'no_load', 'add_percent', 'postfix', 'prefix', 'bar_length') + + def __init__( + self, + name: str, + load: str = "🟥", + no_load: str = "⬜", + add_percent: bool = False, + prefix: str = "", + postfix: str = "", + bar_length: int = 10, + show_on: str = None + ): + if len(load) != 1 or len(no_load) != 1: + raise ValueError("Параметры load и no_load должны быть длиной 1 символ") + if bar_length < 1: + raise ValueError("bar_length должен быть положительным целым числом") + self.name = name + self.load = load + self.no_load = no_load + self.add_percent = add_percent + self.prefix = prefix + self.postfix = postfix + self.bar_length = bar_length + super().__init__(show_on=show_on) + + async def assemble(self, data: dict[str, Any]) -> str: + if self.show_on and not data.get(self.show_on, True): + return "" + + percent = float(data.get(self.name, 0)) + if not (0.0 <= percent <= 100.0): + raise ValueError("Процент должен быть в промежутке от 0 до 100") + + percent_int = int(round(percent)) + filled_length = int(self.bar_length * percent_int / 100) + progress_bar = self.load * filled_length + self.no_load * (self.bar_length - filled_length) + + percents_postfix = f" {percent_int}%" if self.add_percent else "" + + return f"{self.prefix}{progress_bar}{percents_postfix}{self.postfix}" diff --git a/aiogram_renderer/widgets/text/text.py b/aiogram_renderer/widgets/text/text.py new file mode 100644 index 0000000..dca2b52 --- /dev/null +++ b/aiogram_renderer/widgets/text/text.py @@ -0,0 +1,62 @@ +from typing import Any +from aiogram_renderer.widgets import Widget + + +class Text(Widget): + __slots__ = ("content", "end", "end_count") + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + # Добавляем окончание, в зависимости от end_count + self.content = content + self.end = end + self.end_count = end_count + super().__init__(show_on=show_on) + + async def assemble(self, data: dict[str, Any]) -> str: + if self.show_on in data.keys(): + # Если show_on = False, не собираем текст и возвращаем пустую строку + if not data[self.show_on]: + return "" + + text = self.content + # Форматируем по data, если там заданы ключи {key} + for key, value in data.items(): + if "{" + key + "}" in text: + text = text.replace("{" + key + "}", str(value)) + + return text + "".join([self.end for _ in range(self.end_count)]) + + +class Bold(Text): + __slots__ = () + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) + + +class Italic(Text): + __slots__ = () + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) + + +class Code(Text): + __slots__ = () + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) + + +class Underline(Text): + __slots__ = () + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + super().__init__(content=f"{content}", end=end, end_count=end_count, show_on=show_on) + + +class Blockquote(Text): + __slots__ = () + + def __init__(self, content: str, end: str = "\n", end_count: int = 0, show_on: str = None): + super().__init__(content=f"
{content}
", end=end, end_count=end_count, show_on=show_on) diff --git a/aiogram_renderer/window.py b/aiogram_renderer/window.py index ac09c51..ec3c46a 100644 --- a/aiogram_renderer/window.py +++ b/aiogram_renderer/window.py @@ -2,14 +2,11 @@ from aiogram.fsm.state import State from typing import Any from aiogram.types import ReplyKeyboardMarkup, InlineKeyboardMarkup -from .widgets.inline.button import Button, Mode -from .widgets.inline.panel import Panel, DynamicPanel -from .widgets.reply.button import ReplyButton -from .widgets.reply.panel import ReplyPanel -from .widgets.media.bytes import FileBytes -from .widgets.media.path import File -from .widgets.text import Text, Area, Progress -from .widgets.widget import Widget +from .widgets.inline import Button, Mode, Panel, DynamicPanel +from .widgets.media import File, FileBytes +from .widgets.reply import ReplyButton, ReplyPanel +from .widgets import Widget +from .widgets.text import Area, Progress, Text class ABCWindow(ABC): diff --git a/example/windows.py b/example/windows.py index 45e1a9e..7620186 100644 --- a/example/windows.py +++ b/example/windows.py @@ -1,7 +1,6 @@ from states import MenuStates -from aiogram_renderer.widgets.inline.button import Mode, ComeTo -from aiogram_renderer.widgets.inline.panel import DynamicPanel -from aiogram_renderer.widgets.reply.button import ReplyMode +from aiogram_renderer.widgets.inline import Mode, ComeTo, DynamicPanel +from aiogram_renderer.widgets.reply import ReplyMode from aiogram_renderer.widgets.text import Area, Bold, Text, Progress from aiogram_renderer.window import Window, Alert From 819075790b3a9871dc8ee71106869f03d7cc079a Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 14:14:36 +0300 Subject: [PATCH 6/9] refactor: replace assertions with exceptions --- aiogram_renderer/bot_mode.py | 6 ++++-- aiogram_renderer/renderer.py | 9 +++++--- aiogram_renderer/widgets/inline/panel.py | 27 +++++++++++++----------- aiogram_renderer/widgets/reply/panel.py | 6 ++++-- aiogram_renderer/window.py | 8 ++++--- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/aiogram_renderer/bot_mode.py b/aiogram_renderer/bot_mode.py index 6e33f3b..4c91914 100644 --- a/aiogram_renderer/bot_mode.py +++ b/aiogram_renderer/bot_mode.py @@ -14,7 +14,8 @@ class BotMode: def __init__(self, name: str, values: list[str], alert_window: Alert, has_custom_handler: bool = False): for widget in alert_window._widgets: - assert not isinstance(widget, FileBytes), ValueError("В alert_window не может быть файл с байтами") + if isinstance(widget, FileBytes): + raise ValueError("В alert_window не может быть файл с байтами") self.name = name self.values = values @@ -116,7 +117,8 @@ async def update_mode(self, mode: str) -> str: async def get_active_value(self, name: str) -> None: dict_modes = await self.get_dict_modes() - assert dict_modes[name], ValueError("У бота нет данного режима") + if not dict_modes.get(name): + raise ValueError("У бота нет данного режима") fsm_modes = await self.get_fsm_modes() # Активным считается первое значение режима return fsm_modes[name][0] diff --git a/aiogram_renderer/renderer.py b/aiogram_renderer/renderer.py index a51e869..cf4262e 100644 --- a/aiogram_renderer/renderer.py +++ b/aiogram_renderer/renderer.py @@ -112,7 +112,8 @@ async def __get_window_by_state(self, state: str) -> Window: for i, window in enumerate(self.windows, start=1): if window._state == state: return window - assert i != len(self.windows), ValueError("Окно не за задано в конфигурации") + if i == len(self.windows): + raise ValueError("Окно не за задано в конфигурации") async def _switch_dynamic_panel_page(self, name: str, page: int): """ @@ -240,8 +241,10 @@ async def render(self, window: str | Alert | Window, chat_id: int, data: dict[st :return: """ if message_id is None: - assert mode != RenderMode.REPLY, ValueError("message_id is required on REPLY mode") - assert mode != RenderMode.DELETE_AND_SEND, ValueError("message_id is required on mode DELETE_AND_SEND") + if mode == RenderMode.REPLY: + raise ValueError("message_id is required on REPLY mode") + if mode == RenderMode.DELETE_AND_SEND: + raise ValueError("message_id is required on mode DELETE_AND_SEND") if isinstance(window, Alert): fsm_data = await self.fsm.get_data() diff --git a/aiogram_renderer/widgets/inline/panel.py b/aiogram_renderer/widgets/inline/panel.py index 2f088b9..ae0660f 100644 --- a/aiogram_renderer/widgets/inline/panel.py +++ b/aiogram_renderer/widgets/inline/panel.py @@ -10,12 +10,13 @@ class Panel(Widget): __slots__ = ("buttons", "width") def __init__(self, *buttons: Button, width: int = 1, show_on: str = None): - # Минимальная ширина 1 - assert width >= 1, ValueError("Ширина группы должна быть не меньше 1") - # Максимальная ширина inlineKeyboard строки 8 (ограничение Telegram) - assert width <= 8, ValueError("У Telegram ограничение на длину InlineKeyboard - 8 кнопок") - # Максимальная высота inlineKeyboard 100 (ограничение Telegram) - assert len(buttons) / width <= 100, ValueError("У Telegram ограничение на высоту InlineKeyboard - 100 кнопок") + if width < 1: + raise ValueError("Ширина группы должна быть не меньше 1") + if width > 8: + raise ValueError("У Telegram ограничение на длину InlineKeyboard - 8 кнопок") + if len(buttons) / width > 100: + raise ValueError("У Telegram ограничение на высоту InlineKeyboard - 100 кнопок") + self.buttons = list(buttons) self.width = width super().__init__(show_on=show_on) @@ -68,12 +69,14 @@ class DynamicPanel(Widget): # Формат в fsm_data "name": {"page": 1, "text": ["text1", ...], "data": ["data1", ...]} def __init__(self, name: str, width: int = 1, height: int = 1, hide_control_buttons: bool = False, hide_number_pages: bool = False, show_on: str = None): - # Минимальная ширина и высота = 1 - assert width >= 1, ValueError("Ширина группы должна быть не меньше 1") - assert height >= 1, ValueError("Высота группы должна быть не меньше 1") - # Максимальная ширина inlineKeyboard строки 8 (ограничение Telegram) - assert width <= 8, ValueError("У Telegram ограничение на длину InlineKeyboard - 8 кнопок") - assert height <= 99, ValueError("У Telegram ограничение на высоту InlineKeyboard - 100 кнопок") + if width < 1: + raise ValueError("Ширина группы должна быть не меньше 1") + if width > 8: + raise ValueError("У Telegram ограничение на длину InlineKeyboard - 8 кнопок") + if height < 1: + raise ValueError("Высота группы должна быть не меньше 1") + if height > 99: + raise ValueError("У Telegram ограничение на высоту InlineKeyboard - 100 кнопок") super().__init__(show_on=show_on) diff --git a/aiogram_renderer/widgets/reply/panel.py b/aiogram_renderer/widgets/reply/panel.py index 40c069b..e4d853a 100644 --- a/aiogram_renderer/widgets/reply/panel.py +++ b/aiogram_renderer/widgets/reply/panel.py @@ -8,8 +8,10 @@ class ReplyPanel(Widget): __slots__ = ("buttons", "width") def __init__(self, *buttons: ReplyButton, width: int = 1, show_on: str = None): - assert width >= 1, ValueError("Ширина группы должна быть не меньше 1") - assert width <= 12, ValueError("У Telegram ограничение на длину ReplyKeyboard - 12 кнопок") + if width < 1: + raise ValueError("Ширина группы должна б��ть не меньше 1") + if width > 12: + raise ValueError("У Telegram ограничение на длину ReplyKeyboard - 12 кнопок") self.buttons = list(buttons) self.width = width super().__init__(show_on=show_on) diff --git a/aiogram_renderer/window.py b/aiogram_renderer/window.py index ec3c46a..697ba69 100644 --- a/aiogram_renderer/window.py +++ b/aiogram_renderer/window.py @@ -120,6 +120,8 @@ class Alert(ABCWindow): def __init__(self, *widgets: Widget): for widget in widgets: - assert not isinstance(widget, DynamicPanel), ValueError("Alert не поддерживает DynamicPanel (пока)") - assert not isinstance(widget, Mode), ValueError("Alert не поддерживает Mode (пока)") - super().__init__(*widgets) + if isinstance(widget, DynamicPanel): + raise ValueError("Alert не поддерживает DynamicPanel (пока)") + if isinstance(widget, Mode): + raise ValueError("Alert не поддерживает Mode (пока)") + super().__init__(*widgets) \ No newline at end of file From 6568d3a740b67bd4882d5a840ad2ee73361d6103 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 14:29:50 +0300 Subject: [PATCH 7/9] refactor: move `inline` and `reply` in `keyboard` --- aiogram_renderer/renderer.py | 2 +- aiogram_renderer/widgets/keyboard/__init__.py | 0 aiogram_renderer/widgets/{ => keyboard}/inline/__init__.py | 0 aiogram_renderer/widgets/{ => keyboard}/inline/button.py | 0 aiogram_renderer/widgets/{ => keyboard}/inline/panel.py | 2 +- aiogram_renderer/widgets/{ => keyboard}/reply/__init__.py | 0 aiogram_renderer/widgets/{ => keyboard}/reply/button.py | 0 aiogram_renderer/widgets/{ => keyboard}/reply/panel.py | 2 +- aiogram_renderer/window.py | 4 ++-- example/windows.py | 4 ++-- 10 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 aiogram_renderer/widgets/keyboard/__init__.py rename aiogram_renderer/widgets/{ => keyboard}/inline/__init__.py (100%) rename aiogram_renderer/widgets/{ => keyboard}/inline/button.py (100%) rename aiogram_renderer/widgets/{ => keyboard}/inline/panel.py (99%) rename aiogram_renderer/widgets/{ => keyboard}/reply/__init__.py (100%) rename aiogram_renderer/widgets/{ => keyboard}/reply/button.py (100%) rename aiogram_renderer/widgets/{ => keyboard}/reply/panel.py (96%) diff --git a/aiogram_renderer/renderer.py b/aiogram_renderer/renderer.py index cf4262e..108c669 100644 --- a/aiogram_renderer/renderer.py +++ b/aiogram_renderer/renderer.py @@ -8,7 +8,7 @@ from aiogram.types import Message, InputMediaPhoto, InputMediaVideo, InputMediaAudio, InputMediaDocument from .bot_mode import BotModes from .enums import RenderMode -from .widgets.inline import DynamicPanel +from .widgets.keyboard.inline import DynamicPanel from .widgets.media import FileBytes, AudioBytes, VideoBytes, PhotoBytes, File, Audio, Video, Photo from .window import Window, Alert diff --git a/aiogram_renderer/widgets/keyboard/__init__.py b/aiogram_renderer/widgets/keyboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aiogram_renderer/widgets/inline/__init__.py b/aiogram_renderer/widgets/keyboard/inline/__init__.py similarity index 100% rename from aiogram_renderer/widgets/inline/__init__.py rename to aiogram_renderer/widgets/keyboard/inline/__init__.py diff --git a/aiogram_renderer/widgets/inline/button.py b/aiogram_renderer/widgets/keyboard/inline/button.py similarity index 100% rename from aiogram_renderer/widgets/inline/button.py rename to aiogram_renderer/widgets/keyboard/inline/button.py diff --git a/aiogram_renderer/widgets/inline/panel.py b/aiogram_renderer/widgets/keyboard/inline/panel.py similarity index 99% rename from aiogram_renderer/widgets/inline/panel.py rename to aiogram_renderer/widgets/keyboard/inline/panel.py index ae0660f..119405a 100644 --- a/aiogram_renderer/widgets/inline/panel.py +++ b/aiogram_renderer/widgets/keyboard/inline/panel.py @@ -2,7 +2,7 @@ from aiogram.types import InlineKeyboardButton from aiogram_renderer.callback_data import DPanelCD -from aiogram_renderer.widgets.inline import Button +from aiogram_renderer.widgets.keyboard.inline import Button from aiogram_renderer.widgets import Widget diff --git a/aiogram_renderer/widgets/reply/__init__.py b/aiogram_renderer/widgets/keyboard/reply/__init__.py similarity index 100% rename from aiogram_renderer/widgets/reply/__init__.py rename to aiogram_renderer/widgets/keyboard/reply/__init__.py diff --git a/aiogram_renderer/widgets/reply/button.py b/aiogram_renderer/widgets/keyboard/reply/button.py similarity index 100% rename from aiogram_renderer/widgets/reply/button.py rename to aiogram_renderer/widgets/keyboard/reply/button.py diff --git a/aiogram_renderer/widgets/reply/panel.py b/aiogram_renderer/widgets/keyboard/reply/panel.py similarity index 96% rename from aiogram_renderer/widgets/reply/panel.py rename to aiogram_renderer/widgets/keyboard/reply/panel.py index e4d853a..f4496f5 100644 --- a/aiogram_renderer/widgets/reply/panel.py +++ b/aiogram_renderer/widgets/keyboard/reply/panel.py @@ -1,6 +1,6 @@ from typing import Any from aiogram.types import KeyboardButton -from aiogram_renderer.widgets.reply import ReplyButton +from aiogram_renderer.widgets.keyboard.reply import ReplyButton from aiogram_renderer.widgets import Widget diff --git a/aiogram_renderer/window.py b/aiogram_renderer/window.py index 697ba69..aaeca1c 100644 --- a/aiogram_renderer/window.py +++ b/aiogram_renderer/window.py @@ -2,9 +2,9 @@ from aiogram.fsm.state import State from typing import Any from aiogram.types import ReplyKeyboardMarkup, InlineKeyboardMarkup -from .widgets.inline import Button, Mode, Panel, DynamicPanel +from .widgets.keyboard.inline import Button, Mode, Panel, DynamicPanel from .widgets.media import File, FileBytes -from .widgets.reply import ReplyButton, ReplyPanel +from .widgets.keyboard.reply import ReplyButton, ReplyPanel from .widgets import Widget from .widgets.text import Area, Progress, Text diff --git a/example/windows.py b/example/windows.py index 7620186..c0216c7 100644 --- a/example/windows.py +++ b/example/windows.py @@ -1,6 +1,6 @@ from states import MenuStates -from aiogram_renderer.widgets.inline import Mode, ComeTo, DynamicPanel -from aiogram_renderer.widgets.reply import ReplyMode +from aiogram_renderer.widgets.keyboard.inline import Mode, ComeTo, DynamicPanel +from aiogram_renderer.widgets.keyboard.reply import ReplyMode from aiogram_renderer.widgets.text import Area, Bold, Text, Progress from aiogram_renderer.window import Window, Alert From d68a78e3786b70909d206e22c0922b87d1e6936b Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 16:33:22 +0300 Subject: [PATCH 8/9] refactor: simplify get fsm in middleware --- aiogram_renderer/middlewares.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/aiogram_renderer/middlewares.py b/aiogram_renderer/middlewares.py index d288bf3..e79de88 100644 --- a/aiogram_renderer/middlewares.py +++ b/aiogram_renderer/middlewares.py @@ -1,7 +1,9 @@ -from aiogram import types -from aiogram import BaseMiddleware from typing import Any, Callable + +from aiogram import BaseMiddleware from aiogram.fsm.context import FSMContext +from aiogram.types import Message + from .bot_mode import BotModes, BotMode from .renderer import Renderer from .window import Window @@ -12,16 +14,13 @@ def __init__(self, windows: list[Window] = None, modes: list[BotMode] = None): self.windows = windows self.modes = modes - async def __call__(self, handler: Callable, event: types.Message, data: dict[str, Any]) -> Any: - # Если есть FSMContext то передаем его в renderer и bot_modes - for key, value in data.items(): - if isinstance(value, FSMContext): - bot_modes = BotModes(*self.modes, fsm=value) if (self.modes is not None) else None - renderer = Renderer(bot=event.bot, fsm=value, windows=self.windows, bot_modes=bot_modes) - data["renderer"] = renderer - result = await handler(event, data) - del renderer - return result + async def __call__(self, handler: Callable, event: Message, data: dict[str, Any]) -> Any: + # Если есть FSMContext, то передаем его в renderer и bot_modes + if (fsm := data.get('state')) is not None: + data["renderer"] = Renderer( + bot=event.bot, + fsm=fsm, + windows=self.windows, + bot_modes=BotModes(*self.modes, fsm=fsm) if (self.modes is not None) else None) - result = await handler(event, data) - return result + return await handler(event, data) From 7c9bdb2c8434657fcd2110a36e49e72ef6ff92b7 Mon Sep 17 00:00:00 2001 From: m-xim <170838360+m-xim@users.noreply.github.com> Date: Fri, 30 May 2025 16:34:10 +0300 Subject: [PATCH 9/9] refactor: improve bot mode handling in filters --- aiogram_renderer/enums.py | 2 +- aiogram_renderer/filters.py | 77 +++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/aiogram_renderer/enums.py b/aiogram_renderer/enums.py index 97b0524..19d3076 100644 --- a/aiogram_renderer/enums.py +++ b/aiogram_renderer/enums.py @@ -5,4 +5,4 @@ class RenderMode(str, Enum): EDIT = "edit" DELETE_AND_SEND = "delete_and_send" ANSWER = "answer" - REPLY = "reply" \ No newline at end of file + REPLY = "reply" diff --git a/aiogram_renderer/filters.py b/aiogram_renderer/filters.py index 7f61253..428bd13 100644 --- a/aiogram_renderer/filters.py +++ b/aiogram_renderer/filters.py @@ -7,32 +7,24 @@ class IsModeWithNotCustomHandler(BaseFilter): async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> bool: - # Если режимы заданы - if renderer.bot_modes is not None: - mode = None - # Для CallbackQuery проверяем правильно ли задан callback_data по системному префиксу - if isinstance(event, CallbackQuery): - try: - callback_data = ModeCD.unpack(event.data) - except (TypeError, ValueError): - callback_data = None - - if callback_data is not None: - # Проверяем нет ли у данного режима своего хендлера - mode = await renderer.bot_modes.get_mode_by_name(name=callback_data.name) - - # Для Message, ищем его среди списков значений модов и выводим по найденному названию мода - else: - modes_values = await renderer.bot_modes.get_modes_values() - if event.text in modes_values: - # Проверяем нет ли у данного режима своего хендлера - mode = await renderer.bot_modes.get_mode_by_value(value=event.text) - - # Проверяем нашелся ли режим и есть ли у него пользовательский хендлер - if (mode is not None) and (not mode.has_custom_handler): - return True - - return False + bot_modes = renderer.bot_modes + if not bot_modes: # Если режимы не заданы + return False + + mode = None + # Для CallbackQuery проверяем правильно ли задан callback_data по системному префиксу + if isinstance(event, CallbackQuery): + try: + callback_data = ModeCD.unpack(event.data) + mode = await bot_modes.get_mode_by_name(name=callback_data.name) if callback_data else None + except (TypeError, ValueError): + pass + elif isinstance(event, Message): # Ищем его среди списков значений модов и выводим по найденному названию мода + modes_values = await bot_modes.get_modes_values() + if event.text in modes_values: + mode = await bot_modes.get_mode_by_value(value=event.text) + + return bool(mode and not mode.has_custom_handler) class IsMode(BaseFilter): @@ -40,20 +32,23 @@ def __init__(self, name: str): self.name = name async def __call__(self, event: Message | CallbackQuery, renderer: Renderer) -> bool: - # Проверяем заданы ли режимы и есть ли такой режим - if renderer.bot_modes is not None: - dict_modes = await renderer.bot_modes.get_dict_modes() - if self.name in dict_modes.keys(): - mode = await renderer.bot_modes.get_mode_by_name(name=self.name) - # Проверяем равен ли коллбек заданному режиму - if isinstance(event, CallbackQuery): - if event.data == ModeCD(name=self.name).pack() and mode is not None: - return True - # Проверяем есть ли значение Reply text в values режима - elif isinstance(event, Message): - if event.text in mode.values and mode is not None: - return True - else: - raise ValueError("Такого режима нет") + bot_modes = renderer.bot_modes + if not bot_modes: # Если режимы не заданы + return False + + dict_modes = await bot_modes.get_dict_modes() + if self.name not in dict_modes: + raise ValueError("Такого режима нет") + + mode = await bot_modes.get_mode_by_name(name=self.name) + if not mode: + return False + + if isinstance(event, CallbackQuery): + # Проверяем равен ли сallback заданному режиму + return event.data == ModeCD(name=self.name).pack() + elif isinstance(event, Message): + # Проверяем есть ли значение Reply text в values режима + return event.text in mode.values return False