import json import traceback from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Dict, Literal from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator from rich.console import Console from utils.tools import CONFIGS class ToolsConfig(BaseModel): apktool_jar_url: str apktool_wrapper_url: str @field_validator("apktool_jar_url", "apktool_wrapper_url") @classmethod def validate_url(cls, v: str) -> str: if not v.startswith(("http://", "https://")): raise ValueError("URL должен начинаться с http:// или https://") return v class SigningConfig(BaseModel): keystore: Path = Field(default=Path("keystore.jks")) keystore_pass_file: Path = Field(default=Path("keystore.pass")) v1_signing: bool = False v2_signing: bool = True v3_signing: bool = True class BuildConfig(BaseModel): verbose: bool = False force: bool = False clean_after_build: bool = True class Config(BaseModel): tools: ToolsConfig signing: SigningConfig = Field(default_factory=SigningConfig) build: BuildConfig = Field(default_factory=BuildConfig) base: Dict[str, Any] = Field(default_factory=dict) def load_config(console: Console) -> Config: """Загружает и валидирует конфигурацию""" config_path = Path("config.json") if not config_path.exists(): console.print("[red]Файл config.json не найден") raise typer.Exit(1) try: return Config.model_validate_json(config_path.read_text()) except ValidationError as e: console.print(f"[red]Ошибка валидации config.json:\n{e}") raise typer.Exit(1) class PatchTemplate(BaseModel, ABC): model_config = ConfigDict(arbitrary_types_allowed=True, validate_default=True) enabled: bool = Field(default=True, description="Включить или отключить патч") priority: int = Field(default=0, description="Приоритет применения патча") _name: str = PrivateAttr() _applied: bool = PrivateAttr(default=False) _console: Console | None = PrivateAttr(default=None) def __init__(self, name: str, console: Console, **data): loaded_data = self._load_config_static(name, console) merged_data = {**loaded_data, **data} valid_fields = set(self.model_fields.keys()) filtered_data = {k: v for k, v in merged_data.items() if k in valid_fields} super().__init__(**filtered_data) self._name = name self._console = console self._applied = False @staticmethod def _load_config_static(name: str, console: Console | None) -> Dict[str, Any]: """Загружает конфигурацию из файла (статический метод)""" config_path = CONFIGS / f"{name}.json" try: if config_path.exists(): return json.loads(config_path.read_text()) except Exception as e: if console: console.print( f"[red]Ошибка при загрузке конфигурации патча {name}: {e}" ) console.print(f"[yellow]Используются значения по умолчанию") return {} def save_config(self) -> None: """Сохраняет конфигурацию в файл""" config_path = CONFIGS / f"{self._name}.json" config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(self.model_dump_json(indent=2)) @property def name(self) -> str: return self._name @property def applied(self) -> bool: return self._applied @applied.setter def applied(self, value: bool) -> None: self._applied = value @property def console(self) -> Console | None: return self._console @abstractmethod def apply(self, base: Dict[str, Any]) -> Any: raise NotImplementedError( "Попытка применения шаблона патча, а не его реализации" ) def safe_apply(self, base: Dict[str, Any]) -> bool: """Безопасно применяет патч с обработкой ошибок""" try: self._applied = self.apply(base) return self._applied except Exception as e: if self._console: self._console.print(f"[red]Ошибка в патче {self._name}: {e}") if base.get("verbose"): self._console.print_exception() return False