Все мы, специалисты по анализу данных, выполняем множество рутинных и повторяющихся действий. Сюда относятся: создание еженедельных отчетов, ETL-операции (извлечение, преобразование, загрузка), обучение моделей с помощью различных наборов данных и т.д. Зачастую на выходе у нас появляется множество Python
-скриптов, и каждый раз при выполнении кода нам приходится менять его параметры. Лично меня это бесит! Именно поэтому я стал превращать скрипты в повторно используемые инструменты интерфейса командной строки (CLI
-инструменты). Это повысило эффективность и продуктивность моей каждодневной работы. Начинал я с Argparse
, но не особо проникся им, поскольку приходилось писать множество убогого кода. И тут я подумал: неужели нельзя достичь тех же результатов без постоянного переписывания кода? Да и вообще, смогу ли я когда-нибудь получать удовольствие от создания CLI
-инструментов?
Click — это ваш друг и соратник!
Так что же такое Click
? Из официальной документации следует:
Click призван сделать процесс написания инструментов командной строки быстрым и увлекательным, избавляя при этом от всякого рода разочарований из-за невозможности реализации желаемого CLI API.
Звучит шикарно! Как считаете?
В данной статье я поделюсь с вами практическим руководством по пошаговому созданию Python
CLI
с помощью Click
на Python
и продемонстрирую вам базовые опции и преимущества этой библиотеки. Выполнив данный пример, вы научитесь писать CLI
-инструменты быстро и безболезненно ???? Давайте уже займемся делом!
В ходе данного урока мы будем пошагово создавать CLI
с помощью Click
на Python
. Я начну с самых основ и в каждом шаге буду рассказывать про концепцию, предлагаемую Click
. Дополнительно мне понадобится Poetry
для управления пакетами и зависимостями.
Для начала давайте установим Poetry
. Существует множество способов установки, однако здесь мы воспользуемся pip
:
Затем создадим в 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
читает 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
:
Допустим, нам захотелось не просто хранить результат в локальном 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
. Таким образом, при выполнении:
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
. Перед тем, как подвести финальную черту, мне бы хотелось поделиться еще одной полезной подсказкой.
Последним штрихом этой статьи, который никак не связан с самим Click
, но идеально вписывается в тему CLI
, является создание Poetry
-скриптов. Эти скрипты позволяют создавать исполняемые модули для вызова Python
-функций из командной строки так же, как это делается в Setuptools
-скриптах. Как это выглядит? Для начала добавим в файл pyproject.toml
следующие строки:
Значение your-wanted-name
— это псевдоним для функции process
, определенной в модулеcli_tutorial.cli
. Теперь вы можете вызвать ее следующим образом:
Таким образом, вы, например, сможете включать несколько CLI
-функций в один файл, определять псевдонимы и не добавлять блок if __name__ == “__main__”
.
В данной статье я показал вам, как пользоваться Click
и Poetry
для простого и эффективного создания CLI
-инструментов. Это был лишь небольшой пример возможностей Click
. В библиотеке есть и другие полезные функции, как, например, обратные вызовы, вложенные команды или предварительный выбор значений. Еще раз призываю всех заинтересовавшихся темой почитать документацию по Click
.
Перевод статьи Simon Hawe: How to Write Python Command-Line Interfaces like a Pro
Комментарии