| 1 | from __future__ import annotations |
| 2 | |
| 3 | import json |
| 4 | |
| 5 | from app.services.normalize import RawRead |
| 6 | from app.services.parsers.base import BaseParser, ParseResult |
| 7 | |
| 8 | |
| 9 | class JsonFlockParser(BaseParser): |
| 10 | """Handles a top-level list of read objects, or a wrapper like |
| 11 | {"reads": [...]} / {"data": [...]} / {"results": [...]}. Nested objects are flattened |
| 12 | one level (e.g. read["camera"]["id"] -> "camera_id") so the alias mapper can find them.""" |
| 13 | name = "json_flock" |
| 14 | _WRAPPERS = ("reads", "data", "results", "records", "plate_reads", "items") |
| 15 | |
| 16 | def parse(self, data: bytes) -> ParseResult: |
| 17 | errors: list[str] = [] |
| 18 | try: |
| 19 | doc = json.loads(data.decode("utf-8-sig", errors="replace")) |
| 20 | except json.JSONDecodeError as e: |
| 21 | return ParseResult(headers=[], rows=[], errors=[f"invalid json: {e}"]) |
| 22 | |
| 23 | records = doc |
| 24 | if isinstance(doc, dict): |
| 25 | for w in self._WRAPPERS: |
| 26 | if isinstance(doc.get(w), list): |
| 27 | records = doc[w] |
| 28 | break |
| 29 | else: |
| 30 | records = [doc] |
| 31 | if not isinstance(records, list): |
| 32 | return ParseResult(headers=[], rows=[], errors=["json root is not a list of reads"]) |
| 33 | |
| 34 | rows: list[RawRead] = [] |
| 35 | header_set: dict[str, None] = {} |
| 36 | for i, rec in enumerate(records, start=1): |
| 37 | if not isinstance(rec, dict): |
| 38 | errors.append(f"row {i}: not an object") |
| 39 | continue |
| 40 | flat = self._flatten(rec) |
| 41 | for k in flat: |
| 42 | header_set.setdefault(k, None) |
| 43 | rows.append(RawRead(fields=flat, source_row=i)) |
| 44 | return ParseResult(headers=list(header_set), rows=rows, errors=errors) |
| 45 | |
| 46 | @staticmethod |
| 47 | def _flatten(rec: dict) -> dict: |
| 48 | out: dict = {} |
| 49 | for k, v in rec.items(): |
| 50 | if isinstance(v, dict): |
| 51 | for k2, v2 in v.items(): |
| 52 | out[f"{k}_{k2}"] = v2 |
| 53 | elif isinstance(v, list): |
| 54 | out[k] = json.dumps(v) |
| 55 | else: |
| 56 | out[k] = v |
| 57 | return out |