Доброго времени суток всем.
На сегодняшний день Python является одним из наиболее используемых языков в сфере создания не только непосредственно программных продуктов, но также обеспечения их инфраструктуры. Вследствие этого многим девопсам, по их воле или против оной, пришлось учить новый язык для последующего использования в качестве дополнения к старым добрым Bash-скриптам. Однако Bash и Python исповедуют различные подходы к написанию кода и имеют определенные особенности, в виду чего портирование Bash-скриптов на «змеиный язык» иногда оказывается ёмкой и далеко не тривиальной задачей.
Чтобы упростить жизнь девопсам, создано и продолжает создаваться много полезных библиотек и утилит на Python. Данная статья описывает сразу две новых библиотеки, созданные автором сего поста — smart-env и python-shell — и призванные избавить девопса от необходимости уделять много внимания тонкостям работы с Python, оставляя простор для более интересных задач. Сфера деятельности библиотек — переменные окружения и запуск внешних утилит.
Кого заинтересовало, прошу под кат.
Казалось бы, зачем создавать новые пакеты для достаточно обыденных операций? Что мешает использовать напрямую os.environ и subprocess.<метод или класс на ваш вкус>?
Доказательства в пользу каждой из библиотек приведу отдельно.
Перед тем, как писать собственное детище, полезно полезть в Интернет и поискать готовые решения. Конечно, есть риск не найти то, что нужно, но это, скорее, «страховой случай». Как правило, подход срабатывает и экономит кучу времени и сил.
По результатам поиска было выявлено следующее:
И этот список можно продолжать долго. Однако и приведенных выше пунктов хватило, чтобы загореться идеей сделать нечто удобное и универсальное.
Требования, которые ставились перед написанием smart-env:
В конечном итоге, все это удалось реализовать. Вот пример использования:
Как видно из примера, для работы с новым классом достаточно его импортировать (создавать экземпляр не нужно — минус лишнее действие). Доступ к любой переменной окружения достигается путем обращения к ней как к переменной класса ENV, что, фактически, делает этот класс интуитивно понятной обёрткой нативного системного окружения, параллельно превращая его в возможный вариант объекта конфигурации практически любой системы (похожий подход, например, достигается в Django, только там конфигурационным объектом выступает непосредственно модуль/пакет settings).
Включение/выключение режима поддержки автоматической типизации достигается использованием двух методов — enable_automatic_type_cast() и disable_automatic_type_cast(). Это может быть удобно, если в переменной окружения лежит сериализованный JSON-подобный объект или даже просто булевая константа (явное прописывание переменной DEBUG в Django путем сравнения переменной окружения с «допустимыми» строками — один из часто встречающихся случаев). Но теперь нет нужды явно конвертировать строки — большая часть необходимых действий уже заложена в недрах библиотеки и только ждет сигнала к действию. :) В целом же типизация работает прозрачно и поддерживает почти все имеющиеся встроенные типы данных (не тестировались frozenset, complex и bytes).
Требование поддержки Python 2 было реализовано практически без жертв (отказ от typing и некоторых «сахарных конфеток» последних версий Python 3), в частности, благодаря вездесущему six (для решения проблем использования метаклассов).
Но есть и немного ограничений:
Библиотека также исповедует механизм исключений на случай ошибок парсинга. Если строку не удалось распознать ни одним из имеющихся анализаторов, значение остается строковым (скорее, из соображений удобства и обратной совместимости с привычной логикой работы переменных в Bash).
Теперь расскажу о второй библиотеке (описание недостатков имеющихся аналогов опущу — оно похоже на описанное для smart-env. Аналоги — тут и тут).
В целом, идея реализации и требования к ней аналогичны описанным для smart-env, что видно из примера:
Идея такова:
Как и в случае со smart-env, обеспечена поддержка Python 2 (правда, жертвенной крови потребовалось немного больше) и отсутствует поддержка Python 3.0-3.4.
Использовать библиотеки можно уже сейчас: обе выложены на официальный PyPI. Исходники доступны на Github (см. ниже).
Обе библиотеки будут развиваться с учетом фидбека, собранного от заинтересовавшихся. И, если в smart-env, может, и сложно придумать разнообразие новых фич, то в python-shell точно есть еще что добавить:
UPD 23.02.2020:
* Репозитории перенесены, соответствующие ссылки обновлены
* Версия python-shell==1.0.1 готовится к выходу 29.02.2020. Среди изменений — поддержка автокомплита команд и команды dir(Shell), запуск команд с Python-невалидным идентификатором, исправление багов.
На сегодняшний день Python является одним из наиболее используемых языков в сфере создания не только непосредственно программных продуктов, но также обеспечения их инфраструктуры. Вследствие этого многим девопсам, по их воле или против оной, пришлось учить новый язык для последующего использования в качестве дополнения к старым добрым Bash-скриптам. Однако Bash и Python исповедуют различные подходы к написанию кода и имеют определенные особенности, в виду чего портирование Bash-скриптов на «змеиный язык» иногда оказывается ёмкой и далеко не тривиальной задачей.
Чтобы упростить жизнь девопсам, создано и продолжает создаваться много полезных библиотек и утилит на Python. Данная статья описывает сразу две новых библиотеки, созданные автором сего поста — smart-env и python-shell — и призванные избавить девопса от необходимости уделять много внимания тонкостям работы с Python, оставляя простор для более интересных задач. Сфера деятельности библиотек — переменные окружения и запуск внешних утилит.
Кого заинтересовало, прошу под кат.
Новые «велосипеды»?
Казалось бы, зачем создавать новые пакеты для достаточно обыденных операций? Что мешает использовать напрямую os.environ и subprocess.<метод или класс на ваш вкус>?
Доказательства в пользу каждой из библиотек приведу отдельно.
Библиотека smart-env
Перед тем, как писать собственное детище, полезно полезть в Интернет и поискать готовые решения. Конечно, есть риск не найти то, что нужно, но это, скорее, «страховой случай». Как правило, подход срабатывает и экономит кучу времени и сил.
По результатам поиска было выявлено следующее:
- есть пакеты, действительно оборачивающие вызовы к os.environ, однако при этом требующие кучу отвлекающих действий (создание экземпляра класса, спец-параметры в вызовах и пр.);
- есть неплохие пакеты, которые, однако, жестко завязаны на определенную экосистему (в основном, на веб-фреймворки вроде Django) и потому без напильника совсем не универсальные;
- есть редкие попытки сделать что-то новое. Например, добавить типизацию и явно парсить значения переменных путем вызова методов вида
get_<typename>(var_name)
Или вот еще одно решение, которое, однако, не поддерживает ныне опальный Python 2 (на котором, несмотря на официальный R.I.P., все еще остаются горы написанного кода и целые экосистемы); - есть школьно-студенческие поделки, вообще непонятно зачем оказавшиеся в апстримном PyPI и только создающие проблемы с именованием новых пакетов (в частности, название «smart-env» — вынужденная мера).
И этот список можно продолжать долго. Однако и приведенных выше пунктов хватило, чтобы загореться идеей сделать нечто удобное и универсальное.
Требования, которые ставились перед написанием smart-env:
- Максимально простая схема использования
- Легко конфигурируемая поддержка типизации данных
- Совместимость с Python 2.7
- Хорошее покрытие кода тестами
В конечном итоге, все это удалось реализовать. Вот пример использования:
from smart_env import ENV
print(ENV.HOME) # Equals print(os.environ['HOME'])
# assuming you set env variable MYVAR to "True"
ENV.enable_automatic_type_cast()
my_var = ENV.MY_VAR # Equals boolean True
ENV.NEW_VAR = 100 # Sets a new environment variable
Как видно из примера, для работы с новым классом достаточно его импортировать (создавать экземпляр не нужно — минус лишнее действие). Доступ к любой переменной окружения достигается путем обращения к ней как к переменной класса ENV, что, фактически, делает этот класс интуитивно понятной обёрткой нативного системного окружения, параллельно превращая его в возможный вариант объекта конфигурации практически любой системы (похожий подход, например, достигается в Django, только там конфигурационным объектом выступает непосредственно модуль/пакет settings).
Включение/выключение режима поддержки автоматической типизации достигается использованием двух методов — enable_automatic_type_cast() и disable_automatic_type_cast(). Это может быть удобно, если в переменной окружения лежит сериализованный JSON-подобный объект или даже просто булевая константа (явное прописывание переменной DEBUG в Django путем сравнения переменной окружения с «допустимыми» строками — один из часто встречающихся случаев). Но теперь нет нужды явно конвертировать строки — большая часть необходимых действий уже заложена в недрах библиотеки и только ждет сигнала к действию. :) В целом же типизация работает прозрачно и поддерживает почти все имеющиеся встроенные типы данных (не тестировались frozenset, complex и bytes).
Требование поддержки Python 2 было реализовано практически без жертв (отказ от typing и некоторых «сахарных конфеток» последних версий Python 3), в частности, благодаря вездесущему six (для решения проблем использования метаклассов).
Но есть и немного ограничений:
- Поддержка Python 3 подразумевает версию 3.5 и выше (их наличие в вашем проекте — результат либо лени, либо отсутствия необходимости в улучшениях, т.к. сложно придумать объективную причину, почему вы до сих пор сидите на 3.4);
- В Python 2.7 библиотека не поддерживает десериализацию литералов множеств. Описание тут. Но, если кто-нибудь захочет реализовать — welcome:);
Библиотека также исповедует механизм исключений на случай ошибок парсинга. Если строку не удалось распознать ни одним из имеющихся анализаторов, значение остается строковым (скорее, из соображений удобства и обратной совместимости с привычной логикой работы переменных в Bash).
Библиотека python-shell
Теперь расскажу о второй библиотеке (описание недостатков имеющихся аналогов опущу — оно похоже на описанное для smart-env. Аналоги — тут и тут).
В целом, идея реализации и требования к ней аналогичны описанным для smart-env, что видно из примера:
from python_shell import Shell
Shell.ls('-l', '$HOME') # Equals "ls -l $HOME"
command = Shell.whoami() # Equals "whoami"
print(command.output) # prints your current user name
print(command.command) # prints "whoami"
print(command.return_code) # prints "0"
print(command.arguments) # prints ""
Shell.mkdir('-p', '/tmp/new_folder') # makes a new folder
Идея такова:
- Единый класс, олицетворяющий Bash в мире Python;
- Каждая Bash-команда вызывается как функция класса Shell;
- Параметры вызова каждой функции далее пробрасываются в вызов соответствующей команды Bash;
- Каждая команда выполняется «здесь и сейчас» в момент ее вызова, т.е. работает синхронный подход;
- есть возможность получить доступ к выхлопу команды в stdout, а также код ее возврата;
- Если команда отсутствует в системе — бросается исключение.
Как и в случае со smart-env, обеспечена поддержка Python 2 (правда, жертвенной крови потребовалось немного больше) и отсутствует поддержка Python 3.0-3.4.
Планы по развитию библиотек
Использовать библиотеки можно уже сейчас: обе выложены на официальный PyPI. Исходники доступны на Github (см. ниже).
Обе библиотеки будут развиваться с учетом фидбека, собранного от заинтересовавшихся. И, если в smart-env, может, и сложно придумать разнообразие новых фич, то в python-shell точно есть еще что добавить:
- поддержка неблокирующих вызовов;
- возможность интерактивного общения с командой (работа с stdin);
- добавление новых свойств (например, property для получения выхлопа из stderr);
- реализация каталога доступных команд (для использования с функцией dir());
- и т.д.
Ссылки
- Библиотека smart-env: Github и PyPI
- Библиотека python-shell: Github и PyPI
- Телеграм-канал обновлений библиотек
UPD 23.02.2020:
* Репозитории перенесены, соответствующие ссылки обновлены
* Версия python-shell==1.0.1 готовится к выходу 29.02.2020. Среди изменений — поддержка автокомплита команд и команды dir(Shell), запуск команд с Python-невалидным идентификатором, исправление багов.
iig
Я так и не понял, для каких задач необходим этот велосипед. os.environ — куда уж проще. subprocess для задач типа ls -l избыточен, но обёртка вокруг него 5 строк займет.
albartash Автор
Одна из идей класса ENV — убрать лишние манипуляции с преобразованиями типов, которые неизбежны при работе с os.environ (типичный пример — проверка переменной DEBUG в окружении). Меньше отвлекающих действий — чище код. Чище код — меньше потенциальных багов. Обе библиотеки, к слову, хорошо покрываются тестами (в версии 1.0.1 почти 100% покрытие выйдет).
А теперь представьте, что вы пишете скрипт автоматизации сборки или что-то в этом роде. Ладно, если «ls» вы замените на os.listdir(), ну а если понадобится «ls -R»? ПредлОжите девопсу разместить нечто вроде такого?
Ну и аналогично насчет проверок, если команда завершилась с ошибкой и прочие прелести нештатных ситуаций.
А если 90%+ скрипта все же будет завязано на использовании Shell, потому что это просто лаконичнее и удобнее, будет ли смысл писать os.listdir() вместо Shell.ls()?
Создавая эти библиотеки, я хотел дать возможность инженерам портировать (и одновременно стабилизировать) свои Bash-скрипты без необходимости глубоко изучать Python. Но есть и обратная сторона: Python-разработчикам, в случае необходимости, также придется меньше возиться с Bash.
Merifri
То лаконичнее и удобнее использовать чистый shell скрипт, а не Python.
И потом парсить
.output
в python? Красиво получится, да. А если вместо предложенного варианта не использовать однострочник с list comprehension, то можно написать читаемый, понятный код.Не использовать shell-скрипты — хорошо. Не использовать shell-скрипты там, где стоит их использовать — не очень хорошо.
albartash Автор
Вы вырвали фразу из контекста, причем потеряв по пути важную вторую часть.
Но добавлю по Вашему комментарию: Python может дать много возможностей, которые в Bash достигаются через боль, для манипуляций с данными + их передачей между командами Bash'а.
Да, можно, но в примере с циклом Вам нужно будет прочитать больше строк кода, выполняющих то же самое, что 1-2 строчки с использованием Shell.
Библиотека должна одинаково хорошо и легко работать с любой Bash-командой (ls — это просто пример, не более), вплоть до сложных конструкций и последовательностей команд. И она будет это делать в будущих релизах.
iig
Предложу писАть на понятном языке ;)
Или, если нужно писать на питоне — выучить какой-нибудь курс «питон за 10 уроков». Писать на питоне, и испытывать сложности с dictionary… Не нужно даже пытаться.
albartash Автор
Скорее всего, человек, которому поставлена задача смигрировать скрипты на Python, и так изучит его подобным образом. Но речь не о том, есть ли сложности со словарями или нет — вынуть переменную по ключу сможет любой хелловордщик. Если скрипт не предполагает интерпретацию значений из переменных окружения, то smart-env, вероятно, не даст ощутимой выгоды (кроме, может, как конфигурационный объект). Но с тем же успехом можно назвать Openstack «излишней сложностью», если рассматривать его в качестве площадки для поднятия вордпрессового блога на локалхосте.
Посмотрим на Ваш пример с точки зрения качества кода:
1. Название команды само по себе не является аргументом, который следует класть в args. В Вашем примере объект, вызывающий RunMeSimple(), частично осведомлен о внутренностях вызываемой функции — явный признак того, что код надо переписать.
2. Функция возвращает кортеж из четырех (!) значений. Серьёзно? Получается, вызывающему надо будет ещё и правильно распилить полученный кортеж по переменным и, не дай Бог, не поменять stdout и stderr местами и потом гадать, почему docker ps сыпет текст в поток ошибок.
3. Возможно, я и излишне придрался к конкретному примеру, но, опять-таки, пока проект — это пара скриптов и десяток функций, то можно и subprocess дергать, а вот когда это будет мамонт на овер9000 строк — наверняка вспомните опыт других, но уже станет страшно даже чихнуть не в том месте, чтобы все не рухнуло.
Идея python-shell — это упрощение кода, а не усложнение. Ваш пример преобразовался бы в куда более понятное
причем даже без необходимости создавать под это отдельную функцию (в которой на 95.5% гарантированы копипасты блоков кода из аналогичных «функций-вызывалок»).
Вспомнилась фраза: «Это Си, тут Солнце выкатывают и закатывают вручную». Безусловно, можно продолжать городить кучи комбинаций Popen()+communicate()+<и т.д.> (втайне мечтая переехать на «новенький» 3.5 с 2.7, чтобы суметь в subprocess.run) и кушать кактус. Но зачем?
Про JSON — это не портирование Bash, а новая возможность (хотя попытки ее применения, думаю, не новинка).
Про булевое значение: Вам никогда не приходилось интерпретировать значение переменной окружения на предмет того, True или не True?
А теперь, согласно Вашей теории, «усложнённый» вариант:
Надеюсь, я ответил на Ваш вопрос. :)
iig
Не припомню ;) А зачем хотеть уметь
albartash Автор
Жизнь — штука непредсказуемая. :)
Вообще, мне часто попадались веб-проекты, где через переменную окружения задавался режим отладки ( и это нормальное явление, имхо). Да и не только из окружения — раз источником переменных выступал key-value Консула. Получались те же Фаберже, только отдельным сервисом.
В smart-env стиль как раз четко очерчен: строковые константы двух видов — как в Python и как в классическом JSON (как самые потенциально ходовые).
dmitryredkin
А мне больше нравится подход shellpy.
Он обсуждался на хабре.
Что может быть естественнее для девопса, чем:
albartash Автор
Спасибо за ссылку, почитаю.
Да, shellpy делает код очень похожим на pure bash, и в этом одновременно плюс и минус такого подхода. Плюс в том, что код действительно можно переносить практически через Ctrl+C — Ctrl+V. Но в чем тогда резон вообще переносить всё на Python, если скрипты будут выглядеть, как «нечто похожее на Bash, но не Bash»? Я к тому, что читабельности это не добавит, а багов может даже прибавить (на тех операциях, которые в Shell делаются под капотом).
Вообще, библиотека python-shell исповедует всё-таки больше Python-овый стиль, поэтому и была сделана попытка реализовать удобное решение на базе ООП. Вспомните ORM-решения — с ними ведь обычно приятнее и быстрее работать, чем с raw-SQL, который еще и отличается в разных диалектах.