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/
diff --git a/aiogram_renderer/bot_mode.py b/aiogram_renderer/bot_mode.py
index eff2e2b..4c91914 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
@@ -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/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/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 e71aed2..428bd13 100644
--- a/aiogram_renderer/filters.py
+++ b/aiogram_renderer/filters.py
@@ -1,33 +1,30 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message, CallbackQuery
+
+from .callback_data import ModeCD
from .renderer import Renderer
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):
- if event.data.startswith("__mode__:"):
- # Для колбека берем название мода, указанное после "__mode__:"
- mode_name = event.data.replace("__mode__:", "")
- # Проверяем нет ли у данного режима своего хендлера
- mode = await renderer.bot_modes.get_mode_by_name(name=mode_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):
@@ -35,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 == "__mode__:" + self.name) 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
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/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)
diff --git a/aiogram_renderer/renderer.py b/aiogram_renderer/renderer.py
index daab706..108c669 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.keyboard.inline import DynamicPanel
+from .widgets.media import FileBytes, AudioBytes, VideoBytes, PhotoBytes, File, Audio, Video, Photo
from .window import Window, Alert
@@ -113,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):
"""
@@ -241,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/__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/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 87%
rename from aiogram_renderer/widgets/inline/button.py
rename to aiogram_renderer/widgets/keyboard/inline/button.py
index 7fea4d0..0d91255 100644
--- a/aiogram_renderer/widgets/inline/button.py
+++ b/aiogram_renderer/widgets/keyboard/inline/button.py
@@ -1,7 +1,9 @@
from typing import Any
from aiogram.fsm.state import State
from aiogram.types import InlineKeyboardButton
-from aiogram_renderer.widgets.widget import Widget
+
+from aiogram_renderer.callback_data import ComeToCD, ModeCD
+from aiogram_renderer.widgets import Widget
class Button(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/keyboard/inline/panel.py
similarity index 75%
rename from aiogram_renderer/widgets/inline/panel.py
rename to aiogram_renderer/widgets/keyboard/inline/panel.py
index 1a47edb..119405a 100644
--- a/aiogram_renderer/widgets/inline/panel.py
+++ b/aiogram_renderer/widgets/keyboard/inline/panel.py
@@ -1,19 +1,22 @@
from typing import Any
from aiogram.types import InlineKeyboardButton
-from aiogram_renderer.widgets.inline.button import Button
-from aiogram_renderer.widgets.widget import Widget
+
+from aiogram_renderer.callback_data import DPanelCD
+from aiogram_renderer.widgets.keyboard.inline import Button
+from aiogram_renderer.widgets import Widget
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)
@@ -66,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)
@@ -124,37 +129,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
diff --git a/aiogram_renderer/widgets/keyboard/reply/__init__.py b/aiogram_renderer/widgets/keyboard/reply/__init__.py
new file mode 100644
index 0000000..187563f
--- /dev/null
+++ b/aiogram_renderer/widgets/keyboard/reply/__init__.py
@@ -0,0 +1,4 @@
+__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/keyboard/reply/button.py
similarity index 97%
rename from aiogram_renderer/widgets/reply/button.py
rename to aiogram_renderer/widgets/keyboard/reply/button.py
index cfe8582..45aeeea 100644
--- a/aiogram_renderer/widgets/reply/button.py
+++ b/aiogram_renderer/widgets/keyboard/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/keyboard/reply/panel.py
similarity index 82%
rename from aiogram_renderer/widgets/reply/panel.py
rename to aiogram_renderer/widgets/keyboard/reply/panel.py
index b8d9052..f4496f5 100644
--- a/aiogram_renderer/widgets/reply/panel.py
+++ b/aiogram_renderer/widgets/keyboard/reply/panel.py
@@ -1,15 +1,17 @@
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.keyboard.reply import ReplyButton
+from aiogram_renderer.widgets import Widget
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/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
deleted file mode 100644
index dbcaa91..0000000
--- a/aiogram_renderer/widgets/reply/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-__all__ = [
- "ReplyButton",
- "ReplyMode",
- "ReplyPanel",
- "ReplyRow",
- "ReplyColumn"
-]
-
-from .button import ReplyButton, ReplyMode
-from .panel import ReplyPanel, ReplyRow, ReplyColumn
-
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..aaeca1c 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.keyboard.inline import Button, Mode, Panel, DynamicPanel +from .widgets.media import File, FileBytes +from .widgets.keyboard.reply import ReplyButton, ReplyPanel +from .widgets import Widget +from .widgets.text import Area, Progress, Text class ABCWindow(ABC): @@ -123,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 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 87% rename from aiogram_renderer/example/routers.py rename to example/routers.py index 1382658..8bb95e3 100644 --- a/aiogram_renderer/example/routers.py +++ b/example/routers.py @@ -1,9 +1,10 @@ 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 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 @@ -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) 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 87% rename from aiogram_renderer/example/windows.py rename to example/windows.py index 45e1a9e..c0216c7 100644 --- a/aiogram_renderer/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.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