# Created by AG on 24-12-2025 from __future__ import annotations import json import click import random from pathlib import Path from typing import Any, Dict from core.statistics import summary from src.utils import load_contract, is_record_valid from core.runner import Record, runner, parse_runner_response from src.generate_payload import generate_valid_payload, generate_invalid_payload def parse_headers( headers: str | None ) -> Dict[str, str] | None: if not headers: return None headers_json = json.loads(headers) if not isinstance(headers_json, dict): raise click.BadParameter("Header must be a JSON object.") return { str(key): str(value) for key, value in headers_json.items() } def validate_record_type(payload: Any) -> Dict[str, Any] | None: return payload if isinstance(payload, dict) else None def generate_valid_record( parsed_contract: Dict[str, Any], rng: random.Random, attempts_to_make: int = 5 ) -> Dict[str, Any]: for _ in range(max(1, attempts_to_make)): payload = validate_record_type( generate_valid_payload(parsed_contract, rng) ) if payload is not None and is_record_valid(parsed_contract, payload): return payload return { "_failed": True } def generate_invalid_record( parsed_contract: Dict[str, Any], rng: random.Random ) -> Dict[str, Any]: payload = generate_invalid_payload(parsed_contract, rng) if isinstance(payload, dict): return payload return { "_is_invalid": True } @click.group(context_settings={"help_option_names": ["-h", "--help"]}) def avroman() -> None: pass @avroman.command() @click.option("--schema", "contract_path", type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path), required=True, help="Path to schema file") @click.option("--url", required=True, help="Endpoint URL") @click.option("--method", default="POST", show_default=True, help="HTTP method") @click.option("--n-valid", default=25, show_default=True, type=int, help="Number of valid cases") @click.option("--n-invalid", default=25, show_default=True, type=int, help="Number of invalid cases") @click.option("--seed", default=0, show_default=True, type=int, help="RNG seed") @click.option("--headers", default=None, help='Extra headers as JSON string, e.g. \'{"Authorization":"Bearer ..."}\'') @click.option("--timeout", "timeout_s", default=10.0, show_default=True, type=float, help="Request timeout in seconds") @click.option( "--valid-accept-3xx/--valid-accept-2xx", default=False, show_default=True, help="If set, treat any <400 as success for valid cases (accept 3xx). Default: only 2xx is success", ) @click.option( "--fail-on-any/--no-fail-on-any", default=True, show_default=True, help="Exit non-zero if any case is not ok", ) def run( contract_path: Path, url: str, method: str, n_valid: int, n_invalid: int, seed: int, headers: str | None, timeout_s: float, valid_accept_3xx: bool, fail_on_any: bool, ) -> None: parsed_contract = load_contract(contract_path) rng = random.Random(seed) headers = parse_headers(headers) results: list[Record] = [] for i in range(n_valid): payload = generate_valid_record(parsed_contract, rng) status, ms, err, snippet = runner(url, method, payload, headers=headers, timeout=timeout_s) expected, ok = parse_runner_response(True, status, expect_2xx_for_valid=not valid_accept_3xx) results.append( Record( id=f"V{i:03d}", is_valid=True, expected=expected, status_code=status, elapsed_time_ms=ms, error=err, response=snippet, ) ) for i in range(n_invalid): payload = generate_invalid_record(parsed_contract, rng) if isinstance(payload, dict) and is_record_valid(parsed_contract, payload): payload = {"_is_invalid": True} status, ms, err, snippet = runner(url, method, payload, headers=headers, timeout=timeout_s) expected, ok = parse_runner_response(False, status, expect_2xx_for_valid=not valid_accept_3xx) results.append( Record( id=f"I{i:03d}", is_valid=False, expected=expected, status_code=status, elapsed_time_ms=ms, error=err, response=snippet, ) ) summary(results) # if fail_on_any and any(not result.ok for result in results): # raise SystemExit(1) if __name__ == "__main__": avroman()