mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-18 00:20:13 +01:00
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Fast path theme catalog presence & schema sanity validator.
|
|
|
|
Checks:
|
|
1. theme_list.json exists.
|
|
2. Loads JSON and ensures top-level keys present: themes (list), metadata_info (dict).
|
|
3. Basic field contract for each theme: id, theme, synergies (list), description.
|
|
4. Enforces presence of catalog_hash inside metadata_info for drift detection.
|
|
5. Optionally validates against Pydantic models if available (best effort).
|
|
Exit codes:
|
|
0 success
|
|
1 structural failure / missing file
|
|
2 partial validation warnings elevated via --strict
|
|
"""
|
|
from __future__ import annotations
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import pathlib
|
|
import typing as t
|
|
|
|
THEME_LIST_PATH = pathlib.Path('config/themes/theme_list.json')
|
|
|
|
class Problem:
|
|
def __init__(self, level: str, message: str):
|
|
self.level = level
|
|
self.message = message
|
|
def __repr__(self):
|
|
return f"{self.level.upper()}: {self.message}"
|
|
|
|
def load_json(path: pathlib.Path) -> t.Any:
|
|
try:
|
|
return json.loads(path.read_text(encoding='utf-8') or '{}')
|
|
except FileNotFoundError:
|
|
raise
|
|
except Exception as e: # pragma: no cover
|
|
raise RuntimeError(f"parse_error: {e}")
|
|
|
|
def validate(data: t.Any) -> list[Problem]:
|
|
probs: list[Problem] = []
|
|
if not isinstance(data, dict):
|
|
probs.append(Problem('error','top-level not an object'))
|
|
return probs
|
|
themes = data.get('themes')
|
|
if not isinstance(themes, list) or not themes:
|
|
probs.append(Problem('error','themes list missing or empty'))
|
|
meta = data.get('metadata_info')
|
|
if not isinstance(meta, dict):
|
|
probs.append(Problem('error','metadata_info missing or not object'))
|
|
else:
|
|
if not meta.get('catalog_hash'):
|
|
probs.append(Problem('error','metadata_info.catalog_hash missing'))
|
|
if not meta.get('generated_at'):
|
|
probs.append(Problem('warn','metadata_info.generated_at missing'))
|
|
# Per theme spot check (limit to first 50 to keep CI snappy)
|
|
for i, th in enumerate(themes[:50] if isinstance(themes, list) else []):
|
|
if not isinstance(th, dict):
|
|
probs.append(Problem('error', f'theme[{i}] not object'))
|
|
continue
|
|
if not th.get('id'):
|
|
probs.append(Problem('error', f'theme[{i}] id missing'))
|
|
if not th.get('theme'):
|
|
probs.append(Problem('error', f'theme[{i}] theme missing'))
|
|
syns = th.get('synergies')
|
|
if syns is None:
|
|
probs.append(Problem('warn', f'theme[{i}] synergies missing'))
|
|
elif not isinstance(syns, list):
|
|
probs.append(Problem('warn', f'theme[{i}] synergies not a list'))
|
|
else:
|
|
bad_types = [type(item).__name__ for item in syns if not isinstance(item, str)]
|
|
if bad_types:
|
|
probs.append(Problem('warn', f'theme[{i}] synergies contain non-string entries: {set(bad_types)}'))
|
|
if 'description' not in th:
|
|
probs.append(Problem('warn', f'theme[{i}] description missing'))
|
|
return probs
|
|
|
|
def main(argv: list[str]) -> int:
|
|
ap = argparse.ArgumentParser(description='Validate fast path theme catalog build presence & schema.')
|
|
ap.add_argument('--strict-warn', action='store_true', help='Promote warnings to errors (fail CI).')
|
|
args = ap.parse_args(argv)
|
|
if not THEME_LIST_PATH.exists():
|
|
print('ERROR: theme_list.json missing at expected path.', file=sys.stderr)
|
|
return 1
|
|
try:
|
|
data = load_json(THEME_LIST_PATH)
|
|
except FileNotFoundError:
|
|
print('ERROR: theme_list.json missing.', file=sys.stderr)
|
|
return 1
|
|
except Exception as e:
|
|
print(f'ERROR: failed parsing theme_list.json: {e}', file=sys.stderr)
|
|
return 1
|
|
problems = validate(data)
|
|
errors = [p for p in problems if p.level=='error']
|
|
warns = [p for p in problems if p.level=='warn']
|
|
for p in problems:
|
|
stream = sys.stderr if p.level!='info' else sys.stdout
|
|
print(repr(p), file=stream)
|
|
if errors:
|
|
return 1
|
|
if args.strict_warn and warns:
|
|
return 2
|
|
print(f"Fast path validation ok: {len(errors)} errors, {len(warns)} warnings. Checked {min(len(data.get('themes', [])),50)} themes.")
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main(sys.argv[1:]))
|