pyproject.toml devient le point d'entree unique de configuration des outils
(pytest.ini supprime, sa config y est deplacee). Les dependances restent dans
requirements.txt/requirements-dev.txt (deja utilises par setup.bat) plutot que
dupliquees ici : ce projet se distribue par PyInstaller, pas par "pip install".
ruff : configure puis passe sur tout app/ + tests/ (154 corrections
automatiques, purement mecaniques - tri des imports, imports multiples sur une
ligne separes, alias depreciees) suivies de 7 corrections manuelles ciblees :
- app/ssh_connection.py : `raise ... from None` pour le refus explicite de
passphrase (ce n'est pas un echec a diagnostiquer).
- app/controllers/workspace_manager.py : zip() sans strict= documente en
commentaire (deliberement tolerant a un workspace legerement perime).
- tests/test_ai_redaction.py : nom de variable ambigu renomme.
- E702 (construction compacte "widget = X(); widget.setY(...)" dans
sftp_panel.py/tunnel_dialog.py) et E402 (quelques tests qui importent un
symbole juste avant la section qui l'utilise) : ignores explicitement, ce
sont des styles deliberes preexistants, pas des oublis.
`ruff check app main.py tests` est desormais propre.
pyright : mode "basic" (le code n'a jamais ete type). Plus de 200 signalements
sur ~300 etaient le meme faux positif connu de l'ecosysteme PySide6 (acces
plat aux enums Qt, ex. Qt.UserRole, valide a l'execution mais que les stubs ne
reconnaissent que sous leur forme imbriquee) : desactive avec justification en
commentaire. Reste un socle de 77 signalements reels (surtout des acces
Optional non filtres dans app/ui/tabs.py et app/ssh_connection.py) : non
corrige dans cette phase - resserrer le typage la ou pyright a raison est un
travail a part, pas une consequence automatique de l'ajout de l'outil. La CI
le lance en mode informatif (continue-on-error) tant que ce socle n'est pas
resorbe.
.gitea/workflows/ci.yml : lint (ruff, bloquant), tests (pytest, bloquant),
typecheck (pyright, informatif), build (PyInstaller, sur main uniquement,
artefact du dossier portable). Necessite un runner Gitea Actions Windows
enregistre - PySide6/pywinpty/Credential Manager/ACL NTFS sont Windows
uniquement, comme l'application elle-meme ; non verifiable depuis ce poste.
Suite complete revalidee apres coup (33/33) pour confirmer qu'aucune des 154
corrections automatiques n'a modifie de comportement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
233 lines
10 KiB
Python
233 lines
10 KiB
Python
"""Valeurs par defaut d'un groupe : appliquees en pre-remplissage a la creation
|
|
d'une nouvelle session dans ce dossier (voir app.groups)."""
|
|
from __future__ import annotations
|
|
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFormLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QPushButton,
|
|
QSpinBox,
|
|
QStackedWidget,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from app.groups import GroupSettings, GroupSettingsStore
|
|
from app.key_manager import KeyManager
|
|
from app.openssh.config import load_openssh_hosts
|
|
from app.session_manager import (
|
|
AUTH_AGENT,
|
|
AUTH_AUTO,
|
|
AUTH_KEY,
|
|
AUTH_PASSWORD,
|
|
BASTION_MANUAL,
|
|
BASTION_NONE,
|
|
BASTION_OPENSSH,
|
|
BASTION_SESSION,
|
|
BastionConfig,
|
|
SessionStore,
|
|
)
|
|
|
|
|
|
class GroupSettingsDialog(QDialog):
|
|
"""Meme principe que la section Bastion de SessionDialog, en plus compact :
|
|
ce sont des valeurs par defaut, pas la configuration d'une connexion reelle."""
|
|
|
|
def __init__(
|
|
self, group_path: str, store: GroupSettingsStore, key_manager: KeyManager,
|
|
session_store: SessionStore, parent=None,
|
|
):
|
|
super().__init__(parent)
|
|
self.group_path = group_path
|
|
self.store = store
|
|
self.key_manager = key_manager
|
|
self.session_store = session_store
|
|
self.settings = store.get(group_path) or GroupSettings(path=group_path)
|
|
self.setWindowTitle(f"Parametres par defaut — {group_path}")
|
|
self.setMinimumWidth(420)
|
|
|
|
layout = QVBoxLayout(self)
|
|
info = QLabel(
|
|
"Ces valeurs pre-remplissent une nouvelle session creee dans ce dossier "
|
|
"(bouton « Heriter du groupe » dans son editeur). Elles ne changent jamais "
|
|
"les sessions deja creees."
|
|
)
|
|
info.setWordWrap(True)
|
|
layout.addWidget(info)
|
|
|
|
form = QFormLayout()
|
|
self.username_edit = QLineEdit(self.settings.username)
|
|
self.port_spin = QSpinBox()
|
|
self.port_spin.setRange(1, 65535)
|
|
self.port_spin.setValue(self.settings.port or 22)
|
|
self.auth_combo = QComboBox()
|
|
self.auth_combo.addItem("Automatique (comme la commande ssh)", AUTH_AUTO)
|
|
self.auth_combo.addItem("Cle SSH choisie", AUTH_KEY)
|
|
self.auth_combo.addItem("Mot de passe (saisi a la connexion)", AUTH_PASSWORD)
|
|
self.auth_combo.addItem("Agent SSH", AUTH_AGENT)
|
|
idx = self.auth_combo.findData(self.settings.auth_type)
|
|
self.auth_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
|
|
self.key_combo = QComboBox()
|
|
for entry in self.key_manager.keys:
|
|
self.key_combo.addItem(f"{entry.name} ({entry.key_type})", entry.private_path)
|
|
self.browse_key_btn = QPushButton("Parcourir...")
|
|
key_row = QWidget()
|
|
key_layout = QHBoxLayout(key_row)
|
|
key_layout.setContentsMargins(0, 0, 0, 0)
|
|
key_layout.addWidget(self.key_combo, 1)
|
|
key_layout.addWidget(self.browse_key_btn)
|
|
if self.settings.key_path:
|
|
existing = self.key_combo.findData(self.settings.key_path)
|
|
if existing >= 0:
|
|
self.key_combo.setCurrentIndex(existing)
|
|
else:
|
|
self.key_combo.addItem(self.settings.key_path, self.settings.key_path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
form.addRow("Utilisateur :", self.username_edit)
|
|
form.addRow("Port :", self.port_spin)
|
|
form.addRow("Authentification :", self.auth_combo)
|
|
form.addRow("Cle privee :", key_row)
|
|
layout.addLayout(form)
|
|
|
|
layout.addWidget(self._build_bastion_group())
|
|
|
|
keepalive_form = QFormLayout()
|
|
self.keepalive_check = QCheckBox("Keepalive actif")
|
|
self.keepalive_check.setChecked(self.settings.keepalive_enabled)
|
|
self.keepalive_interval_spin = QSpinBox()
|
|
self.keepalive_interval_spin.setRange(5, 3600)
|
|
self.keepalive_interval_spin.setSuffix(" s")
|
|
self.keepalive_interval_spin.setValue(self.settings.keepalive_interval or 30)
|
|
self.keepalive_count_spin = QSpinBox()
|
|
self.keepalive_count_spin.setRange(1, 20)
|
|
self.keepalive_count_spin.setValue(self.settings.keepalive_count_max or 3)
|
|
keepalive_form.addRow(self.keepalive_check)
|
|
keepalive_form.addRow("Intervalle :", self.keepalive_interval_spin)
|
|
keepalive_form.addRow("Echecs avant abandon :", self.keepalive_count_spin)
|
|
layout.addLayout(keepalive_form)
|
|
|
|
self.auth_combo.currentIndexChanged.connect(self._update_key_row)
|
|
self.browse_key_btn.clicked.connect(self._browse_key)
|
|
self._update_key_row()
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self._on_accept)
|
|
buttons.rejected.connect(self.reject)
|
|
layout.addWidget(buttons)
|
|
|
|
def _update_key_row(self) -> None:
|
|
is_key_auth = self.auth_combo.currentData() == AUTH_KEY
|
|
self.key_combo.setEnabled(is_key_auth)
|
|
self.browse_key_btn.setEnabled(is_key_auth)
|
|
|
|
def _browse_key(self) -> None:
|
|
from PySide6.QtWidgets import QFileDialog
|
|
path, _ = QFileDialog.getOpenFileName(self, "Choisir une cle privee")
|
|
if path:
|
|
self.key_combo.addItem(path, path)
|
|
self.key_combo.setCurrentIndex(self.key_combo.count() - 1)
|
|
|
|
# ---------- Bastion (identique dans l'esprit a SessionDialog, en plus compact) ----------
|
|
def _build_bastion_group(self):
|
|
from PySide6.QtWidgets import QGroupBox
|
|
bastion = self.settings.bastion
|
|
group = QGroupBox("Bastion / Jump Host par defaut")
|
|
v = QVBoxLayout(group)
|
|
|
|
self.bastion_mode_combo = QComboBox()
|
|
self.bastion_mode_combo.addItem("Aucun", BASTION_NONE)
|
|
self.bastion_mode_combo.addItem("Une session PySSH existante", BASTION_SESSION)
|
|
self.bastion_mode_combo.addItem("Un Host de ~/.ssh/config", BASTION_OPENSSH)
|
|
self.bastion_mode_combo.addItem("Configuration manuelle", BASTION_MANUAL)
|
|
v.addWidget(self.bastion_mode_combo)
|
|
|
|
self.bastion_stack = QStackedWidget()
|
|
v.addWidget(self.bastion_stack)
|
|
self.bastion_stack.addWidget(QWidget())
|
|
|
|
session_page = QWidget()
|
|
session_form = QFormLayout(session_page)
|
|
session_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_session_combo = QComboBox()
|
|
for s in sorted(self.session_store.sessions, key=lambda s: s.name.lower()):
|
|
self.bastion_session_combo.addItem(f"{s.name} ({s.username}@{s.host})", s.id)
|
|
if self.bastion_session_combo.count() == 0:
|
|
self.bastion_session_combo.addItem("(aucune session enregistree)", "")
|
|
self.bastion_session_combo.setEnabled(False)
|
|
session_form.addRow("Session :", self.bastion_session_combo)
|
|
self.bastion_stack.addWidget(session_page)
|
|
|
|
openssh_page = QWidget()
|
|
openssh_form = QFormLayout(openssh_page)
|
|
openssh_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_openssh_combo = QComboBox()
|
|
hosts = load_openssh_hosts()
|
|
for h in hosts:
|
|
self.bastion_openssh_combo.addItem(f"{h.alias} ({h.display_target})", h.alias)
|
|
if not hosts:
|
|
self.bastion_openssh_combo.addItem("(aucun host dans ~/.ssh/config)", "")
|
|
self.bastion_openssh_combo.setEnabled(False)
|
|
openssh_form.addRow("Host :", self.bastion_openssh_combo)
|
|
self.bastion_stack.addWidget(openssh_page)
|
|
|
|
manual_page = QWidget()
|
|
manual_form = QFormLayout(manual_page)
|
|
manual_form.setContentsMargins(0, 4, 0, 0)
|
|
self.bastion_host_edit = QLineEdit(bastion.host if bastion.mode == BASTION_MANUAL else "")
|
|
self.bastion_port_spin = QSpinBox()
|
|
self.bastion_port_spin.setRange(1, 65535)
|
|
self.bastion_port_spin.setValue(bastion.port or 22)
|
|
self.bastion_user_edit = QLineEdit(bastion.username if bastion.mode == BASTION_MANUAL else "")
|
|
manual_form.addRow("Hote :", self.bastion_host_edit)
|
|
manual_form.addRow("Port :", self.bastion_port_spin)
|
|
manual_form.addRow("Utilisateur :", self.bastion_user_edit)
|
|
self.bastion_stack.addWidget(manual_page)
|
|
|
|
self.bastion_mode_combo.currentIndexChanged.connect(self.bastion_stack.setCurrentIndex)
|
|
mode_idx = self.bastion_mode_combo.findData(bastion.mode)
|
|
self.bastion_mode_combo.setCurrentIndex(mode_idx if mode_idx >= 0 else 0)
|
|
self.bastion_stack.setCurrentIndex(self.bastion_mode_combo.currentIndex())
|
|
if bastion.mode == BASTION_SESSION:
|
|
found = self.bastion_session_combo.findData(bastion.session_id)
|
|
if found >= 0:
|
|
self.bastion_session_combo.setCurrentIndex(found)
|
|
elif bastion.mode == BASTION_OPENSSH:
|
|
found = self.bastion_openssh_combo.findData(bastion.openssh_alias)
|
|
if found >= 0:
|
|
self.bastion_openssh_combo.setCurrentIndex(found)
|
|
return group
|
|
|
|
def _result_bastion(self) -> BastionConfig:
|
|
mode = self.bastion_mode_combo.currentData()
|
|
bastion = BastionConfig(mode=mode)
|
|
if mode == BASTION_SESSION:
|
|
bastion.session_id = self.bastion_session_combo.currentData() or ""
|
|
elif mode == BASTION_OPENSSH:
|
|
bastion.openssh_alias = self.bastion_openssh_combo.currentData() or ""
|
|
elif mode == BASTION_MANUAL:
|
|
bastion.host = self.bastion_host_edit.text().strip()
|
|
bastion.port = self.bastion_port_spin.value()
|
|
bastion.username = self.bastion_user_edit.text().strip()
|
|
return bastion
|
|
|
|
def _on_accept(self) -> None:
|
|
self.settings.path = self.group_path
|
|
self.settings.username = self.username_edit.text().strip()
|
|
self.settings.port = self.port_spin.value()
|
|
self.settings.auth_type = self.auth_combo.currentData()
|
|
self.settings.key_path = self.key_combo.currentData() or ""
|
|
self.settings.bastion = self._result_bastion()
|
|
self.settings.keepalive_enabled = self.keepalive_check.isChecked()
|
|
self.settings.keepalive_interval = self.keepalive_interval_spin.value()
|
|
self.settings.keepalive_count_max = self.keepalive_count_spin.value()
|
|
self.store.set(self.settings)
|
|
self.accept()
|