Python Materials For Beginners
447 subscribers
39 photos
1 video
9 files
150 links
Download Telegram
🟠 Telegram боти. Приблизний список додаткових параметрів в обробниках.

bot: Bot
command: CommandObject
fsm_storage: MemoryStorage
state: FSMContext
dispatcher: Dispatcher
bots: tuple
raw_state: str | None
handler: HandlerObject
event_update: Update
event_router: Router
event_context: EventContext
event_from_user: User
event_chat: Chat

#tg_bots
👍2
🟡 Telegram боти. Приклад використання додаткових параметрів у обробнику

Команди телеграм бота можуть мати аргументи.
Уявімо ситуацію коли ми хочемо реалізувати команду яка рахує суму чисел:
/sum 1 2 3.5

У відповідь будемо очікувати:
Сума: 6.5

До обробника додамо параметр command: CommandObject 🔗 , який полегшує розбір аргументів команди.
Атрибут command.args це рядок з аргументами, якщо вони є, а якщо немає він дорівнює None.
from aiogram.filters import CommandObject

@router.message(Command("sum"))
async def sum_handler(message: Message, command: CommandObject):
success = False
if args := command.args and command.args.split():
try:
result = sum(map(float, args))
success = True
await message.answer(f"Сума: {result}")
except ValueError:
pass

if not success:
await message.answer(
"Щось пішло не так, можливо передані неправильні аргументи /sum"
)

#tg_bots
👍1
🔴 Опис роботи echo-бота. Відправка повідомлень.

Розглянемо код тіла функції-обробника.

Є два способи відправляти повідомлення:
▪️ через об'єкт Message
▪️ через об'єкт Bot

from aiogram import html

@router.message(Command("start"))
async def start_handler(message: Message):
await message.answer(
f"<i>Hello</i> {html.unparse(message.from_user.first_name)}!",
parse_mode=ParseMode.HTML
)


Перевага відправки через bot: Bot у тому що можна відсилати повідомлення у інші чати і гілки чатів.
from aiogram import html

@router.message(Command("start"))
async def start_handler(message: Message, bot: Bot):
await bot.send_message(
chat_id=message.chat.id,
text=f"<i>Hello</i> {html.unparse(message.from_user.first_name)}!",
parse_mode=ParseMode.HTML
)


Telegram дозволяє надсилати форматований текст у форматах HTML і Markdown
(насправді — у їхніх дуже спрощених версіях).
🔗 Telegram Bot API: Markdown v2
🔗 Telegram Bot API: HTML

⚠️ Якщо передати некоректний HTML, виникне виняток типу:
TelegramBadRequest('Telegram server says - Bad Request: can't parse entities ...')


Тому завжди потрібно зважати на текст, який передається у вигляді змінних — чи не може він зламати HTML-розмітку. У нашому прикладі message.from_user.first_name — це ім’я користувача, яке може містити символи < або > що зламає розмітку.

👉 Щоб уникнути таких помилок, використовуйте екранування за допомогою html.unparse() та інші функції для форматування у aiogram3

Корисні посилання по темі:
🔗 Aiogram 3 FAQ : Bad Request: can't parse entities
🔗 aiogram3 documentation: Formatting

#tg_bots
👍1
🟤 Телеграм боти. Використання магічних фільтрів.

🔗 Message.from_user
🔗 User
class User(TelegramObject):
id: int
first_name: str
last_name: str | None
username: str | None
# ...


Як бачимо:
▪️ username і last_name можуть бути None
▪️ cам атрибут from_user може бути None

⚠️ Об'єкти у aiogram3 часто можуть бути None — варто зважати на це.

Те що ми використовуємо first_name у коді обробника не спричиняє проблем.А ось те що не перевіряєм from_user на None це потенційна проблема. Чесно кажучи я не можу назвати випадок коли повідомлення може приходити без відправника, але мабуть може ... і мабуть ми хотіли би уникнути проблем пов'язаних з цим. Крім того тайп чекери типу mypy при перевірці коду будут вказувати на цю потенційну проблему.

👉 Рішенням може бути використання магічних фільтрів:
from aiogram  F
from aiogram.types import User

@router.message(Command("start"), F.from_user.as_("from_user"))
async def start_handler(message: Message, from_user: User):
await message.answer(
f"<i>Hello</i> {html.unparse(from_user.first_name)}!",
parse_mode=ParseMode.HTML
)

Фільтр бере об'єкт message і перевіряє атрибут from_user, якщо from_user is None обробник просто не буде викликатись, якщо не None то from_user буде переданий як додатковий параметр.

Виклик методу .as_() як раз відповідає за передачу додаткового параметру у обробник, у ньому вказується ім'я цього параметру.

#tg_bots
👍1
Чи варто продовжувати писати пости і писати про телеграм ботів?
Здається про тг ботів писати можна вічно. Буде ще один канал по тг ботам?

Але наразі по телеграм ботам такий приблизний план:
▪️ pydantic-settings
▪️ Dependency injection
▪️ Filters / BotAdminFilter
▪️ Робота з фото.

Можливо варто переключитись на іншу тему, наприклад
про скрапінг даних? Якийсь сайт заскрапати?

Пишіть пропозиції у коментарях і голосуйте.
👍1👎1
Що виведе код:
from aiogram import html as aiogram_html  #  unparse
import html as builtin_html # escape

nickname = "DeadInside <x_x>"

print(
aiogram_html.unparse(nickname),
aiogram_html.unparse(nickname) == builtin_html.escape(nickname),
)


👉 🔗 Перечитати пост про екранування html.

🡇🡇🡇 Опитування нижче 🡇🡇🡇
✴️ Клас для налаштувань за допомогою pydantic_settings

python-dotenv — це добре, але лише тоді, коли налаштувань небагато.
Існує бібліотека pydantic_settings яка використовує pydantic та python-dotenv і дозволяє:

1️⃣ Об'єднати всі змінні в один клас.
2️⃣ Конвертувати та валідувати значення змінних із рядків у потрібні типи.
3️⃣ Підтримувати вкладені класи, що дає змогу згрупувати змінні за призначенням.
4️⃣ Зменшити ймовірність розкриття секретних значень при виводі через print або при логувані
5️⃣ Інші тонкі налаштування.

🔗 Pydantic Settings Documentation

Уявімо ситуацію, коли нам потрібно зберігати список ID адміністраторів бота та налаштування підключення до бази даних. pydantic_settings уміє працювати з базовими типами, включно зі списками та множинами (парсити їх за допомогою бібліотеки json). Також бібліотека надає широкий простір налаштування процесу валідації під конкретні потреби.

Файл .env
BOT__TOKEN="Ваш токен"
BOT__ADMINS=[1361728070, 1060557517]

DB__HOST=localhost
DB__PORT=5432
DB__USER=postgres
DB__PASSWORD=mysecretpassword
DB__NAME=mydatabase


Фай main.py
# pip insatall pydantic
from pydantic import BaseModel, SecretStr

# pip insatall pydantic_settings
from pydantic_settings import BaseSettings

class BotSettings(BaseModel):
token: SecretStr # BOT__TOKEN
admins: frozenset[int] # BOT__ADMINS


class DatabaseSettings(BaseModel):
host: str # DB__HOST
port: int = 5432 # DB__PORT
user: str # DB__USER
password: SecretStr # DB__PASSWORD
name: str # DB__NAME

class Settings(BaseSettings):
bot: BotSettings
db: DatabaseSettings


async def main():
settings = Settings(
_env_file=".env",
_env_nested_delimiter='__'
)
print(repr(settings))
# use: settings.bot.token.get_secret_value()
# for get origin token value


if __name__ == "__main__":
asyncio.run(main())

Вивід:
Settings(bot=BotSettings(token=SecretStr('**********'), admins=frozenset({1060557517, 1361728070})), db=DatabaseSettings(host='localhost', port=5432, user='postgres', password=SecretStr('**********'), name='mydatabase')
✳️ Телеграм боти. Dependency Injection

Доопрацюємо нашого бота: зробимо різні привітання для звичайних користувачів та адміністраторів.
ID адміністраторів у нас міститься в об’єкті Settings, і виникає проблема — як його отримати у функції обробника.

Це можна зробити кількома способами:

1️⃣ Створити об’єкт Settings як глобальну змінну.
2️⃣ Використати Middleware для Dependency Injection.
3️⃣ Використати Dispatcher для Dependency Injection.

Dependency Injection (DI) — це підхід у програмуванні, який допомагає відокремити створення сутності від її використання.

Глобальні змінні — це не дуже добре. Використання Middleware — більш складний варіант. Тому давайте зосередимося на останньому варіанті.

🔗 aiogram documentation: Dependency Injection

async def main():
settings = Settings(
_env_file=".env",
_env_nested_delimiter='__'
)
async with Bot(token=settings.bot.token.get_secret_value()) as bot:
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
dp.startup.register(startup)
dp.include_router(router)

# Use Dependency Injection
dp["settings"] = settings

await dp.start_polling(bot)


У Dispatcher можна, як у словник, додати власні об'єкти-залежності, щоб потім отримати їх у обробнику як додаткові параметри:
@router.message(Command("start"), F.from_user.as_("from_user"))
async def start_handler(message: Message, from_user: User, settings: Settings):
if from_user.id in settings.bot.admins:
text = f"<i>Hello admin</i> {html.unparse(from_user.first_name)}!"
else:
text = f"<i>Hello</i> {html.unparse(from_user.first_name)}!"

await message.answer(text=text, parse_mode=ParseMode.HTML)


На жаль, Router такої можливості не має. Якщо потрібно ізолювати дані одного роутера від іншого, варто використати Middleware.

#tg_bots
👍1
❇️ Телеграм-боти. Власний клас-фільтр.

Ми має код обробника у якому є if-else для визначення логіки звичайний користувач/адміністратор.
Але це не дуже добре:

1️⃣ Якщо в нас таких обробників буде багато, то if-else буде дублюватися і ускладнювати читання коду.
2️⃣ Ми не зможемо розділити логіку звич.користувачів і адміністраторів на різні файли-модулі.

Написання власного фільтра дозволить розбити логіку на два обробника.
Ми вже ознайомились з використанням готового бібліотечного фільтра Command та магічних фільтрів,
давайте напишемо тепер свій клас-фільтр:

from aiogram.filters import BaseFilter

class BotAdminFilter(BaseFilter):
async def __call__(
self,
message: Message,
settings: Settings
) -> bool | dict[str, Any]:
if message.from_user and message.from_user.id in settings.bot.admins:
return {"from_user": message.from_user}
return False

Як бачимо фільтр це клас який містить метод __call__ , отже об'єкти цього класу можуть викликатись як функції.

⚠️ Тип першого параметру __call__ буде залежати від типу події на яку підписуємось.

При виклику фільтр вертає:
▪️ вертає False якщо при цьому оновленні не треба викликати обробник — воно не підходить
▪️ вертає dict якщо треба викликати обробник і підставити у нього додатковий аргумент from_user

🔗 aiogram documentation: Writing own filters

@router.message(Command("start"), BotAdminFilter())
async def start_admin_handler(message: Message, from_user: User):
await message.answer(
f"<i>Hello admin</i> {html.unparse(from_user.first_name)}!",
parse_mode=ParseMode.HTML
)

@router.message(Command("start"), F.from_user.as_("from_user"))
async def start_handler(message: Message, from_user: User):
await message.answer(
f"<i>Hello</i> {html.unparse(from_user.first_name)}!",
parse_mode=ParseMode.HTML
)


👉Фільтр у aiogram:
1️⃣ Визначає чи буде викликаний вказаний обробник подій
2️⃣ Може додавати параметри у обробник, вертаючи їх у словнику

#tg_bots
1👍1
🔰 Телеграм-боти. Функція як фільтр.

Класи, об’єкти яких можна викликати, — це добре: можна передавати додаткові налаштування через їхній ініціалізатор.
Але в нашому випадку це не потрібно — список ID адміністраторів ми отримуємо через механізм DI.
Тому ми можемо спростити код, написавши просто функцію, яка буде фільтром:
async def is_bot_admin(message: Message, settings: Settings) -> bool | dict[str, Any]:
if message.from_user and message.from_user.id in settings.bot.admins:
return {"from_user": message.from_user}
return False

@router.message(Command("start"), is_bot_admin)
async def start_admin_handler(message: Message, from_user: User):
... # код обробника


#tg_bots
👍1
🔵 Консольні аргументи (Command-line Arguments). Приклад

Повернемось до теми Корисні речі, які варто знати про процес

Консольні аргументи, також відомі як аргументи командного рядка, — це параметри або значення, які передаються програмі під час її запуску через інтерфейс командного рядка (CLI) або термінал

main.py
import sys 

print(sys.argv)


А тепер запустимо наш скрипт таким чином:
python main.py a b "c d"

Отримаємо
['main.py', 'a', 'b', 'c d']

Це список рядків який містить назву скрипта та аргументи які ми передали. Аргументи розділяються між собою пробілам. Щоб аргумент що містить пробіли правильно сприймався треба його обрамляти у лапки. Це особливо важливо коли ми передаємо шлях до файлів як аргумент.

👉 Консольні аргументи дають додаткову можливість задавати деякі налаштування та хід виконання програми.
👍1
🟣 Консольні аргументи (Command-line Arguments). Теорія.

Терміни з якими варто розібратись:

▪️ Опції/прапорці/ключи (Options/Flags/Keys): часто аргументи починаються з дефісів (наприклад, -l, --long) і позначають певні параметри або прапорці, що змінюють поведінку програми. Один дефіс за звичай використовується у скорочених назвах, два для більш повних. Під Windows також може використовуватись слеш / замість дефісу

▪️ Значення (Values): опції можуть супроводжуватися значеннями (наприклад, --output file.txt), що надають конкретні дані для цієї опції.

▪️ Позиційні аргументи (Positional Arguments): аргументи без певних прапорців часто вважаються позиційними — тобто їхній порядок має значення, і вони призначаються змінним відповідно до своєї позиції у команді.

▪️ Підкоманди (Subcommands) або іноді просто команди: деякі програми підтримують ієрархічну структуру команд, де основна команда має кілька підкоманд, кожна з яких виконує окрему дію або функцію. Наприклад, у команді git commit -m "msg" слово commit є підкомандою, яка визначає конкретну операцію, тоді як -m "msg" — аргументи, що належать до цієї підкоманди.
( -m - опція "msg" — її значення)
Підкоманди дозволяють організовувати складні CLI-програми у логічні модулі та покращують їхню зручність використання.
👍1
🟢 Консольні аргументи (Command-line Arguments). Практика.

У python для розбору консольних аргументів використовується стандартна бібліотека argparse
Вона бере на себe:
1️⃣ Розбір аргументів на підкоманди, опції, позиційні аргументи, значення.
2️⃣ Конвертацію аргументів у заданий тип
3️⃣ Обробку помилок неправильного застосування консольних аргументів
4️⃣ Об'єднання позиційних аргументів у одну змінну-список
5️⃣ Генерацію довідки по консольним аргументам.

Напишемо просту програму яка має дві підкоманди для суми та добутку чисел:
import argparse
import enum
import math

class SubCommand(enum.StrEnum):
SUM = enum.auto()
MULT = enum.auto()

def main():
parser = argparse.ArgumentParser()

subparsers = parser.add_subparsers(
dest='subcommand',
title='subcommands',
required=True,
)

sum_parser = subparsers.add_parser(
SubCommand.SUM.value,
help="sum float values",
)
sum_parser.add_argument('values', type=float, nargs='+')

mult_parser = subparsers.add_parser(
SubCommand.MULT.value,
help="multiply float values",
)
mult_parser.add_argument('values', type=float, nargs='+')

args = parser.parse_args()

match args.subcommand:
case SubCommand.SUM:
result = sum(args.values)
case SubCommand.MULT:
result = math.prod(args.values)

print(result)

if __name__ == '__main__':
main()


Приклад виводу:
python main.py sum 3 4 5
12.0

python main.py mult 3 4 5
60.0

python main.py mult a b
usage: main.py mult [-h] values [values ...]
main.py mult: error: argument values: invalid float value: 'a'


argparse
по замовченню додає опцію —-help по якій виводиться довідка по консольним аргументам:
python main.py --help
usage: main.py [-h] {sum,mult} ...

options:
-h, --help show this help message and exit

subcommands:
{sum,mult}
sum sum float values
mult multiply float values


Виклик методу sum_parser.add_argument('values', type=float, nargs='+') вказує парить позиційні аргументи зі збереженням їх у атрибут args.values у вигляді списку. Якщо ми вказуємо тип type=float відбувається конвертацію аргументів і вказаний тип. Вказання nargs='+' дає нам упаковку декількох аргументів у один список, в даному випадку має бути що найменше один аргумент

🔗 Переклад офіційної документації python: argparse
👍1
♻️ Телеграм боти. Використання консольних аргументів.

Іноді зручно мати кілька .env-файлів із різними налаштуваннями та мати змогу запускати проєкт, указуючи, з яким файлом його запускати. Ми можемо мати, наприклад, два файли — .env.dev та .env.prod, тобто два профілі налаштувань: для розробки та для роботи на сервері.
import argparse

async def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--env-file",
type=str,
default=None,
help="Path to the dotenv-file",
)
args = parser.parse_args()

settings = Settings(
_env_file=args.env_file,
_env_nested_delimiter='__',
)
async with Bot(token=settings.bot.token.get_secret_value()) as bot:
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
dp["settings"] = settings
dp.startup.register(startup)
dp.include_router(router)
await dp.start_polling(bot)

Ми можемо запукати проєкт вказуючи шлях/ім'я до файлу опцією --env-file
python main.py --env-file .env.dev
Або
python main.py --env-file .env.prod

Якщо запускати без зазначення опції — значення братимуться лише з системного оточення; якщо їх немає, виникне помилка валідації.

#tg_bots
👍1🔥1