Написание Python утилит командной строки с click

| Python

Python часто называют языком межпрограммной склейки, потому что он чрезвычайно гибкий и хорошо интегрируется с существующими программами. На текущий момент уже написано огромное количество кода в виде скриптов с интерфейсом командной строки.

Разработка приложений с интерфейсом командной строки (CLI) чрезвычайно полезна, так как позволяет автоматизировать практически все. Но, со временем, CLI программы могут стать довольно сложными. Рассмотрим простой Python скрипт, который обращается по веб-API к серверу и распечатывает выходные данные в консоль:

print_user_agent.py

import requests
json = requests.get('http://httpbin.org/user-agent').json()
print(json['user-agent'])

Скрипт распечатает User-Agent пользователя, выполнив вызов к API. Но что делать, когда Python скрипт командной строки начнёт расти и усложняться?

В данной статье будет рассказано:

  • Почему click – лучшая альтернатива argparse и optparse
  • Как создавать программы с простым CLI
  • Как добавить обязательные аргументы командной строки в скрипты
  • Как парсить флаги и параметры командной строки
  • Как сделать приложения для командной строки более удобными, добавив текст справки

Все примеры кода в этом руководстве реализованы на Python 3.6. Они могут не работать с более ранними версиями Python, но если у вас возникнут проблемы, оставьте комментарий.

Зачем нам писать скрипты и инструменты командной строки на Python?

Приведенный выше фрагмент кода является простым примером и поэтому бесполезен. Реальные сценарии намного сложнее. Обычно они помогают создавать, тестировать и развертывать приложения, выполняя этот процесс автоматически.

В таких случаях очень важно писать скрипты более гибкими и настраиваемыми. Это позволяет передавать в сценарии имена серверов, учетные данные или любую другую информацию.

В стандартную библиотеку Python входят модули optparse и argparse, которые делают жизнь разработчика намного проще. Но прежде чем рассматривать их более подробно, давайте введём терминологию.

Основы интерфейса командной строки

Интерфейс командной строки (CLI) начинается с имени исполняемого файла. Введя его имя в консоли, пользователь получает доступ к главной точке входа в скрипт, например, к pip.

Основными параметрами передаваемые в CLI приложение можно разделить на следующие группы:

  • Аргументы – обязательные параметры, передаваемые скрипту. Если их не определять, CLI вернет ошибку. Например, django– это аргумент в команде pip install django.
  • Опции – необязательные ([]) параметры, объединяющие имя и часть значения, например -cache-dir ./my-cache. Программе pip сообщается, что значение ./my-cache должно использоваться для определения каталога кэша.
  • Флаги – специальные опции, которые включают или отключают определенное поведение. Чаще всего это --help.

Вероятно, вы уже использовали CLI, когда устанавливали Python пакет, выполнив pip install <PACKAGE NAME>. Команда install сообщает CLI, что необходимо получить доступ к функции устанавливающей пакет и предоставить доступ к параметрам, характерным для этой функции.

Фреймворки командной строки, доступные в стандартной библиотеке Python 3.x

Добавление команд и опций в скрипты чрезвычайно полезно, но разбор командной строки не так очевиден, как это может показаться. Вместо того, чтобы начинать писать свои собственные парсеры, рационально воспользоваться одним из множества Python пакетов, которые уже решили эту задачу.

Двумя наиболее известными пакетами являются optparse и argparse. Они являются частью стандартной библиотеки Python по принципу «батарейки идут в комплекте».

Они в основном обеспечивают схожую функциональность и работают очень похоже. Самое большое отличие заключается в том, что библиотека optparse устарела и запрещена в Python 3.2, а argparse считается стандартом для реализации CLI в Python.

Более подробную информацию о них можно найти в документации по Python. Чтобы иметь представление как выглядит скрипт с argparse, приведем пример:

import argparse
parser = argparse.ArgumentParser(description='Процессор над числами.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
                    help='integer число для аккумулятора')
parser.add_argument('--sum', dest='accumulate', action='store_const',
                    const=sum, default=max,
                    help='сумма integer чисел (по умолчанию поиск максимального)')

args = parser.parse_args()
print(args.accumulate(args.integers))

Код приведённый выше нельзя назвать интуитивно понятным и легко читаемым. Поэтому рекомендуется использовать click.

Использование click в качестве лучшей альтернативы

click разрешает ту же задачу, что и optparse и argparse, но использует несколько иной подход. Он использует концепцию декораторов. Это требует реализацию команд в виде функций, которые потом можно обернуть декоратором.

Основная причина, почему используют click – это то, что можно легко создать многофункциональный CLI с небольшим количеством кода. При этом код остаётся легко читаемым, даже когда CLI растет и становится более сложным.

Простой интерфейс командной строки Python с click

Создадим простое CLI приложение на основе click, который распечатывает текст в консоль.

# cli.py
import click

@click.command()
def main():
    print("Hello, World!")

if __name__ == "__main__":
    main()

Всё что нужно сделать – это создать функцию и добавить к ней декоратор @click.command(). Он превращает функцию main в команду click, которая является точкой входа для скрипта. После запуска скрипта получим результат:

$ python cli.py
Hello, World!

Прелесть click – это то, что мы бесплатно получаем некоторые дополнительные функции. Не реализовывая никаких вспомогательных возможностей добавив параметр --help, распечатается основная страница справки в командную строку:

$ python cli.py --help
Usage: cli.py [OPTIONS]

Options:
  --help  Show this message and exit.

Более реалистичный пример CLI с click

Теперь, когда известно, как click упрощает создание простого CLI, рассмотрим более реалистичный пример. Напишем программу, которая позволяет взаимодействовать с веб API. Сегодня многие используются ими, потому что они предоставляют доступ к некоторым любопытным данным.

API который будет рассматриваться для оставшейся части этого материала – это OpenWeatherMap API. Он сообщает текущую погоду, а также прогноз на пять дней для определенного местоположения. Начнем с шаблона API, возвращающего текущую погоду для местоположения.

Одним из инструментов, который позволяет исследовать новый API, является программа HTTPie, которую можно использовать для вызова API ресурса и просмотра возвращаемого результата.

Давайте посмотрим, что произойдет, когда будет вызван API для города Москва:

$ http --body GET http://samples.openweathermap.org/data/2.5/weather q==Moskow \
  appid==a1v15e88fa797225412429c1c50c345a1
{
    "base": "stations",
    "clouds": {
        "all": 90
    },
    "cod": 200,
    "coord": {
        "lat": 51.51,
        "lon": -0.13
    },
    "dt": 1485789600,
    "id": 2643743,
    "main": {
        "humidity": 81,
        "pressure": 1012,
        "temp": 280.32,
        "temp_max": 281.15,
        "temp_min": 279.15
    },
    "name": "Moskow",
    "sys": {
        "country": "RU",
        "id": 5091,
        "message": 0.0103,
        "sunrise": 1485762037,
        "sunset": 1485794875,
        "type": 1
    },
    "visibility": 10000,
    "weather": [
        {
            "description": "light intensity drizzle",
            "icon": "09d",
            "id": 300,
            "main": "Drizzle"
        }
    ],
    "wind": {
        "deg": 80,
        "speed": 4.1
    }
}

Важным наблюдением из приведенного выше примера является то, что для получения текущей погоды определяется два параметра запроса (обозначаемые == при использовании HTTPie) :

  • q – местонахождение
  • appid - это ключ к API

Также можно создать простую реализацию с использованием Python и библиотеки Requests (будем игнорировать обработку ошибок и неудачные запросы для простоты.)

import requests

SAMPLE_API_KEY = 'a1v15e88fa797225412429c1c50c345a1'
def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'http://samples.openweathermap.org/data/2.5/weather'
    query_params = {
        'q': location,
        'appid': api_key,
    }
    response = requests.get(url, params=query_params)
    return response.json()['weather'][0]['description']

Функция создаёт простой запрос к API погоды, используя два параметра запроса. Она принимает обязательный аргумент location, который является строкой. Также можно предоставить ключ API, передав api_key в вызове функции. Он является необязательным и используется для примера по умолчанию.

Результат работы программы для Москвы

>>> current_weather('Moskow')
'light intensity drizzle'

Парсинг обязательных параметров с click

Простая функция current_weather позволяет создавать собственный CLI с передачей пользователем аргумента location. Результат работы программы должен быть таким:

$ python cli.py Moskow
The weather in Moskow right now: light intensity drizzle.

Рассмотрим простой пример, немного изменив предыдущий, указав аргумент location.

@click.command()
@click.argument('location')
def main(location):
    weather = current_weather(location)
    print(f"The weather in {location} right now: {weather}.")

Всё, что нужно сделать, это добавить дополнительный декоратор к функции main и дать ему имя. click использует это имя в качестве имени аргумента, передаваемого в функцию обёртку.

В нашем случае значение расположения будет передано в функцию main через аргумент location.

Можно также использовать тире (-) в именах, таких как API-ключ, который click превратит в «змеиный регистр» для имени аргумента в функции, например main(api_key).

Реализация основывается на простой функции current_weather, которая запрашивает погоду для location. Далее вызывается оператор print для вывода информации о погоде.

Оператор print выглядит немного странно, потому что это новый способ форматирования строк добавленный в Python 3.6+, называемый форматированием f-string.

Парсинг необязательных параметров с click

На данный момент функция current_weather всегда возвращает погоду в Москве начиная с января 2017 года. Также нужно определить реальный ключ к API. Для бесплатного получения ключа доступа зарегистрируйтесь на сайте openweathermap.

Изменим URL для текущей погоды location.

def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'https://api.openweathermap.org/data/2.5/weather'

    # все остальное остается прежним    
...

Произведённые изменения, ломают CLI приложения, поскольку ключ API по умолчанию недействителен. API вернет код статуса HTTP 401 UNAUTHORIZED.

$ http GET https://api.openweathermap.org/data/2.5/weather q==Moskow appid==a1v15e88fa797225412429c1c50c345a1
HTTP/1.1 401 Unauthorized
{
    "cod": 401,
    "message": "Invalid API key."
}

Итак, добавим новый параметр в CLI, который позволяет указать ключ API. Но сначала нужно определиться, должен это быть аргумент или опция. Определим параметр как опция, потому что добавление именованного параметра, такого как --api-key, делает его более явным и очевидным.

$ python cli.py --api-key <your-api-key> Moskow
The weather in Moskow right now: light intensity drizzle.

Модифицированная версия CLI приложения

@click.command()
@click.argument('location')
@click.option('--api-key', '-a')
def main(location, api_key):
    weather = current_weather(location, api_key)
    print(f"The weather in {location} right now: {weather}.")

Таким образом добавлен ещё один декоратор функции main. На этот раз используется декоратор @click.option, добавляя опцию с ведущими двойными тире (-). Также можно определить краткую опцию с одним тире (-), чтобы реализовать краткий пользовательский ввод.

Как уже было сказано, click создает аргумент, переданный функции main из длинной версии имени. В случае опции, фреймворк распаковывает ведущие тире и превращает их в snake case. --API-key превращается в api_key.

Теперь пользователь CLI приложения может использовать собственный ключ и определить любое местоположение:

$ python cli.py --api-key <your-api-key> Novosibirsk
The weather in Novosibirsk right now: broken clouds.

Добавление справки по использованию приложения

Далее реализуем возможность вывода краткой документации для начинающего пользователя.

Сначала проверим, что распечатает приложение после запуска с флагом - -help.

$ python cli.py --help
Usage: cli.py [OPTIONS] LOCATION

Options:
  -a, --api-key TEXT
  --help              Show this message and exit.

Первое, что нужно исправить – это отсутствие описания для ключа API. Поэтому определим текст справки для декоратора @click.option:

@click.command()
@click.argument('location')
@click.option(
    '--api-key', '-a',
    help='your API key for the OpenWeatherMap API',
)
def main(location, api_key):

В заключение добавим справочную информацию о назначении и кратких примерах использования. Самый простой и самый питоничный способ – это добавление docstring к функции main. Это необходимо выполнять в любом случае, так что это даже не лишняя работа:

...
def main(location, api_key):
    """
    A little weather tool that shows you the current weather in a LOCATION of
    your choice. Provide the city name and optionally a two-digit country code.
    Here are two examples:

    1. Moskow

    2. Novosibirsk

    You need a valid API key from OpenWeatherMap for the tool to work.
    """
    ...

Объединяя все изменения, получаем результат:

$ python cli.py --help
Usage: cli.py [OPTIONS] LOCATION

  A little weather tool that shows you the current weather in a LOCATION of
  your choice. Provide the city name and optionally a two-digit country
  code. Here are two examples:

  1. Moskow

  2. Novosibirsk

  You need a valid API key from OpenWeatherMap for the tool to work..

Options:
  -a, --api-key TEXT  your API key for the OpenWeatherMap API
  --help              Show this message and exit.

Результаты и резюме

Итак, в данной статье были даны ответы на следующие вопросы:

  • Почему click лучше, чем argparse и optparse
  • Как создать простой CLI
  • Как добавить обязательные аргументы командной строки в скрипты
  • Как парсить флаги и параметры командной строки
  • Как сделать приложения для командной строки более удобными, добавив текст справки

Ниже приведен полный пример кода. Не стесняйтесь использовать его в своих проектах

import click
import requests

SAMPLE_API_KEY = 'a1v15e88fa797225412429c1c50c345a1'


def current_weather(location, api_key=SAMPLE_API_KEY):
    url = 'https://api.openweathermap.org/data/2.5/weather'

    query_params = {
        'q': location,
        'appid': api_key,
    }

    response = requests.get(url, params=query_params)

    return response.json()['weather'][0]['description']


@click.command()
@click.argument('location')
@click.option(
    '--api-key', '-a',
    help='your API key for the OpenWeatherMap API',
)
def main(location, api_key):
    """
    A little weather tool that shows you the current weather in a LOCATION of
    your choice. Provide the city name and optionally a two-digit country code.
    Here are two examples:
    1. Moskow
    2. Novosibirsk
    You need a valid API key from OpenWeatherMap for the tool to work. 
    """
    weather = current_weather(location, api_key)
    print(f"The weather in {location} right now: {weather}.")


if __name__ == "__main__":
    main()