Spaces:
Running
Running
| """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"] | |