66import logging
77import json
88import asyncio
9- from typing import Optional
9+ from typing import Optional , List
1010from ..models import User , PDL , EnergyProvider , EnergyOffer
1111from ..models .database import get_db
1212from ..middleware import require_admin , require_permission , get_current_user
1313from ..schemas import APIResponse
14- from ..services import rate_limiter , cache_service
14+ from ..services import rate_limiter
1515from ..services .price_update_service import PriceUpdateService
16+ from ..config import settings
17+ import redis .asyncio as redis
1618
1719logger = logging .getLogger (__name__ )
1820
2729 "progress" : 0
2830}
2931
32+ # Cache pour les offres scrapées (évite de re-scraper entre preview et refresh)
33+ # TTL de 5 minutes - les offres scrapées sont réutilisées si le refresh est fait rapidement
34+ SCRAPED_OFFERS_CACHE_TTL = 300 # 5 minutes
35+
36+
37+ async def _get_redis_client ():
38+ """Get Redis client"""
39+ return await redis .from_url (settings .REDIS_URL , encoding = "utf-8" , decode_responses = True )
40+
41+
42+ async def _cache_scraped_offers (provider : str , offers : List [dict ]) -> None :
43+ """Cache les offres scrapées pour éviter un double scraping"""
44+ try :
45+ client = await _get_redis_client ()
46+ cache_key = f"scraped_offers:{ provider } "
47+ await client .setex (cache_key , SCRAPED_OFFERS_CACHE_TTL , json .dumps (offers ))
48+ await client .close ()
49+ logger .info (f"Cached { len (offers )} scraped offers for { provider } (TTL: { SCRAPED_OFFERS_CACHE_TTL } s)" )
50+ except Exception as e :
51+ logger .error (f"Failed to cache offers for { provider } : { e } " )
52+
53+
54+ async def _get_cached_offers (provider : str ) -> List [dict ] | None :
55+ """Récupère les offres scrapées du cache si disponibles"""
56+ try :
57+ client = await _get_redis_client ()
58+ cache_key = f"scraped_offers:{ provider } "
59+ cached = await client .get (cache_key )
60+ await client .close ()
61+ if cached :
62+ offers = json .loads (cached )
63+ logger .info (f"Found { len (offers )} cached offers for { provider } " )
64+ return offers
65+ except Exception as e :
66+ logger .error (f"Failed to get cached offers for { provider } : { e } " )
67+ return None
68+
69+
70+ async def _clear_cached_offers (provider : str ) -> None :
71+ """Supprime les offres scrapées du cache après utilisation"""
72+ try :
73+ client = await _get_redis_client ()
74+ cache_key = f"scraped_offers:{ provider } "
75+ await client .delete (cache_key )
76+ await client .close ()
77+ logger .info (f"Cleared cached offers for { provider } " )
78+ except Exception as e :
79+ logger .error (f"Failed to clear cached offers for { provider } : { e } " )
80+
3081
3182def _update_scraper_progress (step : str , progress : int ):
3283 """Update scraper progress status"""
@@ -952,6 +1003,10 @@ async def preview_offers_update(
9521003 }
9531004 )
9541005
1006+ # Cache scraped offers for later refresh (avoids re-scraping)
1007+ if preview_result .get ("scraped_offers" ):
1008+ await _cache_scraped_offers (provider , preview_result ["scraped_offers" ])
1009+
9551010 return APIResponse (
9561011 success = True ,
9571012 data = {
@@ -1099,9 +1154,25 @@ async def refresh_offers(
10991154 }
11001155 )
11011156
1102- _update_scraper_progress (f"Téléchargement des tarifs { provider } " , 20 )
1103- result = await service .update_provider (provider )
1104- _update_scraper_progress ("Mise à jour de la base de données" , 80 )
1157+ # Check for cached offers from preview (avoids re-scraping)
1158+ cached_offers = await _get_cached_offers (provider )
1159+
1160+ if cached_offers :
1161+ # Continue from where preview left off (80%)
1162+ # Preview: 0% → 20% (download) → 80% (analysis done)
1163+ # Refresh with cache: 80% → 90% (DB update) → 100% (done)
1164+ _update_scraper_progress (f"Utilisation des données en cache pour { provider } " , 82 )
1165+ else :
1166+ _update_scraper_progress (f"Téléchargement des tarifs { provider } " , 20 )
1167+
1168+ result = await service .update_provider (provider , cached_offers = cached_offers )
1169+
1170+ # Progress depends on whether we used cache
1171+ _update_scraper_progress ("Mise à jour de la base de données" , 90 if cached_offers else 80 )
1172+
1173+ # Clear cache after use
1174+ if cached_offers :
1175+ await _clear_cached_offers (provider )
11051176
11061177 if not result .get ("success" ):
11071178 return APIResponse (
@@ -1116,8 +1187,9 @@ async def refresh_offers(
11161187 return APIResponse (
11171188 success = True ,
11181189 data = {
1119- "message" : f"Successfully updated { provider } " ,
1120- "result" : result
1190+ "message" : f"Successfully updated { provider } " + (" (from cache)" if cached_offers else "" ),
1191+ "result" : result ,
1192+ "used_cache" : cached_offers is not None
11211193 }
11221194 )
11231195 else :
@@ -1350,31 +1422,40 @@ async def list_available_scrapers(
13501422
13511423@router .get ("/providers" , response_model = APIResponse )
13521424async def list_providers (
1353- include_missing_scrapers : bool = Query (False , description = "Include providers with scrapers that don't exist in DB yet" ),
13541425 current_user : User = Depends (require_permission ('offers' )),
13551426 db : AsyncSession = Depends (get_db )
13561427) -> APIResponse :
13571428 """
13581429 List all energy providers
13591430
1360- Args:
1361- include_missing_scrapers: If True, also returns providers with scrapers not yet in DB
1431+ Automatically creates missing providers from scrapers with default values.
13621432
13631433 Returns:
13641434 APIResponse with list of providers
13651435 """
13661436 try :
1437+ # First, ensure all scrapers have corresponding providers in DB
1438+ service = PriceUpdateService (db )
1439+ existing_result = await db .execute (select (EnergyProvider .name ))
1440+ existing_names = {row [0 ] for row in existing_result .fetchall ()}
1441+
1442+ # Create missing providers with defaults
1443+ for scraper_name in PriceUpdateService .SCRAPERS .keys ():
1444+ if scraper_name not in existing_names :
1445+ logger .info (f"Auto-creating missing provider: { scraper_name } " )
1446+ await service ._get_or_create_provider (scraper_name )
1447+
1448+ await db .commit ()
1449+
1450+ # Now fetch all providers
13671451 result = await db .execute (
13681452 select (EnergyProvider ).order_by (EnergyProvider .name )
13691453 )
13701454 providers = result .scalars ().all ()
13711455
13721456 providers_data = []
1373- existing_names = set ()
13741457
13751458 for provider in providers :
1376- existing_names .add (provider .name )
1377-
13781459 # Count active offers
13791460 offers_result = await db .execute (
13801461 select (func .count ()).select_from (EnergyOffer ).where (
@@ -1389,6 +1470,11 @@ async def list_providers(
13891470 # Check if this provider has a scraper
13901471 has_scraper = provider .name in PriceUpdateService .SCRAPERS
13911472
1473+ # Get default URLs if provider has none
1474+ scraper_urls = provider .scraper_urls
1475+ if not scraper_urls and has_scraper :
1476+ scraper_urls = PriceUpdateService .get_default_scraper_urls (provider .name )
1477+
13921478 providers_data .append ({
13931479 "id" : provider .id ,
13941480 "name" : provider .name ,
@@ -1397,30 +1483,11 @@ async def list_providers(
13971483 "is_active" : provider .is_active ,
13981484 "active_offers_count" : offers_count ,
13991485 "has_scraper" : has_scraper ,
1400- "scraper_urls" : provider . scraper_urls ,
1486+ "scraper_urls" : scraper_urls ,
14011487 "created_at" : provider .created_at .isoformat (),
14021488 "updated_at" : provider .updated_at .isoformat (),
14031489 })
14041490
1405- # Add providers with scrapers that don't exist in DB yet
1406- if include_missing_scrapers :
1407- for scraper_name in PriceUpdateService .SCRAPERS .keys ():
1408- if scraper_name not in existing_names :
1409- # Generate a placeholder ID and default values
1410- providers_data .append ({
1411- "id" : f"scraper-{ scraper_name .lower ().replace (' ' , '-' )} " ,
1412- "name" : scraper_name ,
1413- "logo_url" : None ,
1414- "website" : None ,
1415- "is_active" : False ,
1416- "active_offers_count" : 0 ,
1417- "has_scraper" : True ,
1418- "scraper_urls" : None ,
1419- "created_at" : None ,
1420- "updated_at" : None ,
1421- "not_in_database" : True , # Flag to indicate this is a placeholder
1422- })
1423-
14241491 # Sort by name
14251492 providers_data .sort (key = lambda x : x ["name" ])
14261493
0 commit comments