initial commit
This commit is contained in:
3
main.py
Normal file
3
main.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from shell.Interactive import Interactive
|
||||||
|
|
||||||
|
Interactive().start()
|
||||||
167
modelspace/ModelPackage.py
Normal file
167
modelspace/ModelPackage.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pythonapp.Libs.ConfigDataClass import Config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PackageInfo(Config):
|
||||||
|
"""Информация о пакете"""
|
||||||
|
uuid: str = ""
|
||||||
|
name: str = ""
|
||||||
|
description: str = ""
|
||||||
|
release_date: str = ""
|
||||||
|
package_type: str = "" # unet, vae, text encoder
|
||||||
|
lineage: str = "" # sd 1.5, sdxl, flux.1
|
||||||
|
size_bytes: int = 0
|
||||||
|
version: str = ""
|
||||||
|
quantization: str = "" # fp8, bf16
|
||||||
|
dependencies: List[str] = None
|
||||||
|
resources: List[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.dependencies is None:
|
||||||
|
self.dependencies = []
|
||||||
|
if self.resources is None:
|
||||||
|
self.resources = []
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
|
||||||
|
class ModelPackage:
|
||||||
|
def __init__(self, package_path: str, file_paths: List[str] = None, package_info: PackageInfo = None):
|
||||||
|
self.path = Path(package_path)
|
||||||
|
|
||||||
|
# Создаем директорию если она не существует
|
||||||
|
self.path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Путь к файлу package.json
|
||||||
|
self.package_file = self.path / "package.json"
|
||||||
|
|
||||||
|
# Загружаем существующую информацию из файла
|
||||||
|
self.info = PackageInfo(filename=str(self.package_file))
|
||||||
|
|
||||||
|
# Если package_info передан и не пустой, обновляем информацию
|
||||||
|
if package_info is not None:
|
||||||
|
# Обновляем только те поля, которые не определены
|
||||||
|
for field in fields(package_info):
|
||||||
|
field_name = field.name
|
||||||
|
field_value = getattr(package_info, field_name)
|
||||||
|
if field_value is not None and field_value != "" and field_name != "filename":
|
||||||
|
current_value = getattr(self.info, field_name)
|
||||||
|
if current_value is None or current_value == "" or current_value == 0 or len(current_value) == 0:
|
||||||
|
setattr(self.info, field_name, field_value)
|
||||||
|
|
||||||
|
# Генерируем UUID если он не определен
|
||||||
|
if not self.info.uuid:
|
||||||
|
self.info.uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Сохраняем информацию о пакете
|
||||||
|
self.info.save()
|
||||||
|
|
||||||
|
# Создаем директорию files
|
||||||
|
self.files_path = self.path / "files"
|
||||||
|
self.files_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
if file_paths:
|
||||||
|
# Перемещаем файлы в директорию files
|
||||||
|
for file_path in file_paths:
|
||||||
|
src = Path(file_path)
|
||||||
|
if src.exists():
|
||||||
|
dst = self.files_path / src.name
|
||||||
|
if src.is_dir():
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def interactive(cls, package_path: str, pkg_uuid = None):
|
||||||
|
"""Интерактивное создание пакета через консоль"""
|
||||||
|
print("Введите информацию о пакете:")
|
||||||
|
if os.path.exists(str(Path(package_path) / "package.json")): raise RuntimeError("package exists!")
|
||||||
|
package_info = PackageInfo(str(Path(package_path) / "package.json"))
|
||||||
|
package_info.name = input("Название: ").strip()
|
||||||
|
package_info.description = input("Описание: ").strip()
|
||||||
|
package_info.release_date = input("Дата выхода (YYYY-MM-DD): ").strip()
|
||||||
|
package_info.package_type = input("Тип пакета (unet, vae, text encoder): ").strip()
|
||||||
|
package_info.lineage = input("Линейка (sd 1.5, sdxl, flux.1): ").strip()
|
||||||
|
|
||||||
|
# Размер в байтах
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
size = int(input("Размер в байтах: ").strip())
|
||||||
|
package_info.size_bytes = size
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Введите корректное число")
|
||||||
|
|
||||||
|
package_info.version = input("Версия: ").strip()
|
||||||
|
package_info.quantization = input("Квантование (fp8, bf16): ").strip()
|
||||||
|
|
||||||
|
# Ввод зависимостей
|
||||||
|
print("Зависимости (введите по одной, пустая строка для завершения):")
|
||||||
|
dependencies = []
|
||||||
|
while True:
|
||||||
|
dep = input().strip()
|
||||||
|
if not dep:
|
||||||
|
break
|
||||||
|
dependencies.append(dep)
|
||||||
|
package_info.dependencies = dependencies
|
||||||
|
|
||||||
|
# Ввод ресурсов
|
||||||
|
print("Ресурсы (введите по одному, пустая строка для завершения):")
|
||||||
|
resources = []
|
||||||
|
while True:
|
||||||
|
resource = input().strip()
|
||||||
|
if not resource:
|
||||||
|
break
|
||||||
|
resources.append(resource)
|
||||||
|
package_info.resources = resources
|
||||||
|
|
||||||
|
# Генерируем UUID случайным образом (не запрашиваем у пользователя)
|
||||||
|
package_info.uuid = pkg_uuid
|
||||||
|
if not package_info.uuid:
|
||||||
|
package_info.uuid = str(uuid.uuid4())
|
||||||
|
print(f"Сгенерирован UUID: {package_info.uuid}")
|
||||||
|
|
||||||
|
# Ввод путей к файлам
|
||||||
|
print("Пути к файлам и директориям (введите по одному, пустая строка для завершения):")
|
||||||
|
file_paths = []
|
||||||
|
while True:
|
||||||
|
file_path = input().strip()
|
||||||
|
if not file_path:
|
||||||
|
break
|
||||||
|
file_paths.append(file_path)
|
||||||
|
|
||||||
|
# Создаем пакет
|
||||||
|
return cls(package_path, file_paths, package_info)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> str:
|
||||||
|
"""Возвращает UUID пакета"""
|
||||||
|
return self.info.uuid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Возвращает название пакета"""
|
||||||
|
return self.info.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dependencies(self) -> List[str]:
|
||||||
|
"""Возвращает список зависимостей пакета"""
|
||||||
|
return self.info.dependencies.copy() # Возвращаем копию для защиты от изменений
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provides(self) -> List[str]:
|
||||||
|
"""Возвращает список ресурсов, предоставляемых пакетом (включая имя пакета)"""
|
||||||
|
provides_list = self.info.resources.copy() # Возвращаем копию
|
||||||
|
if self.info.name: # Добавляем имя пакета, если оно есть
|
||||||
|
provides_list.append(self.info.name)
|
||||||
|
return provides_list
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = ModelPackage('/tmp/pkg')
|
||||||
|
pass
|
||||||
57
modelspace/Repository.py
Normal file
57
modelspace/Repository.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from modelspace.ModelPackage import ModelPackage
|
||||||
|
from pythonapp.Libs.ConfigDataClass import Config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RepoConfig(Config):
|
||||||
|
"""Конфигурация репозитория с сидом"""
|
||||||
|
seed: str = "" # UUID сида
|
||||||
|
|
||||||
|
|
||||||
|
class Repository:
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.path = Path(path)
|
||||||
|
# Создаем директорию если она не существует
|
||||||
|
self.path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.config_file = self.path / "repo.json"
|
||||||
|
|
||||||
|
# Создаем конфигурацию
|
||||||
|
self.config = RepoConfig(
|
||||||
|
filename=str(self.config_file),
|
||||||
|
autosave=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем и устанавливаем сид
|
||||||
|
if not self.config.seed:
|
||||||
|
self._generate_and_save_seed()
|
||||||
|
|
||||||
|
# Создаем поддиректорию model-packages если она не существует
|
||||||
|
self.model_packages_path = self.path / "model-packages"
|
||||||
|
self.model_packages_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def _generate_and_save_seed(self) -> None:
|
||||||
|
"""Генерирует новый UUID и сохраняет его в конфиг"""
|
||||||
|
self.config.seed = str(uuid.uuid4())
|
||||||
|
self.config.save() # Сохраняем сразу после генерации
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seed(self) -> str:
|
||||||
|
"""Возвращает текущий сид"""
|
||||||
|
return self.config.seed
|
||||||
|
|
||||||
|
def add_model_package_interactive(self) -> ModelPackage:
|
||||||
|
"""Добавляет новый пакет модели интерактивно"""
|
||||||
|
# Генерируем новый UUID
|
||||||
|
package_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Создаем путь к новому пакету
|
||||||
|
package_path = self.model_packages_path / package_uuid
|
||||||
|
|
||||||
|
# Вызываем интерактивное создание пакета
|
||||||
|
package = ModelPackage.interactive(str(package_path), package_uuid)
|
||||||
|
return package
|
||||||
0
modelspace/__init__.py
Normal file
0
modelspace/__init__.py
Normal file
BIN
modelspace/__pycache__/ModelPackage.cpython-313.pyc
Normal file
BIN
modelspace/__pycache__/ModelPackage.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modelspace/__pycache__/Repository.cpython-313.pyc
Normal file
BIN
modelspace/__pycache__/Repository.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modelspace/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modelspace/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
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.
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
colorama
|
||||||
85
shell/Handlers/ABS.py
Normal file
85
shell/Handlers/ABS.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from modelspace.Repository import Repository
|
||||||
|
global_repo = Repository(str(Path('..') / 'repo'))
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionError(RuntimeError): pass
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
syntax_error = SyntaxError
|
||||||
|
execution_error = ExecutionError
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.forwarding_table: dict[str, Handler] = {}
|
||||||
|
self.handle_table: dict = {}
|
||||||
|
self.succeed = False
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_arg(keys: dict, arg: str):
|
||||||
|
if not keys.get(arg, None): raise TypeError('Unfilled argument:', arg)
|
||||||
|
|
||||||
|
|
||||||
|
def handle(self, command: list[str], pos = 0):
|
||||||
|
self.succeed = False
|
||||||
|
if len(command) <= pos: raise self.syntax_error
|
||||||
|
verb = command[pos].lower()
|
||||||
|
|
||||||
|
if verb in self.forwarding_table:
|
||||||
|
self.forwarding_table[verb].handle(command, pos + 1)
|
||||||
|
if self.forwarding_table[verb].succeed:
|
||||||
|
self.succeed = True
|
||||||
|
return
|
||||||
|
|
||||||
|
elif verb in self.handle_table:
|
||||||
|
self.handle_table[verb](command, pos + 1)
|
||||||
|
if self.succeed:
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_arguments(args_list: list[str], expected_args: list[str], key_mode=False) -> tuple[dict[str, str | None], list[str]]:
|
||||||
|
"""
|
||||||
|
Парсит список аргументов согласно ожидаемым именам.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args_list: список аргументов для обработки
|
||||||
|
expected_args: список ожидаемых имен аргументов
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Кортеж из двух элементов:
|
||||||
|
- Словарь с ключами из expected_args и значениями аргументов (или None)
|
||||||
|
- Список необработанных аргументов
|
||||||
|
"""
|
||||||
|
# Инициализируем результат значениями None для всех ожидаемых аргументов
|
||||||
|
result: dict[str, str | None] = {arg: None for arg in expected_args}
|
||||||
|
|
||||||
|
# Извлекаем именованные аргументы (в формате "имя:значение")
|
||||||
|
remaining_args = []
|
||||||
|
|
||||||
|
for arg in args_list:
|
||||||
|
if ':' in arg:
|
||||||
|
key, value = arg.split(':', 1)
|
||||||
|
if key in expected_args:
|
||||||
|
result[key] = value
|
||||||
|
else:
|
||||||
|
# Если имя аргумента не ожидается, добавляем в оставшиеся
|
||||||
|
remaining_args.append(arg)
|
||||||
|
else:
|
||||||
|
remaining_args.append(arg)
|
||||||
|
|
||||||
|
# Заполняем оставшиеся аргументы по порядку
|
||||||
|
arg_index = 0
|
||||||
|
if not key_mode:
|
||||||
|
for arg_name in expected_args:
|
||||||
|
if result[arg_name] is None and arg_index < len(remaining_args):
|
||||||
|
result[arg_name] = remaining_args[arg_index]
|
||||||
|
arg_index += 1
|
||||||
|
|
||||||
|
# Все оставшиеся аргументы добавляем в unsorted
|
||||||
|
unsorted = remaining_args[arg_index:]
|
||||||
|
|
||||||
|
return result, unsorted
|
||||||
|
|
||||||
|
|
||||||
27
shell/Handlers/GlobalHandler.py
Normal file
27
shell/Handlers/GlobalHandler.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from shell.Handlers.ABS import Handler
|
||||||
|
from shell.Handlers.PythonappHandler import PythonappHandler
|
||||||
|
from shell.Handlers.ModelSpaceHandler import ModelSpaceHandler
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalHandler(Handler):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.forwarding_table: dict[str, Handler] = {
|
||||||
|
'pythonapp': PythonappHandler(),
|
||||||
|
'modelspace': ModelSpaceHandler(),
|
||||||
|
}
|
||||||
|
self.handle_table: dict = {
|
||||||
|
'tell': self._tell
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def _tell(self, command: list[str], pos = 0):
|
||||||
|
command_str = ''
|
||||||
|
for word in command[pos:]: command_str += word + ' '
|
||||||
|
print(command_str)
|
||||||
|
self.succeed = True
|
||||||
|
|
||||||
|
def _exit(self, command: list[str], pos = 0):
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
18
shell/Handlers/ModelSpaceHandler.py
Normal file
18
shell/Handlers/ModelSpaceHandler.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from shell.Handlers.ABS import Handler, global_repo
|
||||||
|
|
||||||
|
|
||||||
|
class ModelSpaceHandler(Handler):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.forwarding_table: dict[str, Handler] = {
|
||||||
|
}
|
||||||
|
self.handle_table: dict = {
|
||||||
|
'create_inter': self._create_inter
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _create_inter(self, command: list[str], pos=0):
|
||||||
|
global_repo.add_model_package_interactive()
|
||||||
|
self.succeed = True
|
||||||
88
shell/Handlers/PythonappHandler.py
Normal file
88
shell/Handlers/PythonappHandler.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from shell.Handlers.ABS import Handler
|
||||||
|
|
||||||
|
from pythonapp.Instance.Instance import Instance
|
||||||
|
|
||||||
|
|
||||||
|
class PythonappHandler(Handler):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.forwarding_table: dict[str, Handler] = {
|
||||||
|
'package': PackageHandler(self)
|
||||||
|
}
|
||||||
|
self.handle_table: dict = {
|
||||||
|
'create': self._create,
|
||||||
|
'load': self._load,
|
||||||
|
'show': self._show,
|
||||||
|
'activate': self._activate,
|
||||||
|
}
|
||||||
|
self._loaded_instances: dict[str, Instance] = {}
|
||||||
|
self._active_instance: Instance | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _load(self, command: list[str], pos = 0):
|
||||||
|
keys, args = self.parse_arguments(command[pos:], ['path', 'name'])
|
||||||
|
self._check_arg(keys, 'path')
|
||||||
|
if not keys['name']: keys['name'] = 'app'
|
||||||
|
i = Instance(path=str(keys['path']))
|
||||||
|
if not i.config.created: raise self.execution_error("ACTIVATE INSTANCE: instance not exists")
|
||||||
|
self._loaded_instances[keys['name']] = i
|
||||||
|
self._active_instance = i
|
||||||
|
print(f"instance {keys['path']} loaded and activated. identified by {keys['name']}")
|
||||||
|
|
||||||
|
def _activate(self, command: list[str], pos = 0):
|
||||||
|
keys, args = self.parse_arguments(command[pos:], ['name'])
|
||||||
|
self._check_arg(keys, 'name')
|
||||||
|
i = self._loaded_instances.get(command[1], None)
|
||||||
|
if i:
|
||||||
|
self._active_instance = i
|
||||||
|
else:
|
||||||
|
raise ValueError(f"pyapp {keys['name']} not loaded")
|
||||||
|
|
||||||
|
def _show(self, command: list[str], pos = 0):
|
||||||
|
print("Environment type:", self._active_instance.config.env_type)
|
||||||
|
# TODO Add new config info (app section)
|
||||||
|
|
||||||
|
def _create(self, command: list[str], pos = 0):
|
||||||
|
keys, args = self.parse_arguments(command[pos:], ['env', 'path', 'python'])
|
||||||
|
|
||||||
|
|
||||||
|
self.succeed = True
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_instance(self):
|
||||||
|
return self._active_instance
|
||||||
|
|
||||||
|
|
||||||
|
class PackageHandler(Handler):
|
||||||
|
def __init__(self,parent: PythonappHandler):
|
||||||
|
super().__init__()
|
||||||
|
self.forwarding_table: dict[str, Handler] = {}
|
||||||
|
self.handle_table: dict = {
|
||||||
|
'install': self._install,
|
||||||
|
'exclude': self._exclude
|
||||||
|
}
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def _install(self, command: list[str], pos = 0):
|
||||||
|
if not self.parent.active_instance: raise self.execution_error("I don't have active instance yet!")
|
||||||
|
keys, args = self.parse_arguments(command[pos:], ['pin', 'index', 'extra'], key_mode=True)
|
||||||
|
if keys['pin']: pin = True
|
||||||
|
else: pin = False
|
||||||
|
self.parent.active_instance.install_packages(pkgs=args, repo=keys.get('index', None), extra=keys.get('extra', None), pin=pin)
|
||||||
|
|
||||||
|
def _exclude(self, command: list[str], pos = 0):
|
||||||
|
if not self.parent.active_instance: raise self.execution_error("I don't have active instance yet!")
|
||||||
|
pkgs = command[pos:]
|
||||||
|
self.parent.active_instance.exclude_packages(pkgs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self.succeed = True
|
||||||
|
|
||||||
|
|
||||||
0
shell/Handlers/__init__.py
Normal file
0
shell/Handlers/__init__.py
Normal file
BIN
shell/Handlers/__pycache__/ABS.cpython-313.pyc
Normal file
BIN
shell/Handlers/__pycache__/ABS.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/Handlers/__pycache__/GlobalHandler.cpython-313.pyc
Normal file
BIN
shell/Handlers/__pycache__/GlobalHandler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/Handlers/__pycache__/ModelSpaceHandler.cpython-313.pyc
Normal file
BIN
shell/Handlers/__pycache__/ModelSpaceHandler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/Handlers/__pycache__/PythonappHandler.cpython-313.pyc
Normal file
BIN
shell/Handlers/__pycache__/PythonappHandler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/Handlers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
shell/Handlers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
96
shell/Interactive.py
Normal file
96
shell/Interactive.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import sys
|
||||||
|
from colorama import init, Fore, Style
|
||||||
|
|
||||||
|
from shell.Handlers.GlobalHandler import GlobalHandler
|
||||||
|
from shell.Parser import Parser
|
||||||
|
|
||||||
|
# Инициализация colorama для кроссплатформенной поддержки цветов
|
||||||
|
init()
|
||||||
|
|
||||||
|
|
||||||
|
class Interactive:
|
||||||
|
def __init__(self):
|
||||||
|
self.prompt = Interactive._get_colored_prompt()
|
||||||
|
self.parser = Parser()
|
||||||
|
self.handler = GlobalHandler()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_colored_prompt(cls, prompt = "Vaiola> "):
|
||||||
|
"""Создает градиентный цветной промпт 'Vaiola>'"""
|
||||||
|
colored_prompt = ""
|
||||||
|
|
||||||
|
# Цвета для градиента (от ярко-белого к фиолетовому и обратно к белому)
|
||||||
|
colors = [
|
||||||
|
Fore.LIGHTWHITE_EX, # V
|
||||||
|
Fore.MAGENTA, # a
|
||||||
|
Fore.MAGENTA, # i (немного менее яркий фиолетовый)
|
||||||
|
Fore.LIGHTMAGENTA_EX, # o (фиолетовый)
|
||||||
|
Fore.LIGHTMAGENTA_EX, # l (немного более яркий фиолетовый)
|
||||||
|
Fore.LIGHTWHITE_EX, # a (ярко-фиолетовый)
|
||||||
|
Fore.LIGHTWHITE_EX # > (ярко-белый)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Применяем цвета к каждому символу промпта
|
||||||
|
for i, char in enumerate(prompt):
|
||||||
|
if i < len(colors):
|
||||||
|
colored_prompt += colors[i] + char
|
||||||
|
else:
|
||||||
|
colored_prompt += Fore.LIGHTWHITE_EX + char
|
||||||
|
|
||||||
|
colored_prompt += Style.RESET_ALL
|
||||||
|
return colored_prompt
|
||||||
|
|
||||||
|
def input(self):
|
||||||
|
args = ['']
|
||||||
|
while True:
|
||||||
|
new_args = self.parser.parse(input(self.prompt).strip())
|
||||||
|
if len(new_args) == 0:
|
||||||
|
continue
|
||||||
|
args[len(args) - 1] += '\n' + new_args[0] if args[len(args) - 1] != '' else new_args[0]
|
||||||
|
args.extend(new_args[1:])
|
||||||
|
if self.parser.in_quotes:
|
||||||
|
self.prompt = self._get_colored_prompt("\"____\"> ")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.prompt = self._get_colored_prompt()
|
||||||
|
break
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Запускает интерактивную оболочку"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
command = self.input()
|
||||||
|
try:
|
||||||
|
self.handler.handle(command)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
except RuntimeWarning:
|
||||||
|
print("Warning: last command failed")
|
||||||
|
|
||||||
|
# # Выход из цикла по команде exit или quit
|
||||||
|
# if command.lower() in ['exit', 'quit']:
|
||||||
|
# print("До свидания!")
|
||||||
|
# break
|
||||||
|
|
||||||
|
# # Парсим команду
|
||||||
|
# try:
|
||||||
|
# parsed = Parser.parse(command)
|
||||||
|
# print(f"Парсинг результата: {parsed}")
|
||||||
|
# # Здесь можно добавить обработку команд
|
||||||
|
# except ValueError as e:
|
||||||
|
# print(f"Ошибка: {e}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Запуск интерактивной оболочки
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Interactive().start()
|
||||||
42
shell/Parser.py
Normal file
42
shell/Parser.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
class Parser:
|
||||||
|
def __init__(self):
|
||||||
|
self.in_quotes = False
|
||||||
|
|
||||||
|
|
||||||
|
def parse(self, command: str) -> list[str]:
|
||||||
|
tokens = []
|
||||||
|
current_token = []
|
||||||
|
|
||||||
|
for char in command:
|
||||||
|
if char == '"':
|
||||||
|
if self.in_quotes:
|
||||||
|
# Завершаем токен внутри кавычек
|
||||||
|
tokens.append(''.join(current_token))
|
||||||
|
current_token = []
|
||||||
|
self.in_quotes = False
|
||||||
|
else:
|
||||||
|
# Начинаем новый токен в кавычках
|
||||||
|
if current_token:
|
||||||
|
# Если до кавычек были символы, добавляем их как отдельный токен
|
||||||
|
tokens.append(''.join(current_token))
|
||||||
|
current_token = []
|
||||||
|
self.in_quotes = True
|
||||||
|
elif char == ' ':
|
||||||
|
if self.in_quotes:
|
||||||
|
# Внутри кавычек пробелы добавляем к текущему токену
|
||||||
|
current_token.append(char)
|
||||||
|
else:
|
||||||
|
if current_token:
|
||||||
|
# Завершаем текущий токен, если он есть
|
||||||
|
tokens.append(''.join(current_token))
|
||||||
|
current_token = []
|
||||||
|
else:
|
||||||
|
# Любой символ, кроме кавычек и пробела
|
||||||
|
current_token.append(char)
|
||||||
|
|
||||||
|
if current_token:
|
||||||
|
tokens.append(''.join(current_token))
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
0
shell/__init__.py
Normal file
0
shell/__init__.py
Normal file
BIN
shell/__pycache__/Interactive.cpython-313.pyc
Normal file
BIN
shell/__pycache__/Interactive.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/__pycache__/Parser.cpython-313.pyc
Normal file
BIN
shell/__pycache__/Parser.cpython-313.pyc
Normal file
Binary file not shown.
BIN
shell/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
shell/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user