Qualite : pyproject.toml (ruff/pytest/pyright) + CI Gitea (lint/tests/build)

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>
This commit is contained in:
Florian
2026-09-05 07:46:50 +02:00
parent 09b7ed092c
commit c39ef9c038
72 changed files with 697 additions and 268 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from PySide6.QtCore import QThread, Signal
from app.ai.provider import AIProvider, AIError
from app.ai.provider import AIError, AIProvider
KIND_EXPLAIN = "explain"
KIND_DIAGNOSE = "diagnose"

View File

@@ -8,9 +8,9 @@ from __future__ import annotations
from dataclasses import dataclass
from app.ai.provider import AIProvider, OpenAICompatibleProvider
from app.app_settings import get_setting, set_setting
from app.credentials import CredentialStore
from app.ai.provider import AIProvider, OpenAICompatibleProvider
AI_API_KEY_CREDENTIAL = "ai:api_key"

View File

@@ -9,12 +9,17 @@ from __future__ import annotations
from dataclasses import dataclass
from app.session_manager import (
SessionStore, BastionConfig,
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
AUTH_AUTO, AUTH_KEY,
)
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):

View File

@@ -2,7 +2,7 @@
aussi vers tous les autres onglets terminal ouverts, quand le mode est actif."""
from __future__ import annotations
from typing import Callable, Iterable
from collections.abc import Callable, Iterable
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QToolButton, QWidget

View File

@@ -2,21 +2,21 @@
ouverture, connexion rapide, renommage, fermeture."""
from __future__ import annotations
from PySide6.QtCore import Qt, Signal, QObject
from PySide6.QtWidgets import QTabWidget, QWidget, QMenu, QMessageBox, QInputDialog
from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtWidgets import QInputDialog, QMenu, QMessageBox, QTabWidget, QWidget
from app.session_manager import SessionStore, Session, AUTH_PASSWORD, PASSWORD_STORAGE_SECURE
from app.bastion import BastionResolutionError, BastionTarget, resolve_bastion_target
from app.controllers.broadcast_manager import BroadcastManager
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.dialogs.connect_prompt import ask_secret
from app.dialogs.session_dialog import SessionDialog
from app.key_manager import KeyManager
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
from app.dialogs.session_dialog import SessionDialog
from app.dialogs.connect_prompt import ask_secret
from app.services.ssh_target import detect_ssh_target
from app.openssh.config import OpenSSHHost, session_from_openssh_host
from app.bastion import BastionTarget, resolve_bastion_target, BastionResolutionError
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.controllers.broadcast_manager import BroadcastManager
from app.ui.tabs import BaseTerminalTab, SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
from app.ui.split_pane import SplitContainer, PaneSlot, LAYOUT_LABELS
from app.services.ssh_target import detect_ssh_target
from app.session_manager import AUTH_PASSWORD, PASSWORD_STORAGE_SECURE, Session, SessionStore
from app.ui.split_pane import LAYOUT_LABELS, PaneSlot, SplitContainer
from app.ui.tabs import BaseTerminalTab, LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget
class ConnectionController(QObject):

View File

@@ -6,22 +6,33 @@ from __future__ import annotations
import copy
import uuid
from PySide6.QtCore import Qt, Signal, QObject
from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QTreeWidget, QTreeWidgetItem,
QMenu, QMessageBox, QComboBox, QLabel,
QComboBox,
QHBoxLayout,
QLabel,
QMenu,
QMessageBox,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from app.session_manager import SessionStore, Session
from app.key_manager import KeyManager
from app.groups import GroupSettingsStore
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.local_terminal import SHELL_LABELS
from app.dialogs.session_dialog import SessionDialog
from app.dialogs.group_settings_dialog import GroupSettingsDialog
from app.dialogs.session_dialog import SessionDialog
from app.groups import GroupSettingsStore
from app.key_manager import KeyManager
from app.local_terminal import SHELL_LABELS
from app.openssh.config import (
OpenSSHHost, default_config_path, load_openssh_hosts, session_from_openssh_host,
OpenSSHHost,
default_config_path,
load_openssh_hosts,
session_from_openssh_host,
)
from app.session_manager import Session, SessionStore
SESSION_ROLE = Qt.UserRole + 10
LOCAL_SHELL_ROLE = Qt.UserRole + 11

View File

@@ -7,15 +7,20 @@ from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QWidget
from app.app_settings import get_setting, set_setting
from app.controllers.connection_controller import ConnectionController
from app.local_terminal import DEFAULT_SHELL
from app.session_manager import SessionStore
from app.ui.split_pane import LAYOUT_SINGLE, SplitContainer
from app.ui.tabs import LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget
from app.workspace import (
Workspace, WorkspaceStore, WorkspaceTab, WorkspaceItem,
ITEM_SESSION_SSH, ITEM_SESSION_SFTP, ITEM_LOCAL_SHELL,
ITEM_LOCAL_SHELL,
ITEM_SESSION_SFTP,
ITEM_SESSION_SSH,
Workspace,
WorkspaceItem,
WorkspaceStore,
WorkspaceTab,
)
from app.ui.tabs import SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
from app.ui.split_pane import SplitContainer, LAYOUT_SINGLE
from app.controllers.connection_controller import ConnectionController
SETTING_RESTORE_ON_STARTUP = "restore_last_workspace"
SETTING_LAST_WORKSPACE_ID = "last_workspace_id"
@@ -152,7 +157,12 @@ class WorkspaceManager(QObject):
self.cc.split_tab(index, wtab.layout)
container = self.cc.tabs.widget(index)
skipped = 0
for pane, item in zip(container.panes[1:], wtab.items[1:]):
# strict=True est volontairement evite : un workspace legerement perime
# (enregistre par une version anterieure, ou modifie a la main) peut avoir
# une liste d'items plus courte que le nombre de panneaux - on restaure
# alors ce qui correspond et on laisse les panneaux excedentaires a leur
# terminal local par defaut, plutot que d'abandonner toute la restauration.
for pane, item in zip(container.panes[1:], wtab.items[1:]): # noqa: B905
widget = self._widget_for_item(item)
if widget is None:
skipped += 1

View File

@@ -6,7 +6,13 @@ from __future__ import annotations
import re
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QApplication,
QApplication,
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QTextEdit,
QVBoxLayout,
)
_CODE_BLOCK_RE = re.compile(r"```(?:[a-zA-Z0-9_+-]*\n)?(.*?)```", re.DOTALL)

View File

@@ -6,8 +6,8 @@ from __future__ import annotations
from PySide6.QtWidgets import QMessageBox
from app.app_settings import get_setting, set_setting
from app.ai.redaction import detect_secrets
from app.app_settings import get_setting, set_setting
_NOTICE_ACK_KEY = "ai_notice_acknowledged"

View File

@@ -4,13 +4,21 @@ compatibles avec l'API OpenAI (OpenAI, OpenRouter, proxy interne, LLM local...).
from __future__ import annotations
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QFormLayout, QLineEdit, QCheckBox, QDoubleSpinBox,
QPushButton, QDialogButtonBox, QMessageBox, QLabel,
QCheckBox,
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
QFormLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QVBoxLayout,
)
from app.ai.settings import AISettings, load_ai_settings, save_ai_settings, load_api_key, save_api_key
from app.ai.provider import OpenAICompatibleProvider
from app.ai.service import KIND_TEST, AIRequestWorker
from app.ai.settings import AISettings, load_ai_settings, load_api_key, save_ai_settings, save_api_key
from app.credentials import CredentialStore, NullCredentialStore

View File

@@ -3,17 +3,36 @@ d'une nouvelle session dans ce dossier (voir app.groups)."""
from __future__ import annotations
from PySide6.QtWidgets import (
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
QHBoxLayout, QPushButton, QWidget, QVBoxLayout, QCheckBox, QStackedWidget, QLabel,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QSpinBox,
QStackedWidget,
QVBoxLayout,
QWidget,
)
from app.session_manager import (
SessionStore, BastionConfig, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT,
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
)
from app.key_manager import KeyManager
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):

View File

@@ -5,13 +5,28 @@ from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
QMessageBox, QFileDialog, QFormLayout, QLineEdit, QComboBox, QSpinBox,
QDialogButtonBox, QApplication, QLabel,
QApplication,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
)
from app.key_manager import (
KeyManager, KEY_TYPE_ED25519, KEY_TYPE_RSA, KEY_TYPE_LABELS, KEY_TYPE_DESCRIPTIONS,
KEY_TYPE_DESCRIPTIONS,
KEY_TYPE_LABELS,
KEY_TYPE_RSA,
KeyManager,
)

View File

@@ -3,8 +3,15 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QPushButton,
QMessageBox, QLabel, QAbstractItemView,
QAbstractItemView,
QDialog,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
)
from app.known_hosts_manager import KnownHostsStore

View File

@@ -2,12 +2,17 @@
snippets et actions, avec recherche floue et navigation entierement au clavier."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Callable
from PySide6.QtCore import Qt, QEvent
from PySide6.QtCore import QEvent, Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QLineEdit, QListWidget, QListWidgetItem, QMessageBox,
QDialog,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QVBoxLayout,
)
from app.openssh.config import load_openssh_hosts
@@ -111,9 +116,9 @@ class QuickConnectDialog(QDialog):
return results
def _quick_actions(self) -> list[tuple[str, Callable[[], None]]]:
from app.dialogs.workspace_dialog import WorkspaceDialog
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.dialogs.key_dialog import KeyManagerDialog
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.dialogs.workspace_dialog import WorkspaceDialog
return [
("Nouvelle session...", self.session_controller.new_session),

View File

@@ -3,20 +3,44 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QWidget, QVBoxLayout,
QCheckBox, QMessageBox, QGroupBox, QStackedWidget, QCompleter,
QCheckBox,
QComboBox,
QCompleter,
QDialog,
QDialogButtonBox,
QFileDialog,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLineEdit,
QMessageBox,
QPushButton,
QSpinBox,
QStackedWidget,
QTextEdit,
QVBoxLayout,
QWidget,
)
from app.session_manager import (
Session, SessionStore, BastionConfig, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT,
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
PASSWORD_STORAGE_NONE, PASSWORD_STORAGE_SECURE,
)
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.groups import GroupSettingsStore
from app.key_manager import KeyManager
from app.openssh.config import load_openssh_hosts
from app.groups import GroupSettingsStore
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
from app.session_manager import (
AUTH_AGENT,
AUTH_AUTO,
AUTH_KEY,
AUTH_PASSWORD,
BASTION_MANUAL,
BASTION_NONE,
BASTION_OPENSSH,
BASTION_SESSION,
PASSWORD_STORAGE_NONE,
PASSWORD_STORAGE_SECURE,
BastionConfig,
Session,
SessionStore,
)
class SessionDialog(QDialog):

View File

@@ -4,8 +4,19 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QPushButton,
QMessageBox, QLineEdit, QLabel, QFormLayout, QTextEdit, QDialogButtonBox, QComboBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QTextEdit,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
)
from app.snippets import Snippet, SnippetStore

View File

@@ -3,8 +3,19 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QComboBox, QSpinBox, QLineEdit, QLabel, QMessageBox, QAbstractItemView,
QAbstractItemView,
QComboBox,
QDialog,
QFormLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QSpinBox,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
)
from app.tunnels import DIRECTION_DYNAMIC, DIRECTION_LABELS, TunnelManager, describe

View File

@@ -3,12 +3,20 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
QMessageBox, QInputDialog, QLabel, QCheckBox,
QCheckBox,
QDialog,
QHBoxLayout,
QInputDialog,
QLabel,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QVBoxLayout,
)
from app.app_settings import get_setting, set_setting
from app.controllers.workspace_manager import WorkspaceManager, SETTING_RESTORE_ON_STARTUP
from app.controllers.workspace_manager import SETTING_RESTORE_ON_STARTUP, WorkspaceManager
class WorkspaceDialog(QDialog):

View File

@@ -9,12 +9,12 @@ librement modifiables, sans indirection a suivre dans tout le reste du code
(ConnectionController, SSHConnection, resolve_bastion_target...)."""
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
from app.paths import DATA_DIR
from app.session_manager import AUTH_AUTO, BastionConfig
from app.storage.json_store import load_json_document, save_json_document
from app.session_manager import BastionConfig, AUTH_AUTO
GROUPS_FILE = DATA_DIR / "groups.json"
SCHEMA_VERSION = 1
@@ -36,7 +36,7 @@ class GroupSettings:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "GroupSettings":
def from_dict(data: dict) -> GroupSettings:
g = GroupSettings()
for k in GroupSettings.__dataclass_fields__:
if k not in data:

View File

@@ -5,16 +5,16 @@ import base64
import hashlib
import shutil
import uuid
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
import paramiko
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, ed25519, ec
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
from app.storage.json_store import load_json_document, save_json_document
from app.security.key_protection import protect_private_key
from app.storage.json_store import load_json_document, save_json_document
KEYS_FILE = DATA_DIR / "keys.json"
DEFAULT_KEY_DIR = DATA_DIR / "keys"
@@ -116,7 +116,7 @@ class KeyEntry:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "KeyEntry":
def from_dict(data: dict) -> KeyEntry:
e = KeyEntry()
for k in KeyEntry.__dataclass_fields__:
if k in data:

View File

@@ -9,33 +9,37 @@ from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QActionGroup, QKeySequence
from PySide6.QtWidgets import (
QMainWindow, QDockWidget, QMessageBox, QStatusBar, QApplication,
QApplication,
QDockWidget,
QMainWindow,
QMessageBox,
QStatusBar,
)
from app.session_manager import SessionStore
from app.key_manager import KeyManager
from app.local_terminal import SHELL_LABELS, DEFAULT_SHELL
from app.terminal_widget import TerminalWidget
from app.ai.settings import build_provider_from_settings
from app.controllers.broadcast_manager import BroadcastManager
from app.controllers.connection_controller import ConnectionController
from app.controllers.session_controller import SessionController
from app.controllers.workspace_manager import WorkspaceManager
from app.credentials import default_credential_store
from app.dialogs.ai_settings_dialog import AISettingsDialog
from app.dialogs.key_dialog import KeyManagerDialog
from app.dialogs.known_hosts_dialog import KnownHostsDialog
from app.dialogs.workspace_dialog import WorkspaceDialog
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.dialogs.quick_connect_dialog import QuickConnectDialog
from app.dialogs.ai_settings_dialog import AISettingsDialog
from app.ai.settings import build_provider_from_settings
from app.theme import THEME_LABELS, apply_theme, current_theme, save_theme
from app.workspace import WorkspaceStore
from app.snippets import SnippetStore
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.dialogs.workspace_dialog import WorkspaceDialog
from app.groups import GroupSettingsStore
from app.credentials import default_credential_store
from app.controllers.broadcast_manager import BroadcastManager
from app.controllers.session_controller import SessionController
from app.controllers.connection_controller import ConnectionController
from app.controllers.workspace_manager import WorkspaceManager
from app.ui.split_pane import SplitContainer
from app.key_manager import KeyManager
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
# re-exporte pour compatibilite (utilise par tests/test_detect.py et du code existant)
from app.services.ssh_target import detect_ssh_target # noqa: F401
from app.session_manager import SessionStore
from app.snippets import SnippetStore
from app.terminal_widget import TerminalWidget
from app.theme import THEME_LABELS, apply_theme, current_theme, save_theme
from app.ui.split_pane import SplitContainer
from app.workspace import WorkspaceStore
class MainWindow(QMainWindow):

View File

@@ -14,7 +14,7 @@ from pathlib import Path
import paramiko
from app.session_manager import Session, BastionConfig, AUTH_AUTO, AUTH_KEY, BASTION_OPENSSH
from app.session_manager import AUTH_AUTO, AUTH_KEY, BASTION_OPENSSH, BastionConfig, Session
@dataclass
@@ -57,7 +57,7 @@ def load_openssh_hosts(path: Path | None = None) -> list[OpenSSHHost]:
parser = paramiko.SSHConfig()
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
with open(path, encoding="utf-8", errors="replace") as f:
parser.parse(f)
except OSError:
return []

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
import uuid
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
@@ -50,7 +50,7 @@ class BastionConfig:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "BastionConfig":
def from_dict(data: dict) -> BastionConfig:
b = BastionConfig()
for k in BastionConfig.__dataclass_fields__:
if k in data:
@@ -95,7 +95,7 @@ class Session:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "Session":
def from_dict(data: dict) -> Session:
known = {f: data.get(f) for f in Session.__dataclass_fields__ if f in data}
s = Session()
for k, v in known.items():

View File

@@ -11,8 +11,9 @@ from __future__ import annotations
import queue
import threading
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Callable
from typing import Any
from PySide6.QtCore import QThread, Signal

View File

@@ -5,18 +5,30 @@ from __future__ import annotations
import json
import os
import posixpath
import tempfile
import stat
import tempfile
from datetime import datetime
from pathlib import Path
import paramiko
from PySide6.QtCore import Qt, QThread, Signal, QMimeData, QPoint, QFileSystemWatcher
from PySide6.QtCore import QFileSystemWatcher, QMimeData, QPoint, Qt, QThread, Signal
from PySide6.QtGui import QDrag, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QToolButton, QTreeWidget,
QTreeWidgetItem, QSplitter, QLabel, QMessageBox, QInputDialog, QProgressDialog,
QAbstractItemView, QMenu, QHeaderView,
QAbstractItemView,
QHBoxLayout,
QHeaderView,
QInputDialog,
QLabel,
QLineEdit,
QMenu,
QMessageBox,
QProgressDialog,
QSplitter,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from app.sftp.worker import SftpWorker
@@ -125,7 +137,7 @@ class TransferWorker(QThread):
def _upload_dir(self, local_dir: str, remote_dir: str) -> None:
try:
self.sftp.mkdir(remote_dir)
except IOError:
except OSError:
pass
for entry in os.listdir(local_dir):
if self._cancel:
@@ -615,7 +627,7 @@ class RemoteFilePanel(FilePanel):
self._rmdir_recursive(path)
else:
self.sftp.remove(path)
except IOError as exc:
except OSError as exc:
errors.append(f"{name} : {exc}")
return errors
@@ -745,7 +757,7 @@ class RemoteFilePanel(FilePanel):
for entry_name, _ in entries:
try:
self.sftp.chmod(self.join(current_path, entry_name), mode)
except IOError as exc:
except OSError as exc:
errors.append(f"{entry_name} : {exc}")
return errors

View File

@@ -3,7 +3,7 @@ depuis un terminal (inserer sans executer, executer, executer avec Multi-Send)."
from __future__ import annotations
import uuid
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
from app.paths import DATA_DIR
@@ -25,7 +25,7 @@ class Snippet:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "Snippet":
def from_dict(data: dict) -> Snippet:
s = Snippet()
for k in Snippet.__dataclass_fields__:
if k in data:

View File

@@ -7,17 +7,17 @@ import threading
import paramiko
from PySide6.QtCore import QThread, Signal
from app.known_hosts_manager import KnownHostsStore
from app.bastion import BastionTarget
from app.known_hosts_manager import KnownHostsStore
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
KNOWN_HOSTS_FILE = DATA_DIR / "known_hosts"
class TrustPrompt(paramiko.MissingHostKeyPolicy):
"""Politique qui demande confirmation (via signal Qt) avant de faire confiance a une nouvelle cle d'hote."""
def __init__(self, connection: "SSHConnection"):
def __init__(self, connection: SSHConnection):
self.connection = connection
def missing_host_key(self, client, hostname, key):
@@ -379,7 +379,10 @@ class SSHConnection(QThread):
except paramiko.PasswordRequiredException:
pwd = passphrase or self._ask_passphrase(key_path)
if not pwd:
raise paramiko.SSHException("Passphrase requise pour deverrouiller la cle")
# l'utilisateur a annule la demande de passphrase : ce n'est pas un
# echec a diagnostiquer, la PasswordRequiredException d'origine n'a
# rien a apporter au message affiche.
raise paramiko.SSHException("Passphrase requise pour deverrouiller la cle") from None
try:
return loader.from_private_key_file(key_path, password=pwd)
except paramiko.SSHException as exc:

View File

@@ -13,8 +13,8 @@ plutot que de perdre les donnees de l'utilisateur.
from __future__ import annotations
import json
from collections.abc import Callable
from pathlib import Path
from typing import Callable
from app.storage.atomic_file import atomic_write_bytes

View File

@@ -11,9 +11,14 @@ from __future__ import annotations
import pyte
from PySide6.QtCore import QRect, Qt, Signal
from PySide6.QtGui import (
QColor, QFont, QFontMetrics, QPainter, QKeyEvent, QWheelEvent,
QColor,
QFont,
QFontMetrics,
QKeyEvent,
QPainter,
QWheelEvent,
)
from PySide6.QtWidgets import QWidget, QApplication, QScrollBar
from PySide6.QtWidgets import QApplication, QScrollBar, QWidget
from app import theme as app_theme

View File

@@ -9,11 +9,11 @@ from __future__ import annotations
from string import Template
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QFont, QLinearGradient
from PySide6.QtGui import QColor, QFont, QIcon, QLinearGradient, QPainter, QPixmap
from PySide6.QtWidgets import QApplication
from app.paths import RESOURCES_DIR # noqa: E402 (chemins valables aussi en .exe)
from app.app_settings import get_setting, set_setting
from app.paths import RESOURCES_DIR # noqa: E402 (chemins valables aussi en .exe)
STYLESHEET_PATH = RESOURCES_DIR / "theme.qss"

View File

@@ -60,7 +60,7 @@ def _relay(sock: socket.socket, chan: paramiko.Channel) -> None:
if not data:
break
sock.sendall(data)
except (OSError, socket.error):
except OSError:
pass
finally:
for closeable in (chan, sock):

View File

@@ -10,12 +10,19 @@ from __future__ import annotations
import html
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QLineEdit, QApplication,
QApplication,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)
from app.ai.service import AIRequestWorker, KIND_ASK
from app.ai.redaction import redact
from app.dialogs.ai_prompts import ensure_ai_notice_acknowledged, confirm_sensitive_content, MASK, CANCEL
from app.ai.service import KIND_ASK, AIRequestWorker
from app.dialogs.ai_prompts import CANCEL, MASK, confirm_sensitive_content, ensure_ai_notice_acknowledged
_KIND_LABELS = {
"explain": "Expliquer cette sortie",

View File

@@ -7,11 +7,16 @@ existant (SessionTabWidget, LocalTerminalTabWidget, ...) sans le modifier.
"""
from __future__ import annotations
from typing import Callable
from collections.abc import Callable
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QToolButton, QFrame,
QFrame,
QHBoxLayout,
QSplitter,
QToolButton,
QVBoxLayout,
QWidget,
)
LAYOUT_SINGLE = "1"

View File

@@ -11,31 +11,41 @@ import re
from datetime import datetime
from pathlib import Path
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QLabel, QMessageBox, QSplitter,
QFrame, QLineEdit, QFileDialog, QMenu, QInputDialog,
QFileDialog,
QFrame,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QMenu,
QMessageBox,
QSplitter,
QToolButton,
QVBoxLayout,
QWidget,
)
from app.session_manager import Session, AUTH_KEY, AUTH_PASSWORD
from app.ssh_connection import SSHConnection
from app.local_terminal import LocalTerminalConnection, SHELL_POWERSHELL, SHELL_LABELS
from app.terminal_widget import TerminalWidget
from app.sftp_panel import SFTPPanel
from app.dialogs.tunnel_dialog import TunnelDialog
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.tunnels import TunnelManager
from app.dialogs.connect_prompt import ask_secret, confirm_host_key, confirm_host_key_change
from app.services.ssh_target import detect_ssh_target
from app.services.reconnect import compute_backoff_delay
from app.snippets import Snippet, SnippetStore
from app.ai.service import KIND_EXPLAIN, KIND_DIAGNOSE, KIND_SOLUTION, KIND_GENERATE, AIRequestWorker
from app.ai.danger import check_dangerous
from app.ai.redaction import redact
from app.ui.ai_panel import AIPanel
from app.ai.service import KIND_DIAGNOSE, KIND_EXPLAIN, KIND_GENERATE, KIND_SOLUTION, AIRequestWorker
from app.dialogs.ai_command_dialog import AICommandDialog
from app.dialogs.ai_prompts import ensure_ai_notice_acknowledged, confirm_sensitive_content, MASK, CANCEL
from app.dialogs.ai_prompts import CANCEL, MASK, confirm_sensitive_content, ensure_ai_notice_acknowledged
from app.dialogs.connect_prompt import ask_secret, confirm_host_key, confirm_host_key_change
from app.dialogs.snippet_dialog import SnippetManagerDialog
from app.dialogs.tunnel_dialog import TunnelDialog
from app.local_terminal import SHELL_LABELS, SHELL_POWERSHELL, LocalTerminalConnection
from app.services.reconnect import compute_backoff_delay
from app.services.ssh_target import detect_ssh_target
from app.session_manager import AUTH_KEY, AUTH_PASSWORD, Session
from app.sftp_panel import SFTPPanel
from app.snippets import Snippet, SnippetStore
from app.ssh_connection import SSHConnection
from app.terminal_widget import TerminalWidget
from app.tunnels import TunnelManager
from app.ui.ai_panel import AIPanel
# sequences d'echappement ANSI, retirees des journaux pour qu'ils restent lisibles
ANSI_ESCAPE_RE = re.compile(

View File

@@ -7,7 +7,7 @@ mot de passe/passphrase exactement comme une ouverture normale)."""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field, asdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
from app.paths import DATA_DIR
@@ -32,7 +32,7 @@ class WorkspaceItem:
return asdict(self)
@staticmethod
def from_dict(data: dict) -> "WorkspaceItem":
def from_dict(data: dict) -> WorkspaceItem:
item = WorkspaceItem()
for k in WorkspaceItem.__dataclass_fields__:
if k in data:
@@ -56,7 +56,7 @@ class WorkspaceTab:
}
@staticmethod
def from_dict(data: dict) -> "WorkspaceTab":
def from_dict(data: dict) -> WorkspaceTab:
tab = WorkspaceTab(title=data.get("title", ""), layout=data.get("layout", "1"))
tab.items = [WorkspaceItem.from_dict(i) for i in data.get("items", [])]
tab.splitter_sizes = [list(s) for s in data.get("splitter_sizes", [])]
@@ -73,7 +73,7 @@ class Workspace:
return {"id": self.id, "name": self.name, "tabs": [t.to_dict() for t in self.tabs]}
@staticmethod
def from_dict(data: dict) -> "Workspace":
def from_dict(data: dict) -> Workspace:
ws = Workspace(id=data.get("id") or str(uuid.uuid4()), name=data.get("name", ""))
ws.tabs = [WorkspaceTab.from_dict(t) for t in data.get("tabs", [])]
return ws