initial commit
This commit is contained in:
78
pythonapp/Decider/ABS.py
Normal file
78
pythonapp/Decider/ABS.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Set, Dict
|
||||
|
||||
from pythonapp.Instance.ABS import Instance
|
||||
from pythonapp.Libs.pip_api import pip_api
|
||||
from pythonapp.Decider.Loader import Loader
|
||||
from .misc import *
|
||||
|
||||
|
||||
class ConflictSeverity(enum.Enum):
|
||||
FATAL = "Fatal"
|
||||
ERROR = "Error"
|
||||
WARNING = "Warning"
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConflictError:
|
||||
"""Класс для хранения информации о конфликте зависимостей."""
|
||||
conflicting_packages: Set[str]
|
||||
sources: Dict[str, str] # package -> source (installed/requested)
|
||||
severity: ConflictSeverity
|
||||
error_message: str
|
||||
resolution_action: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
# Автоматически определяем источники, если не предоставлены
|
||||
if not self.sources:
|
||||
self.sources = {pkg: "unknown" for pkg in self.conflicting_packages}
|
||||
|
||||
|
||||
|
||||
class DeciderState(enum.Enum):
|
||||
SUCCESS = 0
|
||||
WARN = 30
|
||||
ERROR = 50
|
||||
FATAL = 100
|
||||
|
||||
|
||||
|
||||
class Decider:
|
||||
@classmethod
|
||||
def decide(cls, instance: Instance,
|
||||
packages: list[RequirementInfo] | Path,
|
||||
pip_path: str,
|
||||
index_url: Optional[str] = None,
|
||||
extra_index_urls: Optional[list[str]] = None
|
||||
) -> tuple[list[RequirementInfo], list[ConflictError], DeciderState]:
|
||||
raise NotImplemented
|
||||
|
||||
@classmethod
|
||||
def load(cls, instance: Instance, packages: list[RequirementInfo] | Path, manual=False):
|
||||
if isinstance(packages, Path):
|
||||
opt, req = requirements_separator(packages)
|
||||
packages = requirements_converter(opt, req)
|
||||
p = instance.path
|
||||
c = instance.config
|
||||
installed = Loader.load_existent_requirements(
|
||||
str(p / c.manual_requirements_path), str(p / c.requirements_dir),
|
||||
str(p / c.pinned_packages_path), str(p / c.excluded_packages_path))
|
||||
if not manual:
|
||||
packages = Loader.filter_reqs(packages, str(p / c.pinned_packages_path), str(p / c.excluded_packages_path))
|
||||
installed.extend(packages)
|
||||
return installed, packages
|
||||
|
||||
class SimpleDecider(Decider):
|
||||
@classmethod
|
||||
def decide(cls, instance: Instance,
|
||||
packages: list[RequirementInfo] | Path,
|
||||
pip_path: str,
|
||||
index_url: Optional[str] = None,
|
||||
extra_index_urls: Optional[list[str]] = None,
|
||||
manual=False) -> tuple[list[RequirementInfo] | None, list[ConflictError], DeciderState]:
|
||||
all_packages, packages = cls.load(instance, packages, manual)
|
||||
result = pip_api.run_pip_install(pip_path, [p.requirement_str for p in all_packages], index_url, extra_index_urls, dry_run=True)
|
||||
if result.exit_code != 0: return None, [], DeciderState.FATAL
|
||||
return packages, [], DeciderState.SUCCESS
|
||||
190
pythonapp/Decider/Loader.py
Normal file
190
pythonapp/Decider/Loader.py
Normal file
@@ -0,0 +1,190 @@
|
||||
|
||||
import os
|
||||
from typing import Literal
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequirementInfo:
|
||||
"""
|
||||
Класс для представления информации о требовании к пакету.
|
||||
|
||||
Attributes:
|
||||
requirement_str (str):
|
||||
Полная строка требования как в файле requirements.txt.
|
||||
Пример: "requests>=2.25.0" или "numpy==1.21.0".
|
||||
|
||||
package_name (str):
|
||||
Имя пакета без указания версии.
|
||||
Пример: "requests" или "numpy".
|
||||
|
||||
significance_level (Literal["manual", "required", "optional"]):
|
||||
Уровень значимости требования по убыванию важности:
|
||||
- "manual": пакеты, установленные вручную
|
||||
- "required": обязательные зависимости
|
||||
- "optional": опциональные зависимости
|
||||
|
||||
source_file (str):
|
||||
Имя файла требований, из которого было извлечено это требование.
|
||||
Пример: "core.req" или "dev.opt".
|
||||
"""
|
||||
requirement_str: str
|
||||
package_name: str
|
||||
significance_level: Literal["manual", "required", "optional"]
|
||||
source_file: str
|
||||
|
||||
@classmethod
|
||||
def from_requirement_string(cls, requirement_str: str,
|
||||
significance_level: Literal["manual", "required", "optional"],
|
||||
source_file: str) -> "RequirementInfo | None":
|
||||
"""
|
||||
Создает экземпляр RequirementInfo из строки требования.
|
||||
|
||||
Args:
|
||||
requirement_str: Полная строка требования
|
||||
significance_level: Уровень значимости требования
|
||||
source_file: Имя файла-источника требования
|
||||
|
||||
Returns:
|
||||
Экземпляр RequirementInfo с извлеченным именем пакета
|
||||
"""
|
||||
# Извлекаем имя пакета (все до первого символа сравнения или пробела)
|
||||
if requirement_str == '': return None
|
||||
package_name = requirement_str.split()[0].strip()
|
||||
for comparison_op in ["==", ">=", "<=", ">", "<", "!=", "~="]:
|
||||
if comparison_op in package_name:
|
||||
package_name = package_name.split(comparison_op)[0].strip()
|
||||
break
|
||||
|
||||
return cls(
|
||||
requirement_str=requirement_str,
|
||||
package_name=package_name,
|
||||
significance_level=significance_level,
|
||||
source_file=Path(source_file).name # Сохраняем только имя файла, не полный путь
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Строковое представление требования."""
|
||||
return self.requirement_str
|
||||
|
||||
|
||||
class Loader:
|
||||
@classmethod
|
||||
def _load_req_file(cls, path: str | Path, significance_level: Literal["manual", "required", "optional"], key: str) -> list[RequirementInfo]:
|
||||
with open(str(path), 'r') as f:
|
||||
res = []
|
||||
# Читаем строки, игнорируем пустые и комментарии
|
||||
packages = [
|
||||
line.strip() for line in f
|
||||
if line.strip() and not line.strip().startswith('#')
|
||||
]
|
||||
for pkg in packages:
|
||||
res.append(RequirementInfo.from_requirement_string(pkg, significance_level, key))
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def filter_reqs(cls, req_list: list[RequirementInfo], pinned: str, excluded: str):
|
||||
# Загружаем исключения из pinned файла
|
||||
if pinned and os.path.exists(pinned):
|
||||
pinned_packages = list(cls._load_req_file(pinned, "required", 'pinned'))
|
||||
|
||||
# Удаляем исключенные пакеты
|
||||
criteria = [c.package_name for c in pinned_packages]
|
||||
req_list = [r for r in req_list if r.package_name not in criteria]
|
||||
|
||||
# Загружаем исключения из файла excluded (удаляем точные совпадения)
|
||||
if excluded and os.path.exists(excluded):
|
||||
excluded_packages = list(cls._load_req_file(excluded, "required", 'excluded'))
|
||||
|
||||
# Удаляем исключенные пакеты
|
||||
criteria = [c.requirement_str for c in excluded_packages]
|
||||
req_list = [r for r in req_list if r.requirement_str not in criteria]
|
||||
|
||||
return req_list
|
||||
|
||||
@classmethod
|
||||
def load_existent_requirements(cls,
|
||||
manual_reqs: str,
|
||||
reqs_dir: str,
|
||||
pinned: str,
|
||||
excluded: str
|
||||
) -> list[RequirementInfo]:
|
||||
"""
|
||||
Загружает и обрабатывает требования из файлов.
|
||||
|
||||
1. Загружает все файлы из self.config.requirements_dir с расширением .req, создает из их строк
|
||||
2. Загружает все файлы из self.config.requirements_dir с расширением .opt в списки словаря opt по ключам, соответствующим именам файлов без расширения.
|
||||
3. Удаляет из req и opt все пакеты из файла pinned (версия пакета не важна)
|
||||
4. Удаляет из req и opt все пакеты из файла excluded (версия пакета должна полностью совпадать. Например если в требованиях есть torch>=2.7.0 удаляем именно такие строки)
|
||||
5. Загружает файл manual_req в один из списков словаря с ключом "_manual"
|
||||
6. Возвращает значения
|
||||
Args:
|
||||
manual_reqs: Путь к файлу списка пакетов, установленных вручную
|
||||
reqs_dir: Путь к директории со списками пакетов
|
||||
pinned: Путь к файлу с исключениями установки
|
||||
excluded: Путь к файлу с исключениями проверки
|
||||
|
||||
Returns:
|
||||
Два словаря списков - req и opt
|
||||
"""
|
||||
|
||||
req_list: list[RequirementInfo] = []
|
||||
|
||||
# Загружаем .req файлы
|
||||
for req_file in Path(reqs_dir).glob("*.req"):
|
||||
req_list.extend(cls._load_req_file(req_file, "required", req_file.stem))
|
||||
|
||||
# Загружаем .opt файлы
|
||||
for opt_file in Path(reqs_dir).glob("*.opt"):
|
||||
req_list.extend(cls._load_req_file(opt_file, "optional", opt_file.stem))
|
||||
|
||||
req_list = cls.filter_reqs(req_list, pinned, excluded)
|
||||
|
||||
# Загружаем manual_reqs файл
|
||||
if manual_reqs and os.path.exists(manual_reqs):
|
||||
req_list.extend(cls._load_req_file(manual_reqs, "manual", "manual"))
|
||||
|
||||
# Возвращаем результаты
|
||||
return req_list
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Теперь давай разработаем статический класс Resolver и его метод check integrity. Приведенного ниже алгоритма есть проблема, что он может игнорировать некоторые опциональное зависимости, но не сообщает об этом коду, который далее будет устанавливать пакеты и не удаляет их из self.config.requirements_dir.
|
||||
ВНИМАНИЕ!!! Этот код не должен менять сами файлы. Он может только удалять пакеты из своих временных списков, но не из файлов. Также он должен быть полностью инкапсулирован от класса Instance. Все необходимые для его работы данные должны быть переданы непосредственно при вызове метода check_integrity. Данные класса instance, ровно, как и принадлежащие ему файлы не должны быть изменены в результате работы. Все что делает этот код - получает исходные данные и на их основе принимает решение и возвращает ответ, никак не трогая сами исходные данные.
|
||||
В соответствии с этим классом должен быть изменен метод check_integrity класса Instance, но ничего более.
|
||||
Метод всегда должен возвращать три переменных: список оставшихся пакетов, список ошибок на req, список ошибок на opt. Переделай check integrity класса instance с учетом этого.
|
||||
Также pip dry run должен проводиться в venv окружении self.config.test_venv
|
||||
его алгоритм:
|
||||
ЭТАП A: Загрузка
|
||||
1. Получает на вход список запрашиваемых пакетов в исходном виде, с версиями или без. И флаги required и interactive
|
||||
2. Создает два словаря списков req и opt
|
||||
3. Загружает файл self.config.manual_requirements в один из списков словаря с ключом "_manual"
|
||||
4. Загружает все файлы из self.config.requirements_dir с расширением .req в списки словаря req по ключам, соответствующим именам файлов без расширения.
|
||||
5. Повторяет пункт 4 для файлов .opt и словаря opt
|
||||
6. Находит во всех списках пакеты из self.config.pinned_packages и удаляет их
|
||||
7. Формирует из словарей списков req и opt соответствующие общие списки пакетов.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Напиши статический класс Loader, статически реализующий следующий метод load_requirements
|
||||
Аргументы:
|
||||
manual_reqs - Путь к файлу списка пакетов, установленных вручную
|
||||
reqs_dir - Путь к директории со списками пакетов, необходимых различным модулям программы
|
||||
pinned - Путь к файлу с исключениями установки
|
||||
excluded - Путь к файлу с исключениями проверки
|
||||
Возвращает: dict[str, list[str]], dict[str, list[str]]
|
||||
Два словаря списков - req и opt
|
||||
Алгоритм работы:
|
||||
1. Создает два словаря списков req и opt
|
||||
2. Загружает все файлы из self.config.requirements_dir с расширением .req в списки словаря req по ключам, соответствующим именам файлов без расширения.
|
||||
3. Загружает все файлы из self.config.requirements_dir с расширением .opt в списки словаря opt по ключам, соответствующим именам файлов без расширения.
|
||||
4. Удаляет из req и opt все пакеты из файла pinned (версия пакета не важна)
|
||||
4. Удаляет из req и opt все пакеты из файла excluded (версия пакета должна полностью совпадать. Например если в требованиях есть torch>=2.7.0 удаляем именно такие строки)
|
||||
5. Загружает файл manual_req в один из списков словаря с ключом "_manual"
|
||||
6. Возвращает значения
|
||||
"""
|
||||
0
pythonapp/Decider/__init__.py
Normal file
0
pythonapp/Decider/__init__.py
Normal file
BIN
pythonapp/Decider/__pycache__/ABS.cpython-313.pyc
Normal file
BIN
pythonapp/Decider/__pycache__/ABS.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Decider/__pycache__/Loader.cpython-313.pyc
Normal file
BIN
pythonapp/Decider/__pycache__/Loader.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Decider/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
pythonapp/Decider/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Decider/__pycache__/misc.cpython-313.pyc
Normal file
BIN
pythonapp/Decider/__pycache__/misc.cpython-313.pyc
Normal file
Binary file not shown.
31
pythonapp/Decider/misc.py
Normal file
31
pythonapp/Decider/misc.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
|
||||
from pythonapp.Decider.Loader import RequirementInfo
|
||||
|
||||
|
||||
def requirements_separator(req_file: Path | str):
|
||||
try:
|
||||
with open(req_file, 'r') as file:
|
||||
lines = [line.strip() for line in file]
|
||||
req = []
|
||||
opt = []
|
||||
req_bool = True
|
||||
for line in lines:
|
||||
if line.startswith("#"): req_bool = False
|
||||
if req_bool: req.append(line)
|
||||
else: opt.append(line)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(f"File {req_file} not exists")
|
||||
|
||||
return req, opt
|
||||
|
||||
def requirements_converter(req: list[str], opt: list[str]) -> list[RequirementInfo]:
|
||||
res: list[RequirementInfo] = []
|
||||
for line in req:
|
||||
if line.startswith("#") or line == '': continue
|
||||
res.append(RequirementInfo.from_requirement_string(line.split(" ")[0].strip(), 'required', 'requested'))
|
||||
for line in opt:
|
||||
if line.startswith("#") or line == '': continue
|
||||
res.append(RequirementInfo.from_requirement_string(line.split(" ")[0].strip(), 'optional', 'requested'))
|
||||
|
||||
return res
|
||||
12
pythonapp/Env/Env.py
Normal file
12
pythonapp/Env/Env.py
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
class Env:
|
||||
def __init__(self):
|
||||
self.pip_path = ""
|
||||
pass
|
||||
|
||||
def create(self):
|
||||
raise NotImplemented
|
||||
|
||||
def install_pkgs(self, pkgs: list, repo: str = None, extra: str = None):
|
||||
raise NotImplemented
|
||||
120
pythonapp/Env/StandaloneEnv.py
Normal file
120
pythonapp/Env/StandaloneEnv.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import tarfile
|
||||
import re
|
||||
from typing import List
|
||||
try:
|
||||
from .Env import Env
|
||||
except ImportError:
|
||||
from Env import Env
|
||||
class StandalonePythonEnv(Env):
|
||||
def __init__(self, path: str, version: str):
|
||||
super().__init__()
|
||||
self.version = version
|
||||
self.path = path
|
||||
self.python_path = self._get_python_executable_path()
|
||||
self.pip_path = self._get_pip_executable_path()
|
||||
|
||||
def _get_platform_info(self):
|
||||
system = platform.system().lower()
|
||||
arch = platform.machine().lower()
|
||||
if arch in ['x86_64', 'amd64']:
|
||||
arch = 'amd64'
|
||||
elif arch in ['i386', 'i686', 'x86']:
|
||||
arch = 'win32' if system == 'windows' else 'x86'
|
||||
return system, arch
|
||||
|
||||
def _get_python_executable_path(self):
|
||||
system, _ = self._get_platform_info()
|
||||
if system == 'windows':
|
||||
return os.path.join(self.path, 'python.exe')
|
||||
return os.path.join(self.path, 'bin', 'python3')
|
||||
|
||||
def _get_pip_executable_path(self):
|
||||
system, _ = self._get_platform_info()
|
||||
if system == 'windows':
|
||||
return os.path.join(self.path, 'Scripts', 'pip.exe')
|
||||
return os.path.join(self.path, 'bin', 'pip')
|
||||
|
||||
def _download_and_extract(self):
|
||||
system, arch = self._get_platform_info()
|
||||
base_url = 'https://www.python.org/ftp/python'
|
||||
|
||||
if system == 'windows':
|
||||
url = f'{base_url}/{self.version}/python-{self.version}-embed-{arch}.zip'
|
||||
extract_path = self.path
|
||||
zip_path = os.path.join(self.path, 'python.zip')
|
||||
|
||||
os.makedirs(self.path, exist_ok=True)
|
||||
urllib.request.urlretrieve(url, zip_path)
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_path)
|
||||
os.remove(zip_path)
|
||||
|
||||
elif system == 'linux':
|
||||
url = f'{base_url}/{self.version}/Python-{self.version}.tgz'
|
||||
tar_path = os.path.join(self.path, 'python.tgz')
|
||||
|
||||
os.makedirs(self.path, exist_ok=True)
|
||||
urllib.request.urlretrieve(url, tar_path)
|
||||
|
||||
with tarfile.open(tar_path, 'r:gz') as tar_ref:
|
||||
if hasattr(tarfile, 'data_filter'):
|
||||
tar_ref.extractall(self.path, filter='data')
|
||||
else:
|
||||
tar_ref.extractall(self.path)
|
||||
os.remove(tar_path)
|
||||
|
||||
# Компиляция Python из исходников
|
||||
source_dir = os.path.join(self.path, f'Python-{self.version}')
|
||||
subprocess.run([
|
||||
'./configure', f'--prefix={self.path}',
|
||||
'--enable-optimizations',
|
||||
'--with-ensurepip=install'
|
||||
], cwd=source_dir, check=True)
|
||||
subprocess.run(['make', '-j8'], cwd=source_dir, check=True)
|
||||
subprocess.run(['make', 'install'], cwd=source_dir, check=True)
|
||||
|
||||
|
||||
import shutil
|
||||
shutil.copy2(os.path.join(self.path, 'bin', 'pip3'), os.path.join(self.path, 'bin', 'pip'))
|
||||
|
||||
pass
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported platform: {system}")
|
||||
|
||||
def create(self):
|
||||
self._download_and_extract()
|
||||
|
||||
def install_pkgs(self, pkgs: List[str], repo: str = None, extra :str = None):
|
||||
command = [self.pip_path, 'install'] + pkgs
|
||||
if repo: command.extend(["--index-url", repo])
|
||||
if extra: command.extend(["--extra-index-url", extra])
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
@staticmethod
|
||||
def get_available_versions():
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
url = 'https://www.python.org/downloads/'
|
||||
response = requests.get(url)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
versions = []
|
||||
for release in soup.select('.release-list .release-number'):
|
||||
version_text = release.text.strip().split()[-1]
|
||||
if re.match(r'^\d+\.\d+\.\d+$', version_text):
|
||||
versions.append(version_text)
|
||||
|
||||
return sorted(versions, key=lambda x: tuple(map(int, x.split('.'))), reverse=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
env = StandalonePythonEnv(os.path.join('/tmp', 'build/test_standalone'), '3.12.9')
|
||||
env.create()
|
||||
env.install_pkgs(['requests', 'numpy'])
|
||||
45
pythonapp/Env/Venv.py
Normal file
45
pythonapp/Env/Venv.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import subprocess
|
||||
import os
|
||||
from warnings import deprecated
|
||||
|
||||
try:
|
||||
from .Env import Env
|
||||
except ImportError:
|
||||
from Env import Env
|
||||
|
||||
|
||||
class Venv(Env):
|
||||
def __init__(self, path: str, python='python3'):
|
||||
super().__init__()
|
||||
self.path = path
|
||||
self.pip_path = os.path.join(self.path, "bin", "pip")
|
||||
self.python = python
|
||||
self._check_pip()
|
||||
|
||||
def _check_pip(self):
|
||||
if not os.path.exists(self.pip_path): self.pip_path = os.path.join(self.path, "Scripts", "pip")
|
||||
|
||||
def create(self):
|
||||
command = [self.python, '-m', 'venv', self.path]
|
||||
subprocess.run(command)
|
||||
|
||||
def install_pkgs(self, pkgs: list, repo: str = None, extra :str = None):
|
||||
self._check_pip()
|
||||
command: list = [self.pip_path, "install"]
|
||||
command.extend(pkgs)
|
||||
if repo: command.extend(["--index-url", repo])
|
||||
if extra: command.extend(["--extra-index-url", extra])
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
#@deprecated
|
||||
def install_req(self, req_file: str, extra_index_url=None):
|
||||
self._check_pip()
|
||||
command = [self.pip_path, "install", "-r", req_file]
|
||||
if extra_index_url: command.extend(["--extra-index-url", extra_index_url])
|
||||
subprocess.run(command)
|
||||
|
||||
if __name__ == '__main__':
|
||||
env = Venv(os.path.join('/tmp', 'build/test_venv'))
|
||||
env.create()
|
||||
env.install_pkgs(['requests', 'numpy'])
|
||||
|
||||
0
pythonapp/Env/__init__.py
Normal file
0
pythonapp/Env/__init__.py
Normal file
BIN
pythonapp/Env/__pycache__/Env.cpython-313.pyc
Normal file
BIN
pythonapp/Env/__pycache__/Env.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Env/__pycache__/StandaloneEnv.cpython-313.pyc
Normal file
BIN
pythonapp/Env/__pycache__/StandaloneEnv.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Env/__pycache__/Venv.cpython-313.pyc
Normal file
BIN
pythonapp/Env/__pycache__/Venv.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Env/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
pythonapp/Env/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
79
pythonapp/Instance/ABS.py
Normal file
79
pythonapp/Instance/ABS.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from pythonapp.Libs.ConfigDataClass import Config
|
||||
|
||||
|
||||
class InstanceFileNaming:
|
||||
config_dir = '.vaiola'
|
||||
main_config = 'instance.json'
|
||||
requirements_dir = 'requirements.d'
|
||||
manual_requirements = 'requirements.txt'
|
||||
pinned_packages = 'pinned.txt'
|
||||
excluded_packages = 'excluded.txt'
|
||||
env_dir = 'env'
|
||||
app_dir = 'app'
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstanceConfig(Config):
|
||||
instance_type: str = 'basic'
|
||||
config_dir_rel_path: Path = Path(InstanceFileNaming.config_dir)
|
||||
env_path: Path = Path(InstanceFileNaming.env_dir)
|
||||
env_type: str = None
|
||||
requirements_dir: Path = Path(InstanceFileNaming.config_dir) / InstanceFileNaming.requirements_dir
|
||||
manual_requirements_path: Path = Path(InstanceFileNaming.config_dir) / InstanceFileNaming.manual_requirements
|
||||
pinned_packages_path: Path = Path(InstanceFileNaming.config_dir) / InstanceFileNaming.pinned_packages
|
||||
excluded_packages_path: Path = Path(InstanceFileNaming.config_dir) / InstanceFileNaming.excluded_packages
|
||||
created: bool = False
|
||||
app: Path = Path(InstanceFileNaming.app_dir)
|
||||
app_installed: bool = None
|
||||
app_extensions_dir: Path = None
|
||||
app_models_dir: Path = None
|
||||
app_output_dir: Path = None
|
||||
app_input_dir: Path = None
|
||||
app_user_dir: Path = None
|
||||
|
||||
class Instance:
|
||||
def __init__(self):
|
||||
self.path: Path | None = None
|
||||
self.config: InstanceConfig | None = None
|
||||
|
||||
def insert_component_reqs(self, name: str, req_file: str | Path):
|
||||
try:
|
||||
with open('req_file', 'r') as file:
|
||||
lines = [line.strip() for line in file]
|
||||
req = []
|
||||
opt = []
|
||||
req_bool = True
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
req_bool = False
|
||||
if req_bool: req.append(line)
|
||||
else: opt.append(line)
|
||||
|
||||
with open(self.path / self.config.requirements_dir / (name + '.req'), 'w') as file:
|
||||
for line in req: file.write(line + '\n')
|
||||
with open(self.path / self.config.requirements_dir / (name + '.opt'), 'w') as file:
|
||||
for line in opt: file.write(line + '\n')
|
||||
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(f"Cannot update requirements for {name}, file {req_file} not exists")
|
||||
raise NotImplemented
|
||||
|
||||
def install_git_app(self, url: str, requirements_file_in_app_dir = 'requirements.txt',
|
||||
extensions_dir: str = None,
|
||||
models_dir: str = None,
|
||||
output_dir: str = None,
|
||||
input_dir: str = None,
|
||||
user_dir: str = None,
|
||||
):
|
||||
raise NotImplemented
|
||||
|
||||
def install_reqs(self, name, req_file: Path):
|
||||
raise NotImplemented
|
||||
|
||||
def create(self):
|
||||
raise NotImplemented
|
||||
|
||||
def install_packages(self, pkgs: list, repo, extra, pin=False):
|
||||
raise NotImplemented
|
||||
144
pythonapp/Instance/Instance.py
Normal file
144
pythonapp/Instance/Instance.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from pythonapp.Decider.ABS import SimpleDecider
|
||||
from pythonapp.Env.Venv import Venv, Env
|
||||
from pythonapp.Env.StandaloneEnv import StandalonePythonEnv
|
||||
from pythonapp.Libs.git import git
|
||||
from pythonapp.Decider.misc import *
|
||||
from pythonapp.Libs.pip_api import pip_api
|
||||
from .ABS import InstanceConfig, InstanceFileNaming, Instance as ABSInstance
|
||||
from ..Decider.Loader import Loader
|
||||
|
||||
|
||||
class Instance(ABSInstance):
|
||||
def __init__(self, path: str, env: str = 'venv', python = 'python3'):
|
||||
self.path = Path(path)
|
||||
self.config = InstanceConfig(os.path.join(self.path, InstanceFileNaming.main_config))
|
||||
self.config.env_type = self.config.env_type or env
|
||||
self.config.created = (Path(self.path) / self.config.config_dir_rel_path).exists()
|
||||
|
||||
|
||||
if self.config.env_type == 'venv':
|
||||
self.env: Env = Venv(str(self.path / self.config.env_path), python)
|
||||
elif self.config.env_type == 'standalone':
|
||||
self.env: Env = StandalonePythonEnv(str(self.path / self.config.env_path), python)
|
||||
|
||||
|
||||
def insert_component_reqs(self, name: str, req_file: str | Path):
|
||||
try:
|
||||
req, opt = requirements_separator(req_file)
|
||||
os.makedirs(self.path / self.config.requirements_dir, exist_ok=True)
|
||||
with open(self.path / self.config.requirements_dir / (name + '.req'), 'w') as file:
|
||||
for line in req: file.write(line + '\n')
|
||||
with open(self.path / self.config.requirements_dir / (name + '.opt'), 'w') as file:
|
||||
for line in opt: file.write(line + '\n')
|
||||
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(f"Cannot update requirements for {name}, file {req_file} not exists")
|
||||
|
||||
def _symlink_move_dir(self, orig_dir: Path, dest: Path, obj_name: Path):
|
||||
cwd = os.getcwd()
|
||||
os.chdir(self.path)
|
||||
shutil.move(orig_dir / obj_name, dest)
|
||||
os.symlink(os.path.relpath(orig_dir, dest), orig_dir / obj_name)
|
||||
os.chdir(orig_dir)
|
||||
with open(".gitignore", "a") as file:
|
||||
file.write(str(obj_name) + '\n')
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
def install_git_app(self, url: str, requirements_file_in_app_dir = 'requirements.txt',
|
||||
extensions_dir: str = None,
|
||||
models_dir: str = None,
|
||||
output_dir: str = None,
|
||||
input_dir: str = None,
|
||||
user_dir: str = None,
|
||||
):
|
||||
if os.path.exists(self.config.app):
|
||||
raise RuntimeError("App installed previously. Multiapp instances is not supported")
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
git.clone(url, tmp_dir)
|
||||
try:
|
||||
self.install_reqs('app', Path(tmp_dir) / requirements_file_in_app_dir)
|
||||
except RuntimeError as e: raise TypeError(e)
|
||||
|
||||
self.config.app_extensions_dir = Path(extensions_dir) if extensions_dir else None
|
||||
self.config.app_models_dir = Path(models_dir) if models_dir else None
|
||||
self.config.app_output_dir = Path(output_dir) if output_dir else None
|
||||
self.config.app_input_dir = Path(input_dir) if input_dir else None
|
||||
self.config.app_user_dir = Path(user_dir) if user_dir else None
|
||||
self.config.save()
|
||||
|
||||
git.clone(url, self.path / self.config.app)
|
||||
|
||||
if self.config.app_extensions_dir:
|
||||
self._symlink_move_dir(Path(os.path.dirname(self.config.app / self.config.app_extensions_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_extensions_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_extensions_dir)))
|
||||
if self.config.app_models_dir:
|
||||
self._symlink_move_dir(Path(os.path.dirname(self.config.app / self.config.app_models_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_models_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_models_dir)))
|
||||
if self.config.app_output_dir:
|
||||
self._symlink_move_dir(Path(os.path.dirname(self.config.app / self.config.app_output_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_output_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_output_dir)))
|
||||
if self.config.app_input_dir:
|
||||
self._symlink_move_dir(Path(os.path.dirname(self.config.app / self.config.app_input_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_input_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_input_dir)))
|
||||
if self.config.app_user_dir:
|
||||
self._symlink_move_dir(Path(os.path.dirname(self.config.app / self.config.app_user_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_user_dir)),
|
||||
Path(os.path.basename(self.config.app / self.config.app_user_dir)))
|
||||
|
||||
|
||||
def install_reqs(self, name, req_file: Path):
|
||||
packages, errors, state = SimpleDecider.decide(self, req_file, self.env.pip_path)
|
||||
if not packages: raise RuntimeError("Cannot install packages due conflicts")
|
||||
self.env.install_pkgs([p.requirement_str for p in packages])
|
||||
self.insert_component_reqs(name, req_file)
|
||||
|
||||
|
||||
def create(self):
|
||||
os.makedirs(self.path / self.config.config_dir_rel_path, exist_ok=True)
|
||||
self.config.save()
|
||||
self.env.create()
|
||||
|
||||
|
||||
def install_packages(self, pkgs: list, repo: str = None, extra: str = None, pin=False):
|
||||
packages = [RequirementInfo.from_requirement_string(p, 'manual', 'manual') for p in pkgs]
|
||||
packages, errors, state = SimpleDecider.decide(self, packages, self.env.pip_path, manual=True)
|
||||
pkgs = [p.requirement_str for p in packages]
|
||||
print("manually install packages", pkgs)
|
||||
self.env.install_pkgs(pkgs, repo, extra)
|
||||
|
||||
for pkg in pkgs: self._write_unique_string(pkg, file_path=self.path / self.config.manual_requirements_path)
|
||||
if pin:
|
||||
for pkg in pkgs: self._write_unique_string(pkg, file_path=self.path / self.config.pinned_packages_path)
|
||||
|
||||
def exclude_packages(self, pkgs: list[str]):
|
||||
for pkg in pkgs: self._write_unique_string(pkg, file_path=self.path / self.config.excluded_packages_path)
|
||||
|
||||
|
||||
def _write_unique_string(self, string, file_path):
|
||||
existing_lines = set()
|
||||
if file_path.exists():
|
||||
with open(file_path, 'r') as file:
|
||||
existing_lines = set(line.strip() for line in file)
|
||||
|
||||
with open(file_path, 'a') as file:
|
||||
if string not in existing_lines:
|
||||
file.write(string + '\n')
|
||||
|
||||
def _delete_string(self, string, file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
filtered_lines = [line for line in lines if line.rstrip('\n') != string]
|
||||
with open(file_path, 'w') as file:
|
||||
file.writelines(filtered_lines)
|
||||
|
||||
|
||||
|
||||
0
pythonapp/Instance/__init__.py
Normal file
0
pythonapp/Instance/__init__.py
Normal file
BIN
pythonapp/Instance/__pycache__/ABS.cpython-313.pyc
Normal file
BIN
pythonapp/Instance/__pycache__/ABS.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Instance/__pycache__/Instance.cpython-313.pyc
Normal file
BIN
pythonapp/Instance/__pycache__/Instance.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Instance/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
pythonapp/Instance/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
53
pythonapp/Libs/ConfigDataClass.py
Normal file
53
pythonapp/Libs/ConfigDataClass.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
import atexit
|
||||
from dataclasses import dataclass, asdict, fields
|
||||
from typing import Any, Dict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
filename: str
|
||||
autosave: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
# Загружаем значения из файла при создании экземпляра
|
||||
self.load()
|
||||
|
||||
# Регистрируем автоматическое сохранение при завершении программы
|
||||
if self.autosave:
|
||||
atexit.register(self.save)
|
||||
|
||||
def load(self) -> None:
|
||||
"""Загружает значения полей из файла"""
|
||||
try:
|
||||
if Path(self.filename).exists():
|
||||
with open(self.filename, 'r') as f:
|
||||
data = json.load(f)
|
||||
for field in fields(self):
|
||||
if field.name in data and field.name != 'filename':
|
||||
setattr(self, field.name, data[field.name])
|
||||
except Exception as e:
|
||||
print(f"Error loading config: {e}")
|
||||
|
||||
def save(self) -> None:
|
||||
"""Сохраняет текущие значения полей в файл"""
|
||||
try:
|
||||
# Преобразуем объект в словарь, исключая поле filename
|
||||
data = asdict(self)
|
||||
data.pop('filename', None)
|
||||
|
||||
# Создаем директорию, если она не существует
|
||||
Path(self.filename).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Сохраняем в файл с кастомным сериализатором для Path объектов
|
||||
with open(self.filename, 'w') as f:
|
||||
json.dump(data, f, indent=4, default=self._json_serializer)
|
||||
except Exception as e:
|
||||
print(f"Error saving config: {e}")
|
||||
|
||||
def _json_serializer(self, obj: Any) -> Any:
|
||||
"""Кастомный сериализатор для объектов, которые не могут быть сериализованы по умолчанию"""
|
||||
if isinstance(obj, Path):
|
||||
return str(obj)
|
||||
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|
||||
0
pythonapp/Libs/__init__.py
Normal file
0
pythonapp/Libs/__init__.py
Normal file
BIN
pythonapp/Libs/__pycache__/ConfigDataClass.cpython-313.pyc
Normal file
BIN
pythonapp/Libs/__pycache__/ConfigDataClass.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Libs/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
pythonapp/Libs/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Libs/__pycache__/git.cpython-313.pyc
Normal file
BIN
pythonapp/Libs/__pycache__/git.cpython-313.pyc
Normal file
Binary file not shown.
BIN
pythonapp/Libs/__pycache__/pip_api.cpython-313.pyc
Normal file
BIN
pythonapp/Libs/__pycache__/pip_api.cpython-313.pyc
Normal file
Binary file not shown.
54
pythonapp/Libs/getpytorch.py
Normal file
54
pythonapp/Libs/getpytorch.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
from .pip_api import pip_api
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class PyTorchInfo:
|
||||
"""Датакласс для хранения информации о версиях PyTorch компонентов"""
|
||||
torch: List[str]
|
||||
torchvision: List[str]
|
||||
torchaudio: List[str]
|
||||
|
||||
|
||||
class getpytorch:
|
||||
"""Класс для получения версий компонентов PyTorch"""
|
||||
BASE_URL = "https://download.pytorch.org/whl/"
|
||||
|
||||
def __init__(self, base_url: str = None):
|
||||
self.base_url = base_url or self.BASE_URL
|
||||
|
||||
def get_versions(self, api: str) -> PyTorchInfo:
|
||||
"""Получает версии всех компонентов PyTorch для указанного API"""
|
||||
base_url = f"{self.base_url.rstrip('/')}/{api}"
|
||||
|
||||
return PyTorchInfo(
|
||||
torch=self.get_torch_versions(api),
|
||||
torchvision=self.get_torchvision_versions(api),
|
||||
torchaudio=self.get_torchaudio_versions(api),
|
||||
)
|
||||
|
||||
def get_torch_versions(self, api: str) -> List[str]:
|
||||
"""Получает версии torch"""
|
||||
return pip_api.get_pkg_versions('torch', f"{self.base_url.rstrip('/')}/{api}")
|
||||
|
||||
def get_torchvision_versions(self, api: str) -> List[str]:
|
||||
"""Получает версии torchvision"""
|
||||
return pip_api.get_pkg_versions('torchvision', f"{self.base_url.rstrip('/')}/{api}")
|
||||
|
||||
def get_torchaudio_versions(self, api: str) -> List[str]:
|
||||
"""Получает версии torchaudio"""
|
||||
return pip_api.get_pkg_versions('torchaudio', f"{self.base_url.rstrip('/')}/{api}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Пример использования
|
||||
pytorch = getpytorch()
|
||||
|
||||
api = input("API version (cu121): ") or "cu121"
|
||||
versions = pytorch.get_versions(api)
|
||||
|
||||
print(f"Все версии PyTorch: {versions.torch}")
|
||||
print(f"Все версии torchvision: {versions.torchvision}")
|
||||
print(f"Все версии torchaudio: {versions.torchaudio}")
|
||||
10
pythonapp/Libs/git.py
Normal file
10
pythonapp/Libs/git.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
class git:
|
||||
@staticmethod
|
||||
def clone(url, output_path = None, git = 'git'):
|
||||
command = [git, 'clone', url]
|
||||
if output_path: command.append(output_path)
|
||||
result = subprocess.run(command)
|
||||
return result
|
||||
138
pythonapp/Libs/pip_api.py
Normal file
138
pythonapp/Libs/pip_api.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import subprocess
|
||||
from typing import List, Optional
|
||||
import re
|
||||
import subprocess
|
||||
import shlex
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class PipResult:
|
||||
"""Результат выполнения команды pip."""
|
||||
exit_code: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
command: str
|
||||
success: bool
|
||||
|
||||
|
||||
class pip_api:
|
||||
@staticmethod
|
||||
def get_pkg_versions(package: str, index_url: Optional[str] = None) -> List[str]:
|
||||
# Формируем базовую команду
|
||||
command = [
|
||||
'python3', '-m', 'pip',
|
||||
'install',
|
||||
'--use-deprecated=legacy-resolver', # Для совместимости со старыми репозиториями
|
||||
'--dry-run',
|
||||
'--no-deps',
|
||||
f'{package}==0.0.0.0' # Специально несуществующая версия
|
||||
]
|
||||
|
||||
if index_url:
|
||||
command.extend(['--index-url', index_url])
|
||||
|
||||
# Запускаем процесс
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30 # Таймаут для избежания зависаний
|
||||
)
|
||||
|
||||
#print(result.stdout)
|
||||
#print(result.stderr)
|
||||
# Парсим вывод для получения версий
|
||||
if result.stderr:
|
||||
match = re.search(r'from versions: (.+?)\)', result.stderr)
|
||||
if match:
|
||||
versions = match.group(1).split(', ')
|
||||
return [v.strip() for v in versions if v.strip()]
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def run_pip_install(
|
||||
pip_path: str,
|
||||
packages: List[str],
|
||||
index_url: Optional[str] = None,
|
||||
extra_index_urls: Optional[List[str]] = None,
|
||||
dry_run: bool = False
|
||||
) -> PipResult:
|
||||
"""
|
||||
Выполняет установку пакетов через pip с указанными параметрами.
|
||||
|
||||
Args:
|
||||
pip_path: Путь к исполняемому файлу pip
|
||||
packages: Список пакетов для установки
|
||||
index_url: Основной URL репозитория пакетов
|
||||
extra_index_urls: Дополнительные URLs репозиториев пакетов
|
||||
dry_run: Если True, команда только выводится, но не выполняется
|
||||
|
||||
Returns:
|
||||
PipResult: Объект с результатами выполнения команды
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: Если pip_path не существует
|
||||
ValueError: Если packages пуст
|
||||
"""
|
||||
# Проверка входных параметров
|
||||
if not packages:
|
||||
raise ValueError("Список пакетов не может быть пустым")
|
||||
|
||||
# Формирование команды
|
||||
command = [pip_path, "install"]
|
||||
|
||||
if dry_run:
|
||||
command.append('--dry-run')
|
||||
|
||||
# Добавление основного index-url
|
||||
if index_url:
|
||||
command.extend(["--index-url", index_url])
|
||||
|
||||
# Добавление дополнительных index-urls
|
||||
if extra_index_urls:
|
||||
for url in extra_index_urls:
|
||||
command.extend(["--extra-index-url", url])
|
||||
|
||||
# Добавление пакетов
|
||||
command.extend(packages)
|
||||
|
||||
# Преобразование команды в строку для вывода
|
||||
command_str = " ".join(shlex.quote(arg) for arg in command)
|
||||
|
||||
# Выполнение команды
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False # Не вызываем исключение при ненулевом коде возврата
|
||||
)
|
||||
|
||||
return PipResult(
|
||||
exit_code=result.returncode,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
command=command_str,
|
||||
success=result.returncode == 0
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Файл pip не найден по пути: {pip_path}")
|
||||
except Exception as e:
|
||||
return PipResult(
|
||||
exit_code=-1,
|
||||
stdout="",
|
||||
stderr=str(e),
|
||||
command=command_str,
|
||||
success=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
versions = pip_api.get_pkg_versions("torch", "https://download.pytorch.org/whl/cu121")
|
||||
print(versions)
|
||||
0
pythonapp/__init__.py
Normal file
0
pythonapp/__init__.py
Normal file
BIN
pythonapp/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
pythonapp/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user