Files
Autoparts-DB/models.py
consultoria-as 7b2a904498 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>
2026-02-19 05:24:47 +00:00

411 lines
15 KiB
Python

"""
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();
"""