Implement complete autoparts catalog system (5 phases)
FASE 1: Parts Database - Added part_categories, part_groups, parts, vehicle_parts tables - 12 categories, 190 groups with Spanish translations - API endpoints for categories, groups, parts CRUD FASE 2: Cross-References & Aftermarket - Added manufacturers, aftermarket_parts, part_cross_references tables - 24 manufacturers, quality tier system (economy/standard/premium/oem) - Part number search across OEM and aftermarket FASE 3: Exploded Diagrams - Added diagrams, vehicle_diagrams, diagram_hotspots tables - SVG viewer with zoom controls and interactive hotspots - 3 sample diagrams (brake, oil filter, suspension) FASE 4: Search & VIN Decoder - SQLite FTS5 full-text search with auto-sync triggers - NHTSA VIN decoder API integration with 30-day cache - Unified search endpoint FASE 5: Optimization & UX - API pagination (page/per_page, max 100 items) - Dark mode with localStorage persistence - Keyboard shortcuts (/, Ctrl+K, Escape, Backspace, Ctrl+D) - Breadcrumb navigation - ARIA accessibility (labels, roles, focus management) - Skip link for keyboard users Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
312
vehicle_database/scripts/populate_fase4.py
Executable file
312
vehicle_database/scripts/populate_fase4.py
Executable file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FASE 4: Full-Text Search and VIN Decoder
|
||||
This script creates FASE 4 tables (FTS5, triggers, vin_cache) and populates
|
||||
the parts_fts table with existing parts data.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Database path configuration
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
|
||||
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
|
||||
|
||||
|
||||
class Fase4Manager:
|
||||
"""Manager for FASE 4 tables: parts_fts, vin_cache, and related triggers"""
|
||||
|
||||
def __init__(self, db_path: str = DB_PATH):
|
||||
self.db_path = db_path
|
||||
self.connection = None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the SQLite database"""
|
||||
self.connection = sqlite3.connect(self.db_path)
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
print(f"Connected to database: {self.db_path}")
|
||||
|
||||
def disconnect(self):
|
||||
"""Close the database connection"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
print("Disconnected from database")
|
||||
|
||||
def create_fase4_tables(self):
|
||||
"""Create FASE 4 tables from schema file"""
|
||||
if not os.path.exists(SCHEMA_PATH):
|
||||
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
|
||||
|
||||
with open(SCHEMA_PATH, 'r') as f:
|
||||
schema = f.read()
|
||||
|
||||
if self.connection:
|
||||
cursor = self.connection.cursor()
|
||||
cursor.executescript(schema)
|
||||
self.connection.commit()
|
||||
print("FASE 4 tables created successfully")
|
||||
|
||||
def check_fts_table_exists(self) -> bool:
|
||||
"""Check if the parts_fts FTS5 table exists"""
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
def check_fts_populated(self) -> bool:
|
||||
"""Check if the FTS table has any data"""
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
||||
count = cursor.fetchone()[0]
|
||||
return count > 0
|
||||
except sqlite3.OperationalError:
|
||||
return False
|
||||
|
||||
def populate_fts_from_parts(self):
|
||||
"""Populate the parts_fts table with existing parts data"""
|
||||
if not self.check_fts_table_exists():
|
||||
print("FTS table does not exist, creating tables first...")
|
||||
self.create_fase4_tables()
|
||||
|
||||
# Check if already populated
|
||||
if self.check_fts_populated():
|
||||
print("parts_fts table already has data, skipping population")
|
||||
return
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# Get count of parts
|
||||
cursor.execute("SELECT COUNT(*) FROM parts")
|
||||
parts_count = cursor.fetchone()[0]
|
||||
|
||||
if parts_count == 0:
|
||||
print("No parts found in parts table, nothing to populate")
|
||||
return
|
||||
|
||||
# Populate FTS table from parts
|
||||
cursor.execute("""
|
||||
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
|
||||
SELECT id, oem_part_number, name, name_es, description, description_es FROM parts
|
||||
""")
|
||||
self.connection.commit()
|
||||
|
||||
# Rebuild FTS index for proper search functionality
|
||||
cursor.execute("INSERT INTO parts_fts(parts_fts) VALUES('rebuild')")
|
||||
self.connection.commit()
|
||||
|
||||
# Verify population
|
||||
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
||||
fts_count = cursor.fetchone()[0]
|
||||
print(f"Populated parts_fts with {fts_count} entries from {parts_count} parts")
|
||||
|
||||
def get_vin_by_vin(self, vin: str) -> Optional[int]:
|
||||
"""Get VIN cache entry ID by VIN, returns None if not found"""
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute("SELECT id FROM vin_cache WHERE vin = ?", (vin,))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
def insert_vin_cache(self, vin: str, decoded_data: dict, make: str, model: str,
|
||||
year: int, engine_info: str = None, body_class: str = None,
|
||||
drive_type: str = None, model_year_engine_id: int = None,
|
||||
expires_days: int = 30) -> int:
|
||||
"""Insert a VIN cache entry if it doesn't exist, return its ID"""
|
||||
existing_id = self.get_vin_by_vin(vin)
|
||||
if existing_id:
|
||||
print(f" VIN '{vin}' already exists in cache (ID: {existing_id})")
|
||||
return existing_id
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
expires_at = datetime.now() + timedelta(days=expires_days)
|
||||
|
||||
cursor.execute(
|
||||
"""INSERT INTO vin_cache
|
||||
(vin, decoded_data, make, model, year, engine_info, body_class,
|
||||
drive_type, model_year_engine_id, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(vin, json.dumps(decoded_data), make, model, year, engine_info,
|
||||
body_class, drive_type, model_year_engine_id, expires_at.isoformat())
|
||||
)
|
||||
self.connection.commit()
|
||||
vin_id = cursor.lastrowid
|
||||
print(f" Inserted VIN cache: {vin} -> {make} {model} {year} (ID: {vin_id})")
|
||||
return vin_id
|
||||
|
||||
def populate_sample_vins(self):
|
||||
"""Populate sample VIN cache entries for testing"""
|
||||
print("\nPopulating sample VIN cache entries...")
|
||||
|
||||
sample_vins = [
|
||||
{
|
||||
'vin': '4T1BF1FK5CU123456',
|
||||
'decoded_data': {
|
||||
'Make': 'TOYOTA',
|
||||
'Model': 'Camry',
|
||||
'ModelYear': '2023',
|
||||
'EngineModel': '2.5L I4',
|
||||
'BodyClass': 'Sedan/Saloon',
|
||||
'DriveType': 'FWD',
|
||||
'PlantCountry': 'UNITED STATES (USA)',
|
||||
'VehicleType': 'PASSENGER CAR'
|
||||
},
|
||||
'make': 'Toyota',
|
||||
'model': 'Camry',
|
||||
'year': 2023,
|
||||
'engine_info': '2.5L I4 DOHC 16V',
|
||||
'body_class': 'Sedan',
|
||||
'drive_type': 'FWD'
|
||||
},
|
||||
{
|
||||
'vin': '1HGBH41JXMN109186',
|
||||
'decoded_data': {
|
||||
'Make': 'HONDA',
|
||||
'Model': 'Civic',
|
||||
'ModelYear': '2023',
|
||||
'EngineModel': '2.0L I4',
|
||||
'BodyClass': 'Sedan/Saloon',
|
||||
'DriveType': 'FWD',
|
||||
'PlantCountry': 'UNITED STATES (USA)',
|
||||
'VehicleType': 'PASSENGER CAR'
|
||||
},
|
||||
'make': 'Honda',
|
||||
'model': 'Civic',
|
||||
'year': 2023,
|
||||
'engine_info': '2.0L I4 DOHC 16V',
|
||||
'body_class': 'Sedan',
|
||||
'drive_type': 'FWD'
|
||||
},
|
||||
{
|
||||
'vin': '1FA6P8CF5L5123456',
|
||||
'decoded_data': {
|
||||
'Make': 'FORD',
|
||||
'Model': 'Mustang',
|
||||
'ModelYear': '2020',
|
||||
'EngineModel': '5.0L V8',
|
||||
'BodyClass': 'Coupe',
|
||||
'DriveType': 'RWD',
|
||||
'PlantCountry': 'UNITED STATES (USA)',
|
||||
'VehicleType': 'PASSENGER CAR'
|
||||
},
|
||||
'make': 'Ford',
|
||||
'model': 'Mustang',
|
||||
'year': 2020,
|
||||
'engine_info': '5.0L V8 Coyote',
|
||||
'body_class': 'Coupe',
|
||||
'drive_type': 'RWD'
|
||||
}
|
||||
]
|
||||
|
||||
for vin_data in sample_vins:
|
||||
self.insert_vin_cache(
|
||||
vin=vin_data['vin'],
|
||||
decoded_data=vin_data['decoded_data'],
|
||||
make=vin_data['make'],
|
||||
model=vin_data['model'],
|
||||
year=vin_data['year'],
|
||||
engine_info=vin_data['engine_info'],
|
||||
body_class=vin_data['body_class'],
|
||||
drive_type=vin_data['drive_type']
|
||||
)
|
||||
|
||||
def verify_installation(self):
|
||||
"""Verify FASE 4 installation"""
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("FASE 4 Installation Verification")
|
||||
print("=" * 50)
|
||||
|
||||
# Check FTS table
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
|
||||
)
|
||||
fts_exists = cursor.fetchone() is not None
|
||||
print(f"parts_fts table: {'OK' if fts_exists else 'MISSING'}")
|
||||
|
||||
if fts_exists:
|
||||
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
||||
fts_count = cursor.fetchone()[0]
|
||||
print(f" - FTS entries: {fts_count}")
|
||||
|
||||
# Check triggers
|
||||
triggers = ['parts_fts_insert', 'parts_fts_delete', 'parts_fts_update']
|
||||
for trigger_name in triggers:
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='trigger' AND name=?",
|
||||
(trigger_name,)
|
||||
)
|
||||
trigger_exists = cursor.fetchone() is not None
|
||||
print(f"Trigger {trigger_name}: {'OK' if trigger_exists else 'MISSING'}")
|
||||
|
||||
# Check vin_cache table
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='vin_cache'"
|
||||
)
|
||||
vin_exists = cursor.fetchone() is not None
|
||||
print(f"vin_cache table: {'OK' if vin_exists else 'MISSING'}")
|
||||
|
||||
if vin_exists:
|
||||
cursor.execute("SELECT COUNT(*) FROM vin_cache")
|
||||
vin_count = cursor.fetchone()[0]
|
||||
print(f" - VIN cache entries: {vin_count}")
|
||||
|
||||
cursor.execute("SELECT vin, make, model, year FROM vin_cache")
|
||||
for row in cursor.fetchall():
|
||||
print(f" - {row['vin']}: {row['make']} {row['model']} {row['year']}")
|
||||
|
||||
# Check indexes
|
||||
indexes = ['idx_vin_cache_vin', 'idx_vin_cache_make_model']
|
||||
for index_name in indexes:
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
||||
(index_name,)
|
||||
)
|
||||
index_exists = cursor.fetchone() is not None
|
||||
print(f"Index {index_name}: {'OK' if index_exists else 'MISSING'}")
|
||||
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to populate FASE 4 tables"""
|
||||
print("=" * 60)
|
||||
print("FASE 4: Full-Text Search and VIN Decoder Population")
|
||||
print("=" * 60)
|
||||
|
||||
manager = Fase4Manager()
|
||||
|
||||
try:
|
||||
manager.connect()
|
||||
|
||||
# Step 1: Create FASE 4 tables (FTS5, triggers, vin_cache)
|
||||
print("\n[1/4] Creating FASE 4 tables...")
|
||||
manager.create_fase4_tables()
|
||||
|
||||
# Step 2: Populate FTS table with existing parts
|
||||
print("\n[2/4] Populating Full-Text Search index...")
|
||||
manager.populate_fts_from_parts()
|
||||
|
||||
# Step 3: Add sample VIN cache entries
|
||||
print("\n[3/4] Adding sample VIN cache entries...")
|
||||
manager.populate_sample_vins()
|
||||
|
||||
# Step 4: Verify installation
|
||||
print("\n[4/4] Verifying FASE 4 installation...")
|
||||
manager.verify_installation()
|
||||
|
||||
print("\nFASE 4 population completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError during FASE 4 population: {e}")
|
||||
raise
|
||||
finally:
|
||||
manager.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user