Pull to refresh

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

Level of difficultyEasy
Reading time13 min
Views15K

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

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

Предисловие

Разговор в этой статье пойдет о том, как достать пакеты 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

Tags:
Hubs:
Total votes 16: ↑15 and ↓1+20
Comments9

Articles