From af0f25fbd88f7a840fa0cd47e9276a901f5540dd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 3 Oct 2026 18:04:49 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82:=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20validate=5Fcon?= =?UTF-8?q?tracts.py=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B8=D1=80=D1=83?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20?= =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- scripts/validate_contracts.py | 116 ++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 scripts/validate_contracts.py diff --git a/scripts/validate_contracts.py b/scripts/validate_contracts.py new file mode 100644 index 0000000..a34c2d6 --- /dev/null +++ b/scripts/validate_contracts.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12. + +Запуск из любого cwd: python scripts/validate_contracts.py +Зависимости: стандартная библиотека + jsonschema. +Код возврата: 0 — всё валидно, 1 — любая ошибка. +""" +import datetime +import json +import re +import sys +from pathlib import Path + +from jsonschema import Draft202012Validator, FormatChecker + +ROOT = Path(__file__).resolve().parent.parent +CONTRACTS = ROOT / "contracts" +EXAMPLES = CONTRACTS / "examples" + +SCHEMAS = { + "request": CONTRACTS / "recognition-request.schema.json", + "message": CONTRACTS / "recognition-result-message.schema.json", + "result": CONTRACTS / "recognition-result.schema.json", +} + +# Порядок важен: result-message-* проверяется раньше result-*. +PATTERNS = [ + ("request-", "request"), + ("result-message-", "message"), + ("result-", "result"), +] + +_DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$") + + +def _check_date_time(value): + # Собственная проверка вместо rfc3339-validator: нужна и дата, и время, и часовой пояс. + if not isinstance(value, str): + return True + if not _DATE_TIME_RE.match(value): + return False + datetime.datetime.fromisoformat(value.replace("z", "Z")) # ValueError -> формат не прошёл + return True + + +def make_format_checker(): + checker = FormatChecker() + checker.checks("date-time", raises=ValueError)(_check_date_time) + return checker + + +def short_path(error): + path = "/".join(str(p) for p in error.absolute_path) + return f"{path or ''}: {error.message[:120]}" + + +def main(): + failed = False + validators = {} + format_checker = make_format_checker() + + for kind, path in SCHEMAS.items(): + if not path.is_file(): + print(f"FAIL схема {path.name}: файл не найден") + failed = True + continue + try: + schema = json.loads(path.read_text(encoding="utf-8")) + Draft202012Validator.check_schema(schema) + except Exception as exc: # битый JSON или нарушение метасхемы + print(f"FAIL схема {path.name}: {str(exc).splitlines()[0][:150]}") + failed = True + continue + validators[kind] = Draft202012Validator(schema, format_checker=format_checker) + print(f"OK схема {path.name}") + + counts = {kind: 0 for kind in SCHEMAS} + for example in sorted(EXAMPLES.glob("*.json")): + kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None) + if kind is None: + print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)") + failed = True + continue + counts[kind] += 1 + validator = validators.get(kind) + if validator is None: + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: схема недоступна") + failed = True + continue + try: + instance = json.loads(example.read_text(encoding="utf-8")) + except ValueError as exc: + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: не JSON: {exc}") + failed = True + continue + errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path)) + if errors: + failed = True + print(f"FAIL {example.name} [{SCHEMAS[kind].name}]") + for error in errors: + print(f" {short_path(error)}") + else: + print(f"OK {example.name} [{SCHEMAS[kind].name}]") + + for kind, count in counts.items(): + if count == 0: + print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}") + failed = True + + total = sum(counts.values()) + print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())