SFM/app/pipeline/service.py
2026-06-22 11:48:39 +03:00

290 lines
9.5 KiB
Python
Raw 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 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(),
)
)
if moved_to:
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)
if status == "success":
try:
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}"
)
except Exception as exc: # noqa: BLE001
status = "partial"
message = f"{message} Не удалось переместить в Processed: {exc}"
log_lines.append(
f"{_now()} | move to Processed failed: {remote_file.path}. {exc}"
)
else:
try:
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}"
)
except Exception as exc: # noqa: BLE001
message = f"{message} Не удалось переместить в Err: {exc}"
if status == "success":
status = "partial"
log_lines.append(
f"{_now()} | move to Err failed: {remote_file.path}. {exc}"
)
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 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)
# Use CRLF to keep one-record-per-line in Windows viewers.
local_log.write_text("\r\n".join(log_lines) + "\r\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")