Feat: Sidebar mit Suchfilter und lxml-Parser im XslDependencyDialog

- Ein-/ausblendbare Sidebar mit tab-übergreifender Suche hinzugefügt
- Graph-Suchfilter blendet nicht-betroffene XSL-Dateien aus dem Netzwerkgraph aus
- Regex-basierte XSL-Abhängigkeitserkennung durch lxml-Parser ersetzt
- Suchfilter wird beim Tab-Wechsel erneut angewendet

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 21:15:16 +01:00
parent 48ab596476
commit 3dcbf783b1
6 changed files with 639 additions and 183 deletions
+1
View File
@@ -12,6 +12,7 @@ dependencies = [
"connectorx>=0.4.0", "connectorx>=0.4.0",
"pydantic-yaml>=1.6.0", "pydantic-yaml>=1.6.0",
"psutil>=6.1.1", "psutil>=6.1.1",
"lxml>=6.0.2",
] ]
[tool.ruff] [tool.ruff]
+158 -2
View File
@@ -47,9 +47,17 @@ class XslDependencyDialog(QDialog):
self._graph_loaded = False self._graph_loaded = False
self._temp_html_file: tempfile.NamedTemporaryFile | None = None self._temp_html_file: tempfile.NamedTemporaryFile | None = None
self._sidebar_visible = False
self.ui = Ui_XslDependencyDialog() self.ui = Ui_XslDependencyDialog()
self.ui.setupUi(self) self.ui.setupUi(self)
# Sidebar initial ausblenden
self.ui.settingsButton.setIcon(QIcon.fromTheme("preferences-system"))
self.ui.sidebarWidget.setVisible(False)
self.ui.mainSplitter.setSizes([1000, 0])
self.ui.settingsButton.clicked.connect(self._toggle_sidebar)
# Netzwerkgraph-Tab ausblenden wenn WebEngine nicht verfügbar # Netzwerkgraph-Tab ausblenden wenn WebEngine nicht verfügbar
if not HAS_WEBENGINE: if not HAS_WEBENGINE:
self.ui.tabWidget.removeTab(1) self.ui.tabWidget.removeTab(1)
@@ -72,6 +80,9 @@ class XslDependencyDialog(QDialog):
self.ui.fileTree.currentItemChanged.connect(self._on_file_selected) self.ui.fileTree.currentItemChanged.connect(self._on_file_selected)
self.ui.searchEdit.textChanged.connect(self._on_search_changed) self.ui.searchEdit.textChanged.connect(self._on_search_changed)
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed) self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
self.ui.graphFilterConnectedCheck.toggled.connect(
lambda: self._on_search_changed(self.ui.searchEdit.text())
)
def closeEvent(self, event): def closeEvent(self, event):
"""Räumt temporäre Dateien und QWebEngineView auf.""" """Räumt temporäre Dateien und QWebEngineView auf."""
@@ -197,17 +208,46 @@ class XslDependencyDialog(QDialog):
self.ui.depTree.addTopLevelItem(no_deps_item) self.ui.depTree.addTopLevelItem(no_deps_item)
def _on_search_changed(self, text: str): def _on_search_changed(self, text: str):
"""Filtert je nach aktivem Tab die Dateiliste oder den Netzwerkgraph."""
current_tab = self.ui.tabWidget.currentIndex()
if current_tab == 0:
self._search_tree(text)
elif current_tab == 1 and self._web_view is not None:
self._search_graph(text)
def _search_tree(self, text: str):
"""Filtert die Dateiliste nach dem Suchbegriff.""" """Filtert die Dateiliste nach dem Suchbegriff."""
search_lower = text.lower() search_lower = text.lower()
for i in range(self.ui.fileTree.topLevelItemCount()): for i in range(self.ui.fileTree.topLevelItemCount()):
item = self.ui.fileTree.topLevelItem(i) item = self.ui.fileTree.topLevelItem(i)
matches = search_lower in item.text(0).lower() matches = not search_lower or search_lower in item.text(0).lower()
item.setHidden(not matches) item.setHidden(not matches)
def _search_graph(self, text: str):
"""Filtert oder dimmt Knoten im Netzwerkgraph je nach Checkbox-Einstellung."""
escaped = text.replace("\\", "\\\\").replace("'", "\\'")
hide_unrelated = str(self.ui.graphFilterConnectedCheck.isChecked()).lower()
self._web_view.page().runJavaScript(f"filterNodes('{escaped}', {hide_unrelated})")
# === Sidebar ===
def _toggle_sidebar(self):
"""Blendet die Einstellungs-Sidebar ein oder aus."""
self._sidebar_visible = not self._sidebar_visible
if self._sidebar_visible:
self.ui.sidebarWidget.setVisible(True)
self.ui.mainSplitter.setSizes([750, 250])
else:
self.ui.mainSplitter.setSizes([1000, 0])
self.ui.sidebarWidget.setVisible(False)
# === Netzwerkgraph (vis.js) === # === Netzwerkgraph (vis.js) ===
def _on_tab_changed(self, index: int): def _on_tab_changed(self, index: int):
"""Lazy-Init des Netzwerkgraphs beim ersten Tab-Wechsel.""" """Lazy-Init des Netzwerkgraphs beim ersten Tab-Wechsel und Sidebar-Seite synchronisieren."""
# Sidebar-Seite synchronisieren
self.ui.settingsStack.setCurrentIndex(index)
if not HAS_WEBENGINE: if not HAS_WEBENGINE:
return return
@@ -215,6 +255,11 @@ class XslDependencyDialog(QDialog):
if index == 1 and not self._graph_loaded: if index == 1 and not self._graph_loaded:
self._setup_network_graph() self._setup_network_graph()
# Suchbegriff auf den neuen Tab anwenden
search_text = self.ui.searchEdit.text()
if search_text:
self._on_search_changed(search_text)
def _setup_network_graph(self): def _setup_network_graph(self):
"""Erstellt QWebEngineView und lädt den vis.js Netzwerkgraph.""" """Erstellt QWebEngineView und lädt den vis.js Netzwerkgraph."""
self._web_view = QWebEngineView(self.ui.graphContainer) self._web_view = QWebEngineView(self.ui.graphContainer)
@@ -474,6 +519,117 @@ class XslDependencyDialog(QDialog):
}}); }});
edges.update(edgeUpdates); edges.update(edgeUpdates);
}}); }});
// Suchfunktion: Knoten nach Label filtern
function filterNodes(searchTerm, hideUnrelated) {{
var term = searchTerm.toLowerCase();
if (hideUnrelated) {{
// Originalzustand wiederherstellen
nodes.clear();
edges.clear();
nodes.add(nodesData);
edges.add(edgesData);
allNodes = nodes.get();
if (!term) return;
// 1. Matching-Knoten finden
var matchedIds = new Set();
nodesData.forEach(function(node) {{
if (node.label.toLowerCase().indexOf(term) !== -1) {{
matchedIds.add(node.id);
}}
}});
// 2. Gerichtete Adjazenzlisten aufbauen
var fwd = {{}}; // from → to (Import-Richtung)
var bwd = {{}}; // to → from (Reverse)
edgesData.forEach(function(edge) {{
if (!fwd[edge.from]) fwd[edge.from] = [];
if (!bwd[edge.to]) bwd[edge.to] = [];
fwd[edge.from].push(edge.to);
bwd[edge.to].push(edge.from);
}});
// 3. BFS vorwärts (Nachkommen) + rückwärts (Vorfahren)
var reachable = new Set(matchedIds);
var queue = Array.from(matchedIds);
while (queue.length > 0) {{
var current = queue.shift();
(fwd[current] || []).forEach(function(n) {{
if (!reachable.has(n)) {{ reachable.add(n); queue.push(n); }}
}});
}}
queue = Array.from(matchedIds);
while (queue.length > 0) {{
var current = queue.shift();
(bwd[current] || []).forEach(function(n) {{
if (!reachable.has(n)) {{ reachable.add(n); queue.push(n); }}
}});
}}
// 4. Nicht-erreichbare Knoten und Kanten entfernen
var removeNodeIds = [];
nodesData.forEach(function(node) {{
if (!reachable.has(node.id)) removeNodeIds.push(node.id);
}});
nodes.remove(removeNodeIds);
var removeEdgeIds = [];
edgesData.forEach(function(edge) {{
if (!reachable.has(edge.from) || !reachable.has(edge.to))
removeEdgeIds.push(edge.id);
}});
edges.remove(removeEdgeIds);
// Matching-Knoten hervorheben, verbundene leicht dimmen
var updates = [];
matchedIds.forEach(function(id) {{
updates.push({{ id: id, opacity: 1.0 }});
}});
reachable.forEach(function(id) {{
if (!matchedIds.has(id)) {{
updates.push({{ id: id, opacity: 0.5 }});
}}
}});
nodes.update(updates);
allNodes = nodes.get();
}} else {{
// Originalzustand wiederherstellen falls vorher gefiltert
nodes.clear();
edges.clear();
nodes.add(nodesData);
edges.add(edgesData);
allNodes = nodes.get();
// Bisheriges Verhalten: nur dimmen
var updates = [];
var matchedIds = new Set();
allNodes.forEach(function(node) {{
if (!term || node.label.toLowerCase().indexOf(term) !== -1) {{
updates.push({{ id: node.id, opacity: 1.0 }});
matchedIds.add(node.id);
}} else {{
updates.push({{ id: node.id, opacity: 0.12 }});
}}
}});
nodes.update(updates);
var allEdges = edges.get();
var edgeUpdates = [];
allEdges.forEach(function(edge) {{
if (!term || (matchedIds.has(edge.from) && matchedIds.has(edge.to))) {{
edgeUpdates.push({{ id: edge.id, color: {{ opacity: 0.7 }} }});
}} else {{
edgeUpdates.push({{ id: edge.id, color: {{ opacity: 0.05 }} }});
}}
}});
edges.update(edgeUpdates);
}}
}}
</script> </script>
</body> </body>
</html>""" </html>"""
+276 -145
View File
@@ -15,157 +15,281 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QTabWidget" name="tabWidget"> <layout class="QHBoxLayout" name="toolbarLayout">
<property name="currentIndex"> <item>
<number>0</number> <spacer name="toolbarSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="settingsButton">
<property name="maximumSize">
<size>
<width>28</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>Einstellungen ein-/ausblenden</string>
</property>
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="mainSplitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<widget class="QWidget" name="treeTab"> <property name="childrenCollapsible">
<attribute name="title"> <bool>true</bool>
<string>Baumansicht</string> </property>
</attribute> <widget class="QTabWidget" name="tabWidget">
<layout class="QVBoxLayout" name="treeTabLayout"> <property name="currentIndex">
<item> <number>0</number>
<layout class="QHBoxLayout" name="searchLayout"> </property>
<item> <widget class="QWidget" name="treeTab">
<widget class="QLabel" name="searchLabel"> <attribute name="title">
<property name="text"> <string>Baumansicht</string>
<string>Suche:</string> </attribute>
</property> <layout class="QVBoxLayout" name="treeTabLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="leftWidget" native="true">
<layout class="QVBoxLayout" name="leftLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="leftLabel">
<property name="text">
<string>XSL-Dateien</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="fileTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget> </widget>
</item> <widget class="QWidget" name="rightWidget" native="true">
<item> <layout class="QVBoxLayout" name="rightLayout">
<widget class="QLineEdit" name="searchEdit"> <property name="leftMargin">
<property name="placeholderText"> <number>0</number>
<string>XSL-Datei filtern...</string> </property>
</property> <property name="topMargin">
<property name="clearButtonEnabled"> <number>0</number>
<bool>true</bool> </property>
</property> <property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="rightLabel">
<property name="text">
<string>Abhängigkeiten</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="depTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget> </widget>
</item> </widget>
</layout> </item>
</item> </layout>
</widget>
<widget class="QWidget" name="graphTab">
<attribute name="title">
<string>Netzwerkgraph</string>
</attribute>
<layout class="QVBoxLayout" name="graphTabLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="graphContainer" native="true">
<layout class="QVBoxLayout" name="graphContainerLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="sidebarWidget" native="true">
<layout class="QVBoxLayout" name="sidebarLayout">
<item> <item>
<widget class="QSplitter" name="splitter"> <widget class="QLabel" name="sidebarLabel">
<property name="orientation"> <property name="font">
<enum>Qt::Horizontal</enum> <font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Einstellungen</string>
</property> </property>
<widget class="QWidget" name="leftWidget" native="true">
<layout class="QVBoxLayout" name="leftLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="leftLabel">
<property name="text">
<string>XSL-Dateien</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="fileTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="rightWidget" native="true">
<layout class="QVBoxLayout" name="rightLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="rightLabel">
<property name="text">
<string>Abhängigkeiten</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="depTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
</layout>
</widget>
<widget class="QWidget" name="graphTab">
<attribute name="title">
<string>Netzwerkgraph</string>
</attribute>
<layout class="QVBoxLayout" name="graphTabLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QWidget" name="graphContainer" native="true"> <widget class="QLabel" name="searchLabel">
<layout class="QVBoxLayout" name="graphContainerLayout"> <property name="text">
<property name="leftMargin"> <string>Suche:</string>
<number>0</number> </property>
</property> </widget>
<property name="topMargin"> </item>
<number>0</number> <item>
</property> <widget class="QLineEdit" name="searchEdit">
<property name="rightMargin"> <property name="placeholderText">
<number>0</number> <string>XSL-Datei filtern...</string>
</property> </property>
<property name="bottomMargin"> <property name="clearButtonEnabled">
<number>0</number> <bool>true</bool>
</property> </property>
</layout> </widget>
</item>
<item>
<widget class="QStackedWidget" name="settingsStack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="treeSettingsPage">
<layout class="QVBoxLayout" name="treeSettingsLayout">
<item>
<widget class="QLabel" name="treeSettingsLabel">
<property name="text">
<string>Baumansicht-Einstellungen</string>
</property>
</widget>
</item>
<item>
<spacer name="treeSettingsSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="graphSettingsPage">
<layout class="QVBoxLayout" name="graphSettingsLayout">
<item>
<widget class="QLabel" name="graphSettingsLabel">
<property name="text">
<string>Graph-Einstellungen</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="graphFilterConnectedCheck">
<property name="text">
<string>Nur betroffene Dateien anzeigen</string>
</property>
<property name="toolTip">
<string>Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="graphSettingsSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
</layout> </layout>
@@ -174,6 +298,12 @@
</item> </item>
<item> <item>
<widget class="QLabel" name="statusLabel"> <widget class="QLabel" name="statusLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text"> <property name="text">
<string/> <string/>
</property> </property>
@@ -182,10 +312,10 @@
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="standardButtons"> <property name="standardButtons">
<set>QDialogButtonBox::Close</set> <set>QDialogButtonBox::StandardButton::Close</set>
</property> </property>
<property name="centerButtons"> <property name="centerButtons">
<bool>true</bool> <bool>true</bool>
@@ -194,6 +324,7 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<resources/>
<connections> <connections>
<connection> <connection>
<sender>buttonBox</sender> <sender>buttonBox</sender>
+114 -27
View File
@@ -15,10 +15,12 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog, from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel, QDialog, QDialogButtonBox, QHBoxLayout, QHeaderView,
QLineEdit, QSizePolicy, QSplitter, QTabWidget, QLabel, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QSplitter, QStackedWidget, QTabWidget,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget) QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_XslDependencyDialog(object): class Ui_XslDependencyDialog(object):
def setupUi(self, XslDependencyDialog): def setupUi(self, XslDependencyDialog):
if not XslDependencyDialog.objectName(): if not XslDependencyDialog.objectName():
@@ -26,31 +28,35 @@ class Ui_XslDependencyDialog(object):
XslDependencyDialog.resize(1000, 700) XslDependencyDialog.resize(1000, 700)
self.verticalLayout = QVBoxLayout(XslDependencyDialog) self.verticalLayout = QVBoxLayout(XslDependencyDialog)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setObjectName(u"verticalLayout")
self.tabWidget = QTabWidget(XslDependencyDialog) self.toolbarLayout = QHBoxLayout()
self.toolbarLayout.setObjectName(u"toolbarLayout")
self.toolbarSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.toolbarLayout.addItem(self.toolbarSpacer)
self.settingsButton = QPushButton(XslDependencyDialog)
self.settingsButton.setObjectName(u"settingsButton")
self.settingsButton.setMaximumSize(QSize(28, 28))
self.settingsButton.setFlat(True)
self.toolbarLayout.addWidget(self.settingsButton)
self.verticalLayout.addLayout(self.toolbarLayout)
self.mainSplitter = QSplitter(XslDependencyDialog)
self.mainSplitter.setObjectName(u"mainSplitter")
self.mainSplitter.setOrientation(Qt.Orientation.Horizontal)
self.mainSplitter.setChildrenCollapsible(True)
self.tabWidget = QTabWidget(self.mainSplitter)
self.tabWidget.setObjectName(u"tabWidget") self.tabWidget.setObjectName(u"tabWidget")
self.treeTab = QWidget() self.treeTab = QWidget()
self.treeTab.setObjectName(u"treeTab") self.treeTab.setObjectName(u"treeTab")
self.treeTabLayout = QVBoxLayout(self.treeTab) self.treeTabLayout = QVBoxLayout(self.treeTab)
self.treeTabLayout.setObjectName(u"treeTabLayout") self.treeTabLayout.setObjectName(u"treeTabLayout")
self.searchLayout = QHBoxLayout()
self.searchLayout.setObjectName(u"searchLayout")
self.searchLabel = QLabel(self.treeTab)
self.searchLabel.setObjectName(u"searchLabel")
self.searchLayout.addWidget(self.searchLabel)
self.searchEdit = QLineEdit(self.treeTab)
self.searchEdit.setObjectName(u"searchEdit")
self.searchEdit.setClearButtonEnabled(True)
self.searchLayout.addWidget(self.searchEdit)
self.treeTabLayout.addLayout(self.searchLayout)
self.splitter = QSplitter(self.treeTab) self.splitter = QSplitter(self.treeTab)
self.splitter.setObjectName(u"splitter") self.splitter.setObjectName(u"splitter")
self.splitter.setOrientation(Qt.Horizontal) self.splitter.setOrientation(Qt.Orientation.Horizontal)
self.leftWidget = QWidget(self.splitter) self.leftWidget = QWidget(self.splitter)
self.leftWidget.setObjectName(u"leftWidget") self.leftWidget.setObjectName(u"leftWidget")
self.leftLayout = QVBoxLayout(self.leftWidget) self.leftLayout = QVBoxLayout(self.leftWidget)
@@ -67,7 +73,7 @@ class Ui_XslDependencyDialog(object):
self.fileTree.setHeaderItem(__qtreewidgetitem) self.fileTree.setHeaderItem(__qtreewidgetitem)
self.fileTree.setObjectName(u"fileTree") self.fileTree.setObjectName(u"fileTree")
self.fileTree.setAlternatingRowColors(True) self.fileTree.setAlternatingRowColors(True)
self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection) self.fileTree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.fileTree.setRootIsDecorated(True) self.fileTree.setRootIsDecorated(True)
self.leftLayout.addWidget(self.fileTree) self.leftLayout.addWidget(self.fileTree)
@@ -112,18 +118,87 @@ class Ui_XslDependencyDialog(object):
self.graphTabLayout.addWidget(self.graphContainer) self.graphTabLayout.addWidget(self.graphContainer)
self.tabWidget.addTab(self.graphTab, "") self.tabWidget.addTab(self.graphTab, "")
self.mainSplitter.addWidget(self.tabWidget)
self.sidebarWidget = QWidget(self.mainSplitter)
self.sidebarWidget.setObjectName(u"sidebarWidget")
self.sidebarLayout = QVBoxLayout(self.sidebarWidget)
self.sidebarLayout.setObjectName(u"sidebarLayout")
self.sidebarLabel = QLabel(self.sidebarWidget)
self.sidebarLabel.setObjectName(u"sidebarLabel")
font = QFont()
font.setBold(True)
self.sidebarLabel.setFont(font)
self.verticalLayout.addWidget(self.tabWidget) self.sidebarLayout.addWidget(self.sidebarLabel)
self.searchLabel = QLabel(self.sidebarWidget)
self.searchLabel.setObjectName(u"searchLabel")
self.sidebarLayout.addWidget(self.searchLabel)
self.searchEdit = QLineEdit(self.sidebarWidget)
self.searchEdit.setObjectName(u"searchEdit")
self.searchEdit.setClearButtonEnabled(True)
self.sidebarLayout.addWidget(self.searchEdit)
self.settingsStack = QStackedWidget(self.sidebarWidget)
self.settingsStack.setObjectName(u"settingsStack")
self.treeSettingsPage = QWidget()
self.treeSettingsPage.setObjectName(u"treeSettingsPage")
self.treeSettingsLayout = QVBoxLayout(self.treeSettingsPage)
self.treeSettingsLayout.setObjectName(u"treeSettingsLayout")
self.treeSettingsLabel = QLabel(self.treeSettingsPage)
self.treeSettingsLabel.setObjectName(u"treeSettingsLabel")
self.treeSettingsLayout.addWidget(self.treeSettingsLabel)
self.treeSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.treeSettingsLayout.addItem(self.treeSettingsSpacer)
self.settingsStack.addWidget(self.treeSettingsPage)
self.graphSettingsPage = QWidget()
self.graphSettingsPage.setObjectName(u"graphSettingsPage")
self.graphSettingsLayout = QVBoxLayout(self.graphSettingsPage)
self.graphSettingsLayout.setObjectName(u"graphSettingsLayout")
self.graphSettingsLabel = QLabel(self.graphSettingsPage)
self.graphSettingsLabel.setObjectName(u"graphSettingsLabel")
self.graphSettingsLayout.addWidget(self.graphSettingsLabel)
self.graphFilterConnectedCheck = QCheckBox(self.graphSettingsPage)
self.graphFilterConnectedCheck.setObjectName(u"graphFilterConnectedCheck")
self.graphFilterConnectedCheck.setChecked(True)
self.graphSettingsLayout.addWidget(self.graphFilterConnectedCheck)
self.graphSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.graphSettingsLayout.addItem(self.graphSettingsSpacer)
self.settingsStack.addWidget(self.graphSettingsPage)
self.sidebarLayout.addWidget(self.settingsStack)
self.mainSplitter.addWidget(self.sidebarWidget)
self.verticalLayout.addWidget(self.mainSplitter)
self.statusLabel = QLabel(XslDependencyDialog) self.statusLabel = QLabel(XslDependencyDialog)
self.statusLabel.setObjectName(u"statusLabel") self.statusLabel.setObjectName(u"statusLabel")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
self.statusLabel.setSizePolicy(sizePolicy)
self.verticalLayout.addWidget(self.statusLabel) self.verticalLayout.addWidget(self.statusLabel)
self.buttonBox = QDialogButtonBox(XslDependencyDialog) self.buttonBox = QDialogButtonBox(XslDependencyDialog)
self.buttonBox.setObjectName(u"buttonBox") self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Close) self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close)
self.buttonBox.setCenterButtons(True) self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox) self.verticalLayout.addWidget(self.buttonBox)
@@ -133,6 +208,7 @@ class Ui_XslDependencyDialog(object):
self.buttonBox.rejected.connect(XslDependencyDialog.reject) self.buttonBox.rejected.connect(XslDependencyDialog.reject)
self.tabWidget.setCurrentIndex(0) self.tabWidget.setCurrentIndex(0)
self.settingsStack.setCurrentIndex(0)
QMetaObject.connectSlotsByName(XslDependencyDialog) QMetaObject.connectSlotsByName(XslDependencyDialog)
@@ -140,12 +216,23 @@ class Ui_XslDependencyDialog(object):
def retranslateUi(self, XslDependencyDialog): def retranslateUi(self, XslDependencyDialog):
XslDependencyDialog.setWindowTitle(QCoreApplication.translate("XslDependencyDialog", u"XSL-Abh\u00e4ngigkeitsgraph", None)) XslDependencyDialog.setWindowTitle(QCoreApplication.translate("XslDependencyDialog", u"XSL-Abh\u00e4ngigkeitsgraph", None))
self.searchLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Suche:", None)) #if QT_CONFIG(tooltip)
self.searchEdit.setPlaceholderText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Datei filtern...", None)) self.settingsButton.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen ein-/ausblenden", None))
#endif // QT_CONFIG(tooltip)
self.settingsButton.setText("")
self.leftLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Dateien", None)) self.leftLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Dateien", None))
self.rightLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Abh\u00e4ngigkeiten", None)) self.rightLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Abh\u00e4ngigkeiten", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.treeTab), QCoreApplication.translate("XslDependencyDialog", u"Baumansicht", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.treeTab), QCoreApplication.translate("XslDependencyDialog", u"Baumansicht", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.graphTab), QCoreApplication.translate("XslDependencyDialog", u"Netzwerkgraph", None)) self.tabWidget.setTabText(self.tabWidget.indexOf(self.graphTab), QCoreApplication.translate("XslDependencyDialog", u"Netzwerkgraph", None))
self.sidebarLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen", None))
self.searchLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Suche:", None))
self.searchEdit.setPlaceholderText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Datei filtern...", None))
self.treeSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Baumansicht-Einstellungen", None))
self.graphSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Graph-Einstellungen", None))
self.graphFilterConnectedCheck.setText(QCoreApplication.translate("XslDependencyDialog", u"Nur betroffene Dateien anzeigen", None))
#if QT_CONFIG(tooltip)
self.graphFilterConnectedCheck.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind", None))
#endif // QT_CONFIG(tooltip)
self.statusLabel.setText("") self.statusLabel.setText("")
# retranslateUi # retranslateUi
+26 -9
View File
@@ -7,13 +7,28 @@ Der Graph wird im Speicher gehalten und bei Änderungen (mtime) automatisch inva
""" """
import logging import logging
import re import time
from lxml import etree
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Regex für xsl:import und xsl:include href-Attribute _XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"
_IMPORT_INCLUDE_PATTERN = re.compile(r'<xsl:(?:import|include)\s+href=["\']([^"\']+)["\']', re.IGNORECASE)
def _parse_import_include_hrefs(content: str) -> list[str]:
"""Parst XSL-Dateiinhalt und gibt alle href-Werte von xsl:import/xsl:include zurück."""
try:
root = etree.fromstring(content.encode("utf-8"))
except etree.XMLSyntaxError:
return []
hrefs = []
for tag in ("import", "include"):
for elem in root.iter(f"{{{_XSL_NAMESPACE}}}{tag}"):
href = elem.get("href")
if href:
hrefs.append(href)
return hrefs
class XslDependencyGraph: class XslDependencyGraph:
@@ -84,8 +99,7 @@ class XslDependencyGraph:
return result return result
# Finde alle import/include-Referenzen # Finde alle import/include-Referenzen
for match in _IMPORT_INCLUDE_PATTERN.finditer(content): for href in _parse_import_include_hrefs(content):
href = match.group(1)
referenced_path = (xsl_file.parent / href).resolve() referenced_path = (xsl_file.parent / href).resolve()
if referenced_path.exists() and referenced_path not in visited: if referenced_path.exists() and referenced_path not in visited:
@@ -138,12 +152,14 @@ class XslDependencyGraph:
if not xsl_root_dir.exists(): if not xsl_root_dir.exists():
return graph return graph
start = time.perf_counter()
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")): for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve() xsl_file = xsl_file.resolve()
deps = self.get_dependencies(xsl_file) deps = self.get_dependencies(xsl_file)
graph[xsl_file] = deps graph[xsl_file] = deps
elapsed = time.perf_counter() - start
logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien") logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
return graph return graph
def _get_direct_dependencies(self, xsl_file: Path) -> set[Path]: def _get_direct_dependencies(self, xsl_file: Path) -> set[Path]:
@@ -168,8 +184,7 @@ class XslDependencyGraph:
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})") logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
return result return result
for match in _IMPORT_INCLUDE_PATTERN.finditer(content): for href in _parse_import_include_hrefs(content):
href = match.group(1)
referenced_path = (xsl_file.parent / href).resolve() referenced_path = (xsl_file.parent / href).resolve()
if referenced_path.exists(): if referenced_path.exists():
result.add(referenced_path) result.add(referenced_path)
@@ -191,11 +206,13 @@ class XslDependencyGraph:
if not xsl_root_dir.exists(): if not xsl_root_dir.exists():
return graph return graph
start = time.perf_counter()
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")): for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve() xsl_file = xsl_file.resolve()
graph[xsl_file] = self._get_direct_dependencies(xsl_file) graph[xsl_file] = self._get_direct_dependencies(xsl_file)
elapsed = time.perf_counter() - start
logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien") logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
return graph return graph
def invalidate(self, xsl_file: Path | None = None): def invalidate(self, xsl_file: Path | None = None):
Generated
+64
View File
@@ -38,6 +38,7 @@ version = "1.0.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "connectorx" }, { name = "connectorx" },
{ name = "lxml" },
{ name = "polars", extra = ["connectorx", "pyarrow"] }, { name = "polars", extra = ["connectorx", "pyarrow"] },
{ name = "psutil" }, { name = "psutil" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@@ -55,6 +56,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "connectorx", specifier = ">=0.4.0" }, { name = "connectorx", specifier = ">=0.4.0" },
{ name = "lxml", specifier = ">=6.0.2" },
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.37.0" }, { name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.37.0" },
{ name = "psutil", specifier = ">=6.1.1" }, { name = "psutil", specifier = ">=6.1.1" },
{ name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" },
@@ -69,6 +71,68 @@ dev = [
{ name = "ruff", specifier = ">=0.14.11" }, { name = "ruff", specifier = ">=0.14.11" },
] ]
[[package]]
name = "lxml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
{ url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
{ url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
{ url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
{ url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
{ url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
{ url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
{ url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
{ url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
{ url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
{ url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
{ url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
{ url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
]
[[package]] [[package]]
name = "macholib" name = "macholib"
version = "1.16.4" version = "1.16.4"