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>
119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
"""Snippets de commandes : bibliotheque organisee par categories, utilisable
|
|
depuis un terminal (inserer sans executer, executer, executer avec Multi-Send)."""
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
|
|
from app.paths import DATA_DIR
|
|
from app.storage.json_store import load_json_document, save_json_document
|
|
|
|
SNIPPETS_FILE = DATA_DIR / "snippets.json"
|
|
SCHEMA_VERSION = 1
|
|
|
|
|
|
@dataclass
|
|
class Snippet:
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
name: str = ""
|
|
category: str = ""
|
|
description: str = ""
|
|
command: str = ""
|
|
|
|
def to_dict(self) -> dict:
|
|
return asdict(self)
|
|
|
|
@staticmethod
|
|
def from_dict(data: dict) -> Snippet:
|
|
s = Snippet()
|
|
for k in Snippet.__dataclass_fields__:
|
|
if k in data:
|
|
setattr(s, k, data[k])
|
|
return s
|
|
|
|
|
|
# Quelques exemples fournis d'origine (premier lancement uniquement) : l'utilisateur
|
|
# peut les modifier ou les supprimer librement, ce ne sont que des points de depart.
|
|
DEFAULT_SNIPPETS: list[Snippet] = [
|
|
Snippet(name="Disk Usage", category="Linux",
|
|
description="Espace disque par systeme de fichiers", command="df -h"),
|
|
Snippet(name="Failed Services", category="Linux",
|
|
description="Services systemd en echec", command="systemctl --failed"),
|
|
Snippet(name="Listening Ports", category="Linux",
|
|
description="Ports en ecoute", command="ss -tulpn"),
|
|
Snippet(name="Containers", category="Docker",
|
|
description="Conteneurs (tous statuts)", command="docker ps -a"),
|
|
Snippet(name="Docker Cleanup", category="Docker",
|
|
description="Nettoyer images/conteneurs/volumes inutilises", command="docker system prune -a"),
|
|
Snippet(name="Logs", category="Docker",
|
|
description="Logs d'un conteneur (a completer avec son nom)", command="docker logs -f "),
|
|
Snippet(name="Cluster Status", category="Proxmox",
|
|
description="Etat du cluster Proxmox", command="pvecm status"),
|
|
Snippet(name="VM List", category="Proxmox",
|
|
description="Liste des machines virtuelles", command="qm list"),
|
|
]
|
|
|
|
|
|
class SnippetStore:
|
|
"""Charge/sauvegarde la liste des snippets depuis data/snippets.json."""
|
|
|
|
def __init__(self, path: Path = SNIPPETS_FILE):
|
|
self.path = path
|
|
self.snippets: list[Snippet] = []
|
|
self.load_warning: str | None = None
|
|
self.load()
|
|
|
|
def load(self) -> None:
|
|
was_missing = not self.path.exists()
|
|
document, warning = load_json_document(
|
|
self.path,
|
|
default={"schema_version": SCHEMA_VERSION, "snippets": [s.to_dict() for s in DEFAULT_SNIPPETS]},
|
|
current_version=SCHEMA_VERSION,
|
|
)
|
|
self.load_warning = warning
|
|
self.snippets = [Snippet.from_dict(s) for s in document.get("snippets", [])]
|
|
if was_missing:
|
|
self.save()
|
|
|
|
def save(self) -> None:
|
|
payload = {"schema_version": SCHEMA_VERSION, "snippets": [s.to_dict() for s in self.snippets]}
|
|
save_json_document(self.path, payload)
|
|
|
|
def get(self, snippet_id: str) -> Snippet | None:
|
|
return next((s for s in self.snippets if s.id == snippet_id), None)
|
|
|
|
def add(self, snippet: Snippet) -> None:
|
|
self.snippets.append(snippet)
|
|
self.save()
|
|
|
|
def update(self, snippet: Snippet) -> None:
|
|
for i, s in enumerate(self.snippets):
|
|
if s.id == snippet.id:
|
|
self.snippets[i] = snippet
|
|
break
|
|
self.save()
|
|
|
|
def delete(self, snippet_id: str) -> None:
|
|
self.snippets = [s for s in self.snippets if s.id != snippet_id]
|
|
self.save()
|
|
|
|
def categories(self) -> list[str]:
|
|
return sorted({s.category for s in self.snippets if s.category})
|
|
|
|
def by_category(self) -> dict[str, list[Snippet]]:
|
|
grouped: dict[str, list[Snippet]] = {}
|
|
for s in sorted(self.snippets, key=lambda s: s.name.lower()):
|
|
grouped.setdefault(s.category or "Sans categorie", []).append(s)
|
|
return dict(sorted(grouped.items(), key=lambda kv: kv[0].lower()))
|
|
|
|
def search(self, query: str) -> list[Snippet]:
|
|
q = query.lower().strip()
|
|
if not q:
|
|
return list(self.snippets)
|
|
return [
|
|
s for s in self.snippets
|
|
if q in s.name.lower() or q in s.category.lower()
|
|
or q in s.description.lower() or q in s.command.lower()
|
|
]
|