PIP — Загрузка пакетов python для офлайн установки

от автора

Хочешь понять — объясни другому

(с) Джейсон Стэтхем

Предисловие

Разговор в этой статье пойдет о том, как достать пакеты Python для оффлайн установки на разных платформах и разных версиях Python. Возможно я плохо искал, но на просторах интернета я не смог найти достаточное количество статей на русском языке, которые бы подробно объясняли, как производить загрузку пакетов и дальнейшую их доставку на машины с разными платформами и версиями языка. 

Периодически возникают ситуации или необходимость запуска Python на машинах, которые не имеют доступа к интернету. Зачастую такая необходимость может возникнуть по двум причинам:

  • в случае если машина находится территориально там, где доступ к сети невозможен, в удаленных и малонаселенных районах;

  • когда по тем или иным причинам целевая машина изолирована от внешних сетей (требования безопасности и т.д.).

Тогда старательный разработчик идёт туда где есть интернет, на этой машине производит загрузку необходимых пакетов, записывает их на носитель и несёт всё это дело обратно для установки на целевой платформе. В принципе, звучит не так страшно. Давайте потянем за ниточку и быстро решим все проблемы …

Дерни - проблем точно не будет

Дерни — проблем точно не будет

И в самом деле, будет всё просто, в случае если компьютер на котором вы загружаете пакеты будет иметь аналогичную операционную систему и версию Python, тем что и на целевой машине. Также проблем не будет если вы загружаете один какой-нибудь маленький пакет, у которого минимальное количество зависимостей.

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

В статье я постараюсь разобрать три основных варианта:

  1. Операционная система и версия Python на целевой машине и машине для загрузки пакетов совпадают

  2. Версия Python на целевой машине и машине для загрузки совпадают но операционные системы разные

  3. На целевой машине и машине для загрузки пакетов отличаются и операционная система и версия 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


ссылка на оригинал статьи https://habr.com/ru/articles/806691/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *