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:
76
.gitea/workflows/ci.yml
Normal file
76
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
name: CI
|
||||
|
||||
# Runner Windows requis : PySide6/pywinpty/Windows Credential Manager/ACL NTFS
|
||||
# ne fonctionnent que sous Windows, comme l'application elle-meme.
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Installer les dependances de developpement
|
||||
run: pip install -r requirements-dev.txt
|
||||
- name: ruff check
|
||||
run: ruff check app main.py tests
|
||||
|
||||
tests:
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Installer les dependances
|
||||
run: pip install -r requirements-dev.txt
|
||||
- name: pytest (rendu Qt hors-ecran)
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONUTF8: "1"
|
||||
run: pytest tests -v
|
||||
|
||||
typecheck:
|
||||
runs-on: windows
|
||||
# Informatif pour l'instant : le code n'a jamais ete type avant cette phase,
|
||||
# 77 signalements (Optional non filtre, principalement dans app/ui/tabs.py
|
||||
# et app/ssh_connection.py) restent a trier au cas par cas. A rendre
|
||||
# bloquant une fois ce fond resorbe - voir le commentaire dans pyproject.toml.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Installer les dependances
|
||||
run: pip install -r requirements-dev.txt
|
||||
- name: pyright
|
||||
run: pyright
|
||||
|
||||
build:
|
||||
runs-on: windows
|
||||
needs: [lint, tests]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Installer les dependances et PyInstaller
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
- name: Generer l'icone
|
||||
run: python tools/make_icon.py
|
||||
- name: Construire l'executable portable
|
||||
run: python -m PyInstaller --noconfirm --clean PySSHClient.spec
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PySSH-Client-portable
|
||||
path: dist/PySSH Client
|
||||
@@ -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
|
||||
|
||||
61
pyproject.toml
Normal file
61
pyproject.toml
Normal file
@@ -0,0 +1,61 @@
|
||||
[project]
|
||||
name = "pyssh-client"
|
||||
version = "0.1.0"
|
||||
description = "Client SSH/SFTP local (PySide6) : terminaux, tunnels, snippets, workspaces, assistant IA optionnel"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
# Les dependances (runtime et dev) restent dans requirements.txt /
|
||||
# requirements-dev.txt, deja utilises par setup.bat et la CI : les dupliquer
|
||||
# ici cree un risque de desynchronisation pour aucun benefice reel, puisque
|
||||
# le projet se distribue par PyInstaller (voir PySSHClient.spec), pas par
|
||||
# "pip install". Ce fichier sert de point d'entree unique pour la
|
||||
# configuration des outils (ruff, pytest, pyright).
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 110
|
||||
target-version = "py310"
|
||||
extend-exclude = ["venv", "build", "dist"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
ignore = [
|
||||
"E501", # ligne trop longue : laisse au formateur, pas bloquant en l'etat
|
||||
"E702", # construction compacte "widget = X(); widget.setY(...)" deliberee
|
||||
# dans les panneaux SFTP/tunnels (un widget + sa config tiennent sur
|
||||
# une ligne) : reecrire l'existant pour ca serait pur churn.
|
||||
"E402", # plusieurs tests importent un symbole juste avant la section qui
|
||||
# l'utilise pour la premiere fois, plutot qu'en haut du fichier -
|
||||
# lisible dans des scripts sequentiels longs, et sans risque tant
|
||||
# que l'import n'a pas d'effet de bord ordonne.
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["app"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_all_scripts.py"]
|
||||
|
||||
[tool.pyright]
|
||||
include = ["app", "main.py"]
|
||||
exclude = ["venv", "build", "dist", "tests"]
|
||||
venvPath = "."
|
||||
venv = "venv"
|
||||
# "basic" plutot que "strict" : le code n'a jamais ete type jusqu'ici, un mode
|
||||
# strict noierait les vrais signaux sous des milliers d'avertissements de
|
||||
# typage progressif. A resserrer progressivement, fichier par fichier.
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.10"
|
||||
reportMissingImports = true
|
||||
# Desactive : plus de 200 occurrences sont l'acces "plat" aux enums Qt
|
||||
# (Qt.UserRole, QToolButton.InstantPopup...) - un style PyQt5 historique que
|
||||
# PySide6 continue de supporter a l'execution, mais que ses stubs ne
|
||||
# reconnaissent qu'a travers leur forme imbriquee (Qt.ItemDataRole.UserRole).
|
||||
# Utilise partout dans ce projet, sans exception, et sans le moindre risque
|
||||
# reel : le desactiver evite de noyer les vrais signaux. Le reste (une
|
||||
# vingtaine de cas ou pyright perd le fil sur des patterns dynamiques - le
|
||||
# monkey-patching de pyte dans terminal_widget.py, les QWidget generiques de
|
||||
# split_pane.py) reste dans le rapport, a regarder au cas par cas plutot que
|
||||
# supprime en bloc.
|
||||
reportAttributeAccessIssue = false
|
||||
@@ -1,3 +0,0 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_all_scripts.py
|
||||
@@ -1,3 +1,5 @@
|
||||
-r requirements.txt
|
||||
pytest>=8.0
|
||||
pytest-qt>=4.4
|
||||
ruff>=0.6
|
||||
pyright>=1.1
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""AICommandDialog : extraction de la commande depuis la reponse de l'IA
|
||||
(bloc de code s'il y en a un), et les 3 actions possibles (Copier / Inserer
|
||||
dans le terminal / Annuler) - jamais d'execution automatique."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Detection locale de commandes potentiellement destructives proposees par
|
||||
l'assistant IA (jamais un blocage, seulement un avertissement)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.ai.danger import is_dangerous, check_dangerous
|
||||
from app.ai.danger import check_dangerous, is_dangerous
|
||||
|
||||
print("=== 1. Commandes destructrices detectees ===")
|
||||
dangerous_examples = [
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
la verification de securite (avis + secrets) est bien invoquee avant tout
|
||||
envoi, que les reponses/erreurs mettent a jour la conversation, et qu'une
|
||||
reponse perimee (generation obsolete) est ignoree."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from unittest.mock import patch
|
||||
@@ -10,9 +12,9 @@ from unittest.mock import patch
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.ai.provider import AIProvider, AIError
|
||||
from app.ai.service import KIND_EXPLAIN, KIND_ASK
|
||||
from app.dialogs.ai_prompts import MASK, SEND, CANCEL
|
||||
from app.ai.provider import AIError, AIProvider
|
||||
from app.ai.service import KIND_EXPLAIN
|
||||
from app.dialogs.ai_prompts import CANCEL, MASK, SEND
|
||||
from app.ui.ai_panel import AIPanel
|
||||
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
@@ -3,7 +3,9 @@ affiche une seule fois (persist dans app_settings, redirige vers un fichier
|
||||
temporaire), et verification des secrets a chaque envoi. QMessageBox est
|
||||
simule (exec()/clickedButton() patches) : aucune boite modale reelle ne doit
|
||||
bloquer les tests."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
@@ -13,7 +15,13 @@ from unittest.mock import patch
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
import app.app_settings as app_settings
|
||||
from app.dialogs.ai_prompts import ensure_ai_notice_acknowledged, confirm_sensitive_content, MASK, SEND, CANCEL
|
||||
from app.dialogs.ai_prompts import (
|
||||
CANCEL,
|
||||
MASK,
|
||||
SEND,
|
||||
confirm_sensitive_content,
|
||||
ensure_ai_notice_acknowledged,
|
||||
)
|
||||
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""OpenAICompatibleProvider : requete HTTP construite correctement, et gestion
|
||||
des erreurs (401, 429, timeout, reseau, reponse invalide) - urllib.request.urlopen
|
||||
est entierement mocke, aucun appel reseau reel."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import io
|
||||
import json
|
||||
import urllib.error
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.ai.provider import AIProvider, AIError, OpenAICompatibleProvider
|
||||
from app.ai.provider import AIError, AIProvider, OpenAICompatibleProvider
|
||||
|
||||
|
||||
def fake_response(payload: dict):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Detection/redaction de secrets avant envoi a l'assistant IA."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.ai.redaction import detect_secrets, redact
|
||||
@@ -16,8 +18,8 @@ print("=== 2. Jeton Bearer detecte ===")
|
||||
found = detect_secrets("Authorization: Bearer sk-abcdefghijklmnopqrstuvwxyz123456")
|
||||
labels = [f.label for f in found]
|
||||
print(" detecte:", labels)
|
||||
assert any("bearer" in l.lower() for l in labels)
|
||||
assert any("authorization" in l.lower() for l in labels)
|
||||
assert any("bearer" in label.lower() for label in labels)
|
||||
assert any("authorization" in label.lower() for label in labels)
|
||||
print(" OK")
|
||||
|
||||
print()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""AIRequestWorker : execution d'une requete IA dans un vrai QThread en
|
||||
arriere-plan, jamais sur le thread Qt principal - avec dispatch correct selon
|
||||
le type de requete et remontee des erreurs via signal plutot qu'exception."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import threading
|
||||
@@ -9,9 +11,15 @@ import threading
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.ai.provider import AIProvider, AIError
|
||||
from app.ai.provider import AIError, AIProvider
|
||||
from app.ai.service import (
|
||||
AIRequestWorker, KIND_EXPLAIN, KIND_DIAGNOSE, KIND_SOLUTION, KIND_GENERATE, KIND_ASK, KIND_TEST,
|
||||
KIND_ASK,
|
||||
KIND_DIAGNOSE,
|
||||
KIND_EXPLAIN,
|
||||
KIND_GENERATE,
|
||||
KIND_SOLUTION,
|
||||
KIND_TEST,
|
||||
AIRequestWorker,
|
||||
)
|
||||
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
|
||||
@@ -2,20 +2,28 @@
|
||||
app_settings (redirige vers un fichier temporaire pour ne jamais toucher les
|
||||
vrais reglages), cle API exclusivement via CredentialStore, et construction du
|
||||
fournisseur a partir des deux."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.credentials import CredentialStore
|
||||
from app.ai.settings import (
|
||||
AISettings, load_ai_settings, save_ai_settings, load_api_key, save_api_key,
|
||||
build_provider_from_settings, AI_API_KEY_CREDENTIAL, DEFAULT_BASE_URL,
|
||||
)
|
||||
from app.ai.provider import OpenAICompatibleProvider
|
||||
import app.app_settings as app_settings
|
||||
from app.ai.provider import OpenAICompatibleProvider
|
||||
from app.ai.settings import (
|
||||
AI_API_KEY_CREDENTIAL,
|
||||
DEFAULT_BASE_URL,
|
||||
AISettings,
|
||||
build_provider_from_settings,
|
||||
load_ai_settings,
|
||||
load_api_key,
|
||||
save_ai_settings,
|
||||
save_api_key,
|
||||
)
|
||||
from app.credentials import CredentialStore
|
||||
|
||||
|
||||
class FakeCredentialStore(CredentialStore):
|
||||
|
||||
@@ -5,7 +5,9 @@ automatiquement, avec avertissement si dangereuse).
|
||||
|
||||
LocalTerminalConnection.start() est neutralisee comme dans test_snippets.py :
|
||||
aucun sous-processus reel, send() depose seulement les octets dans une file."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from unittest.mock import patch
|
||||
@@ -13,9 +15,9 @@ from unittest.mock import patch
|
||||
from PySide6.QtCore import QCoreApplication
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.ai.provider import AIProvider
|
||||
from app.dialogs.ai_prompts import SEND
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.ui.ai_panel import AIPanel
|
||||
from app.ui.tabs import LocalTerminalTabWidget
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""Stockage JSON : ecriture atomique, schema_version, migration, recuperation
|
||||
automatique depuis .bak quand le fichier principal est corrompu."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from app.key_manager import KeyManager, compute_fingerprint
|
||||
from app.session_manager import Session, SessionStore
|
||||
from app.storage.atomic_file import atomic_write_text
|
||||
from app.storage.json_store import load_json_document, save_json_document
|
||||
from app.session_manager import SessionStore, Session
|
||||
from app.key_manager import KeyManager, compute_fingerprint
|
||||
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ de programmation du cote de l'onglet, qui n'a besoin d'aucune connexion reseau
|
||||
reelle - cela evite aussi qu'un thread reste bloque dans un connect() lent et
|
||||
ne bloque la fin du script.
|
||||
"""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import time
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""ProxyJump / Bastion : resolution (app.bastion) pour les 3 sources (session,
|
||||
OpenSSH, manuel), et etablissement du canal direct-tcpip par SSHConnection via
|
||||
un bastion entierement mocke (aucun reseau reel)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
@@ -10,12 +12,18 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.bastion import BastionResolutionError, BastionTarget, resolve_bastion_target
|
||||
from app.session_manager import (
|
||||
SessionStore, Session, BastionConfig,
|
||||
AUTH_AUTO, AUTH_KEY,
|
||||
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
||||
AUTH_AUTO,
|
||||
AUTH_KEY,
|
||||
BASTION_MANUAL,
|
||||
BASTION_NONE,
|
||||
BASTION_OPENSSH,
|
||||
BASTION_SESSION,
|
||||
BastionConfig,
|
||||
Session,
|
||||
SessionStore,
|
||||
)
|
||||
from app.bastion import resolve_bastion_target, BastionResolutionError, BastionTarget
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Copier/coller du terminal : selection, presse-papiers, raccourcis."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import QEvent, QPoint, Qt
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from PySide6.QtCore import QEvent, Qt, QPoint
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.terminal_widget import TerminalWidget
|
||||
|
||||
@@ -90,8 +92,8 @@ print(" OK")
|
||||
|
||||
print()
|
||||
print("=== 9. Souris : selection = copie, clic droit = collage (comme sous Unix) ===")
|
||||
from PySide6.QtGui import QMouseEvent, QContextMenuEvent
|
||||
from PySide6.QtCore import QPointF
|
||||
from PySide6.QtGui import QContextMenuEvent, QMouseEvent
|
||||
|
||||
|
||||
def clic(bouton, col, row, type_evt=QEvent.MouseButtonPress):
|
||||
|
||||
@@ -4,7 +4,9 @@ passe (ConnectionController, bastion, suppression de session).
|
||||
|
||||
SSHConnection.start() est neutralisee pour les tests d'integration qui
|
||||
construisent un onglet reel (aucune connexion reseau necessaire)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import json
|
||||
@@ -12,18 +14,21 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||
|
||||
from app.credentials import (
|
||||
CredentialStore, NullCredentialStore, WindowsCredentialStore, session_credential_key,
|
||||
default_credential_store,
|
||||
)
|
||||
from app.session_manager import Session, SessionStore, AUTH_PASSWORD, PASSWORD_STORAGE_SECURE
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.bastion import BastionTarget
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.connection_controller import ConnectionController
|
||||
from app.controllers.session_controller import SessionController
|
||||
from app.bastion import BastionTarget
|
||||
from app.credentials import (
|
||||
CredentialStore,
|
||||
NullCredentialStore,
|
||||
WindowsCredentialStore,
|
||||
default_credential_store,
|
||||
session_credential_key,
|
||||
)
|
||||
from app.session_manager import AUTH_PASSWORD, PASSWORD_STORAGE_SECURE, Session, SessionStore
|
||||
from app.ssh_connection import SSHConnection
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
@@ -103,6 +108,7 @@ session_store = SessionStore(path=tmp / "sessions.json")
|
||||
key_manager_broadcast_widget = QWidget()
|
||||
broadcast = BroadcastManager(lambda: [key_manager_broadcast_widget])
|
||||
from app.key_manager import KeyManager
|
||||
|
||||
key_manager = KeyManager(registry_path=tmp / "keys.json")
|
||||
fake_store = FakeCredentialStore()
|
||||
cc = ConnectionController(session_store, key_manager, broadcast, credential_store=fake_store,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
from app.main_window import detect_ssh_target
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.main_window import MainWindow
|
||||
from app.theme import apply_theme, build_app_icon
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Recherche floue (app.services.fuzzy) : sous-sequence, score, tri."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.services.fuzzy import fuzzy_score
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Keepalive de SSHConnection : verifie l'envoi periodique et l'abandon apres
|
||||
`keepalive_count_max` echecs consecutifs, avec un transport paramiko mocke
|
||||
(aucune connexion reseau reelle)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import threading
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
(sans dependre des ACL reelles du systeme de fichiers, pour rester fiable quelle
|
||||
que soit la machine qui execute le test) et le bon aiguillage vers le repli chmod
|
||||
ailleurs que sous Windows."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import os
|
||||
@@ -11,7 +13,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.security.key_protection import protect_private_key, _protect_windows
|
||||
from app.security.key_protection import _protect_windows, protect_private_key
|
||||
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
key_file = tmp / "id_test"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Generation de cles SSH : tous les types proposes doivent produire une cle
|
||||
relisible par paramiko, donc utilisable pour se connecter."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
@@ -9,7 +11,11 @@ from pathlib import Path
|
||||
import paramiko
|
||||
|
||||
from app.key_manager import (
|
||||
KeyManager, KEY_TYPE_LABELS, KEY_TYPE_RSA, KEY_TYPE_ED25519, ECDSA_CURVES,
|
||||
ECDSA_CURVES,
|
||||
KEY_TYPE_ED25519,
|
||||
KEY_TYPE_LABELS,
|
||||
KEY_TYPE_RSA,
|
||||
KeyManager,
|
||||
)
|
||||
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
|
||||
@@ -4,14 +4,16 @@ sont les seuls endroits, en dehors des fichiers de stockage deja couverts par
|
||||
test_credentials.py/test_ai_settings.py, ou un secret pourrait fuiter sans
|
||||
etre ecrit intentionnellement (une boite de dialogue d'erreur, une trace
|
||||
affichee a l'utilisateur ou capturee dans un rapport de bug)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import io
|
||||
import urllib.error
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.ai.provider import OpenAICompatibleProvider, AIError
|
||||
from app.ai.provider import AIError, OpenAICompatibleProvider
|
||||
|
||||
SECRET_API_KEY = "sk-tres-secret-ne-jamais-fuiter-1234567890"
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Lecture de ~/.ssh/config : hosts detectes, champs minimum, conversion en Session."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
|
||||
@@ -5,29 +5,31 @@ activation.
|
||||
SSHConnection.start()/LocalTerminalConnection.start() sont neutralisees, et
|
||||
~/.ssh/config est redirige vers un fichier absent (pas de dependance au poste
|
||||
qui execute le test)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from app.session_manager import Session, SessionStore
|
||||
from app.key_manager import KeyManager
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
import app.app_settings as app_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.workspace import WorkspaceStore
|
||||
from app.snippets import Snippet, SnippetStore
|
||||
from app.dialogs.quick_connect_dialog import QuickConnectDialog
|
||||
import app.app_settings as app_settings
|
||||
from app.key_manager import KeyManager
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.session_manager import Session, SessionStore
|
||||
from app.snippets import Snippet, SnippetStore
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.workspace import WorkspaceStore
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
@@ -118,6 +120,7 @@ dialog2._activate_current()
|
||||
print(f" onglets avant/apres activation: {before}/{cc2.tabs.count()}")
|
||||
assert cc2.tabs.count() == before + 1
|
||||
from app.ui.tabs import SessionTabWidget
|
||||
|
||||
assert isinstance(cc2.tabs.widget(cc2.tabs.count() - 1), SessionTabWidget)
|
||||
print(" OK")
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Calcul du delai de reconnexion automatique (backoff exponentiel plafonne)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.services.reconnect import compute_backoff_delay
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Editeur de session : section Bastion / Jump Host (construction, pre-remplissage
|
||||
a l'edition, et resultat renvoye pour chacun des 4 modes)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
@@ -9,12 +11,18 @@ from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.session_manager import (
|
||||
SessionStore, Session, BastionConfig,
|
||||
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL, AUTH_KEY,
|
||||
)
|
||||
from app.key_manager import KeyManager
|
||||
from app.dialogs.session_dialog import SessionDialog
|
||||
from app.key_manager import KeyManager
|
||||
from app.session_manager import (
|
||||
AUTH_KEY,
|
||||
BASTION_MANUAL,
|
||||
BASTION_NONE,
|
||||
BASTION_OPENSSH,
|
||||
BASTION_SESSION,
|
||||
BastionConfig,
|
||||
Session,
|
||||
SessionStore,
|
||||
)
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Test de la logique SFTP avec un client SFTP simule (adosse a un dossier temporaire)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import stat as statmod
|
||||
import tempfile
|
||||
@@ -10,9 +11,8 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from app.sftp_panel import SFTPPanel, TransferJob, NAME_ROLE, ISDIR_ROLE, PARENT_ROLE
|
||||
from app.sftp_panel import NAME_ROLE, PARENT_ROLE, SFTPPanel
|
||||
|
||||
|
||||
class FakeAttr:
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
LocalTerminalConnection.start() est neutralisee : send() ne fait que deposer
|
||||
les octets dans une file interne (aucun sous-processus reel), ce qui suffit a
|
||||
verifier ce qui a ete envoye sans avoir besoin d'un vrai terminal."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.snippets import Snippet, SnippetStore, DEFAULT_SNIPPETS
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.snippets import DEFAULT_SNIPPETS, Snippet, SnippetStore
|
||||
from app.ui.tabs import LocalTerminalTabWidget
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
correctement la destination demandee par le client puis relaie le trafic a
|
||||
travers un (faux) transport SSH -- sans connexion SSH reelle, comme le fait
|
||||
deja test_tunnels.py pour la redirection locale."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from app.tunnels import TunnelManager, DIRECTION_DYNAMIC
|
||||
from app.tunnels import DIRECTION_DYNAMIC, TunnelManager
|
||||
|
||||
|
||||
class EchoServer(threading.Thread):
|
||||
|
||||
@@ -5,7 +5,9 @@ fermeture globale d'un onglet divise.
|
||||
SSHConnection.start() et LocalTerminalConnection.start() sont neutralisees :
|
||||
ce module teste l'agencement des panneaux et le cycle de vie des onglets, pas
|
||||
une vraie connexion (deja couverte par test_keepalive.py / test_bastion.py)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import itertools
|
||||
@@ -15,14 +17,13 @@ from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from app.session_manager import SessionStore
|
||||
from app.key_manager import KeyManager
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.connection_controller import ConnectionController
|
||||
from app.ui.tabs import LocalTerminalTabWidget
|
||||
from app.ui.split_pane import SplitContainer, LAYOUT_2V, LAYOUT_4
|
||||
from app.key_manager import KeyManager
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.session_manager import SessionStore
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.ui.split_pane import LAYOUT_2V, LAYOUT_4, SplitContainer
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Tags de session et parametres par defaut d'un groupe : persistance, saisie/
|
||||
normalisation des tags dans l'editeur, heritage explicite, filtrage par tag
|
||||
dans le panneau lateral."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import tempfile
|
||||
@@ -10,12 +12,12 @@ from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from app.session_manager import Session, SessionStore, AUTH_KEY
|
||||
from app.key_manager import KeyManager
|
||||
from app.groups import GroupSettings, GroupSettingsStore
|
||||
from app.dialogs.session_dialog import SessionDialog
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.session_controller import SessionController
|
||||
from app.dialogs.session_dialog import SessionDialog
|
||||
from app.groups import GroupSettings, GroupSettingsStore
|
||||
from app.key_manager import KeyManager
|
||||
from app.session_manager import AUTH_KEY, Session, SessionStore
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Confort du terminal : zoom de police, barre de defilement, recherche dans l'historique."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from PySide6.QtCore import QEvent, Qt
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from app.terminal_widget import TerminalWidget
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@ Le test monte un vrai serveur TCP local et un faux transport SSH dont le canal
|
||||
'direct-tcpip' est un simple socket vers ce serveur : cela valide toute la
|
||||
mecanique de relais sans avoir besoin d'un serveur SSH.
|
||||
"""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from app.tunnels import TunnelManager, DIRECTION_LOCAL, describe
|
||||
from app.tunnels import DIRECTION_LOCAL, TunnelManager, describe
|
||||
|
||||
|
||||
class EchoServer(threading.Thread):
|
||||
|
||||
@@ -6,7 +6,9 @@ SSHConnection.start()/LocalTerminalConnection.start() sont neutralisees (pas de
|
||||
connexion reelle necessaire). app.app_settings.SETTINGS_FILE est redirige vers
|
||||
un fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur
|
||||
(restore() met a jour last_workspace_id a chaque restauration)."""
|
||||
import sys, pathlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||
|
||||
import itertools
|
||||
@@ -16,18 +18,20 @@ from unittest.mock import patch
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
|
||||
from app.session_manager import Session, SessionStore
|
||||
from app.key_manager import KeyManager
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
import app.app_settings as app_settings
|
||||
from app.controllers.broadcast_manager import BroadcastManager
|
||||
from app.controllers.connection_controller import ConnectionController
|
||||
from app.controllers.workspace_manager import (
|
||||
WorkspaceManager, SETTING_RESTORE_ON_STARTUP, SETTING_LAST_WORKSPACE_ID,
|
||||
SETTING_LAST_WORKSPACE_ID,
|
||||
SETTING_RESTORE_ON_STARTUP,
|
||||
WorkspaceManager,
|
||||
)
|
||||
from app.workspace import WorkspaceStore, ITEM_SESSION_SSH, ITEM_LOCAL_SHELL
|
||||
from app.key_manager import KeyManager
|
||||
from app.local_terminal import LocalTerminalConnection
|
||||
from app.session_manager import Session, SessionStore
|
||||
from app.ssh_connection import SSHConnection
|
||||
from app.ui.split_pane import LAYOUT_2V
|
||||
import app.app_settings as app_settings
|
||||
from app.workspace import ITEM_LOCAL_SHELL, ITEM_SESSION_SSH, WorkspaceStore
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
@@ -95,7 +99,8 @@ print(f" onglets ouverts: {cc2.tabs.count()}, elements ignores: {skipped}")
|
||||
assert cc2.tabs.count() == 2
|
||||
assert skipped == 0
|
||||
from app.ui.split_pane import SplitContainer
|
||||
from app.ui.tabs import SessionTabWidget, LocalTerminalTabWidget
|
||||
from app.ui.tabs import LocalTerminalTabWidget, SessionTabWidget
|
||||
|
||||
assert isinstance(cc2.tabs.widget(0), LocalTerminalTabWidget)
|
||||
container = cc2.tabs.widget(1)
|
||||
assert isinstance(container, SplitContainer)
|
||||
|
||||
Reference in New Issue
Block a user