- Ajout des champs a_propos, description, carateristique, details au modèle ProductSnapshot - Sérialisation JSON pour les listes et dictionnaires - Modification du CRUD pour stocker/lire les données étendues - Modification du runner pour passer les données lors du scrape - AddProductModal envoie les données étendues lors de la création - La base SQLite doit être recréée (suppression de suivi.db) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from backend.app.db import models, schemas
|
|
|
|
|
|
"""
|
|
CRUD minimal pour manipuler les produits dans la base SQLite.
|
|
"""
|
|
|
|
|
|
def get_product(db: Session, product_id: int) -> models.Product | None:
|
|
return db.query(models.Product).filter(models.Product.id == product_id).first()
|
|
|
|
|
|
def list_products(db: Session, skip: int = 0, limit: int = 100) -> list[models.Product]:
|
|
return db.query(models.Product).offset(skip).limit(limit).all()
|
|
|
|
|
|
def create_product(db: Session, data: schemas.ProductCreate) -> models.Product:
|
|
# Convertir les HttpUrl en strings pour SQLite
|
|
data_dict = data.model_dump()
|
|
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"])
|
|
|
|
product = models.Product(**data_dict)
|
|
db.add(product)
|
|
try:
|
|
db.commit()
|
|
except IntegrityError:
|
|
db.rollback()
|
|
raise
|
|
db.refresh(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",
|
|
"a_propos",
|
|
"description",
|
|
"carateristique",
|
|
"details",
|
|
]
|
|
snapshot_data = {k: data_dict.pop(k) for k in snapshot_fields if k in data_dict}
|
|
|
|
# Sérialiser les champs JSON pour le stockage
|
|
if snapshot_data.get("a_propos") is not None:
|
|
snapshot_data["a_propos"] = json.dumps(snapshot_data["a_propos"], ensure_ascii=False)
|
|
if snapshot_data.get("carateristique") is not None:
|
|
snapshot_data["carateristique"] = json.dumps(snapshot_data["carateristique"], ensure_ascii=False)
|
|
if snapshot_data.get("details") is not None:
|
|
snapshot_data["details"] = json.dumps(snapshot_data["details"], ensure_ascii=False)
|
|
|
|
# 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)
|
|
db.add(product)
|
|
db.commit()
|
|
db.refresh(product)
|
|
return product
|
|
|
|
|
|
def remove_product(db: Session, product: models.Product) -> None:
|
|
db.delete(product)
|
|
db.commit()
|
|
|
|
|
|
def list_snapshots(db: Session, product_id: int, limit: int = 30) -> list[models.ProductSnapshot]:
|
|
return (
|
|
db.query(models.ProductSnapshot)
|
|
.filter(models.ProductSnapshot.produit_id == product_id)
|
|
.order_by(models.ProductSnapshot.scrape_le.desc())
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
|
|
def get_latest_snapshot(db: Session, product_id: int) -> models.ProductSnapshot | None:
|
|
return (
|
|
db.query(models.ProductSnapshot)
|
|
.filter(models.ProductSnapshot.produit_id == product_id)
|
|
.order_by(models.ProductSnapshot.scrape_le.desc())
|
|
.first()
|
|
)
|
|
|
|
|
|
def get_product_with_snapshot(db: Session, product_id: int) -> dict | None:
|
|
"""Retourne un produit enrichi avec les données du dernier snapshot."""
|
|
product = get_product(db, product_id)
|
|
if not product:
|
|
return None
|
|
return _enrich_product_with_snapshot(db, product)
|
|
|
|
|
|
def list_products_with_snapshots(db: Session, skip: int = 0, limit: int = 100) -> list[dict]:
|
|
"""Retourne la liste des produits enrichis avec leurs derniers snapshots."""
|
|
products = list_products(db, skip=skip, limit=limit)
|
|
return [_enrich_product_with_snapshot(db, p) for p in products]
|
|
|
|
|
|
def _enrich_product_with_snapshot(db: Session, product: models.Product) -> dict:
|
|
"""Ajoute les données du dernier snapshot au produit."""
|
|
snapshot = get_latest_snapshot(db, product.id)
|
|
|
|
result = {
|
|
"id": product.id,
|
|
"boutique": product.boutique,
|
|
"url": str(product.url),
|
|
"asin": product.asin,
|
|
"titre": product.titre,
|
|
"url_image": str(product.url_image) if product.url_image else None,
|
|
"categorie": product.categorie,
|
|
"type": product.type,
|
|
"actif": product.actif,
|
|
"cree_le": product.cree_le,
|
|
"modifie_le": product.modifie_le,
|
|
}
|
|
|
|
if snapshot:
|
|
# Calcul de la réduction en pourcentage
|
|
reduction = None
|
|
if snapshot.prix_actuel and snapshot.prix_conseille:
|
|
reduction = round((1 - snapshot.prix_actuel / snapshot.prix_conseille) * 100)
|
|
|
|
# Désérialiser les champs JSON
|
|
a_propos = None
|
|
if snapshot.a_propos:
|
|
try:
|
|
a_propos = json.loads(snapshot.a_propos)
|
|
except json.JSONDecodeError:
|
|
a_propos = None
|
|
|
|
carateristique = None
|
|
if snapshot.carateristique:
|
|
try:
|
|
carateristique = json.loads(snapshot.carateristique)
|
|
except json.JSONDecodeError:
|
|
carateristique = None
|
|
|
|
details = None
|
|
if snapshot.details:
|
|
try:
|
|
details = json.loads(snapshot.details)
|
|
except json.JSONDecodeError:
|
|
details = None
|
|
|
|
result.update(
|
|
{
|
|
"prix_actuel": snapshot.prix_actuel,
|
|
"prix_conseille": snapshot.prix_conseille,
|
|
"prix_min_30j": snapshot.prix_min_30j,
|
|
"reduction_pourcent": reduction,
|
|
"etat_stock": snapshot.etat_stock,
|
|
"en_stock": snapshot.en_stock,
|
|
"note": snapshot.note,
|
|
"nombre_avis": snapshot.nombre_avis,
|
|
"prime": snapshot.prime,
|
|
"choix_amazon": snapshot.choix_amazon,
|
|
"offre_limitee": snapshot.offre_limitee,
|
|
"exclusivite_amazon": snapshot.exclusivite_amazon,
|
|
"a_propos": a_propos,
|
|
"description": snapshot.description,
|
|
"carateristique": carateristique,
|
|
"details": details,
|
|
"dernier_scrape": snapshot.scrape_le,
|
|
"statut_scrap": snapshot.statut_scrap,
|
|
}
|
|
)
|
|
|
|
return result
|