mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat(preview): sampling, metrics, governance, server mana data
Preview endpoint + fast caches; curated pins + role quotas + rarity/overlap tuning; catalog+preview metrics; governance enforcement flags; server mana/color identity fields; docs/tests/scripts updated.
This commit is contained in:
parent
8f47dfbb81
commit
c4a7fc48ea
40 changed files with 6092 additions and 17312 deletions
100
code/scripts/validate_theme_fast_path.py
Normal file
100
code/scripts/validate_theme_fast_path.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#!/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 not isinstance(syns, list) or not syns:
|
||||
probs.append(Problem('warn', f'theme[{i}] synergies empty or not list'))
|
||||
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:]))
|
||||
Loading…
Add table
Add a link
Reference in a new issue