145 lines
9.1 KiB
Python
145 lines
9.1 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, Boolean, DateTime, Enum, ForeignKey, Index, Integer, String, Text, func
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
from .enums import (
|
|
BODY_TYPE_ENUM_VALUES,
|
|
COUNTRY_ENUM_VALUES,
|
|
CURRENCY_ENUM_VALUES,
|
|
DRIVE_ENUM_VALUES,
|
|
GEARBOX_ENUM_VALUES,
|
|
ORIGIN_ENUM_VALUES,
|
|
SELLING_TYPE_ENUM_VALUES,
|
|
STEERING_WHEEL_ENUM_VALUES,
|
|
)
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class Car(Base):
|
|
__tablename__ = "cars"
|
|
__table_args__ = (
|
|
Index("ix_cars_brand_model", "brand", "model"),
|
|
Index("ix_cars_origin_id_not_sold", "origin_id", "is_sold"),
|
|
)
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
parser_id: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
|
brand: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
|
model: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
year: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
|
price: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
currency: Mapped[str] = mapped_column(Enum(*CURRENCY_ENUM_VALUES, name="currencyenum", native_enum=False, create_constraint=False), nullable=False, default="USD")
|
|
mileage: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
country: Mapped[str] = mapped_column(Enum(*COUNTRY_ENUM_VALUES, name="countryenum", native_enum=False, create_constraint=False), nullable=False, default="NA")
|
|
is_sold: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
|
color: Mapped[str] = mapped_column(String(), nullable=False, default="other")
|
|
drive: Mapped[str | None] = mapped_column(Enum(*DRIVE_ENUM_VALUES, name="driveenum", native_enum=False, create_constraint=False), nullable=True)
|
|
gearbox: Mapped[str | None] = mapped_column(Enum(*GEARBOX_ENUM_VALUES, name="gearboxenum", native_enum=False, create_constraint=False), nullable=True)
|
|
steering_wheel: Mapped[str | None] = mapped_column(Enum(*STEERING_WHEEL_ENUM_VALUES, name="steeringwheelenum", native_enum=False, create_constraint=False), nullable=True)
|
|
body_type: Mapped[str] = mapped_column(Enum(*BODY_TYPE_ENUM_VALUES, name="bodytypeenum", native_enum=False, create_constraint=False), nullable=False, default="OTHER")
|
|
engine_volume: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
selling_type: Mapped[str] = mapped_column(Enum(*SELLING_TYPE_ENUM_VALUES, name="sellingtypeenum", native_enum=False, create_constraint=False), nullable=False, default="NA")
|
|
one_owner: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
new_car: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
is_hidden: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
origin: Mapped[str] = mapped_column(Enum(*ORIGIN_ENUM_VALUES, name="originenum", native_enum=False, create_constraint=False), nullable=False, default="NA")
|
|
origin_url: Mapped[str] = mapped_column(String(), nullable=False, index=True)
|
|
origin_id: Mapped[str] = mapped_column(String(), nullable=False, unique=True, index=True)
|
|
is_damaged: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
evaluation: Mapped[str | None] = mapped_column(String(), nullable=True)
|
|
non_smoking: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
rental: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
repair_history: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
slug: Mapped[str] = mapped_column(String(), nullable=False)
|
|
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), index=True)
|
|
images: Mapped[list["Image"]] = relationship("Image", back_populates="car", cascade="all, delete-orphan")
|
|
|
|
|
|
class Image(Base):
|
|
__tablename__ = "images"
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
fullres_image: Mapped[str] = mapped_column(String(), nullable=False)
|
|
preview_image: Mapped[str] = mapped_column(String(), nullable=False)
|
|
order_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
car_id: Mapped[int] = mapped_column(Integer, ForeignKey("cars.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
car: Mapped[Car] = relationship("Car", back_populates="images")
|
|
|
|
|
|
class SyncRun(Base):
|
|
__tablename__ = "sync_runs"
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
status: Mapped[str] = mapped_column(Text, nullable=False, index=True)
|
|
lane: Mapped[str] = mapped_column(Text, nullable=False)
|
|
ids_fetched: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
cars_upserted: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
cars_failed: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
images_upserted: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
error_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
|
class VehicleCandidate(Base):
|
|
__tablename__ = "vehicle_candidates"
|
|
__table_args__ = (
|
|
Index("ix_vehicle_candidates_url", "url"),
|
|
Index("ix_vehicle_candidates_status_discovered", "status", "discovered_at"),
|
|
)
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
url: Mapped[str] = mapped_column(String(), nullable=False, unique=True, index=True)
|
|
discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), index=True)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending", index=True) # pending, processing, processed, failed
|
|
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
last_attempt_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
raw_snapshots: Mapped[list["VehicleRawSnapshot"]] = relationship("VehicleRawSnapshot", back_populates="candidate", cascade="all, delete-orphan")
|
|
|
|
|
|
class VehicleRawSnapshot(Base):
|
|
__tablename__ = "vehicle_raw_snapshots"
|
|
__table_args__ = (
|
|
Index("ix_vehicle_raw_snapshots_candidate_id", "candidate_id"),
|
|
Index("ix_vehicle_raw_snapshots_captured_at", "captured_at"),
|
|
)
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
candidate_id: Mapped[int] = mapped_column(Integer, ForeignKey("vehicle_candidates.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
candidate: Mapped[VehicleCandidate] = relationship("VehicleCandidate", back_populates="raw_snapshots")
|
|
captured_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), index=True)
|
|
method: Mapped[str] = mapped_column(String(20), nullable=False) # http, browser
|
|
success: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
|
raw_data: Mapped[str | None] = mapped_column(Text, nullable=True) # HTML or JSON content
|
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
parse_results: Mapped[list["VehicleParseResult"]] = relationship("VehicleParseResult", back_populates="snapshot", cascade="all, delete-orphan")
|
|
|
|
|
|
class VehicleParseResult(Base):
|
|
__tablename__ = "vehicle_parse_results"
|
|
__table_args__ = (
|
|
Index("ix_vehicle_parse_results_snapshot_id", "snapshot_id"),
|
|
Index("ix_vehicle_parse_results_parsed_at", "parsed_at"),
|
|
)
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
snapshot_id: Mapped[int] = mapped_column(Integer, ForeignKey("vehicle_raw_snapshots.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
snapshot: Mapped[VehicleRawSnapshot] = relationship("VehicleRawSnapshot", back_populates="parse_results")
|
|
parsed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), index=True)
|
|
success: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
|
parsed_data: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON with parsed vehicle data
|
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
|
class VehicleRetryQueue(Base):
|
|
__tablename__ = "vehicle_retry_queue"
|
|
__table_args__ = (
|
|
Index("ix_vehicle_retry_queue_candidate_id", "candidate_id"),
|
|
Index("ix_vehicle_retry_queue_retry_at", "retry_at"),
|
|
)
|
|
id: Mapped[int] = mapped_column(BigInteger().with_variant(Integer, "sqlite"), primary_key=True, autoincrement=True)
|
|
candidate_id: Mapped[int] = mapped_column(Integer, ForeignKey("vehicle_candidates.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
reason: Mapped[str] = mapped_column(String(50), nullable=False) # parse_failed, capture_failed, etc.
|
|
retry_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
|
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
max_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|