SFM/app/pipeline/service.py
2026-06-22 07:25:27 +03:00

269 lines
8.5 KiB
Python

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
@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="success",
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")