claude fix: improve product scraping and debugging features
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_db
|
||||
from backend.app.db import crud, schemas
|
||||
from backend.app.scraper.runner import scrape_product
|
||||
|
||||
router = APIRouter(prefix="/products", tags=["products"])
|
||||
|
||||
@@ -16,16 +15,18 @@ def list_products(skip: int = 0, limit: int = 50, db: Session = Depends(get_db))
|
||||
return crud.list_products_with_snapshots(db, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.post("", response_model=schemas.ProductRead, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("", response_model=schemas.ProductWithSnapshot, status_code=status.HTTP_201_CREATED)
|
||||
def create_product(
|
||||
payload: schemas.ProductCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
payload: schemas.ProductCreateWithSnapshot,
|
||||
db: Session = Depends(get_db),
|
||||
) -> schemas.ProductRead:
|
||||
product = crud.create_product(db, payload)
|
||||
# Déclenche automatiquement le scraping après création
|
||||
background_tasks.add_task(scrape_product, product.id)
|
||||
return product
|
||||
) -> schemas.ProductWithSnapshot:
|
||||
"""
|
||||
Crée un produit avec ses données de snapshot initiales.
|
||||
Les données proviennent de la prévisualisation (/scrape/preview).
|
||||
"""
|
||||
product = crud.create_product_with_snapshot(db, payload)
|
||||
# Retourner le produit enrichi avec le snapshot créé
|
||||
return crud.get_product_with_snapshot(db, product.id)
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=schemas.ProductWithSnapshot)
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
from backend.app.scraper.runner import scrape_all, scrape_product
|
||||
from backend.app.scraper.runner import scrape_all, scrape_preview, scrape_product
|
||||
|
||||
router = APIRouter(prefix="/scrape", tags=["scrape"])
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
url: HttpUrl
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
def preview_scrape(payload: PreviewRequest):
|
||||
"""
|
||||
Scrape une URL Amazon sans enregistrer en base.
|
||||
Retourne les données pour prévisualisation avant ajout.
|
||||
"""
|
||||
result = scrape_preview(str(payload.url))
|
||||
if not result["success"] and result["error"]:
|
||||
raise HTTPException(status_code=422, detail=result["error"])
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/product/{product_id}")
|
||||
def trigger_single(product_id: int, background_tasks: BackgroundTasks):
|
||||
# on délègue le vrai travail à un background task rapide
|
||||
|
||||
@@ -38,6 +38,58 @@ def create_product(db: Session, data: schemas.ProductCreate) -> models.Product:
|
||||
return product
|
||||
|
||||
|
||||
def create_product_with_snapshot(
|
||||
db: Session, data: schemas.ProductCreateWithSnapshot
|
||||
) -> models.Product:
|
||||
"""Crée un produit avec un snapshot initial (données issues de la prévisualisation)."""
|
||||
data_dict = data.model_dump()
|
||||
|
||||
# Extraire les champs du snapshot
|
||||
snapshot_fields = [
|
||||
"prix_actuel",
|
||||
"prix_conseille",
|
||||
"prix_min_30j",
|
||||
"etat_stock",
|
||||
"en_stock",
|
||||
"note",
|
||||
"nombre_avis",
|
||||
"prime",
|
||||
"choix_amazon",
|
||||
"offre_limitee",
|
||||
"exclusivite_amazon",
|
||||
]
|
||||
snapshot_data = {k: data_dict.pop(k) for k in snapshot_fields if k in data_dict}
|
||||
|
||||
# Convertir les HttpUrl en strings pour SQLite
|
||||
if data_dict.get("url"):
|
||||
data_dict["url"] = str(data_dict["url"])
|
||||
if data_dict.get("url_image"):
|
||||
data_dict["url_image"] = str(data_dict["url_image"])
|
||||
|
||||
# Créer le produit
|
||||
product = models.Product(**data_dict)
|
||||
db.add(product)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise
|
||||
db.refresh(product)
|
||||
|
||||
# Créer le snapshot initial si on a des données
|
||||
has_snapshot_data = any(v is not None for v in snapshot_data.values())
|
||||
if has_snapshot_data:
|
||||
snapshot = models.ProductSnapshot(
|
||||
produit_id=product.id,
|
||||
statut_scrap="preview",
|
||||
**snapshot_data,
|
||||
)
|
||||
db.add(snapshot)
|
||||
db.commit()
|
||||
|
||||
return product
|
||||
|
||||
|
||||
def update_product(db: Session, product: models.Product, changes: schemas.ProductUpdate) -> models.Product:
|
||||
for field, value in changes.dict(exclude_unset=True).items():
|
||||
setattr(product, field, value)
|
||||
|
||||
@@ -87,3 +87,20 @@ class ProductWithSnapshot(ProductBase):
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class ProductCreateWithSnapshot(ProductBase):
|
||||
"""Création d'un produit avec données de snapshot initiales (depuis preview)."""
|
||||
|
||||
# Données du snapshot initial
|
||||
prix_actuel: Optional[float] = None
|
||||
prix_conseille: Optional[float] = None
|
||||
prix_min_30j: Optional[float] = None
|
||||
etat_stock: Optional[str] = None
|
||||
en_stock: Optional[bool] = None
|
||||
note: Optional[float] = None
|
||||
nombre_avis: Optional[int] = None
|
||||
prime: Optional[bool] = None
|
||||
choix_amazon: Optional[bool] = None
|
||||
offre_limitee: Optional[bool] = None
|
||||
exclusivite_amazon: Optional[bool] = None
|
||||
|
||||
@@ -44,5 +44,11 @@ def health() -> dict[str, str]:
|
||||
return {"statut": "ok"}
|
||||
|
||||
|
||||
@app.get("/version")
|
||||
def version() -> dict[str, str]:
|
||||
"""Retourne la version du backend."""
|
||||
return {"version": "0.1.0"}
|
||||
|
||||
|
||||
def main() -> FastAPI:
|
||||
return app
|
||||
|
||||
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |