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>
89 lines
3.4 KiB
Python
89 lines
3.4 KiB
Python
"""Resolution d'un BastionConfig (session/openssh/manuel) en parametres de
|
|
connexion concrets, independamment de la source choisie dans l'editeur.
|
|
|
|
La connexion elle-meme (ouverture du canal direct-tcpip a travers le bastion)
|
|
est geree par SSHConnection (app.ssh_connection), qui consomme un BastionTarget
|
|
sans se soucier de sa provenance.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from app.credentials import session_credential_key
|
|
from app.session_manager import (
|
|
AUTH_AUTO,
|
|
AUTH_KEY,
|
|
BASTION_MANUAL,
|
|
BASTION_NONE,
|
|
BASTION_OPENSSH,
|
|
BASTION_SESSION,
|
|
BastionConfig,
|
|
SessionStore,
|
|
)
|
|
|
|
|
|
class BastionResolutionError(Exception):
|
|
"""Le bastion configure ne peut pas etre resolu (reference cassee, config
|
|
invalide...). Empeche de lancer une connexion vouee a l'echec."""
|
|
|
|
|
|
@dataclass
|
|
class BastionTarget:
|
|
"""Parametres de connexion resolus pour le saut a franchir avant la cible."""
|
|
host: str
|
|
port: int
|
|
username: str
|
|
auth_type: str
|
|
key_path: str = ""
|
|
# Non vide seulement pour un bastion en mode "session PySSH" : permet de
|
|
# reutiliser un mot de passe deja memorise pour cette session (voir
|
|
# app.credentials), sans que ce module ait besoin de connaitre le
|
|
# CredentialStore lui-meme.
|
|
credential_key: str = ""
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return f"{self.username}@{self.host}" if self.username else self.host
|
|
|
|
|
|
def resolve_bastion_target(
|
|
bastion: BastionConfig | None, session_store: SessionStore | None = None,
|
|
) -> BastionTarget | None:
|
|
"""Renvoie None si aucun bastion n'est configure. Leve BastionResolutionError
|
|
si le mode choisi ne peut pas etre resolu en ce moment (session supprimee,
|
|
alias OpenSSH introuvable, config manuelle incomplete...)."""
|
|
if bastion is None or bastion.mode in (BASTION_NONE, ""):
|
|
return None
|
|
|
|
if bastion.mode == BASTION_MANUAL:
|
|
if not bastion.host.strip():
|
|
raise BastionResolutionError("Bastion manuel : aucune adresse renseignee.")
|
|
return BastionTarget(bastion.host.strip(), bastion.port, bastion.username, bastion.auth_type, bastion.key_path)
|
|
|
|
if bastion.mode == BASTION_SESSION:
|
|
if session_store is None:
|
|
raise BastionResolutionError("Bastion vers une session PySSH : aucun registre de sessions disponible.")
|
|
target = session_store.get(bastion.session_id)
|
|
if target is None:
|
|
raise BastionResolutionError(
|
|
"La session utilisee comme bastion est introuvable (supprimee ou renommee)."
|
|
)
|
|
return BastionTarget(
|
|
target.host, target.port, target.username, target.auth_type, target.key_path,
|
|
credential_key=session_credential_key(target.id),
|
|
)
|
|
|
|
if bastion.mode == BASTION_OPENSSH:
|
|
from app.openssh.config import load_openssh_hosts
|
|
|
|
host = next((h for h in load_openssh_hosts() if h.alias == bastion.openssh_alias), None)
|
|
if host is None:
|
|
raise BastionResolutionError(
|
|
f"Host OpenSSH « {bastion.openssh_alias} » introuvable dans ~/.ssh/config."
|
|
)
|
|
key_path = host.identity_files[0] if host.identity_files else ""
|
|
auth_type = AUTH_KEY if key_path else AUTH_AUTO
|
|
return BastionTarget(host.hostname or host.alias, host.port, host.user, auth_type, key_path)
|
|
|
|
raise BastionResolutionError(f"Mode de bastion inconnu : {bastion.mode}")
|