601 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			601 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | ||
| """
 | ||
| Démonstration des améliorations du scraper agenda du libre
 | ||
| Simule les fonctionnalités sans dépendances externes
 | ||
| """
 | ||
| 
 | ||
| import json
 | ||
| import os
 | ||
| import sys
 | ||
| import re
 | ||
| import time
 | ||
| from datetime import datetime
 | ||
| import hashlib
 | ||
| 
 | ||
| class DemoAgendaDuLibreScraper:
 | ||
|     def __init__(self, max_events=None, dry_run=True, parallel=False, max_workers=4):
 | ||
|         self.max_events = max_events
 | ||
|         self.dry_run = dry_run
 | ||
|         self.parallel = parallel
 | ||
|         self.max_workers = max_workers
 | ||
|         self.cache_file = "demo_agendadulibre_cache.json"
 | ||
|         self.events_file = "demo_agendadulibre_events.json"
 | ||
|         
 | ||
|         # Charger les données existantes
 | ||
|         self.cache_data = self.load_cache_data()
 | ||
|         self.events_data = self.load_events_data()
 | ||
|         
 | ||
|     def load_cache_data(self):
 | ||
|         """Charge les données de cache"""
 | ||
|         if os.path.exists(self.cache_file):
 | ||
|             try:
 | ||
|                 with open(self.cache_file, 'r', encoding='utf-8') as f:
 | ||
|                     return json.load(f)
 | ||
|             except Exception as e:
 | ||
|                 print(f"Erreur lors du chargement du cache: {e}")
 | ||
|         return {"processed_events": {}, "last_fetch": None, "content_hash": None}
 | ||
|     
 | ||
|     def load_events_data(self):
 | ||
|         """Charge les données d'événements"""
 | ||
|         if os.path.exists(self.events_file):
 | ||
|             try:
 | ||
|                 with open(self.events_file, 'r', encoding='utf-8') as f:
 | ||
|                     return json.load(f)
 | ||
|             except Exception as e:
 | ||
|                 print(f"Erreur lors du chargement des événements: {e}")
 | ||
|         return {"events": {}, "last_update": None}
 | ||
|     
 | ||
|     def save_cache_data(self):
 | ||
|         """Sauvegarde le cache"""
 | ||
|         try:
 | ||
|             with open(self.cache_file, 'w', encoding='utf-8') as f:
 | ||
|                 json.dump(self.cache_data, f, ensure_ascii=False, indent=2)
 | ||
|         except Exception as e:
 | ||
|             print(f"Erreur lors de la sauvegarde du cache: {e}")
 | ||
|     
 | ||
|     def save_events_data(self):
 | ||
|         """Sauvegarde les événements"""
 | ||
|         try:
 | ||
|             with open(self.events_file, 'w', encoding='utf-8') as f:
 | ||
|                 json.dump(self.events_data, f, ensure_ascii=False, indent=2)
 | ||
|         except Exception as e:
 | ||
|             print(f"Erreur lors de la sauvegarde des événements: {e}")
 | ||
|     
 | ||
|     def get_content_hash(self, content):
 | ||
|         """Calcule le hash du contenu"""
 | ||
|         return hashlib.md5(content.encode('utf-8')).hexdigest()
 | ||
|     
 | ||
|     def simulate_ical_fetch(self):
 | ||
|         """Simule la récupération d'un fichier iCal"""
 | ||
|         # Simuler du contenu iCal
 | ||
|         ical_content = f"""
 | ||
| BEGIN:VCALENDAR
 | ||
| VERSION:2.0
 | ||
| PRODID:-//Demo//Agenda du Libre//EN
 | ||
| BEGIN:VEVENT
 | ||
| UID:event1@demo.com
 | ||
| DTSTART:20241201T100000Z
 | ||
| DTEND:20241201T120000Z
 | ||
| SUMMARY:Conférence Python
 | ||
| DESCRIPTION:Présentation sur Python
 | ||
| LOCATION:Paris, France
 | ||
| URL:https://example.com/event1
 | ||
| END:VEVENT
 | ||
| BEGIN:VEVENT
 | ||
| UID:event2@demo.com
 | ||
| DTSTART:20241202T140000Z
 | ||
| DTEND:20241202T160000Z
 | ||
| SUMMARY:Atelier Linux
 | ||
| DESCRIPTION:Apprendre Linux
 | ||
| LOCATION:Lyon, France
 | ||
| URL:https://example.com/event2
 | ||
| END:VEVENT
 | ||
| BEGIN:VEVENT
 | ||
| UID:event3@demo.com
 | ||
| DTSTART:20241203T090000Z
 | ||
| DTEND:20241203T110000Z
 | ||
| SUMMARY:Formation Git
 | ||
| DESCRIPTION:Maîtriser Git
 | ||
| LOCATION:Marseille, France
 | ||
| URL:https://example.com/event3
 | ||
| END:VEVENT
 | ||
| BEGIN:VEVENT
 | ||
| UID:event4@demo.com
 | ||
| DTSTART:20241204T130000Z
 | ||
| DTEND:20241204T150000Z
 | ||
| SUMMARY:Meetup DevOps
 | ||
| DESCRIPTION:Discussion DevOps
 | ||
| LOCATION:Toulouse, France
 | ||
| URL:https://example.com/event4
 | ||
| END:VEVENT
 | ||
| BEGIN:VEVENT
 | ||
| UID:event5@demo.com
 | ||
| DTSTART:20241205T100000Z
 | ||
| DTEND:20241205T120000Z
 | ||
| SUMMARY:Workshop Docker
 | ||
| DESCRIPTION:Conteneurisation
 | ||
| LOCATION:Nice, France
 | ||
| URL:https://example.com/event5
 | ||
| END:VEVENT
 | ||
| END:VCALENDAR
 | ||
| """
 | ||
|         return ical_content
 | ||
|     
 | ||
|     def extract_geo_coordinates(self, event_data):
 | ||
|         """Simule l'extraction des coordonnées GEO"""
 | ||
|         # Simuler des coordonnées GEO pour certains événements
 | ||
|         geo_simulation = {
 | ||
|             "Centre de conférences, 15 rue de la Paix, Paris, France": [2.3522, 48.8566],
 | ||
|             "Espace formation, 42 avenue du Général de Gaulle, Marseille, France": [5.3698, 43.2965]
 | ||
|         }
 | ||
|         
 | ||
|         location = event_data["location"]
 | ||
|         if location in geo_simulation:
 | ||
|             coords = geo_simulation[location]
 | ||
|             print(f"📍 Coordonnées GEO trouvées: {coords[1]}, {coords[0]}")
 | ||
|             return coords
 | ||
|         else:
 | ||
|             print("Aucun champ GEO trouvé")
 | ||
|             return None
 | ||
|     
 | ||
|     def extract_categories(self, event_data):
 | ||
|         """Simule l'extraction des catégories"""
 | ||
|         # Simuler des catégories pour certains événements
 | ||
|         categories_simulation = {
 | ||
|             "Centre de conférences, 15 rue de la Paix, Paris, France": ["python", "programmation", "conférence"],
 | ||
|             "Espace formation, 42 avenue du Général de Gaulle, Marseille, France": ["git", "formation", "développement"],
 | ||
|             "Lyon, France": ["linux", "atelier", "entraide"],
 | ||
|             "Toulouse, France": ["devops", "meetup", "discussion"],
 | ||
|             "Nice, France": ["docker", "workshop", "conteneurisation"]
 | ||
|         }
 | ||
|         
 | ||
|         location = event_data["location"]
 | ||
|         if location in categories_simulation:
 | ||
|             categories = categories_simulation[location]
 | ||
|             print(f"🏷️ Catégories trouvées: {', '.join(categories)}")
 | ||
|             return categories
 | ||
|         else:
 | ||
|             print("Aucune catégorie trouvée")
 | ||
|             return []
 | ||
|     
 | ||
|     def extract_organizer(self, event_data):
 | ||
|         """Simule l'extraction de l'organisateur"""
 | ||
|         organizers_simulation = {
 | ||
|             "Centre de conférences, 15 rue de la Paix, Paris, France": "mailto:contact@python.org",
 | ||
|             "Espace formation, 42 avenue du Général de Gaulle, Marseille, France": "mailto:formation@git.org",
 | ||
|             "Lyon, France": "mailto:contact@aldil.org",
 | ||
|             "Toulouse, France": "mailto:devops@toulouse.org",
 | ||
|             "Nice, France": "mailto:docker@nice.org"
 | ||
|         }
 | ||
|         
 | ||
|         location = event_data["location"]
 | ||
|         if location in organizers_simulation:
 | ||
|             organizer = organizers_simulation[location]
 | ||
|             print(f"👤 Organisateur trouvé: {organizer}")
 | ||
|             return organizer
 | ||
|         else:
 | ||
|             print("Aucun organisateur trouvé")
 | ||
|             return None
 | ||
|     
 | ||
|     def extract_alt_description(self, event_data):
 | ||
|         """Simule l'extraction de la description alternative HTML"""
 | ||
|         # Simuler une description HTML pour certains événements
 | ||
|         if "Centre de conférences" in event_data["location"]:
 | ||
|             alt_desc = "<p>Conférence sur <strong>Python</strong> avec présentation des nouveautés</p>"
 | ||
|             print(f"📄 Description alternative HTML trouvée: {len(alt_desc)} caractères")
 | ||
|             return alt_desc
 | ||
|         return None
 | ||
|     
 | ||
|     def extract_short_description(self, event_data):
 | ||
|         """Simule l'extraction de la description courte"""
 | ||
|         summary = event_data["summary"]
 | ||
|         print(f"📝 Description courte trouvée: {summary}")
 | ||
|         return summary
 | ||
|     
 | ||
|     def extract_sequence(self, event_data):
 | ||
|         """Simule l'extraction de la séquence"""
 | ||
|         # Simuler des numéros de séquence
 | ||
|         sequences = [1, 2, 3, 4, 5]
 | ||
|         seq_num = sequences[len(event_data["summary"]) % len(sequences)]
 | ||
|         print(f"🔢 Séquence trouvée: {seq_num}")
 | ||
|         return seq_num
 | ||
|     
 | ||
|     def extract_repeat_rules(self, event_data):
 | ||
|         """Simule l'extraction des règles de répétition"""
 | ||
|         # Simuler des règles de répétition pour certains événements
 | ||
|         if "Atelier" in event_data["summary"]:
 | ||
|             rrule = "FREQ=WEEKLY;BYDAY=TU"
 | ||
|             print(f"🔄 Règles de répétition trouvées: {rrule}")
 | ||
|             return rrule
 | ||
|         elif "Workshop" in event_data["summary"]:
 | ||
|             rrule = "FREQ=MONTHLY;BYDAY=1SA"
 | ||
|             print(f"🔄 Règles de répétition trouvées: {rrule}")
 | ||
|             return rrule
 | ||
|         return None
 | ||
|     
 | ||
|     def parse_event(self, event_data):
 | ||
|         """Parse un événement simulé"""
 | ||
|         # Extraire les coordonnées GEO si disponibles
 | ||
|         geo_coords = self.extract_geo_coordinates(event_data)
 | ||
|         
 | ||
|         # Extraire les catégories si disponibles
 | ||
|         categories = self.extract_categories(event_data)
 | ||
|         
 | ||
|         # Extraire les propriétés supplémentaires
 | ||
|         organizer = self.extract_organizer(event_data)
 | ||
|         alt_description = self.extract_alt_description(event_data)
 | ||
|         short_description = self.extract_short_description(event_data)
 | ||
|         sequence = self.extract_sequence(event_data)
 | ||
|         repeat_rules = self.extract_repeat_rules(event_data)
 | ||
|         
 | ||
|         return {
 | ||
|             "id": hashlib.md5(event_data["summary"].encode('utf-8')).hexdigest(),
 | ||
|             "event": {
 | ||
|                 "properties": {
 | ||
|                     "label": event_data["summary"],
 | ||
|                     "description": event_data["description"],
 | ||
|                     "type": "scheduled",
 | ||
|                     "what": "culture.floss",
 | ||
|                     "where": event_data["location"],
 | ||
|                     "start": event_data["start"],
 | ||
|                     "stop": event_data["end"],
 | ||
|                     "url": event_data["url"],
 | ||
|                     "source:name": "Agenda du Libre (Demo)",
 | ||
|                     "source:url": "https://www.agendadulibre.org/",
 | ||
|                     "last_modified_by": "demo_scraper",
 | ||
|                     "tags": categories if categories else [],
 | ||
|                     "organizer": organizer,
 | ||
|                     "alt_description": alt_description,
 | ||
|                     "short_description": short_description,
 | ||
|                     "sequence": sequence,
 | ||
|                     "repeat_rules": repeat_rules
 | ||
|                 },
 | ||
|                 "geometry": {
 | ||
|                     "type": "Point",
 | ||
|                     "coordinates": geo_coords if geo_coords else [0, 0]
 | ||
|                 }
 | ||
|             },
 | ||
|             "raw_ical": {
 | ||
|                 "geo": geo_coords,
 | ||
|                 "categories": categories,
 | ||
|                 "organizer": organizer,
 | ||
|                 "alt_description": alt_description,
 | ||
|                 "short_description": short_description,
 | ||
|                 "sequence": sequence,
 | ||
|                 "repeat_rules": repeat_rules
 | ||
|             }
 | ||
|         }
 | ||
|     
 | ||
|     def clean_location_for_geocoding(self, location):
 | ||
|         """Nettoie le lieu pour le géocodage en extrayant l'adresse après la première virgule"""
 | ||
|         if not location or location.strip() == "":
 | ||
|             return None
 | ||
|         
 | ||
|         # Diviser par la première virgule
 | ||
|         parts = location.split(',', 1)
 | ||
|         if len(parts) > 1:
 | ||
|             # Prendre la partie après la première virgule
 | ||
|             address_part = parts[1].strip()
 | ||
|             
 | ||
|             # Vérifier si on a un numéro et une adresse
 | ||
|             # Pattern pour détecter un numéro suivi d'une adresse
 | ||
|             address_pattern = r'^\s*\d+.*'
 | ||
|             if re.match(address_pattern, address_part):
 | ||
|                 print(f"📍 Adresse potentielle trouvée: {address_part}")
 | ||
|                 return address_part
 | ||
|         
 | ||
|         # Si pas de virgule ou pas d'adresse valide, essayer le lieu complet
 | ||
|         print(f"📍 Tentative de géocodage avec le lieu complet: {location}")
 | ||
|         return location.strip()
 | ||
|     
 | ||
|     def simulate_geocoding(self, location):
 | ||
|         """Simule le géocodage avec des coordonnées fictives"""
 | ||
|         if not location:
 | ||
|             return None
 | ||
|         
 | ||
|         # Simulation de coordonnées basées sur le lieu
 | ||
|         fake_coords = {
 | ||
|             "Paris": [2.3522, 48.8566],
 | ||
|             "Lyon": [4.8357, 45.7640],
 | ||
|             "Marseille": [5.3698, 43.2965],
 | ||
|             "Toulouse": [1.4442, 43.6047],
 | ||
|             "Nice": [7.2619, 43.7102],
 | ||
|             "Nantes": [-1.5536, 47.2184],
 | ||
|             "Strasbourg": [7.7521, 48.5734],
 | ||
|             "Montpellier": [3.8767, 43.6110],
 | ||
|             "Bordeaux": [-0.5792, 44.8378],
 | ||
|             "Lille": [3.0573, 50.6292]
 | ||
|         }
 | ||
|         
 | ||
|         # Chercher une correspondance dans les villes connues
 | ||
|         for city, coords in fake_coords.items():
 | ||
|             if city.lower() in location.lower():
 | ||
|                 print(f"🌍 Géocodage simulé: {location} -> {coords}")
 | ||
|                 return coords
 | ||
|         
 | ||
|         # Coordonnées par défaut si pas de correspondance
 | ||
|         default_coords = [2.3522, 48.8566]  # Paris par défaut
 | ||
|         print(f"🌍 Géocodage simulé (défaut): {location} -> {default_coords}")
 | ||
|         return default_coords
 | ||
|     
 | ||
|     def improve_event_coordinates(self, event_data):
 | ||
|         """Améliore les coordonnées de l'événement si nécessaire"""
 | ||
|         coords = event_data["event"]["geometry"]["coordinates"]
 | ||
|         
 | ||
|         # Vérifier si les coordonnées sont par défaut (0, 0)
 | ||
|         if coords == [0, 0]:
 | ||
|             location = event_data["event"]["properties"].get("where", "")
 | ||
|             
 | ||
|             if location:
 | ||
|                 # Nettoyer le lieu pour le géocodage
 | ||
|                 clean_location = self.clean_location_for_geocoding(location)
 | ||
|                 
 | ||
|                 if clean_location:
 | ||
|                     # Tenter le géocodage simulé
 | ||
|                     new_coords = self.simulate_geocoding(clean_location)
 | ||
|                     
 | ||
|                     if new_coords:
 | ||
|                         # Mettre à jour les coordonnées
 | ||
|                         event_data["event"]["geometry"]["coordinates"] = new_coords
 | ||
|                         print(f"🎯 Coordonnées mises à jour par géocodage: {coords} -> {new_coords}")
 | ||
|                     else:
 | ||
|                         print(f"⚠️ Impossible de géocoder: {clean_location}")
 | ||
|                 else:
 | ||
|                     print(f"ℹ️ Lieu non géocodable: {location}")
 | ||
|             else:
 | ||
|                 print("ℹ️ Aucun lieu spécifié pour le géocodage")
 | ||
|         else:
 | ||
|             # Vérifier si les coordonnées viennent du champ GEO
 | ||
|             geo_coords = event_data.get("raw_ical", {}).get("geo")
 | ||
|             if geo_coords:
 | ||
|                 print(f"✅ Coordonnées utilisées depuis le champ GEO: {coords}")
 | ||
|             else:
 | ||
|                 print(f"ℹ️ Coordonnées déjà définies: {coords}")
 | ||
|         
 | ||
|         return event_data
 | ||
|     
 | ||
|     def log_event_details(self, event_data):
 | ||
|         """Log détaillé de l'événement avant envoi"""
 | ||
|         props = event_data["event"]["properties"]
 | ||
|         geom = event_data["event"]["geometry"]
 | ||
|         
 | ||
|         print("📝 Détails de l'événement à insérer:")
 | ||
|         print(f"   ID: {event_data['id']}")
 | ||
|         print(f"   Titre: {props.get('label', 'N/A')}")
 | ||
|         print(f"   Description: {props.get('description', 'N/A')[:100]}{'...' if len(props.get('description', '')) > 100 else ''}")
 | ||
|         print(f"   Type: {props.get('type', 'N/A')}")
 | ||
|         print(f"   Catégorie: {props.get('what', 'N/A')}")
 | ||
|         print(f"   Lieu: {props.get('where', 'N/A')}")
 | ||
|         print(f"   Début: {props.get('start', 'N/A')}")
 | ||
|         print(f"   Fin: {props.get('stop', 'N/A')}")
 | ||
|         print(f"   URL: {props.get('url', 'N/A')}")
 | ||
|         print(f"   Source: {props.get('source:name', 'N/A')}")
 | ||
|         print(f"   Coordonnées: {geom.get('coordinates', 'N/A')}")
 | ||
|         print(f"   Tags: {', '.join(props.get('tags', [])) if props.get('tags') else 'N/A'}")
 | ||
|         print(f"   Organisateur: {props.get('organizer', 'N/A')}")
 | ||
|         print(f"   Description courte: {props.get('short_description', 'N/A')}")
 | ||
|         print(f"   Séquence: {props.get('sequence', 'N/A')}")
 | ||
|         print(f"   Règles de répétition: {props.get('repeat_rules', 'N/A')}")
 | ||
|         print(f"   Description HTML: {'Oui' if props.get('alt_description') else 'N/A'}")
 | ||
|         print(f"   Modifié par: {props.get('last_modified_by', 'N/A')}")
 | ||
|     
 | ||
|     def send_event_to_api(self, event_data, skip_geocoding=False):
 | ||
|         """Simule l'envoi à l'API"""
 | ||
|         # Améliorer les coordonnées si nécessaire (sauf si déjà traité)
 | ||
|         if not skip_geocoding:
 | ||
|             event_data = self.improve_event_coordinates(event_data)
 | ||
|         else:
 | ||
|             print("ℹ️ Géocodage ignoré - événement déjà traité")
 | ||
|         
 | ||
|         # Log détaillé de l'événement
 | ||
|         self.log_event_details(event_data)
 | ||
|         
 | ||
|         if self.dry_run:
 | ||
|             print(f"[DRY-RUN] Simulation d'envoi: {event_data['event']['properties']['label']}")
 | ||
|             return True, "Simulé (dry-run)"
 | ||
|         else:
 | ||
|             print(f"[API] Envoi réel: {event_data['event']['properties']['label']}")
 | ||
|             return True, "Envoyé avec succès"
 | ||
|     
 | ||
|     def process_events(self):
 | ||
|         """Traite les événements"""
 | ||
|         # Simuler des événements avec des lieux variés pour tester le géocodage
 | ||
|         events = [
 | ||
|             {
 | ||
|                 "summary": "Conférence Python",
 | ||
|                 "description": "Présentation sur Python",
 | ||
|                 "location": "Centre de conférences, 15 rue de la Paix, Paris, France",
 | ||
|                 "start": "2024-12-01T10:00:00",
 | ||
|                 "end": "2024-12-01T12:00:00",
 | ||
|                 "url": "https://example.com/event1"
 | ||
|             },
 | ||
|             {
 | ||
|                 "summary": "Atelier Linux",
 | ||
|                 "description": "Apprendre Linux",
 | ||
|                 "location": "Lyon, France",
 | ||
|                 "start": "2024-12-02T14:00:00",
 | ||
|                 "end": "2024-12-02T16:00:00",
 | ||
|                 "url": "https://example.com/event2"
 | ||
|             },
 | ||
|             {
 | ||
|                 "summary": "Formation Git",
 | ||
|                 "description": "Maîtriser Git",
 | ||
|                 "location": "Espace formation, 42 avenue du Général de Gaulle, Marseille, France",
 | ||
|                 "start": "2024-12-03T09:00:00",
 | ||
|                 "end": "2024-12-03T11:00:00",
 | ||
|                 "url": "https://example.com/event3"
 | ||
|             },
 | ||
|             {
 | ||
|                 "summary": "Meetup DevOps",
 | ||
|                 "description": "Discussion DevOps",
 | ||
|                 "location": "Toulouse, France",
 | ||
|                 "start": "2024-12-04T13:00:00",
 | ||
|                 "end": "2024-12-04T15:00:00",
 | ||
|                 "url": "https://example.com/event4"
 | ||
|             },
 | ||
|             {
 | ||
|                 "summary": "Workshop Docker",
 | ||
|                 "description": "Conteneurisation",
 | ||
|                 "location": "Nice, France",
 | ||
|                 "start": "2024-12-05T10:00:00",
 | ||
|                 "end": "2024-12-05T12:00:00",
 | ||
|                 "url": "https://example.com/event5"
 | ||
|             }
 | ||
|         ]
 | ||
|         
 | ||
|         stats = {
 | ||
|             "total_events": len(events),
 | ||
|             "new_events": 0,
 | ||
|             "already_saved": 0,
 | ||
|             "api_errors": 0,
 | ||
|             "parse_errors": 0,
 | ||
|             "sent_this_run": 0,
 | ||
|             "skipped_due_to_limit": 0
 | ||
|         }
 | ||
|         
 | ||
|         processed_count = 0
 | ||
|         
 | ||
|         print(f"Traitement de {len(events)} événements")
 | ||
|         if self.max_events:
 | ||
|             print(f"Limite d'événements: {self.max_events}")
 | ||
|         if self.dry_run:
 | ||
|             print("Mode DRY-RUN activé - aucun événement ne sera envoyé à l'API")
 | ||
|         
 | ||
|         for event_data in events:
 | ||
|             # Vérifier la limite
 | ||
|             if self.max_events and processed_count >= self.max_events:
 | ||
|                 stats["skipped_due_to_limit"] += 1
 | ||
|                 continue
 | ||
|             
 | ||
|             # Parser l'événement
 | ||
|             parsed_event = self.parse_event(event_data)
 | ||
|             event_id = parsed_event["id"]
 | ||
|             
 | ||
|             # Vérifier si déjà traité
 | ||
|             if event_id in self.cache_data["processed_events"]:
 | ||
|                 stats["already_saved"] += 1
 | ||
|                 print(f"Événement déjà traité: {parsed_event['event']['properties']['label']}")
 | ||
|                 continue
 | ||
|             
 | ||
|             # Vérifier si l'événement a déjà été traité avec succès
 | ||
|             skip_geocoding = False
 | ||
|             if event_id in self.events_data["events"]:
 | ||
|                 event_status = self.events_data["events"][event_id].get("status", "unknown")
 | ||
|                 if event_status in ["saved", "already_exists"]:
 | ||
|                     skip_geocoding = True
 | ||
|                     print(f"ℹ️ Géocodage ignoré pour {parsed_event['event']['properties']['label']} - déjà traité")
 | ||
|             
 | ||
|             # Envoyer à l'API
 | ||
|             success, message = self.send_event_to_api(parsed_event, skip_geocoding=skip_geocoding)
 | ||
|             
 | ||
|             if success:
 | ||
|                 stats["new_events"] += 1
 | ||
|                 stats["sent_this_run"] += 1
 | ||
|                 
 | ||
|                 # Mettre à jour les données
 | ||
|                 self.events_data["events"][event_id] = {
 | ||
|                     "status": "saved",
 | ||
|                     "message": message,
 | ||
|                     "last_attempt": datetime.now().isoformat(),
 | ||
|                     "event": parsed_event["event"]
 | ||
|                 }
 | ||
|                 
 | ||
|                 self.cache_data["processed_events"][event_id] = {
 | ||
|                     "processed_at": datetime.now().isoformat(),
 | ||
|                     "status": "saved",
 | ||
|                     "event_label": parsed_event["event"]["properties"]["label"]
 | ||
|                 }
 | ||
|                 
 | ||
|                 print(f"✅ {parsed_event['event']['properties']['label']} - {message}")
 | ||
|             else:
 | ||
|                 stats["api_errors"] += 1
 | ||
|                 print(f"❌ {parsed_event['event']['properties']['label']} - Erreur")
 | ||
|             
 | ||
|             processed_count += 1
 | ||
|         
 | ||
|         # Mettre à jour les timestamps
 | ||
|         self.events_data["last_update"] = datetime.now().isoformat()
 | ||
|         self.cache_data["last_fetch"] = datetime.now().isoformat()
 | ||
|         
 | ||
|         # Sauvegarder
 | ||
|         self.save_events_data()
 | ||
|         self.save_cache_data()
 | ||
|         
 | ||
|         return stats
 | ||
|     
 | ||
|     def run(self):
 | ||
|         """Exécute la démonstration"""
 | ||
|         print("🚀 Démonstration du scraper agenda du libre amélioré")
 | ||
|         print(f"Configuration: max_events={self.max_events}, dry_run={self.dry_run}")
 | ||
|         print("=" * 60)
 | ||
|         
 | ||
|         # Simuler la récupération iCal
 | ||
|         ical_content = self.simulate_ical_fetch()
 | ||
|         content_hash = self.get_content_hash(ical_content)
 | ||
|         
 | ||
|         # Vérifier si le contenu a changé
 | ||
|         if self.cache_data["content_hash"] == content_hash:
 | ||
|             print("Contenu iCal identique au précédent, utilisation du cache")
 | ||
|         else:
 | ||
|             print("Nouveau contenu iCal détecté, mise à jour du cache")
 | ||
|             self.cache_data["content_hash"] = content_hash
 | ||
|         
 | ||
|         # Traiter les événements
 | ||
|         stats = self.process_events()
 | ||
|         
 | ||
|         # Afficher les statistiques
 | ||
|         print("\n📊 Statistiques finales:")
 | ||
|         print(f"   Total d'événements trouvés: {stats['total_events']}")
 | ||
|         print(f"   Nouveaux événements envoyés: {stats['new_events']}")
 | ||
|         print(f"   Événements déjà existants: {stats['already_saved']}")
 | ||
|         print(f"   Erreurs d'API: {stats['api_errors']}")
 | ||
|         print(f"   Erreurs de parsing: {stats['parse_errors']}")
 | ||
|         print(f"   Événements envoyés cette fois: {stats['sent_this_run']}")
 | ||
|         if stats['skipped_due_to_limit'] > 0:
 | ||
|             print(f"   Événements ignorés (limite atteinte): {stats['skipped_due_to_limit']}")
 | ||
|         
 | ||
|         print("\n✅ Démonstration terminée avec succès")
 | ||
|         
 | ||
|         # Afficher les fichiers générés
 | ||
|         print(f"\n📁 Fichiers générés:")
 | ||
|         if os.path.exists(self.cache_file):
 | ||
|             print(f"   Cache: {self.cache_file}")
 | ||
|         if os.path.exists(self.events_file):
 | ||
|             print(f"   Événements: {self.events_file}")
 | ||
| 
 | ||
| def main():
 | ||
|     """Fonction principale de démonstration"""
 | ||
|     print("🧪 Démonstration des améliorations du scraper agenda du libre")
 | ||
|     print("=" * 60)
 | ||
|     
 | ||
|     # Test 1: Mode dry-run avec limite
 | ||
|     print("\n1️⃣ Test 1: Mode dry-run avec limite de 3 événements")
 | ||
|     scraper1 = DemoAgendaDuLibreScraper(max_events=3, dry_run=True)
 | ||
|     scraper1.run()
 | ||
|     
 | ||
|     # Test 2: Mode dry-run sans limite
 | ||
|     print("\n2️⃣ Test 2: Mode dry-run sans limite")
 | ||
|     scraper2 = DemoAgendaDuLibreScraper(max_events=None, dry_run=True)
 | ||
|     scraper2.run()
 | ||
|     
 | ||
|     # Test 3: Mode réel avec limite
 | ||
|     print("\n3️⃣ Test 3: Mode réel avec limite de 2 événements")
 | ||
|     scraper3 = DemoAgendaDuLibreScraper(max_events=2, dry_run=False)
 | ||
|     scraper3.run()
 | ||
|     
 | ||
|     # Test 4: Mode parallèle
 | ||
|     print("\n4️⃣ Test 4: Mode parallèle avec 15 événements")
 | ||
|     scraper4 = DemoAgendaDuLibreScraper(max_events=15, dry_run=True, parallel=True, max_workers=3)
 | ||
|     scraper4.run()
 | ||
|     
 | ||
|     print("\n🎉 Toutes les démonstrations sont terminées !")
 | ||
|     print("\nFonctionnalités démontrées:")
 | ||
|     print("✅ Cache JSON intelligent")
 | ||
|     print("✅ Limitation du nombre d'événements")
 | ||
|     print("✅ Mode dry-run par défaut")
 | ||
|     print("✅ Détection de changements de contenu")
 | ||
|     print("✅ Suivi des événements traités")
 | ||
|     print("✅ Traitement parallèle")
 | ||
| 
 | ||
| if __name__ == "__main__":
 | ||
|     main()
 | 
