#!/usr/bin/env python3 """Convert saved StoredViolationEvents pages to CSV without network access.""" import argparse import csv import json import math import sys from pathlib import Path def load_json(path): return json.loads(Path(path).read_text(encoding="utf-8")) def lookup(value, path): for part in path.split("."): if not isinstance(value, dict) or part not in value: raise ValueError("отсутствует поле " + path) value = value[part] if isinstance(value, (list, dict)): raise ValueError("поле " + path + " должно быть скалярным") if isinstance(value, float) and not math.isfinite(value): raise ValueError("неконечное число в поле " + path) return "" if value is None else str(value) def spreadsheet_safe(value): return "'" + value if value.lstrip().startswith(("=", "+", "-", "@")) else value def convert(inputs, config, output): columns = config.get("columns") if not isinstance(columns, list) or not columns: raise ValueError("config.columns должен содержать колонки") names = [column.get("name") for column in columns if isinstance(column, dict)] if len(names) != len(columns) or any(not isinstance(n, str) or not n for n in names): raise ValueError("каждая колонка должна иметь непустое имя") if len(set(names)) != len(names): raise ValueError("имена колонок должны быть уникальны") if any(not isinstance(c.get("path"), str) or not c["path"] for c in columns): raise ValueError("каждая колонка должна иметь путь к полю") key = config.get("deduplicate_by") if not isinstance(key, str) or not key: raise ValueError("задайте deduplicate_by с учётом структуры своих данных") rows, seen, duplicates = [], {}, 0 for filename in inputs: page = load_json(filename) if not isinstance(page, dict) or not isinstance(page.get("events"), list): raise ValueError(str(filename) + ": ожидается объект с массивом events") for position, event in enumerate(page["events"], 1): if not isinstance(event, dict): raise ValueError(str(filename) + ": событие должно быть объектом") try: event_key = lookup(event, key) if not event_key: raise ValueError("пустой ключ события") fingerprint = json.dumps(event, sort_keys=True, ensure_ascii=False, allow_nan=False) if event_key in seen: if seen[event_key] != fingerprint: raise ValueError("один ключ у разных версий события: " + event_key) duplicates += 1 continue row = [spreadsheet_safe(lookup(event, c["path"])) for c in columns] seen[event_key] = fingerprint rows.append(row) except ValueError as error: raise ValueError(f"{filename}, событие {position}: {error}") from error # Validate all input before writing so a bad final page cannot leave a partial CSV. with Path(output).open("w", encoding="utf-8-sig", newline="") as stream: writer = csv.writer(stream, lineterminator="\n") writer.writerow(names) writer.writerows(rows) return {"rows": len(rows), "duplicates_skipped": duplicates} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("inputs", nargs="+", help="JSON-страницы в порядке обработки") parser.add_argument("--config", default="mapping.json") parser.add_argument("--output", default="events.csv") args = parser.parse_args() try: result = convert(args.inputs, load_json(args.config), args.output) except (ValueError, OSError, TypeError, AttributeError) as error: print("Ошибка: " + str(error), file=sys.stderr) return 1 print(json.dumps(result, ensure_ascii=False, sort_keys=True)) return 0 if __name__ == "__main__": sys.exit(main())