Spaces:
Runtime error
Runtime error
File size: 7,402 Bytes
c1dcc23 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | """Validate an event-log JSONL file against the official sample_events.jsonl schema.
The challenge's held-out set (and our submitted event log) uses a multi-source
schema with three event families:
entry / exit -> id_token, store_code, event_timestamp, is_staff, demographics
zone_entered / zone_exited -> track_id, store_id, zone_id, event_time, zone_* , hotspots
queue_completed / queue_abandoned -> queue_event_id, queue_*_ts, wait_seconds, abandoned
Usage:
python scripts/validate_events.py data/sample_events.jsonl [more.jsonl ...]
Exit code is non-zero if any HARD error is found (bad JSON, unknown event_type,
missing required field, unparseable timestamp). "Recommended" fields that the
official sample always carries (camera_id, demographics, zone hotspots, ...) are
reported as WARNINGS only β they enrich the stream but are not required to parse.
"""
from __future__ import annotations
import json
import sys
from datetime import datetime
from pathlib import Path
# Windows consoles default to cp1252 and choke on the β/β /β glyphs below.
try:
sys.stdout.reconfigure(encoding="utf-8")
except (AttributeError, ValueError):
pass
OFFICIAL_TYPES = {
"entry", "exit", "zone_entered", "zone_exited",
"queue_completed", "queue_abandoned",
}
# required = must be present & non-null to be a valid event of this family
# recommended = present in the official sample; absence is a warning, not an error
SCHEMA = {
"entry": {
"required": {"event_type", "id_token", "store_code", "event_timestamp", "is_staff"},
"recommended": {"camera_id", "gender_pred", "age_pred", "age_bucket",
"is_face_hidden", "group_id", "group_size"},
"ts": ["event_timestamp"],
},
"exit": {
"required": {"event_type", "id_token", "store_code", "event_timestamp", "is_staff"},
"recommended": {"camera_id", "gender_pred", "age_pred", "age_bucket",
"is_face_hidden", "group_id", "group_size"},
"ts": ["event_timestamp"],
},
"zone_entered": {
"required": {"event_type", "store_id", "zone_id", "event_time"},
"id_one_of": {"track_id", "id_token"},
"recommended": {"camera_id", "zone_name", "zone_type", "is_revenue_zone",
"zone_hotspot_x", "zone_hotspot_y", "gender", "age", "age_bucket"},
"ts": ["event_time"],
},
"zone_exited": {
"required": {"event_type", "store_id", "zone_id", "event_time"},
"id_one_of": {"track_id", "id_token"},
"recommended": {"camera_id", "zone_name", "zone_type", "is_revenue_zone",
"zone_hotspot_x", "zone_hotspot_y", "gender", "age", "age_bucket"},
"ts": ["event_time"],
},
"queue_completed": {
"required": {"queue_event_id", "event_type", "store_id", "zone_id",
"queue_join_ts", "abandoned"},
"id_one_of": {"track_id", "id_token"},
"recommended": {"camera_id", "zone_name", "zone_type", "is_revenue_zone",
"queue_served_ts", "queue_exit_ts", "wait_seconds",
"queue_position_at_join", "zone_hotspot_x", "zone_hotspot_y",
"gender", "age", "age_bucket"},
"ts": ["queue_join_ts"],
},
"queue_abandoned": {
"required": {"queue_event_id", "event_type", "store_id", "zone_id",
"queue_join_ts", "abandoned"},
"id_one_of": {"track_id", "id_token"},
"recommended": {"camera_id", "zone_name", "zone_type", "is_revenue_zone",
"queue_served_ts", "queue_exit_ts", "wait_seconds",
"queue_position_at_join", "zone_hotspot_x", "zone_hotspot_y",
"gender", "age", "age_bucket"},
"ts": ["queue_join_ts"],
},
}
def _parse_ts(v: object) -> bool:
if not isinstance(v, str) or not v:
return False
try:
datetime.fromisoformat(v.replace("Z", "+00:00"))
return True
except ValueError:
return False
def validate_file(path: Path) -> tuple[int, int, int]:
"""Returns (n_events, n_errors, n_warnings) and prints a report for `path`."""
errors: list[str] = []
warnings: list[str] = []
type_counts: dict[str, int] = {}
missing_recommended: dict[str, int] = {}
n = 0
if not path.exists():
print(f" β file not found: {path}")
return (0, 1, 0)
for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not raw.strip():
continue
n += 1
try:
ev = json.loads(raw)
except json.JSONDecodeError as exc:
errors.append(f"line {lineno}: invalid JSON ({exc})")
continue
if not isinstance(ev, dict):
errors.append(f"line {lineno}: not a JSON object")
continue
et = ev.get("event_type")
type_counts[et] = type_counts.get(et, 0) + 1
if et not in OFFICIAL_TYPES:
errors.append(f"line {lineno}: unknown event_type {et!r} "
f"(expected one of {sorted(OFFICIAL_TYPES)})")
continue
rules = SCHEMA[et]
for f in rules["required"]:
if f not in ev or ev[f] is None:
errors.append(f"line {lineno} [{et}]: missing required field {f!r}")
if "id_one_of" in rules and not (rules["id_one_of"] & {k for k, v in ev.items() if v is not None}):
errors.append(f"line {lineno} [{et}]: needs one of "
f"{sorted(rules['id_one_of'])} for visitor identity")
for f in rules["ts"]:
if f in ev and ev[f] is not None and not _parse_ts(ev[f]):
errors.append(f"line {lineno} [{et}]: unparseable timestamp {f}={ev[f]!r}")
for f in rules["recommended"]:
if f not in ev:
missing_recommended[f] = missing_recommended.get(f, 0) + 1
# roll missing-recommended into warnings (one line per field)
for f, c in sorted(missing_recommended.items()):
warnings.append(f"recommended field {f!r} absent on {c} event(s)")
print(f"\n{'='*70}\n{path}")
print(f" events: {n} types: " +
", ".join(f"{k}={v}" for k, v in sorted(type_counts.items(), key=lambda x: str(x[0]))))
if errors:
print(f" β {len(errors)} ERROR(S):")
for e in errors[:25]:
print(f" - {e}")
if len(errors) > 25:
print(f" β¦ and {len(errors) - 25} more")
else:
print(" β no hard errors β valid JSONL, all events match the official schema")
if warnings:
print(f" β {len(warnings)} warning(s) (enrichment fields the sample carries):")
for w in warnings:
print(f" - {w}")
return (n, len(errors), len(warnings))
def main(argv: list[str]) -> int:
files = argv or ["data/sample_events.jsonl"]
total_err = 0
total_events = 0
for f in files:
n, e, _w = validate_file(Path(f))
total_events += n
total_err += e
print(f"\n{'='*70}\nTOTAL: {total_events} events across {len(files)} file(s), "
f"{total_err} hard error(s) β {'PASS β' if total_err == 0 else 'FAIL β'}")
return 0 if total_err == 0 else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
|