SFM/app/pipeline/parser.py
2026-07-10 16:09:04 +03:00

228 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}"