Хочешь понять - объясни другому
(с) Джейсон Стэтхем
Предисловие
Разговор в этой статье пойдет о том, как достать пакеты Python для оффлайн установки на разных платформах и разных версиях Python. Возможно я плохо искал, но на просторах интернета я не смог найти достаточное количество статей на русском языке, которые бы подробно объясняли, как производить загрузку пакетов и дальнейшую их доставку на машины с разными платформами и версиями языка.
Периодически возникают ситуации или необходимость запуска Python на машинах, которые не имеют доступа к интернету. Зачастую такая необходимость может возникнуть по двум причинам:
в случае если машина находится территориально там, где доступ к сети невозможен, в удаленных и малонаселенных районах;
когда по тем или иным причинам целевая машина изолирована от внешних сетей (требования безопасности и т.д.).
Тогда старательный разработчик идёт туда где есть интернет, на этой машине производит загрузку необходимых пакетов, записывает их на носитель и несёт всё это дело обратно для установки на целевой платформе. В принципе, звучит не так страшно. Давайте потянем за ниточку и быстро решим все проблемы ...
И в самом деле, будет всё просто, в случае если компьютер на котором вы загружаете пакеты будет иметь аналогичную операционную систему и версию Python, тем что и на целевой машине. Также проблем не будет если вы загружаете один какой-нибудь маленький пакет, у которого минимальное количество зависимостей.
Но мир начинает играть совсем другими красками, когда у вас между целевой машиной и той, на которой вы производите загрузку имеются отличия и в операционной системе и в версии Python, а набор зависимостей между пакетами начинает зашкаливать.
В статье я постараюсь разобрать три основных варианта:
Операционная система и версия Python на целевой машине и машине для загрузки пакетов совпадают
Версия Python на целевой машине и машине для загрузки совпадают но операционные системы разные
На целевой машине и машине для загрузки пакетов отличаются и операционная система и версия Python
Немного теории
И прежде чем приступить к разбору этих вариантов, сначала я хочу описать основные параметры с которыми нам придётся столкнуться в статье.
Алгоритм работы загрузчика pip можно посмотреть на странице документации. Общий очень упрощенный синтаксис такой:
pip download [options] package_name
основные опции, с которыми встретимся далее:
--requirement, -r - использовать список зависимостей
--only-binary - не используйте пакеты с исходным кодом.
--dest, -d - адрес директории для загрузки пакетов
--platform - версия операционной системы
--python-version - версия python
--implementation - реализация python
Что для нас будет важно. Важно будет понять, как обозначить версию операционной системы, как обозначить версию Python, как обозначить вид пакета, который нам необходимо будет загрузить. Дальше я постараюсь привести пример таблицв в которой будут указаны параметры
Код параметра | Значение параметра |
Операционные системы [--platform] | |
win_amd64 | Любой windows 64 разряда |
win32 | Любой windows 32 разряда |
muslinux_1_1_x86_64 | Alpine linux 32 64 разряда |
muslinux_1_1_aarchh64 | Alpine linux 64 разряда |
manylinux2014_x86_64 | Основной дистрибутив Linux 32 64 разряда |
manylinux2014_aarch64 | Основной дистрибутив Linux 64 разряда |
manylinux_2_17_x86_64 | псевдоним для manylinux2014_x86_64 |
manylinux_2_17_aarch64 | псевдоним для manylinux2014_aarch64 |
macosx_11_0_arm64 | macOS 11 64 разряда |
macosx_10_9_x86_64 | macOS 10 32 64 разряда |
Реализация Python [--implementation] | |
py | Используем ‘py’ когда пакет не зависит от реализации |
cp | Cpython |
ip | Iron python |
jp | Jpython |
pp | PyPy |
Версии Python [--python-version] | |
312 | Python 3.12 используйте только для мажорно-минорной версии |
38 | Python 3.8 используйте только для мажорно-минорной версии |
37 | Python 3.7.0 используйте только для мажорно-минорной версии |
3.7 | Python 3.7.0 |
3.7.3 | Python 3.7.3 |
Вот здесь про реализацию python.
Ещё давайте посмотрим какие предельные версии Python могут быть использованы операционной системой Windows.
Windows | |
Upper Windows 7 | Python 3.12, максимальный на дату статьи |
Windows 7 | Python 3.8 |
Windows XP | Python 3.4.4 |
Основные вводные мы разобрали. Теперь давайте перейдём непосредственно к задачам.
Мы с вами можем попробовать загрузить какой-нибудь простой пакет с минимальным количеством зависимостей и в данном случае конечно же проблемы возникают реже.
Если же мы с вами попытаемся загрузить пакет, у которого большое количество зависимостей - вот здесь-то мы столкнёмся со всем многообразием возможных проблем. Поэтому в качестве эксперимента будем использовать пакет label-studio. У него большое количество различных зависимостей. В том числе, там присутствуют необычные пакеты, которые просто так не загрузить из командной строки.
1. Вариант - Всё одинаково
Вероятно, это самая простая задача. В данном случае достаточно на машине, в которой есть выход в Интернет ввести код загрузки пакета:
mkdir load_packages
pip download -d load_packages label-studio
При этом, в коде загрузки у нас будут отсутствовать какие-то дополнительные параметры, кроме адреса куда скачивать файлы. Конечно, мы можем их ввести, но необходимости в данном случае нет.
Пакеты загрузятся в целевую папку. Этоти пакеты в дальнейшем можно будет использовать для установки на машине в которой выход в Интернет отсутствует.
Для установки откройте виртуальное окружение, где вы хотите установить пакет, укажите в параметрах ссылку на директорию куда перенесли ранее скачанные файлы и название пакета установки.
pip install --no-index --find-links /dir/where/your/package/lives label-studio
Что в данном случае радует, то что мы получим пакет в полном объёме со всеми зависимостями необходимыми для его установки и нам не нужно будет делать каких-то дополнительных действий. Мы получим весь объём необходимых данных всего лишь одной командой - это просто чудо!
2. Вариант - Отличаются только операционной системы
Здесь мы как обычно берём бубен и начинаем плясать. Давайте представим что у нас есть две машины:
целевая машина на которой нам нужно установить пакеты это Windows 7 x 64 разряда и Python 3.8
машина на которой у нас с вами есть выход в Интернет - это Debian 11 core 6.7.10 x 64 разряда и Python 3.8
Если вы попытаетесь повторить предыдущий вариант загрузки, то вы конечно же получите и загрузите пакеты. Только эти пакеты будут доступны для установки только на Linux. Давайте посмотрим что получилось
mkdir load_packages
pip download -d load_packages label-studio
Обратите внимание, что часть пакетов имеют версию платформы "manylinux2014_x86_64" - для Windows такое не подойдёт.
Collecting pydantic<=1.11.0,>=1.7.3
Downloading pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)
|████████████████████████████████| 3.1 MB 9.6 MB/s
Collecting pyyaml>=6.0.0
Downloading PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (705 kB)
Тогда мы приступаем к вводу дополнительных параметров отвечающих за операционную систему. Как мы уже видели в таблице приведённой выше за это отвечает параметр --platform и в данном случае для того чтобы нам загрузить пакеты Windows мы будем использовать значение win_amd64. Отлично код вроде бы ввели попробуем теперь запустить.
pip download --platform win_amd64 label-studio
Итак мы встретились с первой ошибкой, что просто так параметр --platform не работает от нас требуется ввести ещё что-то.
ERROR: When restricting platform and interpreter constraints using --python-version, --platform, --abi, or --implementation, either --no-deps must be set, or --only-binary=:all: must be set and --no-binary must not be set (or must be set to :none:).
Давайте послушаемся программу и добавим новый параметр --only-binary
pip download --only-binary=:all: --platform win_amd64 label-studio
Ну не может же быть все так просто:
ERROR: Cannot install label-studio==0.4.1, label-studio==0.4.2, label-studio==0.4.3, label-studio==0.4.4, label-studio==0.4.4.post1, label-studio==0.4.4.post2, label-studio==0.4.5, label-studio==0.4.6, label-studio==0.4.6.post1, label-studio==0.4.6.post2, label-studio==0.4.7, label-studio==0.4.8, label-studio==0.5.0, label-studio==0.5.1, label-studio==0.6.0, label-studio==0.6.1, label-studio==0.7.0, label-studio==0.7.1, label-studio==0.7.2, label-studio==0.7.3, label-studio==0.7.4, label-studio==0.7.4.post0, label-studio==0.7.4.post1, label-studio==0.7.5.post1, label-studio==0.7.5.post2, label-studio==0.8.0, label-studio==0.8.0.post0, label-studio==0.8.1, label-studio==0.8.1.post0, label-studio==0.8.2, label-studio==0.8.2.post0, label-studio==1.0.0, label-studio==1.0.0.post0, label-studio==1.0.0.post1, label-studio==1.0.0.post2, label-studio==1.0.0.post3, label-studio==1.0.1, label-studio==1.0.2, label-studio==1.0.2.post0, label-studio==1.1.0, label-studio==1.1.1, label-studio==1.10.0, label-studio==1.10.0.post0, label-studio==1.10.1, label-studio==1.11.0, label-studio==1.2, label-studio==1.3, label-studio==1.3.post0, label-studio==1.3.post1, label-studio==1.4, label-studio==1.4.1.post1, label-studio==1.5.0, label-studio==1.5.0.post0, label-studio==1.6.0, label-studio==1.7.0, label-studio==1.7.1, label-studio==1.7.2, label-studio==1.7.3, label-studio==1.8.0, label-studio==1.8.1, label-studio==1.8.2, label-studio==1.8.2.post0, label-studio==1.8.2.post1, label-studio==1.9.0, label-studio==1.9.1, label-studio==1.9.1.post0, label-studio==1.9.2 and label-studio==1.9.2.post0 because these package versions have conflicting dependencies.
Какая-то проблема с зависимостями. Давайте укажем на всякий случай нашу версию Python
pip download --only-binary=:all: --python-version 38 --platform win_amd64 -d import/ label-studio==1.11.0
Что-то новенькое
ERROR: Could not find a version that satisfies the requirement drf-flex-fields==0.9.5 (from label-studio) (from versions: none)
ERROR: No matching distribution found for drf-flex-fields==0.9.5
Думали что всё получится, а тут хлоп и опять ошибка. Оказывается что из списка зависимости не все пакеты могут быть доставлены нам в виде бинарных файлов, либо не удовлетворяет требованиям нашей платформы.
Например пакет drf-flex-fields вот скрин с его странички.
Здесь доступно для скачивания только исходники в архиве.
Давайте попробуем использовать параметр --prefer-binary
ERROR: When restricting platform and interpreter constraints using --python-version, --platform, --abi, or --implementation, either --no-deps must be set, or --only-binary=:all: must be set and --no-binary must not be set (or must be set to :none:).
Ух ты Замкнутый круг! Уже знакомая ошибка - данный параметр невозможно использовать в случае если вы указываете платформу для которой предполагается установка пакетов. Вот что говорит StackOverflow.
В данном случае я сделал для себя вывод (возможно абсолютно неверный) -что мы не можем при указании платформы и версии языка закачивать не бинарные файлы.
В какой-то момент я подумал, что это действительно безвыходная ситуация. И что в момент загрузки пакетов мы рано или поздно просто остановимся на каком-то одном из пакетов из списка зависимости и дальше не пойдём. При этом полный список зависимостей нам пока что будет недоступен.
Не сдаваться - наше всё
Мне показалось правильным распилить эту задачу на несколько вариантов
сначала попробовать закачать те пакеты, которые имеют бинарный вариант с помощью стандартной команды pip download .
далее пакеты, которые не загрузились и вышла ошибка попробовать загрузить либо вручную, либо сделать какой-то автоматический обработчик, который поможет мне их получить.
Теперь возникает вопрос о полном списке зависимостей пакетов, которые мне необходимы. Где же мне его взять. Вероятно существует какие-то умные и действительно правильные способы получение полного списка зависимостей, но я до него пока что ещё не дошёл - я пошёл простым прямым квадратным перпендикулярным способом.
Сейчас у нас ситуация, когда у нас отличаются только версии операционной системы при этом версия Python у нас одна и та же. В нашем случае - 3.8 и на Windows и на Linux. Что же, теперь я попробую установить целевой пакет Label Studio на машину, в которой есть выход в Интернет. Когда все пакеты установились я использую команду
pip freeze > requirements.txt
с помощью неё я получу список всех установленных пакетов с указанием необходимых версий. Все данные запишу в файл requirements.txt.
Таким образом у меня получилось добыть список всех зависимостей. Здорово! Ну что же дальше. Ведь если мы запускаем код загрузки, то в момент когда терминал получает ошибку - он останавливается. Давайте попробуем написать некий bash скрипт, который простым перебором будет пытаться с помощью команды pip download закачивать поочередно каждый пакет из списка зависимостей. А все пакеты, по которым у нас с вами произошла ошибка, он будет записывать в текстовый файл error.txt
#!/bin/bash
mkdir packages
cat requirements.txt | while read line
do
error_output=$(pip download -d ./packages --platform win_amd64 --python-version 38 --only-binary=:all: --implementation cp "$line" 2>&1)
if [[ $error_output =~ "ERROR" ]]; then
echo "${line}"
echo "${line}" >> "error.txt"
fi
done
Отлично! У нас получилось загрузить все пакеты, которые имеют бинарный вид и получить список всех пакетов, по которым бинарный вид отсутствует. При этом мы знаем не только наименование пакета, но что нас больше всего может радовать ещё и его версию.
Теперь чтобы получить недостающие пакеты, в принципе, мы можем пойти простым человеческим путём - зайти на сайт pip.org, найти соответствующий пакет, найти его версию нажать кнопку загрузить. Ну и в принципе, таким образом мы его получим.
С учетом того, что таких ошибочных пакетов у нас небольшое количество - в принципе много времени это не займёт. Но мне стало интересно - а могу ли я автоматизировать и этот процесс. Как оказалось не всё потеряно. Давайте посмотрим с вами на структуру адреса страницы загрузкой пакета drf-flex-fields необходимой версии.
https://pypi.org/project/drf-flex-fields/0.9.5/#files
Мы видим что в адресе страницы присутствуют и наименование пакета и его версия. Таким образом, вероятно, мы сможем автоматически попасть и сформировать себе адреса страниц на которых присутствуют необходимые нам пакеты, а затем каким-то образом спарсить оттуда необходимый нам данные. Чтобы не углубляться в теорию ниже я приведу код Python скрипта который позволит нам это сделать. Назовем его - load_packages.py
import requests
from bs4 import BeautifulSoup
from tqdm import tqdm
# читаем файл с ошибками, формируем список пакетов для загрузки
with open('error.txt', 'r') as err_file:
packages = err_file.readlines()
# функции отправляет запрос на страницу загрузки пакета
def get_request(url: str) -> requests.Response:
try:
return requests.get(url)
except:
print('Request error')
return False
def load_package(package: str) -> None:
# извлекаем необходимые данные о пакете
pack_name, version = package.replace('\n', '').split('==')
# готовим адрес страницы с файлами загрузки и получаем ответ на запрос
url = f'https://pypi.org/project/{pack_name}/{version}/#files'
r = get_request(url)
# убеждаемся, что запрос не пустой
if not r:
return False
# создаём объект bs для дальнейшей обработки
soup = BeautifulSoup(r.text)
# ищем блок div с характерным классом
for element in soup.find_all("div", {"class": "card file__card"}):
# ищем tag <a href...
for tag in element:
if tag.name == 'a':
# пропускаем ссылку на загрузки хешей
if tag.text == 'view hashes':
continue
# очищаем текст от лишних знаков
text = tag.text.replace(' ', '').replace('\n', '')
# преобразуем текст а список
body = text.split('.')
# последний элемент списка это разрешение файла выбираем .gz
if body[-1] != 'gz':
continue
# получаем адрес для скачивания
url = tag.attrs['href']
try:
packet = requests.get(url)
except:
print('Load request error')
continue
# полученные байты записываем в файл
with open(f'packages/{text}', 'wb') as handle:
for data in tqdm(packet.iter_content()):
handle.write(data)
msg = f'Load package {pack_name}, ver. {version}, to file {text}'
print(msg)
for package in packages:
if package == '\n':
continue
print(f'Start load package {package}')
load_package(package)
А теперь я немножко объясню что здесь происходит. Для того чтобы, в будущем в случае каких-то изменений на сайте pip.org или ещё каких-то вещей и ошибок мы могли бы скорректировать код этого скрипта. Основная идея заключается в том, чтобы использую библиотеку request получать данные со страницы загрузки файлов. Далее с помощью пакета Beautifulsoup4 получить информацию из необходимого блока div b вычленить ссылку с пакетом архива, который имеет разрешение ".gz", скачать данные и записать файл из бинарных данных к себе в целевую папку.
Вот так выглядит страница для загрузки необходимого пакета. Так видит эту страницу человек.
А так эту страницу видит машина
В примере я привёл только кусок необходимого блока для загрузки. Здесь мы видим атрибуты блока div по которому мы сможем отфильтровать необходимый нам блок и дальше мы видим, что помимо самих пакетов ещё доступны для скачивания хэши. Соответственно в коде мы это отсеиваем. Из доступных к скачиванию пакетов мы выбираем тот, который имеет архив ".gz". Собственно Вот и вся логика работы скрипта.
Далее мы немножечко улучшим наш Баш скрипт для того, чтобы нам автоматически развернуть виртуальную среду, в которую мы установим необходимые пакеты resquest и Beautifulsoup4. Запустим саму виртуальную среду и запустим наш python скрипт.
#!/bin/bash
mkdir packages
cat requirements.txt | while read line
do
error_output=$(pip download -d ./packages --platform win_amd64 --python-version 38 --implementation cp --only-binary=:all: --implementation cp "$line" 2>&1)
if [[ $error_output =~ "ERROR" ]]; then
echo "${line}"
echo "${line}" >> "error.txt"
fi
done
python -m venv venv
source venv/bin/activate
pip install requests
pip install beautifulsoup4
pip install tqdm
python load_packages.py
В результате запуска Bash скрипта мы выполним два действия:
сначала закачается доступные файлы в бинарном виде запишутся ошибки по незагруженным файлам
далее python script прочитает наименование пакетов с ошибками и попытается загрузить их непосредственно со страницы pip.org
На примере пакета label-studio Windows 7 + python 3.8 данная задача была решена.
3. Вариант - Отличается всё
Давайте теперь задачу усложним ещё на один порядок. Представим что у нас:
целевая машина на которую необходимо загрузить пакеты - это Windows 7 + Python 3.8
машина на которой есть интернет Astra Linux common edition + Python 3.5
В данном случае у нас возникает проблема с получением полного списка зависимостей. Если мы просто поставим с вами пакет Label Studio на Python 3.5, то мы с вами получим зависимости только для Python 3.5. А в нашем конкретном случае мы вообще ничего не получим - потому что label-studio на Python 3.5, в принципе не устанавливается.
Что же тогда делать. Самый простой вариант в данном случае - это попробовать установить на вашу машину c интернетом pyenv (статья Хабр, GIT исходник).
С помощью него выбрать необходимую версию Python и далее пройти по шагам, которые представлены в пункте два.
Обратите внимание, что на старых версиях ОС необходимо указывать python3 или pip3 для вызова Python 3.хх
#!/bin/bash
mkdir packages
cat requirements.txt | while read line
do
error_output=$(pip3 download -d ./packages --platform win_amd64 --python-version 38 --implementation cp --only-binary=:all: --implementation cp "$line" 2>&1)
if [[ $error_output =~ "ERROR" ]]; then
echo "${line}"
echo "${line}" >> "error.txt"
fi
done
python3 -m venv venv
source venv/bin/activate
pip install requests
pip install beautifulsoup4
pip install tqdm
python load_packages.py
Вполне возможно, может так случиться, что pyenv не позволит установить более старшие версии языка на слабой машине или могут возникнуть какие-то иные проблемы - для этих проблем я решение, к сожалению, не нашёл.
В данном случае, вероятно, единственный выход - иметь машину, которая имеет доступ к интернету с возможностью установки на ней максимальных версий Python. Тогда pyenv вам однозначно позволит работать с версиями Python на понижение.
Или развивать легкоатлетические навыки и бегать с пакетами приложений из кабинета в кабинет. Сначала туда ->
А потом обратно <-
Вывод
Не претендую на мастерство - я не профессиональный разработчик и к этой статье я шел долго. Несколько лет бегал, как мужик на картинке выше. Когда же встретился с label-studio - я устал. Попробовал автоматизировать свою работу. И на фоне дефицита подобной информации на русском решил рассказать о своих приключениях
Будет замечательно, если настоящие профессионалы укажут на мои ошибки и подскажут верный путь. Заранее спасибо!
Ещё есть мысль в будущем, каким-то образом постараться добывать список зависимостей автоматически - для каждого пакета, без необходимой установки на текущую машину с интернетом.
Но мне почему-то кажется, что это достаточно сложная задача, с которой пытались справиться большое количество разработчиков - в результате которых, появились такие замечательные решения как poetry, uv и подобные им пакеты управления зависимостями
К картинкам приложили руки моя доча, Шедеврум и Midjourney