"""Execution asynchrone des operations SFTP. paramiko.SFTPClient n'est pas concu pour etre pilote depuis plusieurs threads a la fois : ce module fait en sorte qu'un seul thread possede jamais un client SFTP donne. Toute operation (listage, mkdir, rename, remove, chmod, stat, ...) est soumise a la file de ce thread sous forme de callable ; le resultat revient sur le thread appelant (GUI) via un signal Qt, qui les mets automatiquement en file d'attente quand emetteur et recepteur vivent dans des threads differents. """ from __future__ import annotations import queue import threading from collections.abc import Callable from dataclasses import dataclass from typing import Any from PySide6.QtCore import QThread, Signal @dataclass class _Job: request_id: int fn: Callable[[], Any] class SftpWorker(QThread): """Un worker = un canal SFTP, jamais partage avec un autre thread.""" result_ready = Signal(int, object, object) # request_id, resultat, exception|None def __init__(self, parent=None): super().__init__(parent) self._queue: queue.Queue[_Job | None] = queue.Queue() self._next_id = 0 self._lock = threading.Lock() def submit(self, fn: Callable[[], Any]) -> int: """Met `fn` en file pour execution sur le thread du worker. Renvoie un identifiant a rapprocher du signal `result_ready`.""" with self._lock: self._next_id += 1 request_id = self._next_id self._queue.put(_Job(request_id, fn)) return request_id def run(self) -> None: while True: job = self._queue.get() if job is None: # sentinelle posee par stop() break try: result = job.fn() self.result_ready.emit(job.request_id, result, None) except Exception as exc: # noqa: BLE001 - remonte a l'appelant via le signal self.result_ready.emit(job.request_id, None, exc) def stop(self) -> None: """Arrete le worker une fois les jobs deja en file executes.""" self._queue.put(None)