228 lines
6.9 KiB
Python
228 lines
6.9 KiB
Python
from __future__ import annotations
|
||
|
||
import xml.etree.ElementTree as ET
|
||
from dataclasses import dataclass, field
|
||
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_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 = "Некорректное имя XML-файла"
|
||
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}"
|