This commit is contained in:
2026-01-18 19:21:51 +01:00
parent 9a6448facc
commit 8397ec9793
29 changed files with 5977 additions and 6664 deletions

BIN
.coverage

Binary file not shown.

View File

@@ -2,3 +2,9 @@
from .routes_config import router as config_router
from .routes_products import router as products_router
from .routes_scrape import router as scrape_router
__all__ = [
"config_router",
"products_router",
"scrape_router",
]

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter, Body, HTTPException
from backend.app.core.config import BackendConfig, CONFIG_PATH, load_config

View File

@@ -31,6 +31,6 @@
<div id="acBadge_feature_div">Choix d'Amazon</div>
<div id="dealBadge_feature_div">Offre a duree limitee</div>
<div>Exclusivite Amazon</div>
<div>Exclusivité Amazon</div>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 KiB

After

Width:  |  Height:  |  Size: 959 KiB

View File

@@ -14,12 +14,12 @@
"prix_actuel": 1259.99,
"prix_conseille": 1699.99,
"prix_min_30j": null,
"prix_conseille_reduction": null,
"prix_min_30j_reduction": -26,
"prix_conseille_reduction": -26,
"prix_min_30j_reduction": null,
"etat_stock": "En stock",
"en_stock": true,
"note": 4.7,
"nombre_avis": 7,
"nombre_avis": 8,
"choix_amazon": null,
"offre_limitee": null,
"prime": null,
@@ -110,8 +110,8 @@
"Disponibilité des pièces détachées": "5 Ans",
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B0DQ8M74KL",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (7) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "4884 en Informatique ( Voir les 100 premiers en Informatique ) 127 en Ordinateurs portables classiques",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "1977 en Informatique ( Voir les 100 premiers en Informatique ) 47 en Ordinateurs portables classiques",
"Date de mise en ligne sur Amazon.fr": "1 juillet 2025"
}
},
@@ -119,7 +119,7 @@
"statut": "succes",
"erreurs": [],
"notes": [
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, prime, choix_amazon, offre_limitee",
"Optionnels manquants: prix_min_30j, prix_min_30j_reduction, prime, choix_amazon, offre_limitee",
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-001_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-001_capture.html'}"
]
}
@@ -197,7 +197,7 @@
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B0F32N1ZGH",
"Moyenne des commentaires client": "4,6 4,6 sur 5 étoiles (211) 4,6 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "480 en Informatique ( Voir les 100 premiers en Informatique ) 6 en Cartes graphiques",
"Classement des meilleures ventes d'Amazon": "417 en Informatique ( Voir les 100 premiers en Informatique ) 7 en Cartes graphiques",
"Date de mise en ligne sur Amazon.fr": "16 avril 2025"
}
},
@@ -224,12 +224,12 @@
"prix_actuel": 249.99,
"prix_conseille": 329.99,
"prix_min_30j": null,
"prix_conseille_reduction": null,
"prix_min_30j_reduction": -24,
"prix_conseille_reduction": -24,
"prix_min_30j_reduction": null,
"etat_stock": null,
"en_stock": null,
"note": 4.7,
"nombre_avis": 967,
"nombre_avis": 969,
"choix_amazon": null,
"offre_limitee": null,
"prime": null,
@@ -282,8 +282,8 @@
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B0DWFLPMM5",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (967) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "42 en Informatique ( Voir les 100 premiers en Informatique ) 2 en SSD internes",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (969) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "44 en Informatique ( Voir les 100 premiers en Informatique ) 2 en SSD internes",
"Date de mise en ligne sur Amazon.fr": "10 mars 2025"
}
},
@@ -291,7 +291,7 @@
"statut": "succes",
"erreurs": [],
"notes": [
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, etat_stock, en_stock, prime, choix_amazon, offre_limitee, exclusivite_amazon",
"Optionnels manquants: prix_min_30j, prix_min_30j_reduction, etat_stock, en_stock, prime, choix_amazon, offre_limitee, exclusivite_amazon",
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-003_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-003_capture.html'}"
]
}
@@ -315,7 +315,7 @@
"etat_stock": "En stock",
"en_stock": true,
"note": 4.8,
"nombre_avis": 28247,
"nombre_avis": 28260,
"choix_amazon": true,
"offre_limitee": null,
"prime": null,
@@ -364,8 +364,8 @@
"Disponibilité des pièces détachées": "2 Ans",
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B07RW6Z692",
"Moyenne des commentaires client": "4,8 4,8 sur 5 étoiles (28 247) 4,8 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "210 en Informatique ( Voir les 100 premiers en Informatique ) 3 en Mémoire RAM",
"Moyenne des commentaires client": "4,8 4,8 sur 5 étoiles (28 260) 4,8 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "258 en Informatique ( Voir les 100 premiers en Informatique ) 4 en Mémoire RAM",
"Date de mise en ligne sur Amazon.fr": "8 novembre 2018"
}
},
@@ -397,7 +397,7 @@
"etat_stock": null,
"en_stock": null,
"note": 4.7,
"nombre_avis": 8626,
"nombre_avis": 8629,
"choix_amazon": null,
"offre_limitee": null,
"prime": null,
@@ -438,8 +438,8 @@
"Disponibilité des pièces détachées": "2 Ans",
"Mises à jour logicielles garanties jusquà": "13 avril 2030",
"ASIN": "B08GSTF5NJ",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8 626) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "2130 en Informatique ( Voir les 100 premiers en Informatique ) 41 en Mémoire RAM",
"Moyenne des commentaires client": "4,7 4,7 sur 5 étoiles (8 629) 4,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "3086 en Informatique ( Voir les 100 premiers en Informatique ) 61 en Mémoire RAM",
"Date de mise en ligne sur Amazon.fr": "25 août 2020"
}
},
@@ -464,14 +464,14 @@
"titre": "MSI Modern MD2412P Écran Bureautique 23.8\" Full HD - Dalle IPS 1920x1080, 100Hz, Confort Oculaire, Montage VESA, Haut-Parleurs Intégrés, Display Kit, Réglable 4 Directions - HDMI 1.4b, USB Type-C",
"url_image_principale": "https://m.media-amazon.com/images/I/71bjTFOkcDL._AC_SX679_.jpg",
"prix_actuel": 119.99,
"prix_conseille": 149.99,
"prix_min_30j": null,
"prix_conseille": null,
"prix_min_30j": 149.99,
"prix_conseille_reduction": null,
"prix_min_30j_reduction": -20,
"etat_stock": "En stock",
"en_stock": true,
"note": 4.5,
"nombre_avis": 3872,
"nombre_avis": 3878,
"choix_amazon": true,
"offre_limitee": true,
"prime": null,
@@ -526,8 +526,8 @@
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B0CB4FDJT5",
"Moyenne des commentaires client": "4,5 4,5 sur 5 étoiles (3 872) 4,5 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "43 en Informatique ( Voir les 100 premiers en Informatique ) 1 en Écrans PC",
"Moyenne des commentaires client": "4,5 4,5 sur 5 étoiles (3 878) 4,5 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "40 en Informatique ( Voir les 100 premiers en Informatique ) 1 en Écrans PC",
"Date de mise en ligne sur Amazon.fr": "4 juillet 2023"
}
},
@@ -535,7 +535,7 @@
"statut": "succes",
"erreurs": [],
"notes": [
"Optionnels manquants: prix_conseille_reduction, prix_min_30j, prime, exclusivite_amazon",
"Optionnels manquants: prix_conseille, prix_conseille_reduction, prime, exclusivite_amazon",
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-006_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-006_capture.html'}"
]
}
@@ -552,8 +552,8 @@
"titre": "UGREEN Revodok Pro Docking Station USB C 2 DisplayPort 4K 120Hz et 1 HDMI 4K 60Hz Triple Écran Extension 14 en 1 Hub Station d'Accueil avec Multi USB 10Gbps Ethernet Gigabit 100W PD Charge SD TF 3.5mm",
"url_image_principale": "https://m.media-amazon.com/images/I/61AuBDPwDvL._AC_SY355_.jpg",
"prix_actuel": 118.99,
"prix_conseille": 127.49,
"prix_min_30j": null,
"prix_conseille": 169.99,
"prix_min_30j": 127.49,
"prix_conseille_reduction": -30,
"prix_min_30j_reduction": -7,
"etat_stock": "En stock",
@@ -588,7 +588,7 @@
"Moyenne des commentaires client": "4,6 4,6 sur 5 étoiles (46) 4,6 sur 5 étoiles",
"Numéro du modèle de l'article": "CM843",
"ASIN": "B0FGHX59G2",
"Classement des meilleures ventes d'Amazon": "32 en Stations d'accueil pour ordinateur portable",
"Classement des meilleures ventes d'Amazon": "35 en Stations d'accueil pour ordinateur portable",
"Date de mise en ligne sur Amazon.fr": "25 août 2025"
}
},
@@ -596,7 +596,7 @@
"statut": "succes",
"erreurs": [],
"notes": [
"Optionnels manquants: description, prix_min_30j, prime, choix_amazon, exclusivite_amazon",
"Optionnels manquants: description, prime, choix_amazon, exclusivite_amazon",
"debug={'screenshot': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-007_capture.png', 'html': '/home/gilles/Documents/vscode/suivi_produit/backend/app/samples/debug/sample-007_capture.html'}"
]
}
@@ -620,7 +620,7 @@
"etat_stock": "En stock",
"en_stock": true,
"note": 4.2,
"nombre_avis": 3713,
"nombre_avis": 3716,
"choix_amazon": null,
"offre_limitee": null,
"prime": null,
@@ -656,10 +656,10 @@
"Garantie constructeur": "2 ans constructeur",
"Disponibilité des pièces détachées": "Information indisponible sur les pièces détachées",
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"Moyenne des commentaires client": "4,2 4,2 sur 5 étoiles (3 713) 4,2 sur 5 étoiles",
"Moyenne des commentaires client": "4,2 4,2 sur 5 étoiles (3 716) 4,2 sur 5 étoiles",
"Numéro du modèle de l'article": "A1336",
"ASIN": "B0BYNZXFM2",
"Classement des meilleures ventes d'Amazon": "16846 en High-Tech ( Voir les 100 premiers en High-Tech ) 347 en Blocs d'alimentation portatifs pour téléphone portable",
"Classement des meilleures ventes d'Amazon": "16306 en High-Tech ( Voir les 100 premiers en High-Tech ) 333 en Blocs d'alimentation portatifs pour téléphone portable",
"Date de mise en ligne sur Amazon.fr": "31 juillet 2023"
}
},
@@ -731,7 +731,7 @@
"Mises à jour logicielles garanties jusquà": "Information non disponible",
"ASIN": "B0CWLSQ8FS",
"Moyenne des commentaires client": "3,7 3,7 sur 5 étoiles (27) 3,7 sur 5 étoiles",
"Classement des meilleures ventes d'Amazon": "7941 en Informatique ( Voir les 100 premiers en Informatique ) 176 en Mémoire RAM",
"Classement des meilleures ventes d'Amazon": "10729 en Informatique ( Voir les 100 premiers en Informatique ) 218 en Mémoire RAM",
"Date de mise en ligne sur Amazon.fr": "30 juillet 2024"
}
},

File diff suppressed because one or more lines are too long

View File

@@ -214,15 +214,17 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
if not price_text:
price_text = _safe_attr_soup(soup, "#twister-plus-price-data-price", "value")
price_list_text = _safe_text_soup(
soup, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen"
)
if not price_list_text:
price_list_text = _safe_text_soup(soup, "#priceblock_strikeprice")
# prix conseillé (srpPriceBlock = "Prix conseillé : XXX €")
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .srpPriceBlockAUI .a-offscreen")
if not price_list_text:
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .a-offscreen")
if not price_list_text:
price_list_text = _safe_text_soup(soup, ".srpPriceBlockAUI .a-offscreen")
price_list_text = _safe_text_soup(soup, "#priceblock_strikeprice")
# fallback sur corePriceDisplay (prix barré) si pas de srpPriceBlock
if not price_list_text:
price_list_text = _safe_text_soup(
soup, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen"
)
stock_text = _safe_text_soup(soup, "#availability span")
if not stock_text:
@@ -252,25 +254,33 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
prime_eligible = True
amazon_exclusive = "Exclusivité Amazon" if "Exclusivité Amazon" in soup.get_text() else None
# prix plus bas 30 jours (basisPrice avec mention "30 jours")
lowest_30d_text = _extract_lowest_30d_text_soup(soup)
lowest_30d_price = None
if lowest_30d_text:
lowest_30d_price = parse_price_fr(lowest_30d_text)
if lowest_30d_price is not None:
candidate_list = parse_price_fr(price_list_text)
if candidate_list == lowest_30d_price:
price_list_text = None
if not price_list_text:
price_list_text = _safe_text_soup(soup, ".srpPriceBlock .a-offscreen")
if not price_list_text:
price_list_text = _safe_text_soup(soup, ".srpPriceBlockAUI .a-offscreen")
# si le prix conseillé == prix min 30j, c'est une erreur de détection
# (le prix barré dans corePriceDisplay est en fait le prix min 30j, pas le conseillé)
price_list_value = parse_price_fr(price_list_text)
if price_list_value is not None and lowest_30d_price is not None and price_list_value == lowest_30d_price:
price_list_text = None
price_list_value = None
# réductions
reduction_savings_text = _safe_text_soup(
soup, "#corePriceDisplay_desktop_feature_div .savingsPercentage"
)
reduction_conseille_text = _safe_text_soup(soup, ".srpSavingsPercentageBlock")
reduction_min_30j = _parse_percent(reduction_savings_text)
reduction_conseille = _parse_percent(reduction_conseille_text)
# attribuer correctement les réductions selon ce qui est présent
# - si prix min 30j présent, savingsPercentage = réduction par rapport au min 30j
# - si prix conseillé présent (srpPriceBlock), srpSavingsPercentageBlock = réduction par rapport au conseillé
reduction_min_30j = _parse_percent(reduction_savings_text) if lowest_30d_price is not None else None
reduction_conseille = _parse_percent(reduction_conseille_text) if price_list_value is not None else None
# si pas de srpSavingsPercentageBlock mais un savingsPercentage et un prix conseillé (sans min 30j)
if reduction_conseille is None and price_list_value is not None and lowest_30d_price is None:
reduction_conseille = _parse_percent(reduction_savings_text)
a_propos = _extract_about_bullets(soup)
description = _extract_description(soup)
@@ -285,7 +295,7 @@ def extract_product_data_from_html(html: str, url: str) -> dict[str, Any]:
"titre": title,
"url_image_principale": image_main_url,
"prix_actuel": parse_price_fr(price_text),
"prix_conseille": parse_price_fr(price_list_text),
"prix_conseille": price_list_value,
"prix_min_30j": lowest_30d_price,
"prix_conseille_reduction": reduction_conseille,
"prix_min_30j_reduction": reduction_min_30j,
@@ -333,14 +343,15 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
if not price_text:
price_text = _safe_attr(page, "#twister-plus-price-data-price", "value")
# prix barré / conseillé
price_list_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen")
if not price_list_text:
price_list_text = _safe_text(page, "#priceblock_strikeprice")
# prix conseillé (srpPriceBlock = "Prix conseillé : XXX €")
price_list_text = _safe_text(page, ".srpPriceBlock .srpPriceBlockAUI .a-offscreen")
if not price_list_text:
price_list_text = _safe_text(page, ".srpPriceBlock .a-offscreen")
if not price_list_text:
price_list_text = _safe_text(page, ".srpPriceBlockAUI .a-offscreen")
price_list_text = _safe_text(page, "#priceblock_strikeprice")
# fallback sur corePriceDisplay (prix barré) si pas de srpPriceBlock
if not price_list_text:
price_list_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price span.a-offscreen")
# stock
stock_text = _safe_text(page, "#availability span")
@@ -374,34 +385,44 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
amazon_exclusive = _safe_text(page, "text=Exclusivité Amazon")
# prix plus bas 30 jours
# prix plus bas 30 jours (basisPrice ou corePriceDisplay avec mention "30 jours")
lowest_30d_text = None
lowest_30d_price = None
if page.locator(".basisPrice").count() > 0:
basis_text = page.locator(".basisPrice").first.inner_text()
if basis_text and re.search(r"prix.+(30|trente).+jour", basis_text.lower()):
lowest_30d_text = _safe_text(page, ".basisPrice .a-offscreen") or basis_text
if not lowest_30d_text and page.locator("#priceBadging_feature_div").count() > 0:
lowest_30d_text = _safe_text(page, ".basisPrice .a-price .a-offscreen") or basis_text
lowest_30d_price = parse_price_fr(lowest_30d_text)
# fallback sur corePriceDisplay si contient mention 30 jours
if lowest_30d_price is None and page.locator("#corePriceDisplay_desktop_feature_div .a-text-price").count() > 0:
core_text = page.locator("#corePriceDisplay_desktop_feature_div").first.inner_text()
if core_text and re.search(r"prix.+(30|trente).+jour", core_text.lower()):
lowest_30d_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .a-text-price .a-offscreen")
lowest_30d_price = parse_price_fr(lowest_30d_text)
if not lowest_30d_price and page.locator("#priceBadging_feature_div").count() > 0:
badging_text = page.locator("#priceBadging_feature_div").first.inner_text()
if badging_text and re.search(r"prix.+(30|trente).+jour", badging_text.lower()):
lowest_30d_text = _safe_text(page, "#priceBadging_feature_div .a-offscreen") or badging_text
if lowest_30d_text and not re.search(r"prix.+(30|trente).+jour", lowest_30d_text.lower()):
lowest_30d_text = None
lowest_30d_price = None
if lowest_30d_text and "prix" in lowest_30d_text.lower():
lowest_30d_price = parse_price_fr(lowest_30d_text)
if lowest_30d_price is not None:
candidate_list = parse_price_fr(price_list_text)
if candidate_list == lowest_30d_price:
price_list_text = None
if not price_list_text:
price_list_text = _safe_text(page, ".srpPriceBlock .a-offscreen")
if not price_list_text:
price_list_text = _safe_text(page, ".srpPriceBlockAUI .a-offscreen")
lowest_30d_price = parse_price_fr(lowest_30d_text)
# si le prix conseillé == prix min 30j, c'est une erreur de détection
price_list_value = parse_price_fr(price_list_text)
if price_list_value is not None and lowest_30d_price is not None and price_list_value == lowest_30d_price:
price_list_text = None
price_list_value = None
# réductions
# savingsPercentage dans corePriceDisplay = réduction par rapport au prix min 30j (si présent)
# srpSavingsPercentageBlock = réduction par rapport au prix conseillé
reduction_savings_text = _safe_text(page, "#corePriceDisplay_desktop_feature_div .savingsPercentage")
reduction_conseille_text = _safe_text(page, ".srpSavingsPercentageBlock")
reduction_min_30j = _parse_percent(reduction_savings_text)
reduction_conseille = _parse_percent(reduction_conseille_text)
# attribuer correctement les réductions selon ce qui est présent
reduction_min_30j = _parse_percent(reduction_savings_text) if lowest_30d_price is not None else None
reduction_conseille = _parse_percent(reduction_conseille_text) if price_list_value is not None else None
# si pas de srpSavingsPercentageBlock mais un savingsPercentage et un prix conseillé (sans min 30j)
if reduction_conseille is None and price_list_value is not None and lowest_30d_price is None:
reduction_conseille = _parse_percent(reduction_savings_text)
asin = _safe_attr(page, "input#ASIN", "value") or _extract_asin_from_url(url)
@@ -417,7 +438,7 @@ def extract_product_data(page: Page, url: str) -> dict[str, Any]:
"titre": title,
"url_image_principale": image_main_url,
"prix_actuel": parse_price_fr(price_text),
"prix_conseille": parse_price_fr(price_list_text),
"prix_conseille": price_list_value,
"prix_min_30j": lowest_30d_price,
"prix_conseille_reduction": reduction_conseille,
"prix_min_30j_reduction": reduction_min_30j,

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from pathlib import Path
from playwright.async_api import async_playwright, Browser, BrowserContext

View File

@@ -13,11 +13,11 @@ PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from loguru import logger
from playwright.sync_api import sync_playwright
from loguru import logger # noqa: E402
from playwright.sync_api import sync_playwright # noqa: E402
from backend.app.core.config import load_config
from backend.app.scraper.amazon.parser import extract_product_data
from backend.app.core.config import load_config # noqa: E402
from backend.app.scraper.amazon.parser import extract_product_data # noqa: E402
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "samples"
TESTS_PATH = SAMPLES_DIR / "scrape_test.json"

View File

@@ -153,7 +153,7 @@ def scrape_product(product_id: int) -> None:
# fermeture propre du navigateur
context.close()
browser.close()
except Exception as exc: # pragma: no cover
except Exception: # pragma: no cover
logger.exception("Erreur pendant le scraping de %s", product_id)
_finalize_run(run, session, "erreur")
finally: