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