seed vehicle trims catalog
This commit is contained in:
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.models.car import CarMake
|
||||
from app.models.car import CarMake, CarModel
|
||||
from app.schemas.car import CarMakeRead
|
||||
|
||||
router = APIRouter(prefix="/catalog", tags=["catalog"])
|
||||
@@ -13,9 +13,13 @@ router = APIRouter(prefix="/catalog", tags=["catalog"])
|
||||
@router.get("/makes", response_model=list[CarMakeRead])
|
||||
async def list_makes(session: AsyncSession = Depends(get_session)) -> list[CarMake]:
|
||||
result = await session.execute(
|
||||
select(CarMake).options(selectinload(CarMake.models)).order_by(CarMake.name)
|
||||
select(CarMake)
|
||||
.options(selectinload(CarMake.models).selectinload(CarModel.trims))
|
||||
.order_by(CarMake.name)
|
||||
)
|
||||
makes = list(result.scalars())
|
||||
for make in makes:
|
||||
make.models.sort(key=lambda model: model.name)
|
||||
for model in make.models:
|
||||
model.trims.sort(key=lambda trim: trim.name)
|
||||
return makes
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import argparse
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -7,25 +8,25 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import async_session_factory
|
||||
from app.models.car import Car, CarMake, CarModel
|
||||
from app.models.car import Car, CarMake, CarModel, CarTrim
|
||||
from app.models.expense import FuelEntry, ServiceEntry, ServiceType
|
||||
from app.models.user import User
|
||||
from app.services.catalog_data import CAR_CATALOG
|
||||
from app.services.catalog_data import CAR_CATALOG, CAR_TRIMS, COMMON_TRIMS, MAKE_COUNTRIES
|
||||
|
||||
|
||||
MOCK_PLATE_PREFIX = "MOCK"
|
||||
|
||||
MOCK_CARS = [
|
||||
("KIA Sportage", "KIA", "Sportage", 2021, "gasoline", 36200, Decimal("2450000")),
|
||||
("Toyota Camry", "Toyota", "Camry", 2020, "gasoline", 58400, Decimal("2850000")),
|
||||
("Hyundai Tucson", "Hyundai", "Tucson", 2022, "gasoline", 27100, Decimal("2750000")),
|
||||
("Volkswagen Tiguan", "Volkswagen", "Tiguan", 2019, "gasoline", 73400, Decimal("2300000")),
|
||||
("BMW X3", "BMW", "X3", 2021, "diesel", 48900, Decimal("4350000")),
|
||||
("Mercedes GLC", "Mercedes", "GLC", 2020, "gasoline", 52200, Decimal("4500000")),
|
||||
("Nissan X-Trail", "Nissan", "X-Trail", 2018, "gasoline", 91400, Decimal("1850000")),
|
||||
("Skoda Octavia", "Skoda", "Octavia", 2021, "gasoline", 46800, Decimal("2050000")),
|
||||
("Tesla Model 3", "Tesla", "Model 3", 2022, "electric", 33800, Decimal("3900000")),
|
||||
("Haval Jolion", "Haval", "Jolion", 2023, "gasoline", 19600, Decimal("2150000")),
|
||||
("KIA Sportage", "KIA", "Sportage", "GT-Line 1.6T DCT AWD", 2021, "gasoline", 36200, Decimal("2450000")),
|
||||
("Toyota Camry", "Toyota", "Camry", "Comfort 2.5 AT", 2020, "gasoline", 58400, Decimal("2850000")),
|
||||
("Hyundai Tucson", "Hyundai", "Tucson", "Prestige 2.0 AT AWD", 2022, "gasoline", 27100, Decimal("2750000")),
|
||||
("Volkswagen Tiguan", "Volkswagen", "Tiguan", "Status 2.0 TSI DSG 4Motion", 2019, "gasoline", 73400, Decimal("2300000")),
|
||||
("BMW X3", "BMW", "X3", "20d xDrive", 2021, "diesel", 48900, Decimal("4350000")),
|
||||
("Mercedes GLC", "Mercedes", "GLC", "GLC 300 4MATIC AMG Line", 2020, "gasoline", 52200, Decimal("4500000")),
|
||||
("Nissan X-Trail", "Nissan", "X-Trail", "Premium", 2018, "gasoline", 91400, Decimal("1850000")),
|
||||
("Skoda Octavia", "Skoda", "Octavia", "Comfort", 2021, "gasoline", 46800, Decimal("2050000")),
|
||||
("Tesla Model 3", "Tesla", "Model 3", "Long Range AWD", 2022, "electric", 33800, Decimal("3900000")),
|
||||
("Haval Jolion", "Haval", "Jolion", "Premium", 2023, "gasoline", 19600, Decimal("2150000")),
|
||||
]
|
||||
|
||||
|
||||
@@ -43,15 +44,40 @@ async def seed_catalog(session: AsyncSession) -> None:
|
||||
for make_name, model_names in CAR_CATALOG.items():
|
||||
make = existing.get(make_name)
|
||||
if make is None:
|
||||
make = CarMake(name=make_name)
|
||||
make = CarMake(name=make_name, country=MAKE_COUNTRIES.get(make_name))
|
||||
session.add(make)
|
||||
await session.flush()
|
||||
existing_models = set()
|
||||
else:
|
||||
if not make.country and MAKE_COUNTRIES.get(make_name):
|
||||
make.country = MAKE_COUNTRIES[make_name]
|
||||
existing_models = {model.name for model in make.models}
|
||||
for model_name in model_names:
|
||||
if model_name not in existing_models:
|
||||
session.add(CarModel(make_id=make.id, name=model_name))
|
||||
await session.flush()
|
||||
await seed_trims(session)
|
||||
|
||||
|
||||
async def seed_trims(session: AsyncSession) -> None:
|
||||
result = await session.execute(
|
||||
select(CarMake).options(selectinload(CarMake.models).selectinload(CarModel.trims))
|
||||
)
|
||||
makes = {make.name: make for make in result.scalars()}
|
||||
for make in makes.values():
|
||||
for model in make.models:
|
||||
trim_rows = CAR_TRIMS.get((make.name, model.name)) or [
|
||||
{**item, "body_type": infer_body_type(model.name)} for item in COMMON_TRIMS
|
||||
]
|
||||
existing = {trim.name for trim in model.trims}
|
||||
for row in trim_rows:
|
||||
if row["name"] not in existing:
|
||||
session.add(CarTrim(model_id=model.id, **row))
|
||||
|
||||
|
||||
def infer_body_type(model_name: str) -> str:
|
||||
suv_markers = ("Q", "X", "CX", "CR-V", "RAV", "Tiguan", "Tucson", "Sportage", "Jolion")
|
||||
return "SUV" if any(marker in model_name for marker in suv_markers) else "sedan"
|
||||
|
||||
|
||||
async def pick_owner(session: AsyncSession) -> User:
|
||||
@@ -82,12 +108,16 @@ async def seed_mock_usage(session: AsyncSession, owner: User) -> None:
|
||||
await clear_previous_mock(session)
|
||||
today = date.today()
|
||||
|
||||
for index, (name, make, model, year, fuel_type, start_odo, price) in enumerate(MOCK_CARS, start=1):
|
||||
for index, (name, make, model, trim, year, fuel_type, start_odo, price) in enumerate(
|
||||
MOCK_CARS,
|
||||
start=1,
|
||||
):
|
||||
car = Car(
|
||||
owner_id=owner.id,
|
||||
name=name,
|
||||
make=make,
|
||||
model=model,
|
||||
trim=trim,
|
||||
year=year,
|
||||
plate_number=f"{MOCK_PLATE_PREFIX}-{index:02d}",
|
||||
fuel_type=fuel_type,
|
||||
@@ -201,9 +231,13 @@ async def seed_mock_usage(session: AsyncSession, owner: User) -> None:
|
||||
car.current_odometer = odometer
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async def main(catalog_only: bool = False) -> None:
|
||||
async with async_session_factory() as session:
|
||||
await seed_catalog(session)
|
||||
if catalog_only:
|
||||
await session.commit()
|
||||
print("Seeded vehicle catalog")
|
||||
return
|
||||
owner = await pick_owner(session)
|
||||
await seed_mock_usage(session, owner)
|
||||
await session.commit()
|
||||
@@ -211,4 +245,7 @@ async def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--catalog-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(main(catalog_only=args.catalog_only))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Date, DateTime, ForeignKey, Numeric, String, UniqueConstraint, func
|
||||
from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.base import Base
|
||||
@@ -15,6 +15,7 @@ class Car(Base):
|
||||
name: Mapped[str] = mapped_column(String(160))
|
||||
make: Mapped[str | None] = mapped_column(String(80))
|
||||
model: Mapped[str | None] = mapped_column(String(80))
|
||||
trim: Mapped[str | None] = mapped_column(String(120))
|
||||
year: Mapped[int | None]
|
||||
plate_number: Mapped[str | None] = mapped_column(String(32))
|
||||
vin: Mapped[str | None] = mapped_column(String(32))
|
||||
@@ -53,3 +54,23 @@ class CarModel(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
make = relationship("CarMake", back_populates="models")
|
||||
trims = relationship("CarTrim", back_populates="model", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class CarTrim(Base):
|
||||
__tablename__ = "car_trims"
|
||||
__table_args__ = (UniqueConstraint("model_id", "name", name="uq_car_trims_model_name"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
model_id: Mapped[int] = mapped_column(ForeignKey("car_models.id", ondelete="CASCADE"), index=True)
|
||||
name: Mapped[str] = mapped_column(String(120), index=True)
|
||||
body_type: Mapped[str | None] = mapped_column(String(60))
|
||||
fuel_type: Mapped[str | None] = mapped_column(String(32))
|
||||
transmission: Mapped[str | None] = mapped_column(String(32))
|
||||
drive_type: Mapped[str | None] = mapped_column(String(32))
|
||||
year_from: Mapped[int | None] = mapped_column(Integer)
|
||||
year_to: Mapped[int | None] = mapped_column(Integer)
|
||||
market: Mapped[str | None] = mapped_column(String(80))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
model = relationship("CarModel", back_populates="trims")
|
||||
|
||||
@@ -8,6 +8,7 @@ class CarBase(BaseModel):
|
||||
name: str
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
year: int | None = None
|
||||
plate_number: str | None = None
|
||||
vin: str | None = None
|
||||
@@ -25,6 +26,7 @@ class CarUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
year: int | None = None
|
||||
plate_number: str | None = None
|
||||
vin: str | None = None
|
||||
@@ -42,9 +44,24 @@ class CarRead(CarBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CarTrimRead(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
body_type: str | None = None
|
||||
fuel_type: str | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
year_from: int | None = None
|
||||
year_to: int | None = None
|
||||
market: str | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CarModelRead(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
trims: list[CarTrimRead] = []
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -55,3 +55,103 @@ CAR_CATALOG: dict[str, list[str]] = {
|
||||
"Zeekr": ["001", "007", "009", "X"],
|
||||
"УАЗ": ["Буханка", "Патриот", "Пикап", "Хантер"],
|
||||
}
|
||||
|
||||
MAKE_COUNTRIES: dict[str, str] = {
|
||||
"BMW": "Germany",
|
||||
"Mercedes": "Germany",
|
||||
"Volkswagen": "Germany",
|
||||
"Audi": "Germany",
|
||||
"Porsche": "Germany",
|
||||
"Skoda": "Czech Republic",
|
||||
"Toyota": "Japan",
|
||||
"Lexus": "Japan",
|
||||
"Nissan": "Japan",
|
||||
"Honda": "Japan",
|
||||
"Mazda": "Japan",
|
||||
"Subaru": "Japan",
|
||||
"Mitsubishi": "Japan",
|
||||
"Hyundai": "South Korea",
|
||||
"KIA": "South Korea",
|
||||
"Genesis": "South Korea",
|
||||
"Tesla": "USA",
|
||||
"Ford": "USA",
|
||||
"Chevrolet": "USA",
|
||||
"Haval": "China",
|
||||
"Chery": "China",
|
||||
"Geely": "China",
|
||||
"BYD": "China",
|
||||
"LADA": "Russia",
|
||||
"УАЗ": "Russia",
|
||||
}
|
||||
|
||||
COMMON_TRIMS = [
|
||||
{
|
||||
"name": "Base",
|
||||
"body_type": None,
|
||||
"fuel_type": "gasoline",
|
||||
"transmission": "AT",
|
||||
"drive_type": "FWD",
|
||||
"market": "Global",
|
||||
},
|
||||
{
|
||||
"name": "Comfort",
|
||||
"body_type": None,
|
||||
"fuel_type": "gasoline",
|
||||
"transmission": "AT",
|
||||
"drive_type": "FWD",
|
||||
"market": "Global",
|
||||
},
|
||||
{
|
||||
"name": "Premium",
|
||||
"body_type": None,
|
||||
"fuel_type": "gasoline",
|
||||
"transmission": "AT",
|
||||
"drive_type": "AWD",
|
||||
"market": "Global",
|
||||
},
|
||||
]
|
||||
|
||||
CAR_TRIMS: dict[tuple[str, str], list[dict[str, str | int | None]]] = {
|
||||
("Toyota", "Camry"): [
|
||||
{"name": "Standard 2.0 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
|
||||
{"name": "Comfort 2.5 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
|
||||
{"name": "Prestige 2.5 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
|
||||
{"name": "Hybrid 2.5 e-CVT", "body_type": "sedan", "fuel_type": "hybrid", "transmission": "e-CVT", "drive_type": "FWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("Toyota", "RAV4"): [
|
||||
{"name": "Comfort 2.0 CVT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "CVT", "drive_type": "FWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Style 2.0 CVT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "CVT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Hybrid 2.5 e-CVT AWD", "body_type": "SUV", "fuel_type": "hybrid", "transmission": "e-CVT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("KIA", "Sportage"): [
|
||||
{"name": "Classic 2.0 AT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Comfort 2.0 AT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Prestige 2.0 Diesel AWD", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
|
||||
{"name": "GT-Line 1.6T DCT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DCT", "drive_type": "AWD", "year_from": 2021, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("Hyundai", "Tucson"): [
|
||||
{"name": "Lifestyle 2.0 AT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Prestige 2.0 AT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "Hybrid 1.6T AWD", "body_type": "SUV", "fuel_type": "hybrid", "transmission": "AT", "drive_type": "AWD", "year_from": 2021, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("Volkswagen", "Tiguan"): [
|
||||
{"name": "Respect 1.4 TSI DSG", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "FWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
|
||||
{"name": "Status 2.0 TSI DSG 4Motion", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "AWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
|
||||
{"name": "R-Line 2.0 TSI DSG 4Motion", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "AWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
|
||||
],
|
||||
("BMW", "X3"): [
|
||||
{"name": "20i xDrive", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
{"name": "20d xDrive", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
{"name": "30i xDrive M Sport", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("Mercedes", "GLC"): [
|
||||
{"name": "GLC 200 4MATIC", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "GLC 220d 4MATIC", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
{"name": "GLC 300 4MATIC AMG Line", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
|
||||
],
|
||||
("Tesla", "Model 3"): [
|
||||
{"name": "Rear-Wheel Drive", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "RWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
|
||||
{"name": "Long Range AWD", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "AWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
|
||||
{"name": "Performance AWD", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "AWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user