llm-ready-data / app /services /monetary_field_service.py
validops-east-1's picture
feat: infer/validate monetary_fields object_name from structure, slim report
2ac4af1
Raw
History Blame Contribute Delete
5.91 kB
"""In-place monetary-field parsing for the feature-extraction pipeline.
When a feature-extract request declares ``monetary_fields`` — an object name
plus a list of field names — the extracted ``data`` payload is post-processed
so those fields become real numbers instead of raw strings, and a per-field
status report is returned so callers can see exactly what happened to each
declared field.
Example
-------
Request config::
{"object_name": "invoice", "field_names": ["total", "invoice_no"]}
Extracted data::
{"invoice": [{"number": "INV-2026-00125", "total": "1250.75"}]}
After :func:`apply_monetary_fields` the same dict has ``"total": 1250.75``
(price-parsed) and the returned report explains each field:
* ``total`` -> status ``parsed``, parsed_value 1250.75
* ``invoice_no`` -> status ``not_found`` (no such field in the object)
"""
from __future__ import annotations
from typing import Any, List, Optional
from pydantic import BaseModel, Field
from app.services.price_parser import parse_amount
class MonetaryFieldStatus(BaseModel):
"""Outcome of one declared monetary field, per extracted object item."""
object_name: str
"""Container key the field was looked up in (from the request config)."""
field: str
"""Monetary field name (from the request config)."""
item_index: int = 0
"""Index of the object inside the container this report refers to."""
status: str = Field(
default="not_found",
description="One of: parsed, not_found, not_parsable, skipped.",
)
value: Optional[str] = None
"""The raw value as extracted (string form), when present."""
parsed_value: Optional[float] = None
"""Parsed numeric amount when status == 'parsed'."""
error: Optional[str] = None
"""Human-readable reason when the field was not parsed."""
def _status(object_name: str, field: str, item_index: int, **kw) -> MonetaryFieldStatus:
return MonetaryFieldStatus(object_name=object_name, field=field, item_index=item_index, **kw)
def apply_monetary_fields(data: Any, object_name: str, field_names: List[str]) -> List[MonetaryFieldStatus]:
"""Price-parse the configured monetary fields inside ``data``, in place.
``object_name`` selects the container key in ``data`` (e.g. ``"invoice"``);
its value may be a list of objects or a single object. For every listed
``field_names`` entry inside each object:
* value missing -> status ``not_found`` (error ``field 'x' not found``)
* value ``None`` / blank -> status ``skipped`` (error ``value is empty``)
* value that does not parse -> status ``not_parsable`` with the raw value
* value that parses -> replaced with the numeric amount, status ``parsed``
Returns one :class:`MonetaryFieldStatus` per (object item, field).
"""
report: List[MonetaryFieldStatus] = []
if not isinstance(data, dict) or not object_name:
for field in field_names:
report.append(
_status(
object_name,
field,
0,
status="not_found",
error=f"object '{object_name}' not found in extracted data",
)
)
return report
container = data.get(object_name)
if isinstance(container, list):
items: List[Any] = container
elif isinstance(container, dict):
items = [container]
else:
for field in field_names:
report.append(
_status(
object_name,
field,
0,
status="not_found",
error=f"object '{object_name}' not found in extracted data",
)
)
return report
for idx, item in enumerate(items):
if not isinstance(item, dict):
for field in field_names:
report.append(
_status(
object_name,
field,
idx,
status="not_found",
error="item is not an object",
)
)
continue
for field in field_names:
if field not in item:
report.append(
_status(
object_name,
field,
idx,
status="not_found",
error=f"field '{field}' not found",
)
)
continue
value = item[field]
if value is None or (isinstance(value, str) and not value.strip()):
report.append(
_status(
object_name,
field,
idx,
status="skipped",
error="value is empty",
)
)
continue
parsed = parse_amount(value)
if parsed is None:
report.append(
_status(
object_name,
field,
idx,
status="not_parsable",
value=str(value),
error=f"value is non-parsable: {value}",
)
)
continue
item[field] = parsed.amount_float
report.append(
_status(
object_name,
field,
idx,
status="parsed",
value=str(value),
parsed_value=parsed.amount_float,
)
)
return report
__all__ = ["MonetaryFieldStatus", "apply_monetary_fields"]