testsAndMisc/meta/scripts/_schema_validation.py
Krzysztof kuhy Rudnicki 038e08d2be feat: split oversized modules for 500-line limit, fix kasa coverage gap
Split diet_guard/_gatelock.py, wake_alarm/_alarm.py, and the
usage_report.py/_usage_report_parsing.py pair into focused
sub-modules so every Python file is <= 500 lines, satisfying
test_file_length.py. Install python-kasa into .venv (declared in
requirements but missing after the 3.13->3.14 venv upgrade),
fixing 8 failing smart_plug tests and restoring 100% coverage.

Also includes prior in-progress work from the working tree: the
wake_alarm Progress/View/Hardware field-grouping refactor,
brother_printer query module + tests, diet_guard foodbank/state/cli
updates, new shared coerce/logging_setup helpers, morning_routine
orchestrator tweaks, dwm window-manager config, gaming scripts, and
misc maintenance/digital-wellbeing script updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 07:19:37 +02:00

96 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Shared JSON-schema validation helpers for validate_contract/validate_evidence.
Both CLI scripts validate a JSON artifact against a small required-field schema
and report problems via the same read/parse/dispatch shell. Factored out here so
the duplicated logic is defined once (see pylint duplicate-code).
"""
from __future__ import annotations
import json
from pathlib import Path
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
def is_nonempty_str(value: object) -> bool:
"""Return True if ``value`` is a string with non-whitespace content."""
return isinstance(value, str) and bool(value.strip())
def load_and_check_required(
path: Path,
check_required: Callable[[dict[str, object]], list[str]],
) -> tuple[dict[str, object] | None, str, list[str]]:
"""Load ``path`` as JSON and verify its required top-level keys are present.
Returns ``(data, text, [])`` once the file is valid JSON, is an object, and
``check_required(data)`` reports no problems. Otherwise returns
``(None, "", errors)`` with the relevant problem(s). ``text`` is the raw file
contents (useful for whole-text checks).
"""
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
return None, "", [f"cannot read file ({exc})"]
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
return None, "", [f"invalid JSON ({exc})"]
if not isinstance(data, dict):
return None, "", ["top-level JSON value must be an object"]
errors = check_required(data)
if errors: # without the required keys present, the per-field checks are noise
return None, "", errors
return data, text, []
def check_string_lists(
data: dict[str, object],
keys: Sequence[str],
item_noun: str,
) -> list[str]:
"""Each field in ``keys`` must be a non-empty list of non-empty strings.
``item_noun`` (e.g. "items" or "entries") customizes the per-element message.
"""
errors: list[str] = []
for key in keys:
value = data.get(key)
if not isinstance(value, list) or not value:
errors.append(f"{key} must be a non-empty list")
continue
if any(not is_nonempty_str(item) for item in value):
errors.append(f"{key} {item_noun} must be non-empty strings")
return errors
def run_cli(
argv: Sequence[str],
*,
usage: str,
validate: Callable[[Path], list[str]],
success_message: str,
) -> int:
"""Validate the path named by ``argv[0]`` and report via stdout/stderr.
Returns 2 if ``argv`` is empty (usage error), 1 if validation found
problems, or 0 if the artifact is valid.
"""
if not argv:
sys.stderr.write(f"{usage}\n")
return 2
path = Path(argv[0])
errors = validate(path)
if errors:
for error in errors:
sys.stderr.write(f"{path}: {error}\n")
return 1
sys.stdout.write(f"{path}: {success_message}\n")
return 0