"""The deploy-target tester that dispatches on ``kind``. ``DeployTargetTester`` is one contract with two very different implementations, and the wizard does not care which one it is talking to: it hands over a :class:`DeployTargetSpec` and wants a yes/no and a plain-language reason. The dispatcher exists so the wizard never has to know that `ssh` and `sftp` are the same protocol while `ftp` is a different world with no host key, no atomic switch and a mandatory backup. The *differences* are documented on the deployers themselves; the caller sees one shape. """ from __future__ import annotations from app.application.ports.deploy_target import ( DEPLOY_KINDS, DeployCheck, DeployTargetSpec, ) from app.core.config import settings from app.core.errors import ValidationError from app.infrastructure.adapters.deploy_ftp import FtpDeployer from app.infrastructure.adapters.deploy_ssh import SshDeployer class DispatchingDeployTargetTester: """``test_connection`` routed to the right transport for ``spec.kind``.""" def __init__( self, *, ssh: SshDeployer | None = None, ftp: FtpDeployer | None = None, ) -> None: allow_private = settings.deploy_allow_private_hosts self.ssh = ssh or SshDeployer(allow_private_hosts=allow_private) self.ftp = ftp or FtpDeployer(allow_private_hosts=allow_private) async def test_connection(self, *, target: DeployTargetSpec) -> DeployCheck: if target.kind not in DEPLOY_KINDS: raise ValidationError( "نوع مقصد استقرار پشتیبانی نمی‌شود؛ مقادیر مجاز: " + "، ".join(DEPLOY_KINDS) ) if not target.remote_path.strip(): raise ValidationError("مسیر مقصد روی سرور مشخص نشده است.") if target.kind in ("ssh", "sftp"): return await self.ssh.test_connection(target=target) return await self.ftp.test_connection(target=target) def deployer_for(self, kind: str): """The object that can actually deploy to this kind of target.""" return self.ssh if kind in ("ssh", "sftp") else self.ftp __all__ = ["DispatchingDeployTargetTester"]