#!/usr/bin/env python3
"""Import real JEE Main PYQs from the HostServer001 examgoal-derived database.
Source: https://github.com/HostServer001/jee_mains_pyqs_data_base (MIT)
Release asset: 1762787474-DataBaseChapters-v007.pkl (~24 MB, 14,973 questions).
What this does:
1. Downloads the pickle (or uses --pkl if you already have it).
2. Keeps only: type=mcq, fully text (no
our chapter, chapter -> our subtopic, creating
rows as needed. Keeps the existing 24 seed questions untouched
(dedupes on exact stem match).
5. Shifts: Morning Slot/Shift -> S1, Evening -> S2, Offline/AIEEE -> S1.
Usage:
python3 scripts/import_examgoal.py path/to/app.db [--pkl db.pkl]
Safe to re-run: INSERTs skip stems that already exist.
"""
import argparse, html, pickle, re, sqlite3, sys, urllib.request
from pathlib import Path
PKL_URL = ("https://github.com/HostServer001/jee_mains_pyqs_data_base"
"/releases/download/v007/1762787474-DataBaseChapters-v007.pkl")
ALLOWED = {"br", "p", "sup", "sub", "b", "strong", "i", "em", "u",
"ol", "ul", "li", "table", "tbody", "thead", "tr", "th", "td",
"span", "h3"}
SUBJECT_FIX = {"mathematics": "Maths", "physics": "Physics", "chemistry": "Chemistry"}
def title_case(slug: str) -> str:
return " ".join(w.capitalize() for w in slug.replace("-", " ").split())
def sanitize(raw: str) -> str:
"""Strip to safe HTML. Unescape entities first so < renders as <. KaTeX
handles $$..$$ at render time; nothing else needs dollar-escaping."""
if not raw:
return ""
text = html.unescape(str(raw))
# Drop whole //", "", text, flags=re.S | re.I)
text = re.sub(r"", "", text, flags=re.S | re.I)
text = re.sub(r"?(?:col|colgroup)[^>]*>", "", text, flags=re.I)
# Keep only allowlisted tags, drop all attributes.
def tag(m):
closing, name = m.group(1), m.group(2).lower()
if name in ALLOWED:
return f"<{closing}{name}>"
return ""
text = re.sub(r"<(/?)([a-zA-Z][a-zA-Z0-9]*)(\s[^>]*)?>", tag, text)
# Collapse whitespace runs but keep single spaces inside math.
text = re.sub(r"[ \t\xa0]+", " ", text)
text = re.sub(r"\n\s*\n+", "\n", text)
return text.strip()
def shift_of(paper_title: str, exam_date) -> str:
t = (paper_title or "").lower()
if "evening" in t:
return "S2"
if "morning" in t:
return "S1"
if exam_date:
try:
hour = int(str(exam_date).split()[1].split(":")[0])
return "S2" if hour >= 12 else "S1"
except (IndexError, ValueError):
pass
return "S1"
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("db")
ap.add_argument("--pkl", default=None)
args = ap.parse_args()
default_pkl = Path(__file__).resolve().parent / "examgoal_db.pkl"
pkl_path = Path(args.pkl) if args.pkl else default_pkl
if not pkl_path.exists():
print(f"downloading {PKL_URL} ...", flush=True)
pkl_path.parent.mkdir(parents=True, exist_ok=True)
urllib.request.urlretrieve(PKL_URL, pkl_path)
print(f"loading {pkl_path} ({pkl_path.stat().st_size / 1e6:.1f} MB) ...", flush=True)
sys.path.insert(0, str(Path(__file__).resolve().parent / "examgoal_model"))
from question import Question
from chapter import Chapter
class FixUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if name == "Question":
return Question
if name == "Chapter":
return Chapter
return super().find_class(module, name)
with open(pkl_path, "rb") as f:
data = FixUnpickler(f).load()
con = sqlite3.connect(args.db)
existing = {r[0] for r in con.execute("SELECT stem FROM questions")}
print(f"existing stems in db: {len(existing)}")
chap_cache: dict = {}
sub_cache: dict = {}
def chapter_id(subject: str, chapter: str) -> int:
key = (subject, chapter)
if key not in chap_cache:
row = con.execute(
"SELECT id FROM chapters WHERE subject=? AND name=?",
(subject, chapter)).fetchone()
chap_cache[key] = (row[0] if row
else con.execute(
"INSERT INTO chapters(subject,name) VALUES(?,?)",
(subject, chapter)).lastrowid)
return chap_cache[key]
def subtopic_id(ch_id: int, sub: str) -> int:
key = (ch_id, sub)
if key not in sub_cache:
row = con.execute(
"SELECT id FROM subtopics WHERE chapter_id=? AND name=?",
(ch_id, sub)).fetchone()
sub_cache[key] = (row[0] if row
else con.execute(
"INSERT INTO subtopics(chapter_id,name) VALUES(?,?)",
(ch_id, sub)).lastrowid)
return sub_cache[key]
kept = skipped = 0
diff_map = {"easy": "Easy", "medium": "Medium", "hard": "Hard"}
for _key, chap in data.items():
for q in chap.question_dict.values():
if getattr(q, "type", None) != "mcq":
skipped += 1
continue
if getattr(q, "isImgQuestion", False):
skipped += 1
continue
co = getattr(q, "correct_options", None) or []
if len(co) != 1 or co[0] not in ("A", "B", "C", "D"):
skipped += 1
continue
stem = sanitize(getattr(q, "question", ""))
if not stem or "![]()