Введение: Боль стандартного argparse и почему это важно
Вспомните свой последний скрипт для автоматизации. Возможно, он скачивал данные, обрабатывал файлы или отправлял отчеты. Вы быстро набросали логику, запустили из командной строки, передав пару аргументов через sys.argv, и… всё сработало. Вы молодец.
Проходит месяц. Скрипт нужно запустить снова. В каком порядке шли аргументы? Какой из них был необязательным? А может, коллега просит поделиться вашей утилитой? В этот момент простая автоматизация превращается в проблему юзабилити.
Как ответственный разработчик, вы решаете добавить человеческий интерфейс и обращаетесь к стандартной библиотеке argparse. И вот тут начинается боль:
import argparse
parser = argparse.ArgumentParser(description="Описание вашего скрипта.")
parser.add_argument("input_file", type=str, help="Путь к входному файлу.")
parser.add_argument("--output", "-o", type=str, default="output.csv", help="Путь к файлу для сохранения результата.")
parser.add_argument("--verbose", "-v", action="store_true", help="Включить подробный вывод.")
args = parser.parse_args()
# И только теперь начинается ваша логика...
# main(input_file=args.input_file, output_file=args.output, verbose=args.verbose)
Код для обработки аргументов разрастается, становится многословным и отрывается от основной логики. Каждый новый флаг — это еще несколько строк boilerplate-кода. В результате мы тратим время не на решение задачи, а на создание для нее обвязки.
А что, если я скажу, что можно получить мощный, документированный и удобный CLI, просто написав одну функцию с современными аннотациями типов?
В этой статье мы познакомимся с ударным дуэтом библиотек, которые навсегда изменят ваше отношение к созданию консольных утилит:
Typer — фундамент нашего приложения. Созданный автором FastAPI, он использует магию аннотаций типов для автоматического создания команд, аргументов, валидации и даже генерации справки (
--help). Минимум кода — максимум пользы.Rich — художник, который раскрасит наш вывод. Эта библиотека позволяет без усилий добавлять в терминал цвета, стили, красивые таблицы, прогресс-бары и многое другое, превращая скучный текстовый поток в информативный и приятный для глаз интерфейс.
Часть I: Typer — фундамент нашего приложения
Итак, мы хотим избавиться от громоздкого argparse. На помощь приходит Typer. Его философия проста: ваш код — это и есть ваш интерфейс. Вы описываете параметры функции с помощью стандартных аннотаций типов, а Typer берет на себя всю грязную работу по созданию CLI.
Давайте посмотрим, как это работает на практике.
Первые шаги: "Hello, World!" на Typer
Создадим файл main.py и напишем простейшее приложение, которое приветствует пользователя по имени.
# main.py
import typer
def main(name: str):
"""
Говорит "Привет" пользователю.
"""
print(f"Привет, {name}!")
if __name__ == "__main__":
typer.run(main)
Что здесь происходит?
Мы импортируем
typer.Определяем обычную функцию
mainс одним параметромname, указав его тип —str.В
if __name__ == "__main__":мы передаем нашу функцию вtyper.run().
Теперь откроем терминал и посмотрим на магию. Для начала, попросим у нашей утилиты справку:
$ python main.py --help
И без каких-либо усилий с нашей стороны получим вот такой результат:
Usage: main.py [OPTIONS] NAME
Говорит "Привет" пользователю.
Arguments:
NAME [required]
Options:
--help Show this message and exit.
Typer автоматически:
Превратил параметр
nameв обязательный аргументNAME.Понял, что он должен быть строкой.
Взял наш докстринг и сделал его описанием утилиты.
Добавил стандартную опцию
--help.
И все это — из трех строк кода! Теперь запустим утилиту:
$ python main.py Влад
Привет, Влад!
$ python main.py
Usage: main.py [OPTIONS] NAME
Try 'main.py --help' for help.
Error: Missing argument 'NAME'.
Как видите, Typer автоматически обрабатывает и ошибки, если мы забыли передать обязательный аргумент.
Аргументы, опции и типы данных
CLI-приложения состоят из аргументов (обычно обязательных) и опций (необязательных, часто начинаются с --). Typer элегантно управляется и с теми, и с другими.
Аргументы (Arguments): Как мы уже видели, любой параметр функции без значения по умолчанию становится обязательным аргументом.
Опции (Options): Чтобы добавить необязательный параметр, нужно использовать
typer.Option(). Давайте доработаем наш пример: добавим фамилию и флаг для формального приветствия.
import typer
def main(
name: str,
lastname: str = typer.Option("", help="Фамилия пользователя."),
formal: bool = typer.Option(False, "--formal", "-f", help="Использовать формальное приветствие."),
):
"""
Говорит "Привет" пользователю, опционально используя фамилию и формальный стиль.
"""
if formal:
print(f"Добрый день, {name} {lastname}!")
else:
print(f"Привет, {name}!")
if __name__ == "__main__":
typer.run(main)
Что изменилось:
lastname: Мы задали значение по умолчанию с помощьюtyper.Option(""). Теперь это необязательная опция--lastname.formal: Это булев флаг. Если в командной строке указать--formalили-f, его значение станетTrue.
Посмотрим, как это работает:
# Обычный вызов
$ python main.py Влад
Привет, Влад!
# Используем опцию с фамилией
$ python main.py Влад --lastname Дудин
Привет, Влад!
# Используем флаг и короткий псевдоним -f
$ python main.py Влад --lastname Дудин -f
Добрый день, Влад Дудин!
И снова проверим справку:
$ python main.py --help
``````text
Usage: main.py [OPTIONS] NAME
Говорит "Привет" пользователю, опционально используя фамилию и формальный стиль.
Arguments:
NAME [required]
Options:
--lastname TEXT Фамилия пользователя.
-f, --formal Использовать формальное приветствие.
--help Show this message and exit.
Все наши новые опции, их типы и описания из help на месте.
Наконец, Typer обеспечивает валидацию типов "из коробки". Если мы добавим параметр age: int и попробуем передать ему строку, Typer выдаст понятную ошибку, защищая наш код от некорректных данных.
Мы заложили прочный фундамент. Наше приложение уже умеет парсить аргументы, валидировать их и генерировать документацию. Теперь пора сделать его вывод красивым и информативным с помощью библиотеки Rich.
Часть II: Rich — наводим красоту
Мы построили функциональный каркас нашего приложения с помощью Typer. Оно работает, принимает аргументы и выдает результат. Но пока что этот результат — скучный черный текст на белом (или черном) фоне. Пользовательский опыт — это не только функциональность, но и подача информации. И здесь на сцену выходит Rich.
Rich — это библиотека, которая делает вывод в терминале по-настоящему "богатым" (rich). Забудьте про унылый print(). С Rich мы можем добавить цвета, стили, таблицы, прогресс-бары, подсветку синтаксиса и многое другое.
Основы: цвета и стили
Самый простой способ начать использовать Rich — это заменить стандартный print.
from rich import print
print("Это обычный текст.")
print("[bold green]Это жирный зеленый текст![/bold green]")
print("[italic yellow]А это желтый курсив.[/italic yellow]")
print("[underline cyan]Можно даже так.[/underline cyan]")
print("[bold red on white]Красный на белом фоне![/bold red on white]")
Rich использует простую разметку, похожую на BBCode, прямо внутри строк. Это интуитивно понятно и позволяет легко раскрасить вывод, чтобы привлечь внимание пользователя к важной информации: ошибкам, успешным операциям или предупреждениям.
Структурированный вывод: таблицы
Часто скрипты выводят однотипные данные, которые в виде простого текста сливаются в кашу. Rich позволяет элегантно организовать их в таблицы. Это ключевой элемент для нашей будущей утилиты.
Давайте посмотрим, как легко создать таблицу:
from rich import print
from rich.table import Table
# 1. Создаем объект таблицы
table = Table(title="Список моих любимых фреймворков")
# 2. Добавляем колонки
table.add_column("Название", justify="left", style="cyan", no_wrap=True)
table.add_column("Язык", style="magenta")
table.add_column("Для чего", justify="right", style="green")
# 3. Наполняем данными
table.add_row("FastAPI", "Python", "Веб-API")
table.add_row("React", "JavaScript", "Фронтенд")
table.add_row("Typer", "Python", "CLI-приложения")
# 4. Выводим таблицу в консоль
print(table)
Запустив этот код, вы получите в терминале идеально отформатированную и раскрашенную таблицу. Больше не нужно вручную выравнивать столбцы пробелами!
Интерактивность: прогресс-бары
Если ваш скрипт выполняет длительную операцию (например, обрабатывает много файлов или, как в нашем случае, опрашивает сайты), пользователь может подумать, что он завис. Rich решает эту проблему с помощью невероятно простого в использовании прогресс-бара.
Функция track оборачивает любой итерируемый объект (например, цикл for) и автоматически отображает индикатор выполнения.
import time
from rich.progress import track
for step in track(range(10), description="Обработка данных..."):
# Симулируем какую-то работу
time.sleep(0.5)
print("[bold green]Готово![/bold green]")
Эта небольшая деталь кардинально меняет восприятие вашего инструмента, делая его более профессиональным и дружелюбным.
Теперь у нас есть все необходимые компоненты: Typer для создания структуры и парсинга команд, и Rich для красивого и информативного вывода. Пора объединить их и создать нашу финальную утилиту.
Часть III: Собираем всё вместе — утилита для проверки доступности сайтов
Теория — это хорошо, но настоящая магия начинается на практике. Давайте создадим с нуля полноценную CLI-утилиту site-checker, которая будет:
Принимать на вход один или несколько URL-адресов.
Проверять их доступность, отправляя HTTP-запросы.
Показывать прогресс-бар во время проверки.
Выводить результаты в наглядной и красивой таблице.
Для работы нам понадобится еще одна библиотека — requests, ветеран для выполнения HTTP-запросов. Установим все зависимости:
pip install typer rich requests
Теперь создадим файл site_checker.py и приступим.
Шаг 1: Структура на Typer
Сначала определим "скелет" нашего приложения. Нам нужна функция, которая принимает список строк — наши URL. Typer справляется с этим изящно.
# site_checker.py
import typer
from typing import List
def main(urls: List[str] = typer.Argument(..., help="Список URL для проверки.")):
"""
Проверяет доступность сайтов и выводит результат в виде таблицы.
"""
# Здесь будет наша логика
print(f"Начинаем проверку {len(urls)} сайтов...")
if __name__ == "__main__":
typer.run(main)
Мы используем typer.Argument(...), чтобы явно указать, что это аргумент командной строки, а не опция, и добавить для него описание.
Шаг 2: Добавляем логику и красивый вывод
Теперь самое интересное. Мы объединим requests для сетевых запросов и Rich для визуализации.
# site_checker.py
import typer
import requests
from typing import List
from rich.console import Console
from rich.table import Table
from rich.progress import track
console = Console()
def get_status_emoji(status_code: int) -> str:
"""Возвращает эмодзи в зависимости от статус-кода."""
if 200 <= status_code < 300:
return "✅ OK"
elif 300 <= status_code < 400:
return "➡️ REDIRECT"
elif 400 <= status_code < 500:
return "❌ CLIENT ERROR"
elif 500 <= status_code < 600:
return "🔥 SERVER ERROR"
return "❓ UNKNOWN"
def main(urls: List[str] = typer.Argument(..., help="Список URL для проверки.")):
"""
Проверяет доступность сайтов и выводит результат в виде таблицы.
"""
table = Table(title="Результаты проверки сайтов")
table.add_column("URL", style="cyan", no_wrap=True)
table.add_column("Статус код", justify="center")
table.add_column("Статус", justify="left", style="green")
for url in track(urls, description="Проверка сайтов..."):
try:
response = requests.get(url, timeout=5)
status_code = response.status_code
status_text = get_status_emoji(status_code)
# Раскрашиваем строку в зависимости от статуса
row_style = ""
if 300 <= status_code < 400:
row_style = "yellow"
elif status_code >= 400:
row_style = "red"
table.add_row(url, str(status_code), status_text, style=row_style)
except requests.exceptions.RequestException as e:
table.add_row(url, "N/A", f"💥 ERROR: {e.__class__.__name__}", style="bold red")
console.print(table)
if __name__ == "__main__":
typer.run(main)
Что мы здесь сделали:
Инициализировали
ConsoleиTable: Создали объекты Rich для управления выводом и для нашей будущей таблицы с результатами.Добавили прогресс-бар: Обернули наш цикл по
urlsвtrack(), чтобы пользователь видел, что процесс идет.Отправляем запросы: Внутри цикла используем
requests.get()для проверки каждого сайта. Мы также обернули это вtry...except, чтобы отловить ошибки соединения или таймауты.Наполняем таблицу:
В случае успеха мы получаем статус-код, преобразуем его в понятный текст с эмодзи и добавляем строку в таблицу.
Мы добавили условное форматирование: редиректы подсвечиваются желтым, а ошибки — красным. Это мгновенно фокусирует внимание на проблемах.
В случае исключения (например, сайт не существует) мы добавляем в таблицу строку с сообщением об ошибке, выделенную жирным красным цветом.
Выводим результат: После завершения цикла мы одной командой
console.print(table)выводим всю собранную информацию в красивом виде.
Демонстрация
Теперь давайте запустим нашу утилиту, передав ей несколько адресов, включая один заведомо нерабочий:
python site_checker.py https://goodsite.io https://py-tools.com https://this-is-a-fake-domain.net
Сначала мы увидим прогресс-бар:
Проверка сайтов... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 3/3
А затем — финальный результат:
Результаты проверки сайтов
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ URL ┃ Статус код┃ Статус ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ https://goodsite.io │ 200 │ ✅ OK │
│ https://py-tools.com │ 200 │ ✅ OK │
│ https://this-is-a-fake-domain.net │ N/A │ 💥 ERROR: │
│ │ │ ConnectionError │
└───────────────────────────────────────┴───────────┴────────────────────┘
Согласитесь, это выглядит гораздо профессиональнее и удобнее, чем простой текстовый вывод. Мы создали не просто скрипт, а полноценный инструмент.
Домашнее задание
Теория без практики мертва, а лучший способ закрепить знания — это сразу же применить их. Я подготовил несколько заданий разной сложности, которые помогут вам глубже погрузиться в возможности Typer и Rich.
Уровень 1: Гибкая настройка таймаута
Задание: Сейчас таймаут для HTTP-запроса жестко зашит в коде (timeout=5). Это негибко. Добавьте в нашу утилиту необязательную опцию --timeout (с коротким псевдонимом -t), которая позволит пользователю самому задавать время ожидания ответа от сервера в секундах.
Подсказка: Вам понадобится typer.Option(). Не забудьте указать тип (int), значение по умолчанию и текст для справки.
# Пример того, как может выглядеть параметр в функции main
timeout: int = typer.Option(5, "--timeout", "-t", help="Таймаут для каждого запроса в секундах.")
Уровень 2: Сохранение результатов в файл
Задание: Выводить результат в консоль — это хорошо, но часто ��го нужно сохранить для дальнейшего анализа. Добавьте опцию --output (или -o), которая будет принимать путь к файлу. Если эта опция указана, утилита должна сохранить ту же самую таблицу результатов в CSV-файл.
Подсказка: Используйте стандартную библиотеку csv. Перед циклом проверки откройте файл для записи. Внутри цикла, после получения ответа, записывайте строку не только в таблицу Rich, но и в CSV-файл с помощью csv.writer. Если опция не передана, ничего делать не нужно.
# Пример параметра
output_file: str = typer.Option(None, "--output", "-o", help="Сохранить результат в CSV файл.")
Уровень 3 (со звездочкой): Чтение URL из файла
Задание: Передавать 100 URL через командную строку неудобно. Добавьте опцию --input-file, которая будет принимать путь к текстовому файлу. Если эта опция передана, утилита должна прочитать URL-адреса из этого файла (один URL на строку) и проверить их все. Приложение должно уметь работать либо со списком URL из командной строки, либо с файлом, но не с обоими одновременно.
Подсказка: В начале функции main проверьте, была ли передана опция --input-file. Если да, прочитайте файл, получите список URL и используйте его. Если нет, используйте список urls, который приходит из аргументов. Не забудьте обработать ситуацию, когда пользователь по ошибке указывает и аргументы, и опцию с файлом.
Бонусный челлендж: Для проверки большого количества сайтов синхронные запросы — это медленно. Попробуйте переписать логику проверки с использованием asyncio и библиотеки httpx для асинхронных запросов, чтобы все сайты проверялись параллельно. Это значительно ускорит работу утилиты!
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!
