initial commit

This commit is contained in:
Bacruru Sakaguchi
2025-09-12 17:10:13 +07:00
commit 9e5e214944
57 changed files with 1538 additions and 0 deletions

78
pythonapp/Decider/ABS.py Normal file
View 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
View 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. Возвращает значения
"""

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

31
pythonapp/Decider/misc.py Normal file
View 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
View 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

View 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
View 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'])

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

79
pythonapp/Instance/ABS.py Normal file
View 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

View 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)

View File

Binary file not shown.

View 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")

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
View 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
View 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
View File

Binary file not shown.