Files
xsl-validator/src/conf.py
T
info b900455d69 Fix: Code-Qualität und Effizienz verbessern (v1.5.1)
- main.py: print() durch logging ersetzt, cleanup nach Logger-Init verschoben
- conf.py: funktionsloses global-Statement entfernt
- database.py: unerreichbaren zweiten Projekt-Check entfernt
- hash_calculation.py: deprecated _handle_xml_file_drop entfernt, nutzlosen
  _get_all_project_xml_files-Wrapper entfernt, seen_paths-Scope-Bug in
  rekursiver Traversierung behoben (O(N²) → O(N)), veraltete List[]-Syntax
  und ungenutzte Imports bereinigt
- transform.py: TOCTOU-Muster (exists+stat) durch direktes stat() mit
  FileNotFoundError ersetzt; fop_conf.exists() gecacht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 17:02:59 +02:00

283 lines
8.5 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))