"""Единый роутер импорта lab: XML / PDF / xlsx → preview API.""" from __future__ import annotations import re from typing import Any from app.lab.etl.agrostar_pdf_import import looks_like_agrostar_pdf, parse_agrostar_pdf from app.lab.etl.agrostar_xlsx_import import parse_agrostar_xlsx from app.lab.etl.agrostar_xml_import import AgrostarParseResult, enrich_parse_with_matches, parse_agrostar_xml from app.lab.etl.plinor_pdf_import import ( enrich_plinor_composition, is_plinor_sos, is_plinor_zoo, parse_plinor_sos, parse_plinor_zoo, ) SOURCE_LABELS: dict[str, str] = { "agrostar_xml": "AgroStar XML", "agrostar_pdf": "AgroStar PDF", "agrostar_xlsx": "AgroStar Excel", "plinor_sos": "ПЛИНОР — состав рациона", "plinor_zoo": "ПЛИНОР — показатели рациона", } _PDF_MAGIC = b"%PDF" _XLSX_MAGIC = b"PK\x03\x04" _XML_MARKERS = (b"Standard_XML_Data", b" bool: n = (name or "").lower() return "зоо" in n or re.search(r"(^|[^a-z])zoo([^a-z]|$)", n) is not None def _filename_hint_plinor_sos(name: str) -> bool: n = (name or "").lower() return "сос" in n or re.search(r"(^|[^a-z])sos([^a-z]|$)", n) is not None def _looks_like_xml_agrostar(data: bytes) -> bool: head = data[:16384] return any(marker in head for marker in _XML_MARKERS) def _looks_like_xlsx(data: bytes) -> bool: return data.startswith(_XLSX_MAGIC) def _looks_like_pdf(data: bytes) -> bool: return data.startswith(_PDF_MAGIC) def _pdf_import_available() -> bool: try: import fitz # noqa: F401 except ImportError: return False return True def detect_source_format(filename: str, data: bytes) -> str: """Определение формата по содержимому и имени (расширение — запасной вариант).""" name = (filename or "").lower() if _looks_like_pdf(data): return _detect_pdf_format(data, name) if _looks_like_xlsx(data): return "agrostar_xlsx" if _looks_like_xml_agrostar(data): return "agrostar_xml" if name.endswith(".xml"): return "agrostar_xml" if name.endswith(".xlsx") or name.endswith(".xls"): return "agrostar_xlsx" if name.endswith(".pdf"): return _detect_pdf_format(data, name) return "unknown" def _detect_pdf_format(data: bytes, name: str) -> str: head = data[:8192] if b"Standard_XML_Data" in head: return "agrostar_xml" if looks_like_agrostar_pdf(data): return "agrostar_pdf" pdf_text = "" if _pdf_import_available(): try: pdf_text = _peek_pdf_text(data) except Exception: # noqa: BLE001 pdf_text = "" if pdf_text: if is_plinor_zoo(pdf_text): return "plinor_zoo" if is_plinor_sos(pdf_text): return "plinor_sos" if looks_like_agrostar_pdf(data): return "agrostar_pdf" if _filename_hint_plinor_zoo(name): return "plinor_zoo" if _filename_hint_plinor_sos(name): return "plinor_sos" return "unknown_pdf" def _peek_pdf_text(data: bytes) -> str: try: import fitz except ImportError as exc: raise RuntimeError("Для PDF нужен pymupdf (pip install pymupdf)") from exc doc = fitz.open(stream=data, filetype="pdf") text = doc[0].get_text() if doc.page_count else "" doc.close() return text def parse_lab_import(filename: str, data: bytes) -> dict[str, Any]: """Разбор файла → единый JSON для /lab комбайна.""" if not data: return {"error": True, "message": "Пустой файл", "kind": "error"} fmt = detect_source_format(filename, data) source_label = SOURCE_LABELS.get(fmt, "Неизвестный формат") if fmt == "agrostar_xml": text = _decode_text(data) parsed = parse_agrostar_xml(text) if parsed.errors and not parsed.samples: return { "error": True, "message": "; ".join(parsed.errors), "kind": "error", "sourceFormat": fmt, **parsed.to_api_dict(), } body = enrich_parse_with_matches(parsed) body["kind"] = "lab_samples" body["sourceFormat"] = fmt body["sourceLabel"] = source_label body["fileName"] = filename if parsed.errors: body["warnings"] = parsed.errors return body if fmt == "agrostar_pdf": parsed = parse_agrostar_pdf(data) return _lab_samples_response(parsed, fmt, source_label, filename) if fmt == "agrostar_xlsx": parsed = parse_agrostar_xlsx(data) return _lab_samples_response(parsed, fmt, source_label, filename) if fmt == "plinor_sos": parsed = parse_plinor_sos(data) if parsed.errors and not parsed.feed_lines: return { "error": True, "message": "; ".join(parsed.errors), "kind": "error", "sourceFormat": fmt, } body = enrich_plinor_composition(parsed.to_api_dict()) body["kind"] = "ration_composition" body["sourceFormat"] = fmt body["sourceLabel"] = source_label body["fileName"] = filename if parsed.errors: body["warnings"] = parsed.errors return body if fmt == "plinor_zoo": parsed = parse_plinor_zoo(data) if parsed.errors and not parsed.indicators: return { "error": True, "message": "; ".join(parsed.errors), "kind": "error", "sourceFormat": fmt, } body = parsed.to_api_dict() body["kind"] = "ration_indicators" body["sourceFormat"] = fmt body["sourceLabel"] = source_label body["fileName"] = filename if parsed.errors: body["warnings"] = parsed.errors return body if fmt == "unknown_pdf" and _looks_like_pdf(data) and not _pdf_import_available(): return { "error": True, "message": "Для PDF нужен pymupdf — выполните: pip install pymupdf", "kind": "error", "sourceFormat": fmt, "fileName": filename, } return { "error": True, "message": "Формат не распознан. Поддерживаются: AgroStar xml/pdf/xlsx, ПЛИНОР сос/зоо pdf", "kind": "error", "sourceFormat": fmt, "fileName": filename, } def _lab_samples_response( parsed: AgrostarParseResult, fmt: str, source_label: str, filename: str, ) -> dict[str, Any]: if parsed.errors and not parsed.samples: return { "error": True, "message": "; ".join(parsed.errors), "kind": "error", "sourceFormat": fmt, **parsed.to_api_dict(), } body = enrich_parse_with_matches(parsed) body["kind"] = "lab_samples" body["sourceFormat"] = fmt body["sourceLabel"] = source_label body["fileName"] = filename if parsed.errors: body["warnings"] = parsed.errors return body def _decode_text(data: bytes) -> str: for encoding in ("utf-8", "utf-8-sig", "cp1251"): try: return data.decode(encoding) except UnicodeDecodeError: continue return data.decode("utf-8", errors="replace")