Листы-продолжения форм (STATEMENT_CONTINUATION) идут в конвейер форм, а не в разбор ОСВ (R-5). Refs #14 Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
86 lines
4.5 KiB
Python
86 lines
4.5 KiB
Python
"""Интерфейс разбора файла (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,
|
||
)
|