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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user