Ноды
- Глава
- 5 / 12
- Время
- 4 мин
- Код
- 8
Если совсем по-простому, ноды в telegrinder это способ не таскать одну и ту же подготовительную логику по всем хендлерам вручную.
Вместо такого мышления:
- "сначала достань текст из сообщения"
- "потом попробуй превратить его в число"
- "потом достань пользователя"
можно мыслить так:
- "мне здесь нужен
Text" - "а здесь нужен
TextInteger" - "а тут нужен
UserSource"
И telegrinder сам поймёт, как это собрать.
Первая интуиция#
Нода это просто описание: "вот так из одного значения получается другое".
Например:
- из
Messageможно получитьText - из
Textможно получитьint - из
CallbackQueryможно получитьPayload - из
Sourceможно получить пользователя или чат
Хороший ориентир для новичка такой:
если в хендлере вы раз за разом пишете одну и ту же подготовку данных, почти наверняка это кандидат в ноду.
Что telegrinder уже даёт сам#
Во время обработки события в граф зависимостей уже попадают корневые объекты:
APIUpdateContext
Из них дальше можно собрать всё остальное.
Во встроенных нодах уже есть много полезного:
TextTextIntegerSourceUserSourceChatSourceChatIdPayloadFilePhotoCaptionError
Это значит, что свои ноды часто вообще не нужны на старте. Сначала стоит попробовать built-in варианты.
Первая собственная нода#
Нода определяется через класс и метод __compose__.
from nodnod import NodeError
from telegrinder import Message
from telegrinder.node import scalar_node
@scalar_node
class Text:
@classmethod
def __compose__(cls, message: Message) -> str:
# Если в сообщении нет текста, ноду собрать нельзя.
if not message.text:
raise NodeError("Message has no text.")
# unwrap() безопасен здесь, потому что выше мы уже проверили наличие текста.
return message.text.unwrap()Здесь происходит вот что:
@scalar_nodeговорит telegrinder, что это скалярная нода- параметр
message: Messageозначает "для сборки нуженMessage" -> strозначает, что итоговое значение ноды это строкаNodeErrorговорит "эту ноду сейчас собрать нельзя"
После этого нода используется как обычный параметр:
@bot.on.message()
async def text_message_handler(message: Message, text: Text) -> None:
# В хендлер уже приходит готовый текст.
await message.answer(text.lower())Для новичка это обычно самый приятный момент: хендлер становится короче и читается как "что делать", а не "как добыть входные данные".
Ноды работают и в правилах#
Это особенно удобно.
from telegrinder.bot.rules import ABCRule
class TextIsOfLength(ABCRule):
def __init__(self, length: int) -> None:
self.length = length
async def check(self, text: Text) -> bool:
# Правило получает уже готовый Text.
return len(text) == self.length
@bot.on.message(TextIsOfLength(6))
async def six_handler() -> str:
return "Люблю сообщения такой длины."То есть ноды полезны не только в хендлерах. Они также отлично разгружают кастомные правила.
Цепочки нод#
Самое приятное начинается, когда ноды собираются друг из друга.
from nodnod import NodeError
from telegrinder.node import scalar_node
@scalar_node
class TextInteger:
@classmethod
def __compose__(cls, text: Text) -> int:
# Для этой ноды нужен уже готовый Text.
if not text.isdigit():
raise NodeError("Text is not a digit.")
return int(text)Теперь хендлер можно писать так:
@bot.on.message()
async def number_handler(message: Message, value: TextInteger) -> None:
await message.answer(f"{value} + 3 = {value + 3}")Хотя внутри происходит цепочка:
Message -> Text -> TextInteger
Это и есть главное удобство нод.
Хендлеру не важно, как именно появилось число. Он просто работает с числом.
Более жизненный пример#
Допустим, вы часто берёте id входящего сообщения.
from nodnod.interface.scalar import scalar_node
from telegrinder import Message
MessageId = type("MessageId", (int,), {})
@scalar_node
class IncomingMessageId:
@classmethod
def __compose__(cls, message: Message) -> MessageId:
# Небольшой тип-обёртка иногда помогает сделать код понятнее.
return MessageId(message.message_id)
@bot.on.message()
async def show_id(message: Message, message_id: IncomingMessageId) -> None:
await message.answer(f"Твой message id: {message_id}")На старте это может показаться "слишком абстрактно", но на больших ботах такие маленькие ноды очень хорошо убирают дублирование.
Какие ноды бывают#
На практике чаще всего встречаются:
scalar_nodeдля одного значения одного типаDataNodeдля дата-классовых контейнеровgeneric_nodeдля обобщённых нодpolymorphicдля нод, которые умеют собираться из разных событий
Например, Payload в telegrinder полиморфный. Он умеет доставать payload не только из CallbackQuery, но и из других событий, где это уместно.
В повседневной разработке новичку чаще всего хватает именно scalar_node.
Области жизни#
Иногда важно не только "как собрать ноду", но и "как долго она живёт".
В telegrinder есть три области:
PER_EVENT— на одно событие, это поведение по умолчаниюPER_CALL— каждый раз зановоGLOBAL— один раз на всё приложение
PER_EVENT#
Это дефолт.
Если одна и та же нода нужна в двух местах в рамках одного апдейта, telegrinder не будет собирать её заново.
Это удобно и обычно именно то, что нужно.
PER_CALL#
Полезно, когда вы хотите каждый раз получать "свежий" объект.
import aiosqlite
import typing
from telegrinder.node import per_call, scalar_node
@per_call
@scalar_node
class DB:
@classmethod
async def __compose__(cls) -> typing.AsyncGenerator[aiosqlite.Connection, None]:
# Создаём соединение перед использованием.
connection = await aiosqlite.connect("test.db")
# Передаём значение в обработчик или в другую ноду.
yield connection
# А сюда вернёмся после завершения обработки.
await connection.close()Здесь видно ещё одну важную фишку: нода может быть генератором.
То есть нода умеет:
- подготовить ресурс
- отдать его в работу
- потом корректно закрыть
GLOBAL#
Глобальные ноды хороши для конфигурации, клиентов и почти неизменяемых значений.
from telegrinder.node import DataNode, global_node, scalar_node
@global_node
class Settings(DataNode):
api_url: str
secret: str
@classmethod
def __compose__(cls) -> "Settings":
# Такой объект обычно не нужно создавать на каждый апдейт.
return cls(
api_url=env["API_URL"],
secret=env["APP_SECRET"],
)
@global_node
@scalar_node
class Secret:
@classmethod
def __compose__(cls) -> str:
return generate_secret(16)Когда нода не нужна#
Тоже важный вопрос.
Нода не нужна, если:
- логика используется ровно один раз
- код и так короткий и очевидный
- вы начинаете прятать слишком много простой логики в "магические" обёртки
Новичкам полезно держать баланс. Ноды это инструмент для упрощения кода, а не для усложнения.
Что запомнить#
- нода это способ описать, как получить одно значение из другого
- главный метод ноды в текущем API это
__compose__ - ноды работают и в хендлерах, и в правилах
- по умолчанию ноды живут в пределах одного события
- если вы часто повторяете одну и ту же подготовку данных, попробуйте вынести её в ноду