Student_Analyzer / ipu_scraper.py
akshit7093's picture
Changes before Firebase Studio auto-run
8098153
import requests
import base64
import json
from Crypto.Cipher import AES
from typing import Dict, Optional
import hashlib
class StudentScraper:
def __init__(self, encryption_key: Optional[str] = None):
# Hardcoded encryption key, no config import needed!
self.encryption_key = encryption_key or "Qm9sRG9OYVphcmEK"
self.base_url = "https://api.ipuranklist.com/api/student"
self.headers = {
"Content-Type": "application/json",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}
def get_student_data(self, roll_no: str) -> Dict:
"""
Get student data from IPU Rank List API using roll number.
Args:
roll_no (str): Student roll number
Returns:
Dict: Processed and cleaned student information
"""
try:
encrypted_data = self._fetch_student_data(roll_no)
decrypted_data = self._decrypt_response(encrypted_data)
processed_data = self._preprocess_student_data(decrypted_data)
return processed_data
except requests.exceptions.RequestException as e:
raise Exception(f"Failed to fetch data from IPU API: {str(e)}")
except ValueError as e:
raise Exception(f"Invalid input or data format: {str(e)}")
except Exception as e:
raise Exception(f"An error occurred while getting student data: {str(e)}")
def _fetch_student_data(self, enroll_no: str) -> str:
"""Fetch encrypted student data from the API."""
url = f"{self.base_url}?enroll={enroll_no}"
try:
response = requests.get(url, headers=self.headers, timeout=30)
if response.status_code == 404:
raise Exception("Student not found. Please check the roll number.")
elif response.status_code == 403:
raise Exception("Access forbidden. API may require authentication.")
elif response.status_code == 429:
raise Exception("Rate limit exceeded. Please try again later.")
elif response.status_code == 500:
raise Exception("Server error. Please try again later.")
response.raise_for_status()
return response.text.strip()
except requests.exceptions.Timeout:
raise Exception("Request timeout. Please try again.")
except requests.exceptions.ConnectionError:
raise Exception("Connection error. Please check your internet connection.")
except requests.exceptions.RequestException as e:
raise Exception(f"Network error: {str(e)}")
def _decrypt_response(self, encrypted_data: str) -> Dict:
"""
Decrypt the encrypted response using OpenSSL EVP format.
"""
if not self.encryption_key:
raise Exception(
"Encryption key not provided. Please set IPU_ENCRYPTION_KEY in StudentScraper."
)
try:
# Decode base64
data = base64.b64decode(encrypted_data)
# Check if it starts with "Salted__" (OpenSSL EVP format)
if data[:8] != b"Salted__":
raise ValueError("Not in OpenSSL EVP format")
# Extract salt (next 8 bytes after "Salted__")
salt = data[8:16]
ciphertext = data[16:]
# Derive key and IV using OpenSSL's EVP_BytesToKey method (MD5 based)
key_iv = b""
prev = b""
while len(key_iv) < 48: # 32 bytes key + 16 bytes IV
prev = hashlib.md5(prev + self.encryption_key.encode("utf-8") + salt).digest()
key_iv += prev
key = key_iv[:32]
iv = key_iv[32:48]
# Decrypt using AES-256-CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
# Remove PKCS7 padding
padding_len = decrypted[-1]
if isinstance(padding_len, str):
padding_len = ord(padding_len)
decrypted = decrypted[:-padding_len]
# Parse JSON
decrypted_json = json.loads(decrypted.decode("utf-8"))
return decrypted_json
except Exception as e:
raise Exception(f"Failed to decrypt response: {str(e)}")
def _preprocess_student_data(self, raw_data: Dict) -> Dict:
"""
Preprocess student data to ensure subject names are present.
This version preserves all original fields from the API response.
"""
try:
if "data" not in raw_data or "metadata" not in raw_data:
return raw_data
data = raw_data["data"]
metadata = raw_data["metadata"]
# Create a mapping of subject IDs to names from the metadata.
subject_mapping = {
subject["_id"]: {"name": subject["name"], "credit": subject["credit"]}
for subject in metadata.get("subjects", [])
}
if "results" in data:
for result in data["results"]:
if "subject_results" in result:
for subject_result in result["subject_results"]:
# If subject_name is already present, do nothing.
if subject_result.get("subject_name"):
continue
# If missing, try to map it using subject_id.
subject_id = subject_result.get("subject_id")
if subject_id in subject_mapping:
subject_info = subject_mapping[subject_id]
subject_result["subject_name"] = subject_info.get("name", "Unknown Subject")
if "credit" not in subject_result:
subject_result["credit"] = subject_info.get("credit")
processed_data = {
"status": "success",
"student_info": {
"enroll_no": data.get("enroll_no"),
"name": data.get("name"),
"img": data.get("img"),
},
"academic_summary": {
"overall_performance": {
"total_marks": data.get("total_marks"),
"max_marks": data.get("max_marks"),
"total_credits": data.get("total_credits"),
"max_credits": data.get("max_credits"),
"percentage": data.get("percentage"),
"credit_percentage": data.get("credit_percentage"),
"cgpa": data.get("cgpa"),
},
"semester_results": data.get("results", []),
},
"programme_info": {
"course": metadata.get("programmeData", {}).get("course", {}),
"branch": metadata.get("programmeData", {}).get("branch", {}),
"institute": metadata.get("instituteData", {}),
},
}
return processed_data
except Exception:
return raw_data
def validate_roll_number(self, roll_no: str) -> bool:
"""Validate roll number format."""
if not roll_no or not isinstance(roll_no, str):
return False
cleaned = "".join(filter(str.isalnum, roll_no))
if len(cleaned) < 8 or len(cleaned) > 15:
return False
return True
def get_multiple_students(self, roll_numbers: list) -> Dict:
"""Get data for multiple students."""
results = {
"total_requested": len(roll_numbers),
"successful": 0,
"failed": 0,
"students": [],
"errors": [],
}
for roll_no in roll_numbers:
try:
student_data = self.get_student_data(roll_no)
results["students"].append(student_data)
results["successful"] += 1
except Exception as e:
results["errors"].append({"roll_number": roll_no, "error": str(e)})
results["failed"] += 1
return results
# Example usage:
if __name__ == "__main__":
# No config file needed
scraper = StudentScraper(encryption_key="Qm9sRG9OYVphcmEK")
roll_no = "35214811922"
try:
data = scraper.get_student_data(roll_no)
print(json.dumps(data, indent=2))
except Exception as e:
print(f"Error: {e}")