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 PySide6.QtCore import QThread, Signal
|
||||||
|
|
||||||
from app.ai.provider import AIProvider, AIError
|
from app.ai.provider import AIError, AIProvider
|
||||||
|
|
||||||
KIND_EXPLAIN = "explain"
|
KIND_EXPLAIN = "explain"
|
||||||
KIND_DIAGNOSE = "diagnose"
|
KIND_DIAGNOSE = "diagnose"
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from app.ai.provider import AIProvider, OpenAICompatibleProvider
|
||||||
from app.app_settings import get_setting, set_setting
|
from app.app_settings import get_setting, set_setting
|
||||||
from app.credentials import CredentialStore
|
from app.credentials import CredentialStore
|
||||||
from app.ai.provider import AIProvider, OpenAICompatibleProvider
|
|
||||||
|
|
||||||
AI_API_KEY_CREDENTIAL = "ai:api_key"
|
AI_API_KEY_CREDENTIAL = "ai:api_key"
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
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.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):
|
class BastionResolutionError(Exception):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
aussi vers tous les autres onglets terminal ouverts, quand le mode est actif."""
|
aussi vers tous les autres onglets terminal ouverts, quand le mode est actif."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
|
||||||
from PySide6.QtCore import QObject
|
from PySide6.QtCore import QObject
|
||||||
from PySide6.QtWidgets import QToolButton, QWidget
|
from PySide6.QtWidgets import QToolButton, QWidget
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
ouverture, connexion rapide, renommage, fermeture."""
|
ouverture, connexion rapide, renommage, fermeture."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal, QObject
|
from PySide6.QtCore import QObject, Qt, Signal
|
||||||
from PySide6.QtWidgets import QTabWidget, QWidget, QMenu, QMessageBox, QInputDialog
|
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.key_manager import KeyManager
|
||||||
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
|
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.openssh.config import OpenSSHHost, session_from_openssh_host
|
||||||
from app.bastion import BastionTarget, resolve_bastion_target, BastionResolutionError
|
from app.services.ssh_target import detect_ssh_target
|
||||||
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
|
from app.session_manager import AUTH_PASSWORD, PASSWORD_STORAGE_SECURE, Session, SessionStore
|
||||||
from app.controllers.broadcast_manager import BroadcastManager
|
from app.ui.split_pane import LAYOUT_LABELS, PaneSlot, SplitContainer
|
||||||
from app.ui.tabs import BaseTerminalTab, SessionTabWidget, SFTPTabWidget, LocalTerminalTabWidget
|
from app.ui.tabs import BaseTerminalTab, LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget
|
||||||
from app.ui.split_pane import SplitContainer, PaneSlot, LAYOUT_LABELS
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionController(QObject):
|
class ConnectionController(QObject):
|
||||||
|
|||||||
@@ -6,22 +6,33 @@ from __future__ import annotations
|
|||||||
import copy
|
import copy
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal, QObject
|
from PySide6.QtCore import QObject, Qt, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QTreeWidget, QTreeWidgetItem,
|
QComboBox,
|
||||||
QMenu, QMessageBox, QComboBox, QLabel,
|
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.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.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 (
|
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
|
SESSION_ROLE = Qt.UserRole + 10
|
||||||
LOCAL_SHELL_ROLE = Qt.UserRole + 11
|
LOCAL_SHELL_ROLE = Qt.UserRole + 11
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ from PySide6.QtCore import QObject, Signal
|
|||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
from app.app_settings import get_setting, set_setting
|
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.local_terminal import DEFAULT_SHELL
|
||||||
from app.session_manager import SessionStore
|
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 (
|
from app.workspace import (
|
||||||
Workspace, WorkspaceStore, WorkspaceTab, WorkspaceItem,
|
ITEM_LOCAL_SHELL,
|
||||||
ITEM_SESSION_SSH, ITEM_SESSION_SFTP, 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_RESTORE_ON_STARTUP = "restore_last_workspace"
|
||||||
SETTING_LAST_WORKSPACE_ID = "last_workspace_id"
|
SETTING_LAST_WORKSPACE_ID = "last_workspace_id"
|
||||||
@@ -152,7 +157,12 @@ class WorkspaceManager(QObject):
|
|||||||
self.cc.split_tab(index, wtab.layout)
|
self.cc.split_tab(index, wtab.layout)
|
||||||
container = self.cc.tabs.widget(index)
|
container = self.cc.tabs.widget(index)
|
||||||
skipped = 0
|
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)
|
widget = self._widget_for_item(item)
|
||||||
if widget is None:
|
if widget is None:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
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)
|
_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 PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from app.app_settings import get_setting, set_setting
|
|
||||||
from app.ai.redaction import detect_secrets
|
from app.ai.redaction import detect_secrets
|
||||||
|
from app.app_settings import get_setting, set_setting
|
||||||
|
|
||||||
_NOTICE_ACK_KEY = "ai_notice_acknowledged"
|
_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 __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QFormLayout, QLineEdit, QCheckBox, QDoubleSpinBox,
|
QCheckBox,
|
||||||
QPushButton, QDialogButtonBox, QMessageBox, QLabel,
|
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.provider import OpenAICompatibleProvider
|
||||||
from app.ai.service import KIND_TEST, AIRequestWorker
|
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
|
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 __future__ import annotations
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
|
QCheckBox,
|
||||||
QHBoxLayout, QPushButton, QWidget, QVBoxLayout, QCheckBox, QStackedWidget, QLabel,
|
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.groups import GroupSettings, GroupSettingsStore
|
||||||
|
from app.key_manager import KeyManager
|
||||||
from app.openssh.config import load_openssh_hosts
|
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):
|
class GroupSettingsDialog(QDialog):
|
||||||
|
|||||||
@@ -5,13 +5,28 @@ from pathlib import Path
|
|||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
|
QApplication,
|
||||||
QMessageBox, QFileDialog, QFormLayout, QLineEdit, QComboBox, QSpinBox,
|
QComboBox,
|
||||||
QDialogButtonBox, QApplication, QLabel,
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFileDialog,
|
||||||
|
QFormLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QSpinBox,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.key_manager import (
|
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.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QPushButton,
|
QAbstractItemView,
|
||||||
QMessageBox, QLabel, QAbstractItemView,
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.known_hosts_manager import KnownHostsStore
|
from app.known_hosts_manager import KnownHostsStore
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
snippets et actions, avec recherche floue et navigation entierement au clavier."""
|
snippets et actions, avec recherche floue et navigation entierement au clavier."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass, field
|
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 (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QLineEdit, QListWidget, QListWidgetItem, QMessageBox,
|
QDialog,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.openssh.config import load_openssh_hosts
|
from app.openssh.config import load_openssh_hosts
|
||||||
@@ -111,9 +116,9 @@ class QuickConnectDialog(QDialog):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
def _quick_actions(self) -> list[tuple[str, Callable[[], None]]]:
|
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.key_dialog import KeyManagerDialog
|
||||||
|
from app.dialogs.snippet_dialog import SnippetManagerDialog
|
||||||
|
from app.dialogs.workspace_dialog import WorkspaceDialog
|
||||||
|
|
||||||
return [
|
return [
|
||||||
("Nouvelle session...", self.session_controller.new_session),
|
("Nouvelle session...", self.session_controller.new_session),
|
||||||
|
|||||||
@@ -3,20 +3,44 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QDialogButtonBox,
|
QCheckBox,
|
||||||
QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QWidget, QVBoxLayout,
|
QComboBox,
|
||||||
QCheckBox, QMessageBox, QGroupBox, QStackedWidget, QCompleter,
|
QCompleter,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFileDialog,
|
||||||
|
QFormLayout,
|
||||||
|
QGroupBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QSpinBox,
|
||||||
|
QStackedWidget,
|
||||||
|
QTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.session_manager import (
|
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
|
||||||
Session, SessionStore, BastionConfig, AUTH_AUTO, AUTH_KEY, AUTH_PASSWORD, AUTH_AGENT,
|
from app.groups import GroupSettingsStore
|
||||||
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
|
||||||
PASSWORD_STORAGE_NONE, PASSWORD_STORAGE_SECURE,
|
|
||||||
)
|
|
||||||
from app.key_manager import KeyManager
|
from app.key_manager import KeyManager
|
||||||
from app.openssh.config import load_openssh_hosts
|
from app.openssh.config import load_openssh_hosts
|
||||||
from app.groups import GroupSettingsStore
|
from app.session_manager import (
|
||||||
from app.credentials import CredentialStore, NullCredentialStore, session_credential_key
|
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):
|
class SessionDialog(QDialog):
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QPushButton,
|
QComboBox,
|
||||||
QMessageBox, QLineEdit, QLabel, QFormLayout, QTextEdit, QDialogButtonBox, QComboBox,
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QTextEdit,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.snippets import Snippet, SnippetStore
|
from app.snippets import Snippet, SnippetStore
|
||||||
|
|||||||
@@ -3,8 +3,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QTreeWidget, QTreeWidgetItem,
|
QAbstractItemView,
|
||||||
QPushButton, QComboBox, QSpinBox, QLineEdit, QLabel, QMessageBox, QAbstractItemView,
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QFormLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QSpinBox,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.tunnels import DIRECTION_DYNAMIC, DIRECTION_LABELS, TunnelManager, describe
|
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.QtCore import Qt
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton,
|
QCheckBox,
|
||||||
QMessageBox, QInputDialog, QLabel, QCheckBox,
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.app_settings import get_setting, set_setting
|
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):
|
class WorkspaceDialog(QDialog):
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ librement modifiables, sans indirection a suivre dans tout le reste du code
|
|||||||
(ConnectionController, SSHConnection, resolve_bastion_target...)."""
|
(ConnectionController, SSHConnection, resolve_bastion_target...)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.paths import DATA_DIR
|
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.storage.json_store import load_json_document, save_json_document
|
||||||
from app.session_manager import BastionConfig, AUTH_AUTO
|
|
||||||
|
|
||||||
GROUPS_FILE = DATA_DIR / "groups.json"
|
GROUPS_FILE = DATA_DIR / "groups.json"
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 1
|
||||||
@@ -36,7 +36,7 @@ class GroupSettings:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "GroupSettings":
|
def from_dict(data: dict) -> GroupSettings:
|
||||||
g = GroupSettings()
|
g = GroupSettings()
|
||||||
for k in GroupSettings.__dataclass_fields__:
|
for k in GroupSettings.__dataclass_fields__:
|
||||||
if k not in data:
|
if k not in data:
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
from cryptography.hazmat.primitives import serialization
|
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.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.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"
|
KEYS_FILE = DATA_DIR / "keys.json"
|
||||||
DEFAULT_KEY_DIR = DATA_DIR / "keys"
|
DEFAULT_KEY_DIR = DATA_DIR / "keys"
|
||||||
@@ -116,7 +116,7 @@ class KeyEntry:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "KeyEntry":
|
def from_dict(data: dict) -> KeyEntry:
|
||||||
e = KeyEntry()
|
e = KeyEntry()
|
||||||
for k in KeyEntry.__dataclass_fields__:
|
for k in KeyEntry.__dataclass_fields__:
|
||||||
if k in data:
|
if k in data:
|
||||||
|
|||||||
@@ -9,33 +9,37 @@ from __future__ import annotations
|
|||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from PySide6.QtGui import QAction, QActionGroup, QKeySequence
|
from PySide6.QtGui import QAction, QActionGroup, QKeySequence
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QMainWindow, QDockWidget, QMessageBox, QStatusBar, QApplication,
|
QApplication,
|
||||||
|
QDockWidget,
|
||||||
|
QMainWindow,
|
||||||
|
QMessageBox,
|
||||||
|
QStatusBar,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.session_manager import SessionStore
|
from app.ai.settings import build_provider_from_settings
|
||||||
from app.key_manager import KeyManager
|
from app.controllers.broadcast_manager import BroadcastManager
|
||||||
from app.local_terminal import SHELL_LABELS, DEFAULT_SHELL
|
from app.controllers.connection_controller import ConnectionController
|
||||||
from app.terminal_widget import TerminalWidget
|
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.key_dialog import KeyManagerDialog
|
||||||
from app.dialogs.known_hosts_dialog import KnownHostsDialog
|
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.quick_connect_dialog import QuickConnectDialog
|
||||||
from app.dialogs.ai_settings_dialog import AISettingsDialog
|
from app.dialogs.snippet_dialog import SnippetManagerDialog
|
||||||
from app.ai.settings import build_provider_from_settings
|
from app.dialogs.workspace_dialog import WorkspaceDialog
|
||||||
from app.theme import THEME_LABELS, apply_theme, current_theme, save_theme
|
|
||||||
from app.workspace import WorkspaceStore
|
|
||||||
from app.snippets import SnippetStore
|
|
||||||
from app.groups import GroupSettingsStore
|
from app.groups import GroupSettingsStore
|
||||||
from app.credentials import default_credential_store
|
from app.key_manager import KeyManager
|
||||||
from app.controllers.broadcast_manager import BroadcastManager
|
from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS
|
||||||
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
|
|
||||||
|
|
||||||
# re-exporte pour compatibilite (utilise par tests/test_detect.py et du code existant)
|
# 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.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):
|
class MainWindow(QMainWindow):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import paramiko
|
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
|
@dataclass
|
||||||
@@ -57,7 +57,7 @@ def load_openssh_hosts(path: Path | None = None) -> list[OpenSSHHost]:
|
|||||||
|
|
||||||
parser = paramiko.SSHConfig()
|
parser = paramiko.SSHConfig()
|
||||||
try:
|
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)
|
parser.parse(f)
|
||||||
except OSError:
|
except OSError:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
|
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
|
||||||
@@ -50,7 +50,7 @@ class BastionConfig:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "BastionConfig":
|
def from_dict(data: dict) -> BastionConfig:
|
||||||
b = BastionConfig()
|
b = BastionConfig()
|
||||||
for k in BastionConfig.__dataclass_fields__:
|
for k in BastionConfig.__dataclass_fields__:
|
||||||
if k in data:
|
if k in data:
|
||||||
@@ -95,7 +95,7 @@ class Session:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@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}
|
known = {f: data.get(f) for f in Session.__dataclass_fields__ if f in data}
|
||||||
s = Session()
|
s = Session()
|
||||||
for k, v in known.items():
|
for k, v in known.items():
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
from PySide6.QtCore import QThread, Signal
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,30 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import tempfile
|
|
||||||
import stat
|
import stat
|
||||||
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import paramiko
|
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.QtGui import QDrag, QKeySequence, QShortcut
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QToolButton, QTreeWidget,
|
QAbstractItemView,
|
||||||
QTreeWidgetItem, QSplitter, QLabel, QMessageBox, QInputDialog, QProgressDialog,
|
QHBoxLayout,
|
||||||
QAbstractItemView, QMenu, QHeaderView,
|
QHeaderView,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMenu,
|
||||||
|
QMessageBox,
|
||||||
|
QProgressDialog,
|
||||||
|
QSplitter,
|
||||||
|
QToolButton,
|
||||||
|
QTreeWidget,
|
||||||
|
QTreeWidgetItem,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.sftp.worker import SftpWorker
|
from app.sftp.worker import SftpWorker
|
||||||
@@ -125,7 +137,7 @@ class TransferWorker(QThread):
|
|||||||
def _upload_dir(self, local_dir: str, remote_dir: str) -> None:
|
def _upload_dir(self, local_dir: str, remote_dir: str) -> None:
|
||||||
try:
|
try:
|
||||||
self.sftp.mkdir(remote_dir)
|
self.sftp.mkdir(remote_dir)
|
||||||
except IOError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
for entry in os.listdir(local_dir):
|
for entry in os.listdir(local_dir):
|
||||||
if self._cancel:
|
if self._cancel:
|
||||||
@@ -615,7 +627,7 @@ class RemoteFilePanel(FilePanel):
|
|||||||
self._rmdir_recursive(path)
|
self._rmdir_recursive(path)
|
||||||
else:
|
else:
|
||||||
self.sftp.remove(path)
|
self.sftp.remove(path)
|
||||||
except IOError as exc:
|
except OSError as exc:
|
||||||
errors.append(f"{name} : {exc}")
|
errors.append(f"{name} : {exc}")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@@ -745,7 +757,7 @@ class RemoteFilePanel(FilePanel):
|
|||||||
for entry_name, _ in entries:
|
for entry_name, _ in entries:
|
||||||
try:
|
try:
|
||||||
self.sftp.chmod(self.join(current_path, entry_name), mode)
|
self.sftp.chmod(self.join(current_path, entry_name), mode)
|
||||||
except IOError as exc:
|
except OSError as exc:
|
||||||
errors.append(f"{entry_name} : {exc}")
|
errors.append(f"{entry_name} : {exc}")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ depuis un terminal (inserer sans executer, executer, executer avec Multi-Send)."
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.paths import DATA_DIR
|
from app.paths import DATA_DIR
|
||||||
@@ -25,7 +25,7 @@ class Snippet:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "Snippet":
|
def from_dict(data: dict) -> Snippet:
|
||||||
s = Snippet()
|
s = Snippet()
|
||||||
for k in Snippet.__dataclass_fields__:
|
for k in Snippet.__dataclass_fields__:
|
||||||
if k in data:
|
if k in data:
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import threading
|
|||||||
import paramiko
|
import paramiko
|
||||||
from PySide6.QtCore import QThread, Signal
|
from PySide6.QtCore import QThread, Signal
|
||||||
|
|
||||||
from app.known_hosts_manager import KnownHostsStore
|
|
||||||
from app.bastion import BastionTarget
|
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)
|
from app.paths import DATA_DIR # noqa: E402 (chemins valables aussi en .exe)
|
||||||
|
|
||||||
KNOWN_HOSTS_FILE = DATA_DIR / "known_hosts"
|
KNOWN_HOSTS_FILE = DATA_DIR / "known_hosts"
|
||||||
|
|
||||||
|
|
||||||
class TrustPrompt(paramiko.MissingHostKeyPolicy):
|
class TrustPrompt(paramiko.MissingHostKeyPolicy):
|
||||||
"""Politique qui demande confirmation (via signal Qt) avant de faire confiance a une nouvelle cle d'hote."""
|
"""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
|
self.connection = connection
|
||||||
|
|
||||||
def missing_host_key(self, client, hostname, key):
|
def missing_host_key(self, client, hostname, key):
|
||||||
@@ -379,7 +379,10 @@ class SSHConnection(QThread):
|
|||||||
except paramiko.PasswordRequiredException:
|
except paramiko.PasswordRequiredException:
|
||||||
pwd = passphrase or self._ask_passphrase(key_path)
|
pwd = passphrase or self._ask_passphrase(key_path)
|
||||||
if not pwd:
|
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:
|
try:
|
||||||
return loader.from_private_key_file(key_path, password=pwd)
|
return loader.from_private_key_file(key_path, password=pwd)
|
||||||
except paramiko.SSHException as exc:
|
except paramiko.SSHException as exc:
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ plutot que de perdre les donnees de l'utilisateur.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from app.storage.atomic_file import atomic_write_bytes
|
from app.storage.atomic_file import atomic_write_bytes
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,14 @@ from __future__ import annotations
|
|||||||
import pyte
|
import pyte
|
||||||
from PySide6.QtCore import QRect, Qt, Signal
|
from PySide6.QtCore import QRect, Qt, Signal
|
||||||
from PySide6.QtGui import (
|
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
|
from app import theme as app_theme
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ from __future__ import annotations
|
|||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
from PySide6.QtCore import Qt
|
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 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.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"
|
STYLESHEET_PATH = RESOURCES_DIR / "theme.qss"
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def _relay(sock: socket.socket, chan: paramiko.Channel) -> None:
|
|||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
sock.sendall(data)
|
sock.sendall(data)
|
||||||
except (OSError, socket.error):
|
except OSError:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
for closeable in (chan, sock):
|
for closeable in (chan, sock):
|
||||||
|
|||||||
@@ -10,12 +10,19 @@ from __future__ import annotations
|
|||||||
import html
|
import html
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
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.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 = {
|
_KIND_LABELS = {
|
||||||
"explain": "Expliquer cette sortie",
|
"explain": "Expliquer cette sortie",
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ existant (SessionTabWidget, LocalTerminalTabWidget, ...) sans le modifier.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QToolButton, QFrame,
|
QFrame,
|
||||||
|
QHBoxLayout,
|
||||||
|
QSplitter,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
LAYOUT_SINGLE = "1"
|
LAYOUT_SINGLE = "1"
|
||||||
|
|||||||
@@ -11,31 +11,41 @@ import re
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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.QtGui import QKeySequence, QShortcut
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QLabel, QMessageBox, QSplitter,
|
QFileDialog,
|
||||||
QFrame, QLineEdit, QFileDialog, QMenu, QInputDialog,
|
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.danger import check_dangerous
|
||||||
from app.ai.redaction import redact
|
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_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
|
# sequences d'echappement ANSI, retirees des journaux pour qu'ils restent lisibles
|
||||||
ANSI_ESCAPE_RE = re.compile(
|
ANSI_ESCAPE_RE = re.compile(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ mot de passe/passphrase exactement comme une ouverture normale)."""
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field, asdict
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.paths import DATA_DIR
|
from app.paths import DATA_DIR
|
||||||
@@ -32,7 +32,7 @@ class WorkspaceItem:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "WorkspaceItem":
|
def from_dict(data: dict) -> WorkspaceItem:
|
||||||
item = WorkspaceItem()
|
item = WorkspaceItem()
|
||||||
for k in WorkspaceItem.__dataclass_fields__:
|
for k in WorkspaceItem.__dataclass_fields__:
|
||||||
if k in data:
|
if k in data:
|
||||||
@@ -56,7 +56,7 @@ class WorkspaceTab:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@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 = WorkspaceTab(title=data.get("title", ""), layout=data.get("layout", "1"))
|
||||||
tab.items = [WorkspaceItem.from_dict(i) for i in data.get("items", [])]
|
tab.items = [WorkspaceItem.from_dict(i) for i in data.get("items", [])]
|
||||||
tab.splitter_sizes = [list(s) for s in data.get("splitter_sizes", [])]
|
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]}
|
return {"id": self.id, "name": self.name, "tabs": [t.to_dict() for t in self.tabs]}
|
||||||
|
|
||||||
@staticmethod
|
@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 = 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", [])]
|
ws.tabs = [WorkspaceTab.from_dict(t) for t in data.get("tabs", [])]
|
||||||
return ws
|
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
|
-r requirements.txt
|
||||||
pytest>=8.0
|
pytest>=8.0
|
||||||
pytest-qt>=4.4
|
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
|
"""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
|
(bloc de code s'il y en a un), et les 3 actions possibles (Copier / Inserer
|
||||||
dans le terminal / Annuler) - jamais d'execution automatique."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Detection locale de commandes potentiellement destructives proposees par
|
"""Detection locale de commandes potentiellement destructives proposees par
|
||||||
l'assistant IA (jamais un blocage, seulement un avertissement)."""
|
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))
|
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 ===")
|
print("=== 1. Commandes destructrices detectees ===")
|
||||||
dangerous_examples = [
|
dangerous_examples = [
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
la verification de securite (avis + secrets) est bien invoquee avant tout
|
la verification de securite (avis + secrets) est bien invoquee avant tout
|
||||||
envoi, que les reponses/erreurs mettent a jour la conversation, et qu'une
|
envoi, que les reponses/erreurs mettent a jour la conversation, et qu'une
|
||||||
reponse perimee (generation obsolete) est ignoree."""
|
reponse perimee (generation obsolete) est ignoree."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -10,9 +12,9 @@ from unittest.mock import patch
|
|||||||
from PySide6.QtCore import QCoreApplication
|
from PySide6.QtCore import QCoreApplication
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.ai.provider import AIProvider, AIError
|
from app.ai.provider import AIError, AIProvider
|
||||||
from app.ai.service import KIND_EXPLAIN, KIND_ASK
|
from app.ai.service import KIND_EXPLAIN
|
||||||
from app.dialogs.ai_prompts import MASK, SEND, CANCEL
|
from app.dialogs.ai_prompts import CANCEL, MASK, SEND
|
||||||
from app.ui.ai_panel import AIPanel
|
from app.ui.ai_panel import AIPanel
|
||||||
|
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
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
|
temporaire), et verification des secrets a chaque envoi. QMessageBox est
|
||||||
simule (exec()/clickedButton() patches) : aucune boite modale reelle ne doit
|
simule (exec()/clickedButton() patches) : aucune boite modale reelle ne doit
|
||||||
bloquer les tests."""
|
bloquer les tests."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -13,7 +15,13 @@ from unittest.mock import patch
|
|||||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
import app.app_settings as app_settings
|
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)
|
app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
"""OpenAICompatibleProvider : requete HTTP construite correctement, et gestion
|
"""OpenAICompatibleProvider : requete HTTP construite correctement, et gestion
|
||||||
des erreurs (401, 429, timeout, reseau, reponse invalide) - urllib.request.urlopen
|
des erreurs (401, 429, timeout, reseau, reponse invalide) - urllib.request.urlopen
|
||||||
est entierement mocke, aucun appel reseau reel."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import urllib.error
|
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):
|
def fake_response(payload: dict):
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Detection/redaction de secrets avant envoi a l'assistant IA."""
|
"""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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from app.ai.redaction import detect_secrets, redact
|
from app.ai.redaction import detect_secrets, redact
|
||||||
@@ -16,8 +18,8 @@ print("=== 2. Jeton Bearer detecte ===")
|
|||||||
found = detect_secrets("Authorization: Bearer sk-abcdefghijklmnopqrstuvwxyz123456")
|
found = detect_secrets("Authorization: Bearer sk-abcdefghijklmnopqrstuvwxyz123456")
|
||||||
labels = [f.label for f in found]
|
labels = [f.label for f in found]
|
||||||
print(" detecte:", labels)
|
print(" detecte:", labels)
|
||||||
assert any("bearer" in l.lower() for l in labels)
|
assert any("bearer" in label.lower() for label in labels)
|
||||||
assert any("authorization" in l.lower() for l in labels)
|
assert any("authorization" in label.lower() for label in labels)
|
||||||
print(" OK")
|
print(" OK")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""AIRequestWorker : execution d'une requete IA dans un vrai QThread en
|
"""AIRequestWorker : execution d'une requete IA dans un vrai QThread en
|
||||||
arriere-plan, jamais sur le thread Qt principal - avec dispatch correct selon
|
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."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
@@ -9,9 +11,15 @@ import threading
|
|||||||
from PySide6.QtCore import QCoreApplication
|
from PySide6.QtCore import QCoreApplication
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.ai.provider import AIProvider, AIError
|
from app.ai.provider import AIError, AIProvider
|
||||||
from app.ai.service import (
|
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)
|
app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
|||||||
@@ -2,20 +2,28 @@
|
|||||||
app_settings (redirige vers un fichier temporaire pour ne jamais toucher les
|
app_settings (redirige vers un fichier temporaire pour ne jamais toucher les
|
||||||
vrais reglages), cle API exclusivement via CredentialStore, et construction du
|
vrais reglages), cle API exclusivement via CredentialStore, et construction du
|
||||||
fournisseur a partir des deux."""
|
fournisseur a partir des deux."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
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
|
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):
|
class FakeCredentialStore(CredentialStore):
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ automatiquement, avec avertissement si dangereuse).
|
|||||||
|
|
||||||
LocalTerminalConnection.start() est neutralisee comme dans test_snippets.py :
|
LocalTerminalConnection.start() est neutralisee comme dans test_snippets.py :
|
||||||
aucun sous-processus reel, send() depose seulement les octets dans une file."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -13,9 +15,9 @@ from unittest.mock import patch
|
|||||||
from PySide6.QtCore import QCoreApplication
|
from PySide6.QtCore import QCoreApplication
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.local_terminal import LocalTerminalConnection
|
|
||||||
from app.ai.provider import AIProvider
|
from app.ai.provider import AIProvider
|
||||||
from app.dialogs.ai_prompts import SEND
|
from app.dialogs.ai_prompts import SEND
|
||||||
|
from app.local_terminal import LocalTerminalConnection
|
||||||
from app.ui.ai_panel import AIPanel
|
from app.ui.ai_panel import AIPanel
|
||||||
from app.ui.tabs import LocalTerminalTabWidget
|
from app.ui.tabs import LocalTerminalTabWidget
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
"""Stockage JSON : ecriture atomique, schema_version, migration, recuperation
|
"""Stockage JSON : ecriture atomique, schema_version, migration, recuperation
|
||||||
automatique depuis .bak quand le fichier principal est corrompu."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
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.atomic_file import atomic_write_text
|
||||||
from app.storage.json_store import load_json_document, save_json_document
|
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())
|
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
|
reelle - cela evite aussi qu'un thread reste bloque dans un connect() lent et
|
||||||
ne bloque la fin du script.
|
ne bloque la fin du script.
|
||||||
"""
|
"""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""ProxyJump / Bastion : resolution (app.bastion) pour les 3 sources (session,
|
"""ProxyJump / Bastion : resolution (app.bastion) pour les 3 sources (session,
|
||||||
OpenSSH, manuel), et etablissement du canal direct-tcpip par SSHConnection via
|
OpenSSH, manuel), et etablissement du canal direct-tcpip par SSHConnection via
|
||||||
un bastion entierement mocke (aucun reseau reel)."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -10,12 +12,18 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from app.bastion import BastionResolutionError, BastionTarget, resolve_bastion_target
|
||||||
from app.session_manager import (
|
from app.session_manager import (
|
||||||
SessionStore, Session, BastionConfig,
|
AUTH_AUTO,
|
||||||
AUTH_AUTO, AUTH_KEY,
|
AUTH_KEY,
|
||||||
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Copier/coller du terminal : selection, presse-papiers, raccourcis."""
|
"""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))
|
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.QtGui import QKeyEvent
|
||||||
from PySide6.QtCore import QEvent, Qt, QPoint
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.terminal_widget import TerminalWidget
|
from app.terminal_widget import TerminalWidget
|
||||||
|
|
||||||
@@ -90,8 +92,8 @@ print(" OK")
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
print("=== 9. Souris : selection = copie, clic droit = collage (comme sous Unix) ===")
|
print("=== 9. Souris : selection = copie, clic droit = collage (comme sous Unix) ===")
|
||||||
from PySide6.QtGui import QMouseEvent, QContextMenuEvent
|
|
||||||
from PySide6.QtCore import QPointF
|
from PySide6.QtCore import QPointF
|
||||||
|
from PySide6.QtGui import QContextMenuEvent, QMouseEvent
|
||||||
|
|
||||||
|
|
||||||
def clic(bouton, col, row, type_evt=QEvent.MouseButtonPress):
|
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
|
SSHConnection.start() est neutralisee pour les tests d'integration qui
|
||||||
construisent un onglet reel (aucune connexion reseau necessaire)."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -12,18 +14,21 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
|
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||||
|
|
||||||
from app.credentials import (
|
from app.bastion import BastionTarget
|
||||||
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.controllers.broadcast_manager import BroadcastManager
|
from app.controllers.broadcast_manager import BroadcastManager
|
||||||
from app.controllers.connection_controller import ConnectionController
|
from app.controllers.connection_controller import ConnectionController
|
||||||
from app.controllers.session_controller import SessionController
|
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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
@@ -103,6 +108,7 @@ session_store = SessionStore(path=tmp / "sessions.json")
|
|||||||
key_manager_broadcast_widget = QWidget()
|
key_manager_broadcast_widget = QWidget()
|
||||||
broadcast = BroadcastManager(lambda: [key_manager_broadcast_widget])
|
broadcast = BroadcastManager(lambda: [key_manager_broadcast_widget])
|
||||||
from app.key_manager import KeyManager
|
from app.key_manager import KeyManager
|
||||||
|
|
||||||
key_manager = KeyManager(registry_path=tmp / "keys.json")
|
key_manager = KeyManager(registry_path=tmp / "keys.json")
|
||||||
fake_store = FakeCredentialStore()
|
fake_store = FakeCredentialStore()
|
||||||
cc = ConnectionController(session_store, key_manager, broadcast, credential_store=fake_store,
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
from app.main_window import detect_ssh_target
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.main_window import MainWindow
|
from app.main_window import MainWindow
|
||||||
from app.theme import apply_theme, build_app_icon
|
from app.theme import apply_theme, build_app_icon
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Recherche floue (app.services.fuzzy) : sous-sequence, score, tri."""
|
"""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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from app.services.fuzzy import fuzzy_score
|
from app.services.fuzzy import fuzzy_score
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Keepalive de SSHConnection : verifie l'envoi periodique et l'abandon apres
|
"""Keepalive de SSHConnection : verifie l'envoi periodique et l'abandon apres
|
||||||
`keepalive_count_max` echecs consecutifs, avec un transport paramiko mocke
|
`keepalive_count_max` echecs consecutifs, avec un transport paramiko mocke
|
||||||
(aucune connexion reseau reelle)."""
|
(aucune connexion reseau reelle)."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
(sans dependre des ACL reelles du systeme de fichiers, pour rester fiable quelle
|
(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
|
que soit la machine qui execute le test) et le bon aiguillage vers le repli chmod
|
||||||
ailleurs que sous Windows."""
|
ailleurs que sous Windows."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -11,7 +13,7 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
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())
|
tmp = Path(tempfile.mkdtemp())
|
||||||
key_file = tmp / "id_test"
|
key_file = tmp / "id_test"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Generation de cles SSH : tous les types proposes doivent produire une cle
|
"""Generation de cles SSH : tous les types proposes doivent produire une cle
|
||||||
relisible par paramiko, donc utilisable pour se connecter."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -9,7 +11,11 @@ from pathlib import Path
|
|||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
from app.key_manager import (
|
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())
|
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
|
test_credentials.py/test_ai_settings.py, ou un secret pourrait fuiter sans
|
||||||
etre ecrit intentionnellement (une boite de dialogue d'erreur, une trace
|
etre ecrit intentionnellement (une boite de dialogue d'erreur, une trace
|
||||||
affichee a l'utilisateur ou capturee dans un rapport de bug)."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import urllib.error
|
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"
|
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."""
|
"""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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|||||||
@@ -5,29 +5,31 @@ activation.
|
|||||||
SSHConnection.start()/LocalTerminalConnection.start() sont neutralisees, et
|
SSHConnection.start()/LocalTerminalConnection.start() sont neutralisees, et
|
||||||
~/.ssh/config est redirige vers un fichier absent (pas de dependance au poste
|
~/.ssh/config est redirige vers un fichier absent (pas de dependance au poste
|
||||||
qui execute le test)."""
|
qui execute le test)."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QEvent
|
from PySide6.QtCore import QEvent, Qt
|
||||||
from PySide6.QtGui import QKeyEvent
|
from PySide6.QtGui import QKeyEvent
|
||||||
from PySide6.QtWidgets import QApplication, QWidget
|
from PySide6.QtWidgets import QApplication, QWidget
|
||||||
|
|
||||||
from app.session_manager import Session, SessionStore
|
import app.app_settings as app_settings
|
||||||
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.broadcast_manager import BroadcastManager
|
||||||
from app.controllers.connection_controller import ConnectionController
|
from app.controllers.connection_controller import ConnectionController
|
||||||
from app.controllers.session_controller import SessionController
|
from app.controllers.session_controller import SessionController
|
||||||
from app.controllers.workspace_manager import WorkspaceManager
|
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
|
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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
@@ -118,6 +120,7 @@ dialog2._activate_current()
|
|||||||
print(f" onglets avant/apres activation: {before}/{cc2.tabs.count()}")
|
print(f" onglets avant/apres activation: {before}/{cc2.tabs.count()}")
|
||||||
assert cc2.tabs.count() == before + 1
|
assert cc2.tabs.count() == before + 1
|
||||||
from app.ui.tabs import SessionTabWidget
|
from app.ui.tabs import SessionTabWidget
|
||||||
|
|
||||||
assert isinstance(cc2.tabs.widget(cc2.tabs.count() - 1), SessionTabWidget)
|
assert isinstance(cc2.tabs.widget(cc2.tabs.count() - 1), SessionTabWidget)
|
||||||
print(" OK")
|
print(" OK")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Calcul du delai de reconnexion automatique (backoff exponentiel plafonne)."""
|
"""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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from app.services.reconnect import compute_backoff_delay
|
from app.services.reconnect import compute_backoff_delay
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Editeur de session : section Bastion / Jump Host (construction, pre-remplissage
|
"""Editeur de session : section Bastion / Jump Host (construction, pre-remplissage
|
||||||
a l'edition, et resultat renvoye pour chacun des 4 modes)."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -9,12 +11,18 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from PySide6.QtWidgets import QApplication
|
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.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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Test de la logique SFTP avec un client SFTP simule (adosse a un dossier temporaire)."""
|
"""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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
import os
|
import os
|
||||||
import posixpath
|
|
||||||
import shutil
|
import shutil
|
||||||
import stat as statmod
|
import stat as statmod
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -10,9 +11,8 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import QApplication
|
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:
|
class FakeAttr:
|
||||||
|
|||||||
@@ -4,18 +4,20 @@
|
|||||||
LocalTerminalConnection.start() est neutralisee : send() ne fait que deposer
|
LocalTerminalConnection.start() est neutralisee : send() ne fait que deposer
|
||||||
les octets dans une file interne (aucun sous-processus reel), ce qui suffit a
|
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."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
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.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
|
from app.ui.tabs import LocalTerminalTabWidget
|
||||||
|
|
||||||
app = QApplication.instance() or QApplication([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
correctement la destination demandee par le client puis relaie le trafic a
|
correctement la destination demandee par le client puis relaie le trafic a
|
||||||
travers un (faux) transport SSH -- sans connexion SSH reelle, comme le fait
|
travers un (faux) transport SSH -- sans connexion SSH reelle, comme le fait
|
||||||
deja test_tunnels.py pour la redirection locale."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from app.tunnels import TunnelManager, DIRECTION_DYNAMIC
|
from app.tunnels import DIRECTION_DYNAMIC, TunnelManager
|
||||||
|
|
||||||
|
|
||||||
class EchoServer(threading.Thread):
|
class EchoServer(threading.Thread):
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ fermeture globale d'un onglet divise.
|
|||||||
SSHConnection.start() et LocalTerminalConnection.start() sont neutralisees :
|
SSHConnection.start() et LocalTerminalConnection.start() sont neutralisees :
|
||||||
ce module teste l'agencement des panneaux et le cycle de vie des onglets, pas
|
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)."""
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -15,14 +17,13 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QWidget
|
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.broadcast_manager import BroadcastManager
|
||||||
from app.controllers.connection_controller import ConnectionController
|
from app.controllers.connection_controller import ConnectionController
|
||||||
from app.ui.tabs import LocalTerminalTabWidget
|
from app.key_manager import KeyManager
|
||||||
from app.ui.split_pane import SplitContainer, LAYOUT_2V, LAYOUT_4
|
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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Tags de session et parametres par defaut d'un groupe : persistance, saisie/
|
"""Tags de session et parametres par defaut d'un groupe : persistance, saisie/
|
||||||
normalisation des tags dans l'editeur, heritage explicite, filtrage par tag
|
normalisation des tags dans l'editeur, heritage explicite, filtrage par tag
|
||||||
dans le panneau lateral."""
|
dans le panneau lateral."""
|
||||||
import sys, pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -10,12 +12,12 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QWidget
|
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.broadcast_manager import BroadcastManager
|
||||||
from app.controllers.session_controller import SessionController
|
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([])
|
app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Confort du terminal : zoom de police, barre de defilement, recherche dans l'historique."""
|
"""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))
|
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.QtCore import QEvent, Qt
|
||||||
|
from PySide6.QtGui import QKeyEvent
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
from app.terminal_widget import TerminalWidget
|
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
|
'direct-tcpip' est un simple socket vers ce serveur : cela valide toute la
|
||||||
mecanique de relais sans avoir besoin d'un serveur SSH.
|
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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from app.tunnels import TunnelManager, DIRECTION_LOCAL, describe
|
from app.tunnels import DIRECTION_LOCAL, TunnelManager, describe
|
||||||
|
|
||||||
|
|
||||||
class EchoServer(threading.Thread):
|
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
|
connexion reelle necessaire). app.app_settings.SETTINGS_FILE est redirige vers
|
||||||
un fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur
|
un fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur
|
||||||
(restore() met a jour last_workspace_id a chaque restauration)."""
|
(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))
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -16,18 +18,20 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QWidget
|
from PySide6.QtWidgets import QApplication, QWidget
|
||||||
|
|
||||||
from app.session_manager import Session, SessionStore
|
import app.app_settings as app_settings
|
||||||
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.broadcast_manager import BroadcastManager
|
||||||
from app.controllers.connection_controller import ConnectionController
|
from app.controllers.connection_controller import ConnectionController
|
||||||
from app.controllers.workspace_manager import (
|
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
|
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([])
|
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 cc2.tabs.count() == 2
|
||||||
assert skipped == 0
|
assert skipped == 0
|
||||||
from app.ui.split_pane import SplitContainer
|
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)
|
assert isinstance(cc2.tabs.widget(0), LocalTerminalTabWidget)
|
||||||
container = cc2.tabs.widget(1)
|
container = cc2.tabs.widget(1)
|
||||||
assert isinstance(container, SplitContainer)
|
assert isinstance(container, SplitContainer)
|
||||||
|
|||||||
Reference in New Issue
Block a user