File size: 4,360 Bytes
f976464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7364904
f976464
 
1712908
 
 
 
 
f976464
 
 
 
 
 
7364904
f976464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1712908
 
 
 
f976464
 
 
 
7364904
f976464
 
 
 
 
7364904
f976464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7364904
f976464
 
 
7364904
 
 
 
 
 
 
 
 
 
 
f976464
 
 
 
 
 
 
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
from __future__ import annotations

import argparse
import csv
from pathlib import Path
import re
import sys

import numpy as np

ROOT_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT_DIR))

from app.main import api_recommendations, catalog, health


def main() -> None:
    parser = argparse.ArgumentParser(description="Validate runtime artifacts and app-level recommendation readiness.")
    parser.add_argument("--catalog-path", type=Path, default=Path("data/processed/movie_catalog.csv"))
    parser.add_argument("--neighbors-path", type=Path, default=Path("data/processed/item_neighbors.csv"))
    parser.add_argument("--semantic-path", type=Path, default=Path("data/processed/semantic_index.npz"))
    parser.add_argument("--static-dir", type=Path, default=Path("static"))
    parser.add_argument("--query", default="Avatar")
    parser.add_argument("--min-movies", type=int, default=1000)
    parser.add_argument(
        "--require-learned-embeddings",
        action="store_true",
        help="Fail unless the semantic artifact was built with the SentenceTransformer backend.",
    )
    args = parser.parse_args()

    failures: list[str] = []
    catalog_rows = count_csv_rows(args.catalog_path)
    neighbor_rows = count_csv_rows(args.neighbors_path)
    semantic_rows = count_semantic_rows(args.semantic_path)
    static_failures = validate_static_assets(args.static_dir)
    health_payload = health()
    catalog_payload = catalog()
    recommendations = api_recommendations(args.query, 5).recommendations

    if catalog_rows < args.min_movies:
        failures.append(f"catalog has only {catalog_rows} rows")
    if neighbor_rows == 0:
        failures.append("item neighbor artifact is empty")
    if semantic_rows != catalog_rows:
        failures.append(f"semantic index rows ({semantic_rows}) do not match catalog rows ({catalog_rows})")
    if not health_payload.get("tmdb_enabled"):
        failures.append("TMDb is not enabled; check TMDB_API_KEY")
    if not health_payload.get("item_cf_enabled"):
        failures.append("item-CF artifact is not enabled")
    if not health_payload.get("semantic_enabled"):
        failures.append("semantic artifact is not enabled")
    if args.require_learned_embeddings and health_payload.get("semantic_backend") != "sentence-transformer":
        failures.append(
            "semantic artifact is not a SentenceTransformer index; rebuild it before deployment"
        )
    if not recommendations:
        failures.append(f"no recommendations returned for {args.query!r}")
    if any(normalize_title(item.title) == normalize_title(args.query) for item in recommendations):
        failures.append(f"self-recommendation returned for {args.query!r}")
    failures.extend(static_failures)

    print("Runtime validation")
    print(f"- catalog rows: {catalog_rows}")
    print(f"- item neighbor rows: {neighbor_rows}")
    print(f"- semantic rows: {semantic_rows}")
    print(f"- static assets: {args.static_dir}")
    print(f"- health: {health_payload}")
    print(f"- catalog: {catalog_payload}")
    print(f"- sample recommendations: {[item.title for item in recommendations]}")

    if failures:
        print("\nFailures:")
        for failure in failures:
            print(f"- {failure}")
        sys.exit(1)

    print("\nValidation passed.")


def count_csv_rows(path: Path) -> int:
    if not path.exists():
        return 0
    with path.open(newline="", encoding="utf-8") as handle:
        return sum(1 for _ in csv.DictReader(handle))


def count_semantic_rows(path: Path) -> int:
    if not path.exists():
        return 0
    data = np.load(path, allow_pickle=False)
    _ = data["titles"]
    return int(data["embeddings"].shape[0])


def validate_static_assets(static_dir: Path) -> list[str]:
    failures = []
    for filename in ["style.css", "recommend.js", "autocomplete.js", "image.jpg", "loader.gif", "default.jpg"]:
        path = static_dir / filename
        if not path.exists():
            failures.append(f"missing static asset: {path}")
        elif path.stat().st_size == 0:
            failures.append(f"empty static asset: {path}")
    return failures


def normalize_title(value: str) -> str:
    value = re.sub(r"\(\d{4}\)\s*$", "", value)
    return " ".join(re.findall(r"[a-z0-9]+", value.lower()))


if __name__ == "__main__":
    main()