from __future__ import annotations import xml.etree.ElementTree as ET from dataclasses import dataclass, field from pathlib import Path from typing import Iterable from .config import validate_file_name as validate_pipeline_file_name @dataclass class ParticipantRow: file_name: str operation_index: int record_id: str operation_fields: dict[str, str] participant_fields: dict[str, str] @dataclass class OperationParseError: operation_index: int record_id: str reason: str @dataclass class FileParseResult: file_name: str rows: list[ParticipantRow] = field(default_factory=list) total_operations: int = 0 operation_errors: list[OperationParseError] = field(default_factory=list) fatal_error: str | None = None @property def is_fatal(self) -> bool: return self.fatal_error is not None def validate_file_name(file_name: str) -> bool: return validate_pipeline_file_name(file_name) def parse_xml_file(file_path: Path) -> FileParseResult: try: content = file_path.read_bytes() except OSError as exc: result = FileParseResult(file_name=file_path.name) result.fatal_error = f"Ошибка чтения файла: {exc}" return result return parse_xml_content(file_name=file_path.name, xml_content=content) def parse_xml_content(file_name: str, xml_content: bytes | str) -> FileParseResult: result = FileParseResult(file_name=file_name) if not validate_file_name(file_name): result.fatal_error = "Некорректное имя файла" return result try: root = ET.fromstring(xml_content) except ET.ParseError as exc: result.fatal_error = f"Ошибка XML: {exc}" return result try: operations = _get_operations(root) common_fields = _extract_common_fields(root) except ValueError as exc: result.fatal_error = str(exc) return result for index, operation in enumerate(operations, start=1): result.total_operations += 1 record_id = f"ОПЕРАЦИЯ_{index}" try: operation_fields = _extract_direct_fields( operation, excluded_tags={"УчастникОп"} ) operation_fields = {**common_fields, **operation_fields} record_id = _extract_record_id(operation_fields, index) participants = _find_children_by_name(operation, "УчастникОп") if participants: for participant in participants: participant_fields = _extract_direct_fields(participant) result.rows.append( ParticipantRow( file_name=file_name, operation_index=index, record_id=record_id, operation_fields=operation_fields, participant_fields=participant_fields, ) ) else: result.rows.append( ParticipantRow( file_name=file_name, operation_index=index, record_id=record_id, operation_fields=operation_fields, participant_fields={}, ) ) except Exception as exc: # noqa: BLE001 result.operation_errors.append( OperationParseError( operation_index=index, record_id=record_id, reason=f"Ошибка обработки операции: {exc}", ) ) return result def _get_operations(root: ET.Element) -> list[ET.Element]: if _normalize_tag(root.tag) != "СообщОперКО": raise ValueError("Некорректный корневой элемент XML (ожидался СообщОперКО)") inform_part = _find_child_by_name(root, "ИнформЧасть") if inform_part is None: raise ValueError("В XML отсутствует элемент ИнформЧасть") details = _find_child_by_name(inform_part, "СведКО") if details is None: raise ValueError("В XML отсутствует элемент СведКО") return _find_children_by_name(details, "Операция") def _extract_direct_fields( element: ET.Element, excluded_tags: Iterable[str] | None = None, ) -> dict[str, str]: excluded = set(excluded_tags or ()) fields: dict[str, str] = {} for child in element: tag = _normalize_tag(child.tag) if tag in excluded: continue _collect_leaf_fields(child, tag, fields) return fields def _extract_record_id(operation_fields: dict[str, str], operation_index: int) -> str: possible_keys = ( "ИдентификаторЗаписи", "НомерЗаписи", "ИдЗаписи", "ИдЗап", "ИдОпер", "ID", ) for key in possible_keys: if operation_fields.get(key): return operation_fields[key] return f"ОПЕРАЦИЯ_{operation_index}" def _normalize_tag(tag: str) -> str: if "}" in tag: return tag.split("}", maxsplit=1)[1] return tag def _find_child_by_name(parent: ET.Element, child_name: str) -> ET.Element | None: for child in parent: if _normalize_tag(child.tag) == child_name: return child return None def _find_children_by_name(parent: ET.Element, child_name: str) -> list[ET.Element]: return [child for child in parent if _normalize_tag(child.tag) == child_name] def _extract_common_fields(root: ET.Element) -> dict[str, str]: common: dict[str, str] = {} service_part = _find_child_by_name(root, "СлужЧасть") if service_part is not None: common.update(_extract_direct_fields(service_part)) inform_part = _find_child_by_name(root, "ИнформЧасть") if inform_part is None: return common info_bank = _find_child_by_name(inform_part, "ИнфБанк") if info_bank is not None: common.update(_extract_direct_fields(info_bank)) details = _find_child_by_name(inform_part, "СведКО") if details is None: return common common.update( _extract_direct_fields(details, excluded_tags={"Операция", "ИнфФилиал"}) ) branch_info = _find_child_by_name(details, "ИнфФилиал") if branch_info is not None: common.update(_extract_direct_fields(branch_info)) return common def _collect_leaf_fields( element: ET.Element, current_path: str, fields: dict[str, str] ) -> None: children = list(element) if not children: _append_value(fields, current_path, (element.text or "").strip()) return for child in children: child_name = _normalize_tag(child.tag) _collect_leaf_fields(child, f"{current_path}.{child_name}", fields) def _append_value(fields: dict[str, str], key: str, value: str) -> None: if key not in fields: fields[key] = value return existing = fields[key] if not value: return if not existing: fields[key] = value return if value in existing.split("; "): return fields[key] = f"{existing}; {value}"