Python

Все мы, специалисты по анализу данных, выполняем множество рутинных и повторяющихся действий. Сюда относятся: создание еженедельных отчетов, ETL-операции (извлечение, преобразование, загрузка), обучение моделей с помощью различных наборов данных и т.д. Зачастую на выходе у нас появляется множество Python-скриптов, и каждый раз при выполнении кода нам приходится менять его параметры. Лично меня это бесит! Именно поэтому я стал превращать скрипты в повторно используемые инструменты интерфейса командной строки (CLI-инструменты). Это повысило эффективность и продуктивность моей каждодневной работы. Начинал я с Argparse, но не особо проникся им, поскольку приходилось писать множество убогого кода. И тут я подумал: неужели нельзя достичь тех же результатов без постоянного переписывания кода? Да и вообще, смогу ли я когда-нибудь получать удовольствие от создания CLI-инструментов?

Click — это ваш друг и соратник!

Так что же такое Click? Из официальной документации следует:

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

Звучит шикарно! Как считаете?

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

Обучающий урок

В ходе данного урока мы будем пошагово создавать CLI с помощью Click на Python. Я начну с самых основ и в каждом шаге буду рассказывать про концепцию, предлагаемую Click. Дополнительно мне понадобится Poetry для управления пакетами и зависимостями.

Подготовка

Для начала давайте установим Poetry. Существует множество способов установки, однако здесь мы воспользуемся pip:

pip install poetry==0.12.7

Затем создадим в Poetry новый проект и назовем его cli-tutorial. Далее добавим зависимости click и funcy и создадим файл cli.py, который позже заполним кодом.

poetry new cli-tutorial
cd cli-tutorial
poetry add click funcy
# Создание файла, в который мы перенесем весь код 
touch cli_tutorial/cli.py

Я включил сюда funcy, поскольку он пригодится мне в дальнейшем. Ну а сейчас мы готовы к реализации своего первого CLI. Небольшое примечание: пример кода можно найти на GitHub.

Наш первый CLI на Click

Наш первоначальный CLI читает CSV-файл с диска, обрабатывает его (как именно он это делает — пока что не важно) и сохраняет результат в Excel. Пути к входному и выходному файлу настраиваются пользователем. И пользователь должен указать путь к входному файлу. Путь к выходному файлу указывается по желанию. Обычно им считаетсяoutput.xlsx. Вот так выглядит этот код при использовании Click:

import [email protected]()
@click.option("--in", "-i", "in_file", required=True,
    help="Path to csv file to be processed.",
)
@click.option("--out-file", "-o", default="./output.xlsx",
    help="Path to excel file to store the result.")
def process(in_file, out_file):
    """ Processes the input file IN and stores the result to 
    output file OUT.
    """
    input = read_csv(in_file)
    output = process_csv(input)
    write_excel(output, out_file)if __name__ =="__main__":
    process()

И что мы тут делаем?

1. Мы декорируем метод process, который будет вызываться из командной строки черезclick.command.

2. Затем определяем аргументы командной строки через декораторclick.option. Но внимательно следите за правильными названиями аргументов в декорированной функции. Если в click.option добавляется строка без дефиса, то аргумент должен совпадать с этой строкой. Этим и объясняется --in и in_file. Если все имена начинаются с дефисов, то Click создает название аргумента по самому длинному имени и заменяет все дефисы внутри слова на нижнее подчеркивание. Название пишется в нижнем регистре. Пример: --out-file и out_file. Более подробно можно почитать в документации по Click.

3. Через соответствующие аргументы click.option задаем наши предварительные условия значениями по умолчанию или необходимыми аргументами.

4. Добавляем текст справки к нашим аргументами. Он будет показываться при вызове функции через --help. Здесь же отображается docstring из нашей функции.

Теперь можете вызвать этот CLI несколькими способами:

# Печатает help
python -m cli_tutorial.cli --help
# Используйте -i для загрузки файла
python -m cli_tutorial.cli -i path/to/some/file.csv
# Указываем оба файла
python -m cli_tutorial.cli --in path/to/file.csv --out-file out.xlsx

Круто! Вот мы и создали свой первый CLI с помощью Click!

Обратите внимание: я не прописываю read_csv, process_csv и write_excel, т.к. предполагаю, что они существуют и корректно выполняют свою работу.

Одной из проблем CLI является то, что мы передаем параметры как общие строки. Почему же это проблема? Да потому, что такие строки должны быть преобразованы к фактическим типам. А это может приводить к ошибкам из-за плохо отформатированного пользовательского ввода. Взгляните на пример, в котором мы использовали пути и пытались загрузить CSV-файл. Пользователь может указать строку, которая и вовсе не является путем. И даже если эту строку правильно отформатировать, нужный файл может отсутствовать либо же у вас не окажется прав доступа. Разве не правильнее было бы автоматически проверять ввод и интерпретировать его или сразу выдавать ошибку с информативным сообщением? И в идеале все это делалось бы без написания длиннющих кусков кода. Click с нами полностью согласен. Поэтому в нем можно задавать тип аргументов.

Спецификация типов

В нашем примере с CLI мы хотели, чтобы пользователь передавал корректный путь к существующему файлу, для которого у нас есть разрешения на чтение. Если эти условия соблюдены, то мы загружаем входной файл. Кроме того, пользователь может задать путь к выходному файлу, и этот путь также должен быть действительным. Все это можно сделать, передав объект click.Path в аргумент type декоратора click.option.

@click.command()
@click.option("--in", "-i", "in_file", required=True,
    help="Путь к CSV-файлу для обработки.",
    type=click.Path(exists=True, dir_okay=False, readable=True),
)
@click.option("--out-file", "-o", default="./output.csv",
    help="Путь к CSV-файлу для хранения результата.",
    type=click.Path(dir_okay=False),
)
def process(in_file, out_file):
    """ Обработка входного файла IN с сохранением результата в выходном файле OUT.
    """
    input = read_csv(in_file)
    output = process_csv(input)
    write_excel(output, out_file)
...

click.Path — это один из нескольких готовых типов в Click. Помимо стандартных решений, вы можете создавать настраиваемые типы. Однако в данной статье эта тема не освещается. Почитать подробнее про пользовательские типы можно в документации.

Логические флаги

Еще одна полезная функция Click — это логические флаги. И, пожалуй, самым известным из них является флагverbose. При значении true ваш инструмент выводит всю информацию в терминал. При значении false показываются только некоторые данные. В Click это можно реализовать следующим образом:

from funcy import identity
...
@click.option('--verbose', is_flag=True, help="Verbose output")
def process(in_file, out_file, verbose):
    """ Обработка входного файла IN с сохранением результата в выходном файле OUT.
    """
    print_func = print if verbose else identity    print_func("We will start with the input")
    input = read_csv(in_file)
    print_func("Next we procees the data")
    output = process_csv(input)
    print_func("Finally, we dump it")
    write_excel(output, out_file)

Все, что от вас требуется, — это добавить еще один декоратор click.option и установить is_flag=True. Теперь для получения подробного вывода нужно всего лишь вызвать CLI:

python -m cli_tutorial.cli -i path/to/some/file.cs --verbose

Переключатель функций

Допустим, нам захотелось не просто хранить результат в локальном process_csv, но и загружать его на сервер. Кроме того, есть не только целевой сервер, но и сервера для разработки, тестирования и реальной базы. И ко всем им нужно обращаться по разным URL. Один из способов выбора сервера — это передача полного URL— адреса как аргумента, который пользователь должен будет прописать. Причем, этот способ не просто рискованный в плане ошибок, но и весьма кропотлив. Поэтому для облегчения жизни пользователей я использую переключатели функций. Принцип их работы лучше всего иллюстрирует код ниже:

...
@click.option(
    "--dev", "server_url", help="Загрузить на сервер разработки",
    flag_value='https://dev.server.org/api/v2/upload',
)
@click.option(
    "--test", "server_url", help="Загрузить на тестовый сервер",
    flag_value='https://test.server.com/api/v2/upload',
)
@click.option(
    "--prod", "server_url", help="Загрузить на основной сервер",
    flag_value='https://real.server.com/api/v2/upload',
    default=True
)
def process(in_file, out_file, verbose, server_url):
    """ Обработка входного файла IN и хранение результата в выходном файле OUT.
    """
    print_func = print if verbose else identity
    print_func("Мы начнем с входного значения")
    input = read_csv(in_file)
    print_func("Затем обработаем данные")
    output = process_csv(input)
    print_func("И, наконец, выдадим готовый файл")
    write_excel(output, out_file)
    print_func("Загрузим его на сервер")
    upload_to(server_url, output)
...

Здесь я добавил три декоратораclick.option для трех возможных URL-адресов серверов. Важный момент: все три опции содержат одну общую переменнуюserver_url. В зависимости от выбранной опции значение server_url соответствует значению, определенному в flag_value. Их вы выбираете, добавляя в качестве аргумента --dev, --test или --prod. Таким образом, при выполнении:

python -m cli_tutorial.cli -i path/to/some/file.csv --test

server_url соответствует https://test.server.com/api/v2/upload. Если оставить флажки пустыми, то Click возьмет значение --prod, поскольку я прописал default=True.

Запрос на ввод логина и пароля

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

Поэтому мы предпочитаем выдавать пользователю запрос на ввод пароля, не передавая его в терминал и не сохраняя в истории команд. Что до логина, то здесь нам по душе простой запрос на ввод с передачей в терминал. А при определенных познаниях в Click ничего проще и не придумаешь. Вот наш код:

import os
...
@click.option('--user', prompt=True,
              default=lambda: os.environ.get('USER', ''))
@click.password_option()
def process(in_file, out_file, verbose, server_url, user, password):
    ...
    upload_to(server_url, output, user, password)

Чтобы добавить подсказку для ввода аргумента, установите prompt=True. Это действие добавит запрос везде, где пользователь не проставил аргумент --user, однако он может потребоваться. Если нажать на Enter в запросе, то проставится значение по умолчанию. Оно определяется с помощью другой полезной функции Click.

Запрос на ввод и подтверждение пароля без отправки его в терминал стало чем-то настолько обыденным, что Click придумал для этого специальный декораторpassword_option. Важное примечание: пользователь все равно будет передавать пароль через --password MYSECRETPASSWORD. Однако так он сможет этого не делать.

Вот и все. Мы создали полноценный CLI. Перед тем, как подвести финальную черту, мне бы хотелось поделиться еще одной полезной подсказкой.

Poetry-скрипты

Последним штрихом этой статьи, который никак не связан с самим Click, но идеально вписывается в тему CLI, является создание Poetry-скриптов. Эти скрипты позволяют создавать исполняемые модули для вызова Python-функций из командной строки так же, как это делается в Setuptools-скриптах. Как это выглядит? Для начала добавим в файл pyproject.toml следующие строки:

[tool.poetry.scripts]
your-wanted-name = ‘cli_tutorial.cli:process’

Значение your-wanted-name — это псевдоним для функции process, определенной в модулеcli_tutorial.cli. Теперь вы можете вызвать ее следующим образом:

poetry run your-wanted-name -i ./dummy.csv — verbose — dev

Таким образом, вы, например, сможете включать несколько CLI-функций в один файл, определять псевдонимы и не добавлять блок if __name__ == “__main__”.

Итог

В данной статье я показал вам, как пользоваться Click и Poetry для простого и эффективного создания CLI-инструментов. Это был лишь небольшой пример возможностей Click. В библиотеке есть и другие полезные функции, как, например, обратные вызовы, вложенные команды или предварительный выбор значений. Еще раз призываю всех заинтересовавшихся темой почитать документацию по Click.

Читайте также:


Перевод статьи Simon Hawe: How to Write Python Command-Line Interfaces like a Pro