feat: migrate to PostgreSQL + SQLAlchemy ORM, rebrand to Nexus Autoparts
- Migrate from SQLite to PostgreSQL with normalized schema - Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission, materials, position_part, manufacture_type, quality_tier, countries, reference_type, shapes) - Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries - Rewrite console/db.py (27 methods) using SQLAlchemy ORM - Add models.py with 27 SQLAlchemy model definitions - Add config.py for centralized DB_URL configuration - Add migrate_to_postgres.py migration script - Add docs/METABASE_GUIDE.md with complete data entry guide - Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS" - Fill vehicle data gaps via NHTSA API + heuristics: engines (cylinders, power, torque), brands (country, founded_year), models (body_type, production years), MYE (drivetrain, transmission, trim) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
410
models.py
Normal file
410
models.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""
|
||||
SQLAlchemy ORM models for Nexus Autoparts.
|
||||
"""
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Float, Boolean, Text, DateTime, ForeignKey,
|
||||
UniqueConstraint, Index, func, text
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
||||
from sqlalchemy.orm import relationship, DeclarativeBase
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Lookup tables
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
class FuelType(Base):
|
||||
__tablename__ = "fuel_type"
|
||||
id_fuel = Column(Integer, primary_key=True)
|
||||
name_fuel = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class BodyType(Base):
|
||||
__tablename__ = "body_type"
|
||||
id_body = Column(Integer, primary_key=True)
|
||||
name_body = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class Drivetrain(Base):
|
||||
__tablename__ = "drivetrain"
|
||||
id_drivetrain = Column(Integer, primary_key=True)
|
||||
name_drivetrain = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class Transmission(Base):
|
||||
__tablename__ = "transmission"
|
||||
id_transmission = Column(Integer, primary_key=True)
|
||||
name_transmission = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class Material(Base):
|
||||
__tablename__ = "materials"
|
||||
id_material = Column(Integer, primary_key=True)
|
||||
name_material = Column(String(100), nullable=False, unique=True)
|
||||
|
||||
|
||||
class PositionPart(Base):
|
||||
__tablename__ = "position_part"
|
||||
id_position_part = Column(Integer, primary_key=True)
|
||||
name_position_part = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class ManufactureType(Base):
|
||||
__tablename__ = "manufacture_type"
|
||||
id_type_manu = Column(Integer, primary_key=True)
|
||||
name_type_manu = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class QualityTier(Base):
|
||||
__tablename__ = "quality_tier"
|
||||
id_quality_tier = Column(Integer, primary_key=True)
|
||||
name_quality = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class Country(Base):
|
||||
__tablename__ = "countries"
|
||||
id_country = Column(Integer, primary_key=True)
|
||||
name_country = Column(String(100), nullable=False, unique=True)
|
||||
|
||||
|
||||
class ReferenceType(Base):
|
||||
__tablename__ = "reference_type"
|
||||
id_ref_type = Column(Integer, primary_key=True)
|
||||
name_ref_type = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
class Shape(Base):
|
||||
__tablename__ = "shapes"
|
||||
id_shape = Column(Integer, primary_key=True)
|
||||
name_shape = Column(String(50), nullable=False, unique=True)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Core tables
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
class Brand(Base):
|
||||
__tablename__ = "brands"
|
||||
id_brand = Column(Integer, primary_key=True)
|
||||
name_brand = Column(String(200), nullable=False, unique=True)
|
||||
country = Column(String(100))
|
||||
founded_year = Column(Integer)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
models = relationship("Model", back_populates="brand")
|
||||
|
||||
|
||||
class Year(Base):
|
||||
__tablename__ = "years"
|
||||
id_year = Column(Integer, primary_key=True)
|
||||
year_car = Column(Integer, nullable=False, unique=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class Engine(Base):
|
||||
__tablename__ = "engines"
|
||||
id_engine = Column(Integer, primary_key=True)
|
||||
name_engine = Column(String(300), nullable=False)
|
||||
displacement_cc = Column(Float)
|
||||
cylinders = Column(Integer)
|
||||
id_fuel = Column(Integer, ForeignKey("fuel_type.id_fuel"))
|
||||
power_hp = Column(Integer)
|
||||
torque_nm = Column(Integer)
|
||||
engine_code = Column(String(100))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
fuel_type = relationship("FuelType")
|
||||
|
||||
|
||||
class Model(Base):
|
||||
__tablename__ = "models"
|
||||
id_model = Column(Integer, primary_key=True)
|
||||
brand_id = Column(Integer, ForeignKey("brands.id_brand"), nullable=False)
|
||||
name_model = Column(String(300), nullable=False)
|
||||
id_body = Column(Integer, ForeignKey("body_type.id_body"))
|
||||
generation = Column(String(100))
|
||||
production_start_year = Column(Integer)
|
||||
production_end_year = Column(Integer)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
brand = relationship("Brand", back_populates="models")
|
||||
body_type = relationship("BodyType")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_models_brand", "brand_id"),
|
||||
)
|
||||
|
||||
|
||||
class ModelYearEngine(Base):
|
||||
__tablename__ = "model_year_engine"
|
||||
id_mye = Column(Integer, primary_key=True)
|
||||
model_id = Column(Integer, ForeignKey("models.id_model"), nullable=False)
|
||||
year_id = Column(Integer, ForeignKey("years.id_year"), nullable=False)
|
||||
engine_id = Column(Integer, ForeignKey("engines.id_engine"), nullable=False)
|
||||
trim_level = Column(String(100))
|
||||
id_drivetrain = Column(Integer, ForeignKey("drivetrain.id_drivetrain"))
|
||||
id_transmission = Column(Integer, ForeignKey("transmission.id_transmission"))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
model = relationship("Model")
|
||||
year = relationship("Year")
|
||||
engine = relationship("Engine")
|
||||
drivetrain = relationship("Drivetrain")
|
||||
transmission = relationship("Transmission")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("model_id", "year_id", "engine_id", "trim_level",
|
||||
name="uq_mye_combo"),
|
||||
Index("idx_mye_model", "model_id"),
|
||||
Index("idx_mye_year", "year_id"),
|
||||
Index("idx_mye_engine", "engine_id"),
|
||||
)
|
||||
|
||||
|
||||
class PartCategory(Base):
|
||||
__tablename__ = "part_categories"
|
||||
id_part_category = Column(Integer, primary_key=True)
|
||||
name_part_category = Column(String(200), nullable=False)
|
||||
name_es = Column(String(200))
|
||||
parent_id = Column(Integer, ForeignKey("part_categories.id_part_category"))
|
||||
slug = Column(String(200), unique=True)
|
||||
icon_name = Column(String(100))
|
||||
display_order = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
parent = relationship("PartCategory", remote_side="PartCategory.id_part_category")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_part_categories_parent", "parent_id"),
|
||||
Index("idx_part_categories_slug", "slug"),
|
||||
)
|
||||
|
||||
|
||||
class PartGroup(Base):
|
||||
__tablename__ = "part_groups"
|
||||
id_part_group = Column(Integer, primary_key=True)
|
||||
category_id = Column(Integer, ForeignKey("part_categories.id_part_category"), nullable=False)
|
||||
name_part_group = Column(String(200), nullable=False)
|
||||
name_es = Column(String(200))
|
||||
slug = Column(String(200))
|
||||
display_order = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
category = relationship("PartCategory")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_part_groups_category", "category_id"),
|
||||
)
|
||||
|
||||
|
||||
class Part(Base):
|
||||
__tablename__ = "parts"
|
||||
id_part = Column(Integer, primary_key=True)
|
||||
oem_part_number = Column(String(100), nullable=False)
|
||||
name_part = Column(String(300), nullable=False)
|
||||
name_es = Column(String(300))
|
||||
group_id = Column(Integer, ForeignKey("part_groups.id_part_group"))
|
||||
description = Column(Text)
|
||||
description_es = Column(Text)
|
||||
weight_kg = Column(Float)
|
||||
id_material = Column(Integer, ForeignKey("materials.id_material"))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
search_vector = Column(TSVECTOR)
|
||||
|
||||
group = relationship("PartGroup")
|
||||
material = relationship("Material")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_parts_oem", "oem_part_number"),
|
||||
Index("idx_parts_group", "group_id"),
|
||||
Index("idx_parts_search", "search_vector", postgresql_using="gin"),
|
||||
)
|
||||
|
||||
|
||||
class VehiclePart(Base):
|
||||
__tablename__ = "vehicle_parts"
|
||||
id_vehicle_part = Column(Integer, primary_key=True)
|
||||
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"), nullable=False)
|
||||
part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
|
||||
quantity_required = Column(Integer, default=1)
|
||||
id_position_part = Column(Integer, ForeignKey("position_part.id_position_part"))
|
||||
fitment_notes = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
model_year_engine = relationship("ModelYearEngine")
|
||||
part = relationship("Part")
|
||||
position = relationship("PositionPart")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("model_year_engine_id", "part_id", "id_position_part",
|
||||
name="uq_vehicle_part"),
|
||||
Index("idx_vehicle_parts_mye", "model_year_engine_id"),
|
||||
Index("idx_vehicle_parts_part", "part_id"),
|
||||
)
|
||||
|
||||
|
||||
class Manufacturer(Base):
|
||||
__tablename__ = "manufacturers"
|
||||
id_manufacture = Column(Integer, primary_key=True)
|
||||
name_manufacture = Column(String(200), nullable=False, unique=True)
|
||||
id_type_manu = Column(Integer, ForeignKey("manufacture_type.id_type_manu"))
|
||||
id_quality_tier = Column(Integer, ForeignKey("quality_tier.id_quality_tier"))
|
||||
id_country = Column(Integer, ForeignKey("countries.id_country"))
|
||||
logo_url = Column(String(500))
|
||||
website = Column(String(500))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
manufacture_type = relationship("ManufactureType")
|
||||
quality_tier = relationship("QualityTier")
|
||||
country = relationship("Country")
|
||||
|
||||
|
||||
class AftermarketPart(Base):
|
||||
__tablename__ = "aftermarket_parts"
|
||||
id_aftermarket_parts = Column(Integer, primary_key=True)
|
||||
oem_part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
|
||||
manufacturer_id = Column(Integer, ForeignKey("manufacturers.id_manufacture"), nullable=False)
|
||||
part_number = Column(String(100), nullable=False)
|
||||
name_aftermarket_parts = Column(String(300))
|
||||
name_es = Column(String(300))
|
||||
id_quality_tier = Column(Integer, ForeignKey("quality_tier.id_quality_tier"))
|
||||
price_usd = Column(Float)
|
||||
warranty_months = Column(Integer)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
oem_part = relationship("Part")
|
||||
manufacturer = relationship("Manufacturer")
|
||||
quality_tier = relationship("QualityTier")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_aftermarket_oem", "oem_part_id"),
|
||||
Index("idx_aftermarket_manufacturer", "manufacturer_id"),
|
||||
Index("idx_aftermarket_part_number", "part_number"),
|
||||
)
|
||||
|
||||
|
||||
class PartCrossReference(Base):
|
||||
__tablename__ = "part_cross_references"
|
||||
id_part_cross_ref = Column(Integer, primary_key=True)
|
||||
part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
|
||||
cross_reference_number = Column(String(100), nullable=False)
|
||||
id_ref_type = Column(Integer, ForeignKey("reference_type.id_ref_type"))
|
||||
source_ref = Column(String(200))
|
||||
notes = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
part = relationship("Part")
|
||||
reference_type = relationship("ReferenceType")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_cross_ref_part", "part_id"),
|
||||
Index("idx_cross_ref_number", "cross_reference_number"),
|
||||
)
|
||||
|
||||
|
||||
class Diagram(Base):
|
||||
__tablename__ = "diagrams"
|
||||
id_diagram = Column(Integer, primary_key=True)
|
||||
name_diagram = Column(String(300), nullable=False)
|
||||
name_es = Column(String(300))
|
||||
group_id = Column(Integer, ForeignKey("part_groups.id_part_group"), nullable=False)
|
||||
image_path = Column(String(500), nullable=False)
|
||||
thumbnail_path = Column(String(500))
|
||||
display_order = Column(Integer, default=0)
|
||||
source_diagram = Column(String(200))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
group = relationship("PartGroup")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_diagrams_group", "group_id"),
|
||||
)
|
||||
|
||||
|
||||
class VehicleDiagram(Base):
|
||||
__tablename__ = "vehicle_diagrams"
|
||||
id_vehicle_dgr = Column(Integer, primary_key=True)
|
||||
diagram_id = Column(Integer, ForeignKey("diagrams.id_diagram"), nullable=False)
|
||||
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"), nullable=False)
|
||||
notes = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
diagram = relationship("Diagram")
|
||||
model_year_engine = relationship("ModelYearEngine")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("diagram_id", "model_year_engine_id", name="uq_vehicle_diagram"),
|
||||
Index("idx_vehicle_diagrams_diagram", "diagram_id"),
|
||||
Index("idx_vehicle_diagrams_mye", "model_year_engine_id"),
|
||||
)
|
||||
|
||||
|
||||
class DiagramHotspot(Base):
|
||||
__tablename__ = "diagram_hotspots"
|
||||
id_dgr_hotspot = Column(Integer, primary_key=True)
|
||||
diagram_id = Column(Integer, ForeignKey("diagrams.id_diagram"), nullable=False)
|
||||
part_id = Column(Integer, ForeignKey("parts.id_part"))
|
||||
callout_number = Column(Integer)
|
||||
id_shape = Column(Integer, ForeignKey("shapes.id_shape"))
|
||||
coords = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
diagram = relationship("Diagram")
|
||||
part = relationship("Part")
|
||||
shape = relationship("Shape")
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_hotspots_diagram", "diagram_id"),
|
||||
Index("idx_hotspots_part", "part_id"),
|
||||
)
|
||||
|
||||
|
||||
class VinCache(Base):
|
||||
__tablename__ = "vin_cache"
|
||||
id = Column(Integer, primary_key=True)
|
||||
vin = Column(String(17), nullable=False, unique=True)
|
||||
decoded_data = Column(JSONB, nullable=False)
|
||||
make = Column(String(100))
|
||||
model = Column(String(100))
|
||||
year = Column(Integer)
|
||||
engine_info = Column(String(200))
|
||||
body_class = Column(String(100))
|
||||
drive_type = Column(String(100))
|
||||
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
expires_at = Column(DateTime)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_vin_cache_vin", "vin"),
|
||||
Index("idx_vin_cache_make_model", "make", "model", "year"),
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Full-text search trigger SQL (run after table creation)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
SEARCH_VECTOR_TRIGGER_SQL = """
|
||||
CREATE OR REPLACE FUNCTION parts_search_vector_update() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.search_vector := to_tsvector('spanish',
|
||||
coalesce(NEW.oem_part_number, '') || ' ' ||
|
||||
coalesce(NEW.name_part, '') || ' ' ||
|
||||
coalesce(NEW.name_es, '') || ' ' ||
|
||||
coalesce(NEW.description, '')
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_parts_search_vector ON parts;
|
||||
CREATE TRIGGER trg_parts_search_vector
|
||||
BEFORE INSERT OR UPDATE ON parts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION parts_search_vector_update();
|
||||
"""
|
||||
Reference in New Issue
Block a user