Python Materials For Beginners
447 subscribers
39 photos
1 video
9 files
150 links
Download Telegram
🔴 Опис роботи 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
🔴 Exit code

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

Код завершення процесу (exit code, return code) — це числове значення, яке процес повертає операційній системі після свого завершення.

Воно показує, як саме процес завершився:

0️⃣ 0 — успішне виконання без помилок;
1️⃣ Будь-яке ненульове значення, вказує на помилку або особливу причину завершення, залежно від програми.

Код завершення використовується іншими процесами, скриптами чи системами автоматизації (наприклад, bash, make, systemd) для визначення, чи потрібно вважати виконання команди успішним або повторити/обробити помилку.

У Linux перевірити exit code останньої команди можна через змінну $?.
У Windows щоб отримати код завершення останньої команди, використовують змінну %ERRORLEVEL%.

Для прикладу розглянемо код
import sys

def main() -> int:
if len(sys.argv) != 3:
print("Usage: python script.py <num1> <num2>", file=sys.stderr)
return 1
try:
a = float(sys.argv[1])
b = float(sys.argv[2])
print(a / b)
return 0
except ValueError:
print("Error: arguments must be numbers", file=sys.stderr)
return 2
except ZeroDivisionError:
print("Error: division by zero", file=sys.stderr)
return 3

if __name__ == "__main__":
sys.exit(main())

sys.exit() задає код завершення процесу, для помилок ми використали значення 1,2,3

Запустимо наш код у командному рядку:

Linux
python3 main.py 1 0
echo $?

Windows
python main.py 1 0
echo %ERRORLEVEL%

Команда echo виведе нам код з яким завершився процес.
Так як відбувається ділення на 0 — код завершення буде 3.
Інший процес або скрипт може легко дізнатись код завершення і прийняти відповідне рішення.
1👍1