Как создавать и публиковать консольные приложения на Python


Подробное руководство по созданию и публикации консольных приложений на Python

Консольные приложения — это те, которые вы запускаете в терминале. Скорее всего, вы уже пытались их создать. Или, по крайней мере, думали об их создании.

Но создание консольного приложения — это одно, а публикация его в репозиторий с открытым кодом (например, PyPI) — совсем другое. Хотя ни первое, ни второе не является чем-то запредельно трудным.

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

Начало

Не так давно я занялся изучением уязвимостей open-source кода и понял, что хочу иметь в арсенале инструмент командной строки, который мог бы находить уязвимости напрямую из терминала. Уязвимости open-source кода обычно публикуются в открытых базах данных. Их можно найти на таких сайтах, как CVE, NVD, WhiteSource Vuln и т.д.

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

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

Для создания виртуальной среды можно воспользоваться командой python -m venv <path/name> (для Python 3) либо установить virtualenvwrapper с помощью pip install virtualenvwrapper и создать виртуальную среду virtualenv через mkvirtualenv -p /path/topython <path/name>.

После того, как виртуальная среда настроена и активирована, вы можете создать папку проекта и установить необходимые модули:

mkvirtualenv -p /usr/bin/python cvecli-env mkdir cvecli && cd cvecli mkdir cver && touch setup.py && touch README.md && touch cver/__init__.py && touch .gitignore pip install requests beautifulsoup4 lxml twine click pip freeze > requirements.txt

Как только все успешно запустилось, откройте свой проект в любом редакторе кода. Вы увидите похожую структуру:

Создание веб-скрейпера

Для того, чтобы искать и просматривать уязвимости на сайте CVE, потребуется веб-скрейпер. Он поможет нам собирать информацию об уязвимостях. Мы создаем скрейпер в Requests и BeautifulSoup. Вот что будет делать наш скрейпер:

1. искать уязвимости;

2. получать информацию об уязвимости по ее названию на CVE.

Теперь откроем папку cver и создадим в ней файл под названием cve_scraper. Затем пропишем его базовые настройки:

import requests from bs4 from BeautifulSoup SEARCH_URL = "https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=" CVE_URL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" def get_html(url): request = requests.get(url) if request.status_code == 200: return request.content else: raise Exception("Bad request") def search(s): pass def lookup_cve(name): pass

Поиск уязвимостей

Для поиска уязвимостей на CVE используется URL в следующем формате: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=<ключевое_слово>. Такой формат позволяет извлекать список уязвимостей, соответствующих ключевому слову.

Например, через URL можно получить список всех уязвимостей, связанных с Python:

Для извлечения данных открываем инструменты разработчика (Developer Console) и исследуем DOM-элемент с нужным представлением. Для этого кликните правой кнопкой по любому месту на странице и выберите “исследовать элемент” (inspect element) либо нажмите Ctrl + F12.

Если вы присмотритесь к DOM-структуре выше, то увидите, что результаты представлены в виде таблицы, а каждое значение указано в отдельной строке под таблицей. Такие данные можно запросто извлечь:

def search(s): url = f"{SEARCH_URL}{s}" results=[] html = get_html(url) soup = BeautifulSoup(html, "lxml") result_rows = soup.select("#TableWithRules table tr") for row in result_rows: _row = {} name = row.select_one("td a") description = row.select_one("td:nth-child(2)") if all([name, description]): _row["name"] = name.text _row["url"] = name.get("href") _row["description"] = description.text results.append(_row) return results

В коде выше мы:

1. отправляем запрос в SEARCH_URL с помощью Requests и получаем DOM-содержимое;

2. преобразуем DOM-содержимое в объекты BeautifulSoup. Это позволит нам выделять DOM-элементы с помощью CSS-селекторов, XPATH и других методов;

3. выделяем все tr под таблицей #TableWithRules. Выделяем первый столбец строки в качестве названия, а в качестве описания берем второй. Затем извлекаем текст.

Просмотр информации об уязвимостях

Чтобы просмотреть информацию об уязвимости, нужно взять ее CVE-ID и передать по этому адресу: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-ID.

Откройте инструменты разработчика и исследуйте DOM-структуру.

Такая структура чуть сложнее, поскольку в строках таблицы отсутствует ID или название класса. Поэтому нам нужно пройтись циклом по каждой строке и проверить, не является ли она подзаголовком. Если да, то следующий элемент берется в качестве содержимого-потомка. Каждый подзаголовок отображается в th, а его содержимое — в td.

def lookup_cve(name): url = f"{CVE_URL}{name}" html = get_html(url) soup = BeautifulSoup(html, "lxml") result_rows = soup.select("#GeneratedTable table tr") subtitle = "" description = "" raw_results = {} for row in result_rows: head = row.select_one("th") if head: subtitle = head.text else: body = row.select_one("td") description = body.text.strip().strip("\n") raw_results[subtitle.lower()] = description return raw_results

Готово! Мы успешно создали веб-скрейпер с CVE. Теперь добавим в него две функции (search и lookup_sve), которые будут искать уязвимости и получать информацию по ним через CVE-ID.

import requests from bs4 import BeautifulSoup SEARCH_URL = "https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=" CVE_URL = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" def get_html(url): request = requests.get(url) if request.status_code == 200: return request.content else: raise Exception("Bad request") def search(s): url = f"{SEARCH_URL}{s}" results=[] html = get_html(url) soup = BeautifulSoup(html, "lxml") result_rows = soup.select("#TableWithRules table tr") for row in result_rows: _row = {} name = row.select_one("td a") description = row.select_one("td:nth-child(2)") if all([name, description]): _row["name"] = name.text _row["url"] = name.get("href") _row["description"] = description.text results.append(_row) return results def lookup_cve(name): url = f"{CVE_URL}{name}" html = get_html(url) soup = BeautifulSoup(html, "lxml") result_rows = soup.select("#GeneratedTable table tr") subtitle = "" description = "" raw_results = {} for row in result_rows: head = row.select_one("th") if head: subtitle = head.text else: body = row.select_one("td") description = body.text.strip().strip("\n") raw_results[subtitle.lower()] = description return raw_results

Создание консольного приложения

Наш следующий шаг — структурирование и создание консольного приложения через библиотеку Click.

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

Click позволяет создавать интерфейсы командной строки любого уровня — от самых простых до навороченных (например, Heroku).

В нашем CLI мы реализуем две команды:

1. поиск уязвимости;

2. просмотр уязвимости.

В папке cver создаем файл под названием __main__.py и прописываем его базовые настройки:

import sys import click @click.group() @click.version_option("1.0.0") def main(): """A CVE Search and Lookup CLI""" print("Hye") pass @main.command() @click.argument('keyword', required=False) def search(**kwargs): """Search through CVE Database for vulnerabilities""" click.echo(kwargs) pass @main.command() @click.argument('name', required=False) def look_up(**kwargs): """Get vulnerability details using its CVE-ID on CVE Database""" click.echo(kwargs) pass if __name__ == '__main__': args = sys.argv if "--help" in args or len(args) == 1: print("CVE") main()

Поиск уязвимостей

Здесь мы будем импортировать функцию поиска search из веб-скрейпера и передавать ей аргумент keyword из командной строки. Таким образом, приложение будет искать уязвимости, совпадающие с ключевым словом:

from scraper import search as cve_search, lookup_cve @main.command() @click.argument('keyword', required=False) def search(**kwargs): """Search through CVE Database for vulnerabilities""" results = cve_search(kwargs.get("keyword")) for res in results: click.echo(f'{res["name"]} - {res["url"]} \n{res["description"]}')

Для запуска этой команды:

python cver/__main__.py search python

Просмотр уязвимости

Принцип тот же: используем lookup_cve из веб-скрейпера и передаем туда аргумент name из команды look_up.

@main.command() @click.argument('name', required=False) def look_up(**kwargs): """Get vulnerability details using its CVE-ID on CVE Database""" details = lookup_cve(kwargs.get("name")) click.echo(f'CVE-ID \n\n{details["cve-id"]}\n') click.echo(f'Description \n\n{details["description"]}\n') click.echo(f'References \n\n{details["references"]}\n') click.echo(f'Assigning CNA \n\n{details["assigning cna"]}\n') click.echo(f'Date Entry \n\n{details["date entry created"]}')

Для запуска этой команды:

python cver/__main__.py look-up CVE-2013–4238

Готово! Мы успешно создали инструмент командной строки по поиску с CVE.

Публикация консольного приложения на PyPI

После того, как мы создали консольное приложение и убедились в его работоспособности, можно опубликовать его на PyPI в публичном доступе.

PyPI — это хранилище приложений для пакетов Python. Там можно найти практически все пакеты, которые устанавливаются через pip. Для публикации пакета потребуется аккаунт на PyPI. Если он у вас уже есть, то смело читайте дальше.

Настройка пакета

Следующий шаг — это настройка Python-пакета с помощью setup.py. Если вы хотите, чтобы ваш пакет попал на PyPI, то нужно снабдить его базовым описанием. Эта информация указывается в файле setup.py.

Откройте setup.py в основной директории проекта и поместите в начало файла следующий код:

from setuptools import setup, find_packages from io import open from os import path import pathlib # Директория, в которой содержится этот файл HERE = pathlib.Path(__file__).parent # Текст README-файла README = (HERE / "README.md").read_text() # Автоматически собирает в requirements.txt все модули для install_requires, а также настраивает ссылки на зависимости with open(path.join(HERE, 'requirements.txt'), encoding='utf-8') as f: all_reqs = f.read().split('\n') install_requires = [x.strip() for x in all_reqs if ('git+' not in x) and ( not x.startswith('#')) and (not x.startswith('-'))] dependency_links = [x.strip().replace('git+', '') for x in all_reqs \ if 'git+' not in x]

В примере выше мы преобразовали содержимое файла README.md в одну строку для дальнейшего использования. Кроме того, мы перечислили все необходимые модули из requirements.txt и сгенерировали ссылки на их зависимости.

Ваш файл requirements.txt выглядит примерно так:

click requests beautifulsoup4 lxml twine

Теперь давайте рассмотрим параметры настроек:

setup ( name = 'cver', description = 'A simple commandline app for searching and looking up opensource vulnerabilities', version = '1.0.0', packages = find_packages(), # list of all packages install_requires = install_requires, python_requires='>=2.7', # any python greater than 2.7 entry_points=''' [console_scripts] cver=cver.__main__:main ''', author="Oyetoke Toby", keyword="cve, vuln, vulnerabilities, security, nvd", long_description=README, long_description_content_type="text/markdown", license='MIT', url='https://github.com/CITGuru/cver', download_url='https://github.com/CITGuru/cver/archive/1.0.0.tar.gz', dependency_links=dependency_links, author_email='[email protected]', classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", ] )

В коде выше мы добавили множество строк. Но подробнее обсудим только те из них, которые нужны для установки.

1. name — название пакета, которое появится на PyPI;

2. version — текущая версия пакета;

3. packages — пакеты и подпакеты с исходным кодом. В ходе установки мы пользуемся модулем find_packages. Он автоматически находит все подпакеты;

4. install_requires — используется для перечисления всех зависимостей или сторонних библиотек пакета. В cver мы пользуемся Requests, Beautifulsoup 4 и Click. Их нужно включить в требования к установке install_requires. Нам не нужно добавлять эту информацию вручную, поскольку она присутствует в requirements.txt;

5. entry_points — используется для создания скриптов, которые вызывают функцию внутри пакета. В данном случае мы создаем новый скрипт cver, который вызывает main() внутри файла cver/__main__.py. Наш основной элемент — это __main__.py, который вызывает функцию main() для запуска Click.

До того, как опубликовать пакет на PyPI или выложить в открытый доступ, необходимо снабдить его документацией. То, как будет выглядеть документация, целиком и полностью зависит от самого проекта. Это может быть как простой файл README.md, так и Readme.rst.

Пример хорошо оформленного README.md:

# CVER A simple commandline app for searching and looking up opensource vulnerabilities # Установка ## Через Pip ```bash $ pip install cver ``` ## Вручную ```bash $ git clone https://github.com/citguru/cevr $ cd cver $ python setup.py install ``` # Использование ```bash $ cver ``` ## Поиск `search <keyword>` ```bash $ cver search python ``` ## Просмотр `search <name>` ```bash $ cver look-up CVE-2020-2121 ```

Кроме того, не забудьте создать файл .gitignore:

# Байтовая компиляция / оптимизация / DLL-файлы __pycache__/ *.py[cod] *$py.class # C-расширения *.so # Сборка дистрибутива / пакета .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Обычно такие файлы пишутся Python-скриптом по шаблону # до того, как PyInstaller создаст exe. Это нужно для добавления в файл даты и прочей информации. *.manifest *.spec # Логи установщика pip-log.txt pip-delete-this-directory.txt # Модульные тесты / отчеты по покрытию htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Переводы *.mo *.pot # Всякое на Django: *.log local_settings.py db.sqlite3 # Всякое на Flask: instance/ .webassets-cache # Всякое на Scrapy: .scrapy # Sphinx-документация docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # Schedule-файл Celery Beat celerybeat-schedule # Проанализированные файлы SageMath *.sage.py # Окружения .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Настройки Spyder Project .spyderproject .spyproject # Настройки Rope Project .ropeproject # Документация mkdocs /site # mypy .mypy_cache/

Вот и все.

from setuptools import setup, find_packages from io import open from os import path import pathlib # Директория, в которой содержится этот файл HERE = pathlib.Path(__file__).parent # Текст README-файла README = (HERE / "README.md").read_text() # Автоматически собирает в requirements.txt все модули для install_requires with open(path.join(HERE, 'requirements.txt'), encoding='utf-8') as f: all_reqs = f.read().split('\n') install_requires = [x.strip() for x in all_reqs if ('git+' not in x) and ( not x.startswith('#')) and (not x.startswith('-'))] dependency_links = [x.strip().replace('git+', '') for x in all_reqs \ if 'git+' not in x] setup ( name = 'cver', description = 'A simple commandline app for searching and looking up opensource vulnerabilities', version = '1.0.0', packages = find_packages(), # list of all packages install_requires = install_requires, python_requires='>=2.7', # any python greater than 2.7 entry_points=''' [console_scripts] cver=cver.__main__:main ''', author="Oyetoke Toby", keyword="cve, vuln, vulnerabilities, security, nvd", long_description=README, long_description_content_type="text/markdown", license='MIT', url='https://github.com/CITGuru/cver', download_url='https://github.com/CITGuru/cver/archive/1.0.0.tar.gz', dependency_links=dependency_links, author_email='[email protected]', classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", ] )

Публикация на PyPI

Теперь ваш пакет готов к публикации на PyPI. Еще раз проверьте, есть ли у вас уже аккаунт на PyPI. Добавьте к нему тестовый аккаунт на тестовом сервере PyPI. Этот аккаунт нужен для тестирования пакетов перед публикацией их на рабочем сервере.

Загружать Python-пакеты на PyPI мы будем с помощью специального инструмента — Twine. По идее, вы уже установили его на одном из предыдущих этапов. Если нет, то это можно сделать через pip install twine.

Создание и локальное тестирование пакета на тестовом сервере

Python-пакеты, опубликованные на PyPI, не распространяются в виде «голого» кода. Они оборачиваются в дистрибутивы. Самыми распространенными форматами дистрибутивов в Python являются Wheels и Source Archives.

Wheels — это zip-архив с кодом и готовыми расширениями. Source Archives содержит исходный код и вспомогательные файлы, упакованные в tar-архив.

Для локального тестирования пакета выполните:

python setup.py install

Теперь мы можем использовать его как:

cver search python

Для проверки пакета на тестовом сервере PyPI нужно сгенерировать сборку для локального тестирования. При создании этой сборки сгенерируются архивы Wheels и Source Archives.

Создание сборки:

python setup.py sdist bdist_wheel

Код ниже сгенерирует два файла в директории dist:

cvecli/ │ └── dist/ ├── cver-1.0.0-py3-none-any.whl └── cver-1.0.0.tar.gz

Затем воспользуемся Twine. Теперь мы можем загрузить пакет на тестовый сервер PyPI:

twine upload — repository-url https://test.pypi.org/legacy/ dist/*

Затем у вас спросят логин и пароль.

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

Для установки из TestPyPI выполните следующую команду:

pip install -i https://test.pypi.org/simple/ cver==1.0.0

В тестовой среде вы можете проверить все команды и убедиться в их правильности перед публикацией пакета на рабочем сервере.

Протестировав все команды локально, переходите к публикации пакета на рабочем сервере:

twine upload dist/*

В процессе загрузки укажите свой логин и пароль. Вот и все!

Теперь пакет можно установить через:

pip install cver

Поздравляю! Ваш пакет был опубликован на PyPI. Просмотреть его можно здесь!

Заключение

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


Перевод статьи Oyetoke Tobi Emmanuel: How to Build And Publish Command-Line Applications With Python


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.