#!/usr/bin/env python3 """ SFTP client for file operations. Handles connection, file discovery, and download operations. """ import paramiko import os from pathlib import Path from logging_config import get_logger from config import get_config logger = get_logger(__name__) class SFTPClient: """SFTP operations for ACH file processing.""" def __init__(self): """Initialize SFTP client.""" self.config = get_config() self.sftp = None self.ssh = None def connect(self) -> bool: """ Establish SFTP connection. Returns: True if successful, False otherwise """ try: # Create SSH client self.ssh = paramiko.SSHClient() self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # Connect self.ssh.connect( self.config.sftp_host, port=self.config.sftp_port, username=self.config.sftp_username, password=self.config.sftp_password, timeout=10 ) # Get SFTP channel self.sftp = self.ssh.open_sftp() logger.info(f"Connected to SFTP server: {self.config.sftp_host}:{self.config.sftp_port}") return True except Exception as e: logger.error(f"Failed to connect to SFTP server: {e}", exc_info=True) return False def disconnect(self): """Close SFTP connection.""" try: if self.sftp: self.sftp.close() if self.ssh: self.ssh.close() logger.info("SFTP connection closed") except Exception as e: logger.error(f"Error closing SFTP connection: {e}") def list_files(self, remote_path: str, pattern: str = 'ACH_*.txt') -> list: """ List files matching pattern in remote directory. Args: remote_path: Path on SFTP server pattern: File pattern to match (e.g., 'ACH_*.txt') Returns: List of matching filenames """ if not self.sftp: logger.error("SFTP not connected") return [] try: files = [] try: items = self.sftp.listdir_attr(remote_path) except FileNotFoundError: logger.warning(f"Directory not found: {remote_path}") return [] import fnmatch for item in items: if fnmatch.fnmatch(item.filename, pattern): files.append(item.filename) logger.debug(f"Found {len(files)} files matching {pattern} in {remote_path}") return sorted(files) except Exception as e: logger.error(f"Error listing files in {remote_path}: {e}", exc_info=True) return [] def download_file(self, remote_path: str, local_path: str) -> bool: """ Download file from SFTP server. Args: remote_path: Full path on SFTP server local_path: Local destination path Returns: True if successful, False otherwise """ if not self.sftp: logger.error("SFTP not connected") return False try: # Create local directory if needed Path(local_path).parent.mkdir(parents=True, exist_ok=True) # Download file self.sftp.get(remote_path, local_path) logger.info(f"Downloaded file: {remote_path} -> {local_path}") return True except Exception as e: logger.error(f"Error downloading file {remote_path}: {e}", exc_info=True) return False def get_file_size(self, remote_path: str) -> int: """ Get size of remote file. Args: remote_path: Full path on SFTP server Returns: File size in bytes, or -1 if error """ if not self.sftp: logger.error("SFTP not connected") return -1 try: stat = self.sftp.stat(remote_path) return stat.st_size except Exception as e: logger.error(f"Error getting file size {remote_path}: {e}") return -1 def __enter__(self): """Context manager entry.""" self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.disconnect()