feat: VIN-specific data — recall check, safety ratings, full VPIC specs

This commit is contained in:
2026-07-03 13:12:11 -04:00
parent 3eee5b0acf
commit 078128f7b1
4 changed files with 315 additions and 46 deletions
+80 -22
View File
@@ -1,30 +1,31 @@
import httpx
_VPIC_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles"
_VPIC_BASE = "https://vpic.nhtsa.dot.gov/api/vehicles"
_NHTSA_BASE = "https://api.nhtsa.gov"
_VPIC_FIELDS = {
"Make", "Model", "Model Year", "Trim", "Body Class",
"Displacement (L)", "Engine Number of Cylinders", "Fuel Type - Primary",
"Drive Type", "Transmission Style", "Plant Country",
"Electrification Level", "Engine Model",
_FIELD_MAP = {
"Make": "Make",
"Model": "Model",
"Model Year": "ModelYear",
"Trim": "Trim",
"Body Class": "BodyClass",
"Doors": "Doors",
"Displacement (L)": "DisplacementL",
"Engine Number of Cylinders": "Cylinders",
"Engine Horsepower": "Horsepower",
"Engine Model": "EngineModel",
"Fuel Type - Primary": "FuelType",
"Drive Type": "DriveType",
"Transmission Style": "Transmission",
"Brake System Type": "BrakeSystem",
"Gross Vehicle Weight Rating": "GVWR",
"Plant Country": "PlantCountry",
"Plant City": "PlantCity",
"Electrification Level": "EVLevel",
"Series": "Series",
}
_FIELD_MAP = {
"Make": "Make",
"Model": "Model",
"Model Year": "ModelYear",
"Trim": "Trim",
"Body Class": "BodyClass",
"Displacement (L)": "DisplacementL",
"Engine Number of Cylinders": "Cylinders",
"Fuel Type - Primary": "FuelType",
"Drive Type": "DriveType",
"Transmission Style": "Transmission",
"Plant Country": "PlantCountry",
"Electrification Level": "EVLevel",
"Engine Model": "EngineModel",
}
_SKIP_VALS = {"Not Applicable", "null", "None", "0", ""}
async def decode_vin(vin: str) -> dict:
@@ -41,12 +42,69 @@ async def decode_vin(vin: str) -> dict:
for item in data.get("Results", []):
var = item.get("Variable", "")
val = (item.get("Value") or "").strip()
if var in _FIELD_MAP and val and val not in ("Not Applicable", "null", "None"):
if var in _FIELD_MAP and val and val not in _SKIP_VALS:
result[_FIELD_MAP[var]] = val
return result
async def fetch_vin_recalls(vin: str) -> list:
"""Returns recalls that apply to this specific VIN (not just make/model/year)."""
url = f"{_NHTSA_BASE}/recalls/recallsByVehicleId"
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url, params={"vin": vin})
resp.raise_for_status()
return resp.json().get("results", [])
except Exception:
return []
async def fetch_safety_ratings(year: str, make: str, model: str) -> dict:
"""Returns NHTSA 5-star crash test ratings for the vehicle."""
if not (year and make and model):
return {}
try:
async with httpx.AsyncClient(timeout=15) as client:
# Step 1: get list of vehicle variants
r1 = await client.get(
f"{_NHTSA_BASE}/SafetyRatings/modelyear/{year}/make/{make}/model/{model}"
)
r1.raise_for_status()
variants = r1.json().get("Results", [])
if not variants:
return {}
vehicle_id = variants[0].get("VehicleId")
if not vehicle_id:
return {}
# Step 2: get ratings for that variant
r2 = await client.get(f"{_NHTSA_BASE}/SafetyRatings/VehicleId/{vehicle_id}")
r2.raise_for_status()
results = r2.json().get("Results", [])
if not results:
return {}
r = results[0]
return {
"vehicle_desc": r.get("VehicleDescription", ""),
"overall": r.get("OverallRating", "NR"),
"frontal_driver": r.get("FrontCrashDriversideRating", "NR"),
"frontal_passenger": r.get("FrontCrashPassengersideRating", "NR"),
"side_driver": r.get("SideCrashDriversideRating", "NR"),
"side_passenger": r.get("SideCrashPassengersideRating", "NR"),
"rollover": r.get("RolloverRating", "NR"),
"rollover_risk_pct": r.get("RolloverPossibility", ""),
"pole_crash": r.get("SidePoleCrashRating", "NR"),
"esc": r.get("NHTSAElectronicStabilityControl", ""),
"fcw": r.get("NHTSAForwardCollisionWarning", ""),
"ldw": r.get("NHTSALaneDepartureWarning", ""),
}
except Exception:
return {}
async def fetch_recalls(make: str, model: str, year: str) -> list:
url = f"{_NHTSA_BASE}/recalls/recallsByVehicle"
params = {"make": make, "model": model, "modelYear": year}