from __future__ import annotations import tempfile from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from .config import ( build_process_log_file_name, build_protocol_file_name, build_report_file_name, join_path, ) from .file_manager import ( check_input_dir_access, connect_samba, create_run_context, move_file_to_error, move_file_to_processed, read_remote_file_bytes, scan_input_files, upload_local_file, ) from .parser import FileParseResult, parse_xml_content from .protocol import ProtocolRow, write_protocol from .report import ReportRow, write_report EMPTY_BATCH_STATUS = "info" @dataclass class BatchResult: status: str processed: int partial: int errors: int report_path: str protocol_path: str log_path: str messages: list[str] = field(default_factory=list) def run_batch( smb_base_path: str, input_dir: str, samba_user: str, samba_password: str, launcher: str, ) -> BatchResult: messages: list[str] = [] if not samba_user: return BatchResult( status="error", processed=0, partial=0, errors=1, report_path="", protocol_path="", log_path="", messages=["Введите логин SAMBA."], ) samba_conn = connect_samba( smb_base_path=smb_base_path, user_params={"user_name": samba_user, "password": samba_password}, ) is_ok, access_error = check_input_dir_access( input_dir=input_dir, samba_conn=samba_conn ) if not is_ok: return BatchResult( status="error", processed=0, partial=0, errors=1, report_path="", protocol_path="", log_path="", messages=[access_error or "Ошибка доступа к входной папке."], ) run_context = create_run_context(input_dir=input_dir, samba_conn=samba_conn) report_rows: list[ReportRow] = [] protocol_rows: list[ProtocolRow] = [] log_lines: list[str] = [] scan_result = scan_input_files(input_dir=input_dir, samba_conn=samba_conn) log_lines.append( f"{_now()} | scan files: valid={len(scan_result.valid_files)} invalid={len(scan_result.invalid_files)}" ) for remote_file in scan_result.invalid_files: reason = "Некорректное имя файла" moved_to = move_file_to_error( file_path=remote_file.path, file_name=remote_file.name, error_dir=run_context.error_dir, samba_conn=samba_conn, ) protocol_rows.append( ProtocolRow( file_name=remote_file.name, status="error", message=reason, operations_total=0, rows_written=0, launcher=launcher, processed_at=datetime.now(), ) ) log_lines.append(f"{_now()} | invalid -> err: {remote_file.path} => {moved_to}") for remote_file in scan_result.valid_files: parse_result = _parse_remote_xml(remote_file.name, remote_file.path, samba_conn) report_rows.extend(_report_rows_from_parse(parse_result)) status, message = _resolve_parse_status(parse_result) protocol_rows.append( ProtocolRow( file_name=remote_file.name, status=status, message=message, operations_total=parse_result.total_operations, rows_written=len(parse_result.rows), launcher=launcher, processed_at=datetime.now(), ) ) if status == "success": moved_to = move_file_to_processed( file_path=remote_file.path, file_name=remote_file.name, processed_dir=run_context.processed_dir, samba_conn=samba_conn, ) log_lines.append( f"{_now()} | success -> processed: {remote_file.path} => {moved_to}" ) else: moved_to = move_file_to_error( file_path=remote_file.path, file_name=remote_file.name, error_dir=run_context.error_dir, samba_conn=samba_conn, ) log_lines.append( f"{_now()} | {status} -> err: {remote_file.path} => {moved_to}" ) if not protocol_rows: protocol_rows.append( ProtocolRow( file_name="-", status=EMPTY_BATCH_STATUS, message="Новых файлов для обработки нет.", operations_total=0, rows_written=0, launcher=launcher, processed_at=datetime.now(), ) ) log_lines.append(f"{_now()} | empty batch") report_file_name = build_report_file_name( run_context.date_key, run_context.run_number ) protocol_file_name = build_protocol_file_name( run_context.date_key, run_context.run_number ) log_file_name = build_process_log_file_name( run_context.date_key, run_context.run_number ) report_path = join_path(run_context.run_dir, report_file_name) protocol_path = join_path(run_context.run_dir, protocol_file_name) log_path = join_path(run_context.run_dir, log_file_name) _upload_artifacts( report_rows=report_rows, protocol_rows=protocol_rows, log_lines=log_lines, report_path=report_path, protocol_path=protocol_path, log_path=log_path, samba_conn=samba_conn, ) processed = sum(1 for row in protocol_rows if row.status == "success") partial = sum(1 for row in protocol_rows if row.status == "partial") errors = sum(1 for row in protocol_rows if row.status == "error") status = ( "success" if errors == 0 and partial == 0 else ("partial" if partial > 0 else "error") ) messages.extend(log_lines[-5:]) return BatchResult( status=status, processed=processed, partial=partial, errors=errors, report_path=report_path, protocol_path=protocol_path, log_path=log_path, messages=messages, ) def _parse_remote_xml(file_name: str, remote_path: str, samba_conn) -> FileParseResult: try: content = read_remote_file_bytes(remote_path=remote_path, samba_conn=samba_conn) except Exception as exc: # noqa: BLE001 result = FileParseResult(file_name=file_name) result.fatal_error = f"Ошибка чтения файла из SAMBA: {exc}" return result return parse_xml_content(file_name=file_name, xml_content=content) def _report_rows_from_parse(parse_result: FileParseResult) -> list[ReportRow]: result: list[ReportRow] = [] for row in parse_result.rows: result.append( ReportRow( file_name=row.file_name, record_id=row.record_id, operation_index=row.operation_index, operation_fields=row.operation_fields, participant_fields=row.participant_fields, ) ) return result def _resolve_parse_status(parse_result: FileParseResult) -> tuple[str, str]: if parse_result.fatal_error: return "error", parse_result.fatal_error if parse_result.operation_errors: details = "; ".join( f"операция {item.operation_index}: {item.reason}" for item in parse_result.operation_errors ) return "partial", details return "success", "Успешно обработан." def _upload_artifacts( report_rows: list[ReportRow], protocol_rows: list[ProtocolRow], log_lines: list[str], report_path: str, protocol_path: str, log_path: str, samba_conn, ) -> None: with tempfile.TemporaryDirectory() as temp_dir: temp_dir_path = Path(temp_dir) local_report = temp_dir_path / "report.xlsx" local_protocol = temp_dir_path / "protocol.xlsx" local_log = temp_dir_path / "process.log" write_report(report_rows, local_report) write_protocol(protocol_rows, local_protocol) local_log.write_text("\n".join(log_lines) + "\n", encoding="utf-8") upload_local_file(local_report, report_path, samba_conn=samba_conn) upload_local_file(local_protocol, protocol_path, samba_conn=samba_conn) upload_local_file(local_log, log_path, samba_conn=samba_conn) def _now() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S")