● Zion Boggan
repos/Sightline/backend/app/services/parsers/json_flock.py
zionboggan.com ↗
57 lines · python
History for this file →
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