Andrey Epifancev 43ee753d74 refactor(parser): BookParser — оркестратор книги; OsvParser — конвейер листа ОСВ; заглушка форм до конвейера
Листы-продолжения форм (STATEMENT_CONTINUATION) идут в конвейер форм, а не в разбор ОСВ (R-5).

Refs #14

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 15:49:27 +04:00

86 lines
4.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.

"""Интерфейс разбора файла (design D-3): «байты + тип файла → документы / метрики / ошибка».
Парсер не знает о NATS и S3: транспорт (`core/worker.py`) читает файл, вызывает `Parser.parse`, пишет результат.
Реализация по умолчанию — `BookParser` (`book.py`; листы ОСВ — `osv.py`); `NullParser` (заглушка Среза 0)
остаётся для тестов воркера.
`parse` — обычный синхронный метод: разбор XLS/XLSX/XLSB CPU-bound, внутри нет await. Воркер (asyncio) вызывает его
через `asyncio.to_thread(parser.parse, data, file_type)`, чтобы не блокировать цикл событий.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol, runtime_checkable
from src.contract import ErrorCode, FileType, MessageError, Metrics, OsvDocument, ResultStatus, StatementDocument
from src.core.config import Settings
from src.parser.rules import DEFAULT_RULES_DIR, load_rules
__all__ = ["NullParser", "ParseOutcome", "Parser", "get_parser"]
NOT_IMPLEMENTED_MESSAGE = "разбор XLS/XLSX/XLSB реализуется в Срезе 2"
@dataclass(frozen=True, slots=True)
class ParseOutcome:
"""Итог разбора. Правила — как у файла результата контракта: `done | needs_check` требуют документов,
`error` — `error {code, message}`.
"""
status: ResultStatus
documents: tuple[OsvDocument | StatementDocument, ...] = ()
metrics: Metrics | None = None
error: MessageError | None = None
def __post_init__(self) -> None:
if self.status is ResultStatus.ERROR:
if self.error is None:
raise ValueError("при status = error обязательно поле error")
elif not self.documents:
raise ValueError(f"при status = {self.status.value} список documents не может быть пустым")
@runtime_checkable
class Parser(Protocol):
"""Разбор файла: байты + тип → `ParseOutcome`.
Ожидаемые сбои разбора (не тот формат, нечитаемый лист и т. п.) возвращаются исходом `status = error` с
`error {code, message}`; непредвиденное исключение воркер превращает в `UNKNOWN_ERROR` (design D-5).
"""
def parse(self, data: bytes, file_type: FileType) -> ParseOutcome: ...
class NullParser:
"""Заглушка Среза 0: всегда `error NOT_IMPLEMENTED`; входные байты не читает."""
def parse(self, data: bytes, file_type: FileType) -> ParseOutcome:
return ParseOutcome(
status=ResultStatus.ERROR,
error=MessageError(code=ErrorCode.NOT_IMPLEMENTED, message=NOT_IMPLEMENTED_MESSAGE),
)
def get_parser(settings: Settings | None = None, *, rules_dir: Path = DEFAULT_RULES_DIR) -> Parser:
"""Фабрика парсера для воркера: `BookParser` со словарями `rules_dir` и лимитами `PARSE_MAX_BYTES` /
`PARSE_TIMEOUT_SECONDS` из `settings` (без настроек — значения по умолчанию из `Settings`).
Словари читаются и валидируются здесь, один раз при старте (design D-7): ошибка словаря — `RuntimeError` с файлом
и ключом, сервис не стартует, а не падает на первом файле.
"""
# `book.py` и `osv.py` импортируют `ParseOutcome` из этого пакета: импорт на уровне модуля был бы циклическим
from src.parser.book import BookParser, SkippingStatementParser # noqa: PLC0415 - цикл импорта book → src.parser
from src.parser.osv import OsvParser # noqa: PLC0415 - цикл импорта osv → src.parser
fields = Settings.model_fields
max_bytes = fields["PARSE_MAX_BYTES"].default if settings is None else settings.PARSE_MAX_BYTES
timeout_seconds = fields["PARSE_TIMEOUT_SECONDS"].default if settings is None else settings.PARSE_TIMEOUT_SECONDS
rules = load_rules(rules_dir)
return BookParser(
rules,
osv=OsvParser(rules),
statements=SkippingStatementParser(),
max_bytes=max_bytes,
timeout_seconds=timeout_seconds,
)