Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
"""Сериализация в json-совместимые значения и обратно (для golden-тестов и обмена)."""
|
||
|
||
from collections.abc import Mapping
|
||
from dataclasses import fields, is_dataclass
|
||
from datetime import date
|
||
from decimal import Decimal
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
from arm_statements.model import (
|
||
Cell,
|
||
CellValue,
|
||
DocumentType,
|
||
Evidence,
|
||
FormCandidate,
|
||
HeaderCells,
|
||
Severity,
|
||
Source,
|
||
StatementWarning,
|
||
)
|
||
|
||
|
||
def _value_to_jsonable(value: CellValue) -> Any:
|
||
"""Тег типа значения ячейки; bool проверяется раньше прочего (не подкласс Decimal/date, но явно)."""
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, bool):
|
||
return {"bool": value}
|
||
if isinstance(value, Decimal):
|
||
return {"decimal": str(value)}
|
||
return {"date": value.isoformat()}
|
||
|
||
|
||
def _scalar_to_jsonable(obj: object) -> Any:
|
||
if isinstance(obj, Enum):
|
||
return obj.value
|
||
if isinstance(obj, Decimal):
|
||
return str(obj)
|
||
if isinstance(obj, date):
|
||
return obj.isoformat()
|
||
return obj
|
||
|
||
|
||
def to_jsonable(obj: object) -> Any:
|
||
"""Dataclass пакета → json-совместимое значение."""
|
||
if isinstance(obj, Cell):
|
||
return {
|
||
"text": obj.text,
|
||
"value": _value_to_jsonable(obj.value),
|
||
"confidence": obj.confidence,
|
||
"ref": obj.ref,
|
||
}
|
||
if is_dataclass(obj) and not isinstance(obj, type):
|
||
return {f.name: to_jsonable(getattr(obj, f.name)) for f in fields(obj)}
|
||
if isinstance(obj, tuple | list):
|
||
return [to_jsonable(item) for item in obj]
|
||
return _scalar_to_jsonable(obj)
|
||
|
||
|
||
def _value_from_dict(raw: Mapping[str, Any] | None) -> CellValue:
|
||
if raw is None:
|
||
return None
|
||
if "decimal" in raw:
|
||
return Decimal(raw["decimal"])
|
||
if "date" in raw:
|
||
return date.fromisoformat(raw["date"])
|
||
if "bool" in raw:
|
||
return bool(raw["bool"])
|
||
raise ValueError(f"неизвестный тег значения ячейки: {sorted(raw)}")
|
||
|
||
|
||
def _cell_from_dict(raw: Mapping[str, Any] | None) -> Cell | None:
|
||
if raw is None:
|
||
return None
|
||
return Cell(
|
||
text=raw["text"],
|
||
value=_value_from_dict(raw["value"]),
|
||
confidence=float(raw["confidence"]),
|
||
ref=raw["ref"],
|
||
)
|
||
|
||
|
||
def _warning_from_dict(raw: Mapping[str, Any]) -> StatementWarning:
|
||
return StatementWarning(
|
||
code=raw["code"],
|
||
severity=Severity(raw["severity"]),
|
||
message=raw["message"],
|
||
line_refs=tuple(raw.get("line_refs", ())),
|
||
column_index=raw.get("column_index"),
|
||
field=raw.get("field"),
|
||
locator=raw.get("locator"),
|
||
)
|
||
|
||
|
||
def candidate_from_dict(data: Mapping[str, Any]) -> FormCandidate:
|
||
"""Обратная к `to_jsonable` для кандидата."""
|
||
type_hint = data["type_hint"]
|
||
evidence = data["evidence"]
|
||
return FormCandidate(
|
||
source=Source(data["source"]),
|
||
locator=data["locator"],
|
||
type_hint=None if type_hint is None else DocumentType(type_hint),
|
||
evidence=Evidence(okud=_cell_from_dict(evidence["okud"]), title=_cell_from_dict(evidence["title"])),
|
||
header=HeaderCells(**{name: _cell_from_dict(raw) for name, raw in data["header"].items()}),
|
||
rows=tuple(tuple(_cell_from_dict(raw) for raw in row) for row in data["rows"]),
|
||
segment_breaks=tuple(data.get("segment_breaks", ())),
|
||
warnings=tuple(_warning_from_dict(raw) for raw in data.get("warnings", ())),
|
||
)
|