|
| 1 | +import json |
| 2 | +import sys |
| 3 | +from dataclasses import dataclass |
| 4 | +from typing import Any, Dict, List, Optional |
| 5 | + |
| 6 | + |
| 7 | +@dataclass(frozen=True) |
| 8 | +class EventSource: |
| 9 | + """Identifies where events were loaded from (for error reporting).""" |
| 10 | + |
| 11 | + path: str |
| 12 | + |
| 13 | + |
| 14 | +class EventInputError(ValueError): |
| 15 | + pass |
| 16 | + |
| 17 | + |
| 18 | +def _ensure_event_dict(obj: Any, source: EventSource) -> Dict[str, Any]: |
| 19 | + if not isinstance(obj, dict): |
| 20 | + raise EventInputError(f"{source.path}: expected JSON object, got {type(obj).__name__}") |
| 21 | + return obj |
| 22 | + |
| 23 | + |
| 24 | +def load_events_from_text(text: str, source: EventSource) -> List[Dict[str, Any]]: |
| 25 | + """ |
| 26 | + Load one-or-more events from JSON text. |
| 27 | +
|
| 28 | + Supported formats: |
| 29 | + - A single JSON object: {"k": "v"} |
| 30 | + - A JSON array of objects: [{"k": "v"}, ...] |
| 31 | + - NDJSON / JSON Lines: one JSON object per line |
| 32 | + """ |
| 33 | + stripped = text.strip() |
| 34 | + if not stripped: |
| 35 | + return [] |
| 36 | + |
| 37 | + # First try "normal" JSON: object or array. |
| 38 | + try: |
| 39 | + parsed = json.loads(stripped) |
| 40 | + if isinstance(parsed, list): |
| 41 | + return [_ensure_event_dict(item, source) for item in parsed] |
| 42 | + return [_ensure_event_dict(parsed, source)] |
| 43 | + except json.JSONDecodeError: |
| 44 | + pass |
| 45 | + |
| 46 | + # Fall back to NDJSON / JSON Lines |
| 47 | + events: List[Dict[str, Any]] = [] |
| 48 | + for line_no, line in enumerate(text.splitlines(), start=1): |
| 49 | + if not line.strip(): |
| 50 | + continue |
| 51 | + try: |
| 52 | + parsed_line = json.loads(line) |
| 53 | + except json.JSONDecodeError as exc: |
| 54 | + raise EventInputError( |
| 55 | + f"{source.path}:{line_no}: invalid JSON line ({exc.msg})" |
| 56 | + ) from exc |
| 57 | + events.append(_ensure_event_dict(parsed_line, source)) |
| 58 | + return events |
| 59 | + |
| 60 | + |
| 61 | +def load_events_from_path(path: str, encoding: str = "utf-8") -> List[Dict[str, Any]]: |
| 62 | + """ |
| 63 | + Load events from a path, or from stdin when path == "-". |
| 64 | + """ |
| 65 | + source = EventSource(path=path) |
| 66 | + if path == "-": |
| 67 | + return load_events_from_text(sys.stdin.read(), source) |
| 68 | + |
| 69 | + try: |
| 70 | + with open(path, "r", encoding=encoding) as handle: |
| 71 | + return load_events_from_text(handle.read(), source) |
| 72 | + except OSError as exc: |
| 73 | + raise EventInputError(f"{path}: unable to read file ({exc})") from exc |
| 74 | + |
| 75 | + |
| 76 | +def apply_log_type( |
| 77 | + events: List[Dict[str, Any]], |
| 78 | + log_type: Optional[str], |
| 79 | + overwrite: bool, |
| 80 | +) -> List[Dict[str, Any]]: |
| 81 | + if not log_type: |
| 82 | + return events |
| 83 | + for event in events: |
| 84 | + if overwrite or "p_log_type" not in event: |
| 85 | + event["p_log_type"] = log_type |
| 86 | + return events |
| 87 | + |
0 commit comments