import os import sys from pathlib import Path from sys import platform from typing import Tuple, Type from pydantic import Field from pydantic_yaml import to_yaml_str from ruamel.yaml import YAML from enum import Enum import logging from pydantic import BaseModel from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, JsonConfigSettingsSource logger = logging.getLogger(__name__) app_name = "DocuMentor" if platform == "win32": tmp_config_path = f"%APPDATA%\\{app_name}\\config.json" elif platform in ("linux", "linux2"): tmp_config_path = f"~/.config/{app_name}/config.json" elif platform == "darwin": tmp_config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json" else: tmp_config_path = f"~/.config/{app_name}/config.json" config_path = Path(os.path.expandvars(tmp_config_path)).expanduser() class JavaVm(BaseModel): id: int version: str path_to_binary_file: Path class DiffPdf(BaseModel): id: int version: str path_to_binary_file: Path default_params: list[str] output_file_extension: str = "pdf" class SaxonJar(BaseModel): id: int version: str path_to_jar_file: Path output_file_extension: str = "fo" class ApacheFop(BaseModel): id: int version: str path_to_dir: Path output_file_extension: str = "pdf" class XslDir(BaseModel): id: int name: str path_to_root_dir: Path class SSLMode(str, Enum): DISABLE = "disable" ALLOW = "allow" PREFER = "prefer" REQUIRE = "require" VERIFY_CA = "verify-ca" VERIFY_FULL = "verify-full" class XsltVersion(str, Enum): """XSLT-Version für Saxon-Transformationen.""" XSLT_1_0 = "1.0" # JAXP API (nur XSLT 1.0) XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0) class GraphLayout(str, Enum): """vis.js Physics-Solver / Layout-Modus.""" BARNES_HUT = "barnesHut" FORCE_ATLAS2 = "forceAtlas2Based" REPULSION = "repulsion" HIERARCHICAL = "hierarchical" class HierarchicalDirection(str, Enum): """Richtung für hierarchisches Layout.""" UD = "UD" DU = "DU" LR = "LR" RL = "RL" class HierarchicalSortMethod(str, Enum): """Sortiermethode für hierarchisches Layout.""" HUBSIZE = "hubsize" DIRECTED = "directed" class GraphLayoutSettings(BaseModel): """Persistierte vis.js Layout-Einstellungen für den XSL-Abhängigkeitsgraph.""" layout: GraphLayout = GraphLayout.BARNES_HUT # barnesHut bh_gravitational_constant: int = -3000 bh_central_gravity: float = 0.3 bh_spring_length: int = 150 bh_spring_constant: float = 0.04 bh_damping: float = 0.09 # forceAtlas2Based fa_gravitational_constant: int = -50 fa_central_gravity: float = 0.01 fa_spring_length: int = 100 fa_spring_constant: float = 0.08 fa_damping: float = 0.4 # repulsion re_node_distance: int = 120 re_central_gravity: float = 0.0 re_spring_length: int = 200 re_spring_constant: float = 0.05 re_damping: float = 0.09 # hierarchical hi_direction: HierarchicalDirection = HierarchicalDirection.UD hi_sort_method: HierarchicalSortMethod = HierarchicalSortMethod.HUBSIZE hi_level_separation: int = 150 hi_node_spacing: int = 100 hi_tree_spacing: int = 200 class PostgreSqlDb(BaseModel): id: int name: str host: str port: int = 5432 database: str username: str password: str ssl_mode: SSLMode = SSLMode.PREFER timeout: int = 10 class Project(BaseModel): id: int = Field(..., description="Eindeutige Projekt-ID", gt=0) name: str = Field(..., description="Projekt-Name", min_length=1, max_length=255) project_dir: Path = Field(..., description="Pfad zum Projekt-Verzeichnis") java_vm_id: int = Field(..., description="ID der Java VM", gt=0) diff_pdf_id: int = Field(..., description="ID der diff-pdf Konfiguration", gt=0) saxon_jar_id: int = Field(..., description="ID der Saxon JAR Konfiguration", gt=0) apache_fop_id: int = Field(..., description="ID der Apache FOP Konfiguration", gt=0) xsl_dir_id: int = Field(..., description="ID des XSL-Verzeichnisses", gt=0) postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0) fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis") xslt_params: dict[str, str] = Field(default_factory=dict, description="Projektweite XSLT-Parameter") @staticmethod def _lookup(collection, item_id: int, attr: str) -> str: """Sucht einen Wert in einer Konfigurationsliste anhand der ID.""" value = [getattr(x, attr) for x in collection if x.id == item_id] return value[0] if value else "" def getXsl(self) -> str: return self._lookup(app_settings.xsl_dirs, self.xsl_dir_id, "name") def getJavaVm(self) -> str: return self._lookup(app_settings.java_vms, self.java_vm_id, "version") def getSaxon(self) -> str: return self._lookup(app_settings.saxon_jars, self.saxon_jar_id, "version") def getApacheFop(self) -> str: return self._lookup(app_settings.apache_fops, self.apache_fop_id, "version") def getDiffPdf(self) -> str: return self._lookup(app_settings.diff_pdfs, self.diff_pdf_id, "version") def getPostgreSqlDb(self) -> str: return self._lookup(app_settings.postgresql_dbs, self.postgre_sql_db_id, "name") class AppSettings(BaseSettings): java_vms: list[JavaVm] = [] diff_pdfs: list[DiffPdf] = [] saxon_jars: list[SaxonJar] = [] apache_fops: list[ApacheFop] = [] xsl_dirs: list[XslDir] = [] pdf_projects: list[Project] = [] postgresql_dbs: list[PostgreSqlDb] = [] theme: str | None = None max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8) use_saxon_worker_pool: bool = True # SaxonWorkerPool aktivieren (schneller, benötigt JDK) saxon_xslt_version: XsltVersion = XsltVersion.XSLT_2_0_3_0 # XSLT-Version für Saxon (Standard: 2.0/3.0 mit s9api) use_fop_worker_pool: bool = True # FopWorkerPool aktivieren (schneller, benötigt JDK) # UI-Zustand window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height) splitter_sizes: list[int] | None = None # Splitter-Positionen tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten graph_layout_settings: GraphLayoutSettings = Field(default_factory=GraphLayoutSettings) model_config = SettingsConfigDict(json_file=config_path) @classmethod def settings_customise_sources( cls, settings_cls: Type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> Tuple[PydanticBaseSettingsSource, ...]: return (JsonConfigSettingsSource(settings_cls),) def save(self): # Ordner existert nicht if not config_path.parent.exists(): config_path.parent.mkdir(parents=True, exist_ok=True) if not config_path.parent.is_dir() or not os.access(config_path.parent, os.W_OK): logger.exception(f"{config_path.parent} ist kein Verzeichnis oder es gibt keine Schreibrechte") sys.exit(1) # Konfiguration speichern with open(config_path, "wb") as c: c.write(app_settings.model_dump_json(indent=4).encode()) app_settings = AppSettings() class XmlFile(BaseModel): xml: Path # blake2b hashsum hashsum: str | None = None class XslFile(BaseModel): id: tuple bez: str xsl_file: Path xslt_params: dict[str, str] = {} xmls: list[XmlFile] = [] class TreeNode(BaseModel): id: tuple bez: str xslt_params: dict[str, str] = {} children: list["TreeNode|XslFile"] class ProjectData(BaseModel): """ Speichert die Projekteinstellungen direkt im Projektordner in einer .yaml-Datei. """ nodes: list[TreeNode] = [] expanded_nodes: list[tuple] | None = None # Optional: IDs der aufgeklappten Knoten (TreeNode und XslFile) @classmethod def readSettings(cls, project_dir: Path): # Explizit UTF-8 Encoding verwenden project_yaml_path = project_dir / "project.yaml" with open(project_yaml_path, "r", encoding="utf-8") as f: yaml = YAML(typ="safe") yaml_data = yaml.load(f) return cls.model_validate(yaml_data) def writeSettings(self, project_dir: Path): with open(project_dir / "project.yaml", "w", encoding="utf8") as f: f.write(to_yaml_str(self))