add fastapi rest api and celery workers
This commit is contained in:
144
iaai_scraper/api/routes/cars.py
Normal file
144
iaai_scraper/api/routes/cars.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Cars endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from ..deps import get_persistence
|
||||
from ...storage.db import PersistenceService
|
||||
from ...storage.models import Car, Image
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/cars")
|
||||
def list_cars(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
brand: str | None = None,
|
||||
model: str | None = None,
|
||||
year_min: int | None = None,
|
||||
year_max: int | None = None,
|
||||
is_sold: bool | None = None,
|
||||
persistence: PersistenceService = Depends(get_persistence),
|
||||
):
|
||||
"""Список автомобилей с пагинацией и фильтрами."""
|
||||
with persistence.session_scope() as session:
|
||||
query = select(Car)
|
||||
|
||||
if brand:
|
||||
query = query.where(Car.brand.ilike(f"%{brand}%"))
|
||||
if model:
|
||||
query = query.where(Car.model.ilike(f"%{model}%"))
|
||||
if year_min is not None:
|
||||
query = query.where(Car.year >= year_min)
|
||||
if year_max is not None:
|
||||
query = query.where(Car.year <= year_max)
|
||||
if is_sold is not None:
|
||||
query = query.where(Car.is_sold == is_sold)
|
||||
|
||||
# Общее количество.
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total = session.execute(count_query).scalar() or 0
|
||||
|
||||
# Пагинация.
|
||||
offset = (page - 1) * per_page
|
||||
cars = session.execute(
|
||||
query.order_by(Car.last_seen_at.desc()).offset(offset).limit(per_page)
|
||||
).scalars().all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"pages": (total + per_page - 1) // per_page if per_page else 0,
|
||||
"items": [_car_to_dict(car) for car in cars],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/cars/{car_id}")
|
||||
def get_car(
|
||||
car_id: int,
|
||||
persistence: PersistenceService = Depends(get_persistence),
|
||||
):
|
||||
"""Детальная информация об автомобиле с изображениями."""
|
||||
with persistence.session_scope() as session:
|
||||
car = session.get(Car, car_id)
|
||||
if car is None:
|
||||
raise HTTPException(status_code=404, detail="Car not found")
|
||||
return _car_to_dict(car, include_images=True)
|
||||
|
||||
|
||||
@router.get("/cars/by-origin/{origin_id}")
|
||||
def get_car_by_origin(
|
||||
origin_id: str,
|
||||
persistence: PersistenceService = Depends(get_persistence),
|
||||
):
|
||||
"""Поиск автомобиля по origin_id."""
|
||||
with persistence.session_scope() as session:
|
||||
car = session.execute(
|
||||
select(Car).where(Car.origin_id == origin_id)
|
||||
).scalars().first()
|
||||
if car is None:
|
||||
raise HTTPException(status_code=404, detail="Car not found")
|
||||
return _car_to_dict(car, include_images=True)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def get_stats(persistence: PersistenceService = Depends(get_persistence)):
|
||||
"""Общая статистика по БД."""
|
||||
with persistence.session_scope() as session:
|
||||
total_cars = session.execute(select(func.count(Car.id))).scalar() or 0
|
||||
total_images = session.execute(select(func.count(Image.id))).scalar() or 0
|
||||
brands = session.execute(
|
||||
select(Car.brand, func.count(Car.id))
|
||||
.group_by(Car.brand)
|
||||
.order_by(func.count(Car.id).desc())
|
||||
.limit(20)
|
||||
).all()
|
||||
|
||||
return {
|
||||
"total_cars": total_cars,
|
||||
"total_images": total_images,
|
||||
"top_brands": [{"brand": b, "count": c} for b, c in brands],
|
||||
}
|
||||
|
||||
|
||||
def _car_to_dict(car: Car, include_images: bool = False) -> dict:
|
||||
"""Сериализация Car в dict."""
|
||||
result = {
|
||||
"id": car.id,
|
||||
"parser_id": car.parser_id,
|
||||
"brand": car.brand,
|
||||
"model": car.model,
|
||||
"year": car.year,
|
||||
"price": car.price,
|
||||
"currency": car.currency,
|
||||
"mileage": car.mileage,
|
||||
"country": car.country,
|
||||
"is_sold": car.is_sold,
|
||||
"color": car.color,
|
||||
"drive": car.drive,
|
||||
"gearbox": car.gearbox,
|
||||
"steering_wheel": car.steering_wheel,
|
||||
"body_type": car.body_type,
|
||||
"engine_volume": car.engine_volume,
|
||||
"selling_type": car.selling_type,
|
||||
"origin": car.origin,
|
||||
"origin_url": car.origin_url,
|
||||
"origin_id": car.origin_id,
|
||||
"is_damaged": car.is_damaged,
|
||||
"slug": car.slug,
|
||||
"last_seen_at": car.last_seen_at.isoformat() if car.last_seen_at else None,
|
||||
"content_hash": car.content_hash,
|
||||
}
|
||||
if include_images:
|
||||
result["images"] = [
|
||||
{
|
||||
"id": img.id,
|
||||
"fullres_image": img.fullres_image,
|
||||
"preview_image": img.preview_image,
|
||||
"order_index": img.order_index,
|
||||
}
|
||||
for img in sorted(car.images, key=lambda i: i.order_index)
|
||||
]
|
||||
return result
|
||||
Reference in New Issue
Block a user